特定のMisskeyタンスにログインできる
This commit is contained in:
parent
75005c3870
commit
7517f58a3c
|
@ -186,7 +186,7 @@ Apache License
|
|||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2013 Christopher Jenkins
|
||||
Copyright 2017-2018 tateisu
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -12,8 +12,8 @@ android {
|
|||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
|
||||
versionCode 266
|
||||
versionName "2.6.6"
|
||||
versionCode 267
|
||||
versionName "2.6.7"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// https://stackoverflow.com/questions/47791227/java-lang-illegalstateexception-dex-archives-setting-dex-extension-only-for
|
||||
|
|
|
@ -77,6 +77,19 @@
|
|||
/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data
|
||||
android:host="*"
|
||||
android:pathPattern=".*"
|
||||
android:scheme="misskeyclientproto"
|
||||
/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
|
|
|
@ -38,8 +38,7 @@ import android.widget.LinearLayout
|
|||
import android.widget.TextView
|
||||
import jp.juggler.subwaytooter.action.*
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
import jp.juggler.subwaytooter.api.entity.EntityIdLong
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
|
||||
import org.apache.commons.io.IOUtils
|
||||
|
||||
|
@ -52,8 +51,6 @@ import java.util.ArrayList
|
|||
import java.util.HashSet
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.dialog.AccountPicker
|
||||
import jp.juggler.subwaytooter.dialog.DlgTextInput
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
|
@ -1464,15 +1461,13 @@ class ActMain : AppCompatActivity()
|
|||
|
||||
// ActOAuthCallbackで受け取ったUriを処理する
|
||||
private fun handleIntentUri(uri : Uri) {
|
||||
|
||||
if("subwaytooter" == uri.scheme) {
|
||||
try {
|
||||
|
||||
when(uri.scheme){
|
||||
"subwaytooter","misskeyclientproto" -> return try{
|
||||
handleOAuth2CallbackUri(uri)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val url = uri.toString()
|
||||
|
@ -1627,59 +1622,108 @@ class ActMain : AppCompatActivity()
|
|||
|
||||
override fun background(client : TootApiClient) : TootApiResult? {
|
||||
|
||||
// エラー時
|
||||
// subwaytooter://oauth
|
||||
// ?error=access_denied
|
||||
// &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
|
||||
// &state=db%3A3
|
||||
val error = uri.getQueryParameter("error_description")
|
||||
if(error?.isNotEmpty() == true) {
|
||||
return TootApiResult(error)
|
||||
}
|
||||
|
||||
// subwaytooter://oauth
|
||||
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
|
||||
// &state=host%3Amastodon.juggler.jp
|
||||
|
||||
val code = uri.getQueryParameter("code")
|
||||
if(code?.isEmpty() != false) {
|
||||
return TootApiResult("missing code in callback url.")
|
||||
}
|
||||
|
||||
val sv = uri.getQueryParameter("state")
|
||||
if(sv?.isEmpty() != false) {
|
||||
return TootApiResult("missing state in callback url.")
|
||||
}
|
||||
|
||||
if(sv.startsWith("db:")) {
|
||||
try {
|
||||
val dataId = sv.substring(3).toLong(10)
|
||||
val sa = SavedAccount.loadAccount(this@ActMain, dataId)
|
||||
?: return TootApiResult("missing account db_id=$dataId")
|
||||
this.sa = sa
|
||||
client.account = sa
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return TootApiResult(ex.withCaption("invalid state"))
|
||||
val uriStr = uri.toString()
|
||||
if( uriStr.startsWith("subwaytooter://misskey/auth_callback")
|
||||
|| uriStr.startsWith("misskeyclientproto://misskeyclientproto/auth_callback")
|
||||
){
|
||||
|
||||
// Misskey 認証コールバック
|
||||
val token = uri.getQueryParameter("token")
|
||||
if(token?.isEmpty() != false) {
|
||||
return TootApiResult("missing token in callback URL")
|
||||
}
|
||||
val prefDevice = PrefDevice.prefDevice(this@ActMain)
|
||||
|
||||
val db_id = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID,-1L)
|
||||
|
||||
val instance = prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE,null)
|
||||
?: return TootApiResult("missing instance name.")
|
||||
|
||||
if( db_id != -1L ){
|
||||
try {
|
||||
val sa = SavedAccount.loadAccount(this@ActMain, db_id)
|
||||
?: return TootApiResult("missing account db_id=$db_id")
|
||||
this.sa = sa
|
||||
client.account = sa
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return TootApiResult(ex.withCaption("invalid state"))
|
||||
}
|
||||
}else{
|
||||
client.instance = instance
|
||||
}
|
||||
|
||||
} else if(sv.startsWith("host:")) {
|
||||
val host = sv.substring(5)
|
||||
client.instance = host
|
||||
this.host = instance
|
||||
val client_name = Pref.spClientName(this@ActMain)
|
||||
val result = client.authentication2Misskey(client_name, token)
|
||||
this.ta = TootParser(
|
||||
this@ActMain,
|
||||
object : LinkHelper {
|
||||
override val host : String?
|
||||
get() = instance
|
||||
},
|
||||
serviceType = ServiceType.MISSKEY
|
||||
).account(result?.jsonObject)
|
||||
return result
|
||||
|
||||
}else{
|
||||
// Mastodon 認証コールバック
|
||||
|
||||
// エラー時
|
||||
// subwaytooter://oauth
|
||||
// ?error=access_denied
|
||||
// &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
|
||||
// &state=db%3A3
|
||||
val error = uri.getQueryParameter("error_description")
|
||||
if(error?.isNotEmpty() == true) {
|
||||
return TootApiResult(error)
|
||||
}
|
||||
|
||||
// subwaytooter://oauth
|
||||
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
|
||||
// &state=host%3Amastodon.juggler.jp
|
||||
|
||||
val code = uri.getQueryParameter("code")
|
||||
if(code?.isEmpty() != false) {
|
||||
return TootApiResult("missing code in callback url.")
|
||||
}
|
||||
|
||||
val sv = uri.getQueryParameter("state")
|
||||
if(sv?.isEmpty() != false) {
|
||||
return TootApiResult("missing state in callback url.")
|
||||
}
|
||||
|
||||
if(sv.startsWith("db:")) {
|
||||
try {
|
||||
val dataId = sv.substring(3).toLong(10)
|
||||
val sa = SavedAccount.loadAccount(this@ActMain, dataId)
|
||||
?: return TootApiResult("missing account db_id=$dataId")
|
||||
this.sa = sa
|
||||
client.account = sa
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return TootApiResult(ex.withCaption("invalid state"))
|
||||
}
|
||||
|
||||
} else if(sv.startsWith("host:")) {
|
||||
val host = sv.substring(5)
|
||||
client.instance = host
|
||||
}
|
||||
|
||||
val instance = client.instance
|
||||
?: return TootApiResult("missing instance in callback url.")
|
||||
|
||||
this.host = instance
|
||||
val client_name = Pref.spClientName(this@ActMain)
|
||||
val result = client.authentication2(client_name, code)
|
||||
this.ta = TootParser(this@ActMain, object : LinkHelper {
|
||||
override val host : String?
|
||||
get() = instance
|
||||
})
|
||||
.account(result?.jsonObject)
|
||||
return result
|
||||
}
|
||||
|
||||
val instance = client.instance
|
||||
?: return TootApiResult("missing instance in callback url.")
|
||||
|
||||
this.host = instance
|
||||
val client_name = Pref.spClientName(this@ActMain)
|
||||
val result = client.authentication2(client_name, code)
|
||||
this.ta = TootParser(this@ActMain, object : LinkHelper {
|
||||
override val host : String?
|
||||
get() = instance
|
||||
})
|
||||
.account(result?.jsonObject)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun handleResult(result : TootApiResult?) {
|
||||
|
@ -1757,7 +1801,13 @@ class ActMain : AppCompatActivity()
|
|||
// アカウント追加時
|
||||
val user = ta.username + "@" + host
|
||||
|
||||
val row_id = SavedAccount.insert(host, user, jsonObject, token_info)
|
||||
val row_id = SavedAccount.insert(
|
||||
host,
|
||||
user,
|
||||
jsonObject,
|
||||
token_info,
|
||||
isMisskey = token_info.optBoolean(TootApiClient.KEY_IS_MISSKEY)
|
||||
)
|
||||
val account = SavedAccount.loadAccount(this@ActMain, row_id)
|
||||
if(account != null) {
|
||||
var bModified = false
|
||||
|
|
|
@ -102,14 +102,17 @@ class App1 : Application() {
|
|||
// 2018/5/16 v252 25=>26 SubscriptionServerKey テーブルを丸ごと変更
|
||||
// 2018/8/5 v264 26 => 27 SavedAccountテーブルに項目追加
|
||||
// 2018/8/17 v267 27 => 28 SavedAccountテーブルに項目追加
|
||||
internal const val DB_VERSION = 28
|
||||
// 2018/8/19 v267 28 => 29 ContentWarningMisskey, MediaShownMisskey テーブルを追加
|
||||
internal const val DB_VERSION = 29
|
||||
|
||||
private val tableList = arrayOf(
|
||||
LogData,
|
||||
SavedAccount,
|
||||
ClientInfo,
|
||||
MediaShown,
|
||||
MediaShownMisskey,
|
||||
ContentWarning,
|
||||
ContentWarningMisskey,
|
||||
NotificationTracking,
|
||||
MutedApp,
|
||||
UserRelation,
|
||||
|
|
|
@ -4590,6 +4590,8 @@ class Column(
|
|||
if(access_info.isMisskey) {
|
||||
if(parser!=null) parser.serviceType = ServiceType.MISSKEY
|
||||
params.put("limit", 100)
|
||||
val apiKey = access_info.token_info?.parseString(TootApiClient.KEY_API_KEY_MISSKEY)
|
||||
if( apiKey?.isNotEmpty() == true) params.put("i",apiKey)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
|
|
@ -14,4 +14,7 @@ object PrefDevice {
|
|||
internal const val KEY_DEVICE_TOKEN = "device_token"
|
||||
internal const val KEY_INSTALL_ID = "install_id"
|
||||
|
||||
const val LAST_AUTH_INSTANCE="lastAuthInstance"
|
||||
const val LAST_AUTH_SECRET="lastAuthSecret"
|
||||
const val LAST_AUTH_DB_ID ="lastAuthDbId"
|
||||
}
|
||||
|
|
|
@ -70,22 +70,25 @@ internal class StatusButtons(
|
|||
if(access_info.isNicoru(status.account)) R.attr.ic_nicoru else R.attr.btn_favourite
|
||||
|
||||
val replies_count = status.replies_count
|
||||
|
||||
setButton(
|
||||
btnReply,
|
||||
true,
|
||||
color_normal,
|
||||
R.attr.btn_reply,
|
||||
when{
|
||||
when {
|
||||
replies_count == null || status.visibility == TootStatus.VISIBILITY_DIRECT -> ""
|
||||
else->when(Pref.ipRepliesCount(activity.pref)){
|
||||
Pref.RC_SIMPLE -> when{
|
||||
replies_count >= 1 -> "1+"
|
||||
else -> replies_count.toString()
|
||||
else -> when(Pref.ipRepliesCount(activity.pref)) {
|
||||
Pref.RC_SIMPLE -> when {
|
||||
replies_count >= 2L -> "1+"
|
||||
replies_count == 1L -> "1"
|
||||
else -> ""
|
||||
}
|
||||
Pref.RC_ACTUAL -> replies_count.toString()
|
||||
else->""
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
},
|
||||
activity.getString(R.string.reply)
|
||||
)
|
||||
|
||||
// ブーストボタン
|
||||
|
@ -95,33 +98,27 @@ internal class StatusButtons(
|
|||
false,
|
||||
color_accent,
|
||||
R.attr.ic_mail,
|
||||
""
|
||||
"",
|
||||
activity.getString(R.string.boost)
|
||||
)
|
||||
// マストドン2.4.0から非公開トゥートもブーストできるようになった
|
||||
// TootStatus.VISIBILITY_PRIVATE == status.visibility -> setButton(
|
||||
// btnBoost,
|
||||
// false,
|
||||
// color_accent,
|
||||
// R.attr.ic_lock,
|
||||
// ""
|
||||
// )
|
||||
|
||||
activity.app_state.isBusyBoost(access_info, status) -> setButton(
|
||||
btnBoost,
|
||||
false,
|
||||
color_normal,
|
||||
R.attr.btn_refresh,
|
||||
"?"
|
||||
"?",
|
||||
activity.getString(R.string.boost)
|
||||
)
|
||||
|
||||
else -> {
|
||||
setButton(
|
||||
btnBoost,
|
||||
true,
|
||||
if(status.reblogged) color_accent else color_normal,
|
||||
R.attr.btn_boost,
|
||||
status.reblogs_count?.toString() ?: ""
|
||||
)
|
||||
}
|
||||
else -> setButton(
|
||||
btnBoost,
|
||||
true,
|
||||
if(status.reblogged) color_accent else color_normal,
|
||||
R.attr.btn_boost,
|
||||
status.reblogs_count?.toString() ?: "",
|
||||
activity.getString(R.string.boost)
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
|
@ -130,18 +127,18 @@ internal class StatusButtons(
|
|||
false,
|
||||
color_normal,
|
||||
R.attr.btn_refresh,
|
||||
"?"
|
||||
"?",
|
||||
activity.getString(R.string.favourite)
|
||||
)
|
||||
|
||||
else -> {
|
||||
setButton(
|
||||
btnFavourite,
|
||||
true,
|
||||
if(status.favourited) color_accent else color_normal,
|
||||
fav_icon_attr,
|
||||
status.favourites_count?.toString() ?: ""
|
||||
)
|
||||
}
|
||||
else -> setButton(
|
||||
btnFavourite,
|
||||
true,
|
||||
if(status.favourited) color_accent else color_normal,
|
||||
fav_icon_attr,
|
||||
status.favourites_count?.toString() ?: "",
|
||||
activity.getString(R.string.favourite)
|
||||
)
|
||||
}
|
||||
|
||||
val account = status.account
|
||||
|
@ -163,12 +160,14 @@ internal class StatusButtons(
|
|||
enabled : Boolean,
|
||||
color : Int,
|
||||
icon_attr : Int,
|
||||
text : String
|
||||
text : String,
|
||||
contentDescription : String
|
||||
) {
|
||||
val d = Styler.getAttributeDrawable(activity, icon_attr).mutate()
|
||||
d.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||
b.setCompoundDrawablesRelativeWithIntrinsicBounds(d, null, null, null)
|
||||
b.text = text
|
||||
b.contentDescription = contentDescription + text
|
||||
b.setTextColor(color)
|
||||
b.isEnabled = enabled
|
||||
}
|
||||
|
|
|
@ -157,36 +157,43 @@ object Styler {
|
|||
// follow button
|
||||
val color_attr : Int
|
||||
val icon_attr : Int
|
||||
val contentDescription : String
|
||||
|
||||
when {
|
||||
relation.blocking -> {
|
||||
icon_attr = R.attr.ic_block
|
||||
color_attr = R.attr.colorImageButton
|
||||
contentDescription = context.getString(R.string.follow)
|
||||
}
|
||||
|
||||
relation.muting -> {
|
||||
icon_attr = R.attr.ic_mute
|
||||
color_attr = R.attr.colorImageButton
|
||||
contentDescription = context.getString(R.string.follow)
|
||||
}
|
||||
|
||||
relation.getFollowing(who) -> {
|
||||
icon_attr = R.attr.ic_follow_cross
|
||||
color_attr = R.attr.colorImageButtonAccent
|
||||
contentDescription = context.getString(R.string.unfollow)
|
||||
}
|
||||
|
||||
relation.getRequested(who) -> {
|
||||
icon_attr = R.attr.ic_follow_wait
|
||||
color_attr = R.attr.colorRegexFilterError
|
||||
contentDescription = context.getString(R.string.unfollow)
|
||||
}
|
||||
|
||||
else -> {
|
||||
icon_attr = R.attr.ic_follow_plus
|
||||
color_attr = R.attr.colorImageButton
|
||||
contentDescription = context.getString(R.string.follow)
|
||||
}
|
||||
}
|
||||
|
||||
val color = Styler.getAttributeColor(context, color_attr)
|
||||
setIconCustomColor(context, ibFollow, color, icon_attr)
|
||||
ibFollow.contentDescription = contentDescription
|
||||
}
|
||||
|
||||
// 色を指定してRippleDrawableを生成する
|
||||
|
|
|
@ -39,8 +39,7 @@ object Action_Account {
|
|||
return if(bPseudoAccount || bInputAccessToken) {
|
||||
client.getInstanceInformation()
|
||||
} else {
|
||||
val client_name = Pref.spClientName(activity)
|
||||
client.authentication1(client_name)
|
||||
client.authentication1(Pref.spClientName(activity))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,6 @@ object Action_ListMember {
|
|||
|
||||
var result : TootApiResult?
|
||||
|
||||
// TODO: リスト追加時に 422 既に登録されてます みたいなエラーが出る
|
||||
if(bFollow) {
|
||||
val relation : TootRelationShip?
|
||||
if(access_info.isLocalUser(local_who)) {
|
||||
|
|
|
@ -2,17 +2,15 @@ package jp.juggler.subwaytooter.api
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import jp.juggler.subwaytooter.*
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.Pref
|
||||
import jp.juggler.subwaytooter.table.ClientInfo
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.entity.ServiceType
|
||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||
import jp.juggler.subwaytooter.put
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
import okhttp3.*
|
||||
import org.json.JSONArray
|
||||
|
@ -65,6 +63,19 @@ class TootApiClient(
|
|||
private const val AUTH_VERSION = 3
|
||||
private const val REDIRECT_URL = "subwaytooter://oauth/"
|
||||
|
||||
const val KEY_IS_MISSKEY = "isMisskey"
|
||||
const val KEY_API_KEY_MISSKEY = "apiKeyMisskey"
|
||||
|
||||
// APIからsecretを得られないバグがあるので定数を渡す
|
||||
const val appSecretError =
|
||||
"Currently Misskey does not allow client registration from API, please tell me notify instance name that you want login via Subway Tooter."
|
||||
val testAppSecretMap = mapOf(
|
||||
Pair("misskey.xyz", "NGiWNZFP37WiAee3SGcVe8eSiDyLbbWf")
|
||||
, Pair("misskey.jp", "GO45N7JgeEWtlNUS4xRcOFY56JMjUTZk")
|
||||
, Pair("msky.cafe", "lvU12i7CXAB5xiqkABwzyJRzdAqhf0k3")
|
||||
, Pair("misskey.m544.net", "SLcaqff0Puymh4Fl30JCc09i6uumwJ4t")
|
||||
)
|
||||
|
||||
private const val NO_INFORMATION = "(no information)"
|
||||
|
||||
private val reStartJsonArray = Pattern.compile("\\A\\s*\\[")
|
||||
|
@ -183,6 +194,19 @@ class TootApiClient(
|
|||
else -> "read+write+follow"
|
||||
}
|
||||
|
||||
fun getScopeArrayMisskey(@Suppress("UNUSED_PARAMETER") ti : TootInstance) =
|
||||
JSONArray().apply {
|
||||
put("account-read")
|
||||
put("account-write")
|
||||
put("note-write")
|
||||
put("reaction-write")
|
||||
put("following-write")
|
||||
put("drive-read")
|
||||
put("drive-write")
|
||||
put("notification-read")
|
||||
put("notification-write")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
|
@ -440,42 +464,331 @@ class TootApiClient(
|
|||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// misskey authentication
|
||||
|
||||
// 疑似アカウントの追加時に、インスタンスの検証を行う
|
||||
fun getInstanceInformation() : TootApiResult? {
|
||||
private fun getInstanceInformationMisskey() : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
if(sendRequest(result) {
|
||||
JSONObject().apply {
|
||||
put("dummy", 1)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("https://$instance/api/meta")
|
||||
.build()
|
||||
}) {
|
||||
parseJson(result) ?: return null
|
||||
result.jsonObject?.put(KEY_IS_MISSKEY, true)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// インスタンス情報を取得する
|
||||
private fun getInstanceInformation2Misskey() : TootApiResult? {
|
||||
val r = getInstanceInformationMisskey()
|
||||
if(r != null) {
|
||||
val json = r.jsonObject
|
||||
if(json != null) {
|
||||
val parser = TootParser(
|
||||
context,
|
||||
object : LinkHelper {
|
||||
override val host : String?
|
||||
get() = instance
|
||||
},
|
||||
serviceType = ServiceType.MISSKEY
|
||||
)
|
||||
val ti = parser.instance(json)
|
||||
if(ti != null) {
|
||||
r.data = ti
|
||||
} else {
|
||||
r.setError("can't parse data in /api/meta")
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
private fun getAppInfoMisskey(appId : String?) : TootApiResult? {
|
||||
appId ?: return TootApiResult("missing app id")
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
if(sendRequest(result) {
|
||||
JSONObject().apply {
|
||||
put("appId", appId)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("https://$instance/api/app/show")
|
||||
.build()
|
||||
}) {
|
||||
parseJson(result) ?: return null
|
||||
result.jsonObject?.put(KEY_IS_MISSKEY, true)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun prepareBrowserUrlMisskey(@Suppress("UNUSED_PARAMETER") clientInfo : JSONObject) : String? {
|
||||
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
|
||||
if(result.error != null) {
|
||||
showToast(context, false, result.error)
|
||||
return null
|
||||
}
|
||||
|
||||
val appSecret = testAppSecretMap[instance?.toLowerCase()]
|
||||
if(appSecret == null) {
|
||||
showToast(context, true, appSecretError)
|
||||
return null
|
||||
}
|
||||
|
||||
if(! sendRequest(result) {
|
||||
JSONObject().apply {
|
||||
put("appSecret", appSecret)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("https://$instance/api/auth/session/generate")
|
||||
.build()
|
||||
}
|
||||
) {
|
||||
val error = result.error
|
||||
if(error != null) {
|
||||
showToast(context, false, error)
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
parseJson(result) ?: return null
|
||||
|
||||
val jsonObject = result.jsonObject
|
||||
if(jsonObject == null) {
|
||||
showToast(context, false, result.error)
|
||||
return null
|
||||
}
|
||||
// {"token":"0ba88e2d-4b7d-4599-8d90-dc341a005637","url":"https://misskey.xyz/auth/0ba88e2d-4b7d-4599-8d90-dc341a005637"}
|
||||
|
||||
// ブラウザで開くURL
|
||||
val url = jsonObject.parseString("url")
|
||||
if(url?.isEmpty() != false) {
|
||||
showToast(context, false, "missing 'url' in auth session response.")
|
||||
return null
|
||||
}
|
||||
|
||||
val e = PrefDevice.prefDevice(context)
|
||||
.edit()
|
||||
.putString(PrefDevice.LAST_AUTH_INSTANCE, instance)
|
||||
.putString(PrefDevice.LAST_AUTH_SECRET, appSecret)
|
||||
|
||||
val account = this.account
|
||||
if(account != null) {
|
||||
e.putLong(PrefDevice.LAST_AUTH_DB_ID, account.db_id)
|
||||
} else {
|
||||
e.remove(PrefDevice.LAST_AUTH_DB_ID)
|
||||
}
|
||||
|
||||
e.apply()
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
private fun registerClientMisskey(
|
||||
scope_array : JSONArray,
|
||||
client_name : String
|
||||
) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
if(sendRequest(result) {
|
||||
JSONObject().apply {
|
||||
put("nameId", "SubwayTooter")
|
||||
put("name", client_name)
|
||||
put("description", "Android app for federated SNS")
|
||||
put("callbackUrl", "subwaytooter://misskey/auth_callback")
|
||||
put("permission", scope_array)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("https://$instance/api/app/create")
|
||||
.build()
|
||||
}) {
|
||||
parseJson(result) ?: return null
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun authentication1Misskey(clientNameArg : String, ti : TootInstance) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(this.instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
|
||||
// クライアントIDがアプリ上に保存されているか?
|
||||
val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
|
||||
val client_info = ClientInfo.load(instance, client_name)
|
||||
|
||||
// スコープ一覧を取得する
|
||||
val scope_array = getScopeArrayMisskey(ti)
|
||||
|
||||
if(client_info != null && client_info.optBoolean(KEY_IS_MISSKEY)) {
|
||||
val r2 = getAppInfoMisskey(client_info.parseString("id"))
|
||||
val tmpClientInfo = r2?.jsonObject
|
||||
// tmpClientInfo はsecretを含まないので保存してはいけない
|
||||
if(tmpClientInfo != null // アプリが登録済みで
|
||||
&& client_name == tmpClientInfo.parseString("name") // クライアント名が一致してて
|
||||
&& tmpClientInfo.optJSONArray("permission")?.length() == scope_array.length() // パーミッションが同じ
|
||||
) {
|
||||
// クライアント情報を再利用する
|
||||
result.data = prepareBrowserUrlMisskey(client_info)
|
||||
return result
|
||||
} else {
|
||||
// XXX appSecretを使ってクライアント情報を削除できるようにするべきだが、該当するAPIが存在しない
|
||||
}
|
||||
}
|
||||
|
||||
val r2 = registerClientMisskey(scope_array, client_name)
|
||||
val jsonObject = r2?.jsonObject ?: return r2
|
||||
|
||||
// {
|
||||
// "createdAt": "2018-08-19T00:43:10.105Z",
|
||||
// "userId": null,
|
||||
// "name": "Via芸",
|
||||
// "nameId": "test1",
|
||||
// "description": "test1",
|
||||
// "permission": [
|
||||
// "account-read",
|
||||
// "account-write",
|
||||
// "note-write",
|
||||
// "reaction-write",
|
||||
// "following-write",
|
||||
// "drive-read",
|
||||
// "drive-write",
|
||||
// "notification-read",
|
||||
// "notification-write"
|
||||
// ],
|
||||
// "callbackUrl": "test1://test1/auth_callback",
|
||||
// "id": "5b78bd1ea0db0527f25815c3",
|
||||
// "iconUrl": "https://misskey.xyz/files/app-default.jpg"
|
||||
// }
|
||||
|
||||
// 2018/8/19現在、/api/app/create のレスポンスにsecretが含まれないので認証に使えない
|
||||
// https://github.com/syuilo/misskey/issues/2343
|
||||
|
||||
jsonObject.put(KEY_IS_MISSKEY, true)
|
||||
jsonObject.put(KEY_AUTH_VERSION, AUTH_VERSION)
|
||||
ClientInfo.save(instance, client_name, jsonObject.toString())
|
||||
result.data = prepareBrowserUrlMisskey(jsonObject)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// oAuth2認証の続きを行う
|
||||
fun authentication2Misskey(clientNameArg : String, token : String) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val client_info = ClientInfo.load(instance, client_name)
|
||||
?: return result.setError("missing client id")
|
||||
|
||||
// XXX: client_info中にsecretがあればそれを使う
|
||||
val appSecret = testAppSecretMap[instance.toLowerCase()]
|
||||
?: return result.setError(appSecretError)
|
||||
|
||||
if(! sendRequest(result) {
|
||||
JSONObject().apply {
|
||||
put("appSecret", appSecret)
|
||||
put("token", token)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("https://$instance/api/auth/session/userkey")
|
||||
.build()
|
||||
}
|
||||
) {
|
||||
return result
|
||||
}
|
||||
|
||||
parseJson(result) ?: return null
|
||||
|
||||
val token_info = result.jsonObject ?: return result
|
||||
|
||||
// {"accessToken":"XSdxQcaCCS5VDRNigfDDj9xNDBpPlD8K","user":{…}}
|
||||
|
||||
val access_token = token_info.parseString("accessToken")
|
||||
if(access_token?.isEmpty() != false) {
|
||||
return result.setError("missing accessToken in the response.")
|
||||
}
|
||||
|
||||
val user = token_info.optJSONObject("user")
|
||||
?: result.setError("missing user in the response.")
|
||||
token_info.remove("user")
|
||||
|
||||
val apiKey = "$access_token$appSecret".encodeUTF8().digestSHA256().encodeHexLower()
|
||||
|
||||
// ユーザ情報を読めたならtokenInfoを保存する
|
||||
token_info.put(KEY_IS_MISSKEY, true)
|
||||
token_info.put(KEY_AUTH_VERSION, AUTH_VERSION)
|
||||
token_info.put(KEY_API_KEY_MISSKEY, apiKey)
|
||||
|
||||
// tokenInfoとユーザ情報の入ったresultを返す
|
||||
result.tokenInfo = token_info
|
||||
result.data = user
|
||||
return result
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
|
||||
// 疑似アカウントの追加時に、インスタンスの検証を行う
|
||||
private fun getInstanceInformationMastodon() : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
|
||||
if(sendRequest(result) {
|
||||
Request.Builder().url("https://$instance/api/v1/instance").build()
|
||||
}
|
||||
&& parseJson(result) != null
|
||||
&& result.jsonObject != null
|
||||
) {
|
||||
// インスタンス情報のjsonを読めたらマストドンのインスタンス
|
||||
return result
|
||||
}
|
||||
|
||||
// misskeyか試してみる
|
||||
val r2 = TootApiResult.makeWithCaption(instance)
|
||||
if(sendRequest(r2) {
|
||||
Request.Builder().post(RequestBody.create(MEDIA_TYPE_JSON, JSONObject().apply {
|
||||
put("dummy", 1)
|
||||
}.toString()))
|
||||
.url("https://$instance/api/notes/local-timeline").build()
|
||||
}
|
||||
) {
|
||||
if(parseJson(r2) != null && r2.jsonArray != null) {
|
||||
r2.data = JSONObject().apply {
|
||||
put("isMisskey", true)
|
||||
}
|
||||
return r2
|
||||
}
|
||||
parseJson(result) ?: return null
|
||||
}
|
||||
|
||||
// misskeyの事は忘れて本来のエラー情報を返す
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getInstanceInformation2Mastodon() : TootApiResult? {
|
||||
val r = getInstanceInformationMastodon()
|
||||
if(r != null) {
|
||||
val json = r.jsonObject
|
||||
if(json != null) {
|
||||
val parser = TootParser(context, object : LinkHelper {
|
||||
override val host : String?
|
||||
get() = instance
|
||||
})
|
||||
val ti = parser.instance(json)
|
||||
if(ti != null) {
|
||||
r.data = ti
|
||||
} else {
|
||||
r.setError("can't parse data in /api/v1/instance")
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// 疑似アカウントの追加時に、インスタンスの検証を行う
|
||||
fun getInstanceInformation() : TootApiResult? {
|
||||
// マストドンのインスタンス情報を読めたら、それはマストドンのインスタンス
|
||||
val r1 = getInstanceInformationMastodon() ?: return null // null means cancelled.
|
||||
if(r1.jsonObject != null) return r1
|
||||
|
||||
// misskeyのインスタンス情報を読めたら、それはmisskeyのインスタンス
|
||||
val r2 = getInstanceInformationMisskey() ?: return null // null means cancelled.
|
||||
if(r2.jsonObject != null) return r2
|
||||
|
||||
return r1 // 通信エラーの表示ならr1でもr2でも構わないはず
|
||||
}
|
||||
|
||||
// インスタンス情報を取得する
|
||||
internal fun getInstanceInformation2() : TootApiResult? {
|
||||
val r = getInstanceInformation()
|
||||
|
@ -498,7 +811,7 @@ class TootApiClient(
|
|||
}
|
||||
|
||||
// クライアントをタンスに登録
|
||||
internal fun registerClient(scope_string : String, clientName : String) : TootApiResult? {
|
||||
private fun registerClient(scope_string : String, clientName : String) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(this.instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
|
@ -577,8 +890,8 @@ class TootApiClient(
|
|||
return parseJson(result)
|
||||
}
|
||||
|
||||
// // client_credentialを無効にする
|
||||
internal fun revokeClientCredential(
|
||||
// client_credentialを無効にする
|
||||
private fun revokeClientCredential(
|
||||
client_info : JSONObject,
|
||||
client_credential : String
|
||||
) : TootApiResult? {
|
||||
|
@ -619,21 +932,19 @@ class TootApiClient(
|
|||
+ "&redirect_uri=" + REDIRECT_URL.encodePercent()
|
||||
+ "&scope=$scope_string"
|
||||
+ "&scopes=$scope_string"
|
||||
+ "&state=" + (if(account != null) "db:" + account.db_id else "host:" + instance)
|
||||
+ "&state=" + (if(account != null) "db:${account.db_id}" else "host:$instance")
|
||||
+ "&grant_type=authorization_code"
|
||||
+ "&approval_prompt=force"
|
||||
// +"&access_type=offline"
|
||||
)
|
||||
}
|
||||
|
||||
// クライアントを登録してブラウザで開くURLを生成する
|
||||
fun authentication1(clientNameArg : String) : TootApiResult? {
|
||||
|
||||
// インスタンス情報の取得
|
||||
val ri = getInstanceInformation2()
|
||||
val ti = ri?.data as? TootInstance ?: return ri
|
||||
val scope_string = getScopeString(ti)
|
||||
private fun authentication1Mastodon(
|
||||
clientNameArg : String,
|
||||
ti : TootInstance
|
||||
) : TootApiResult? {
|
||||
|
||||
// 前準備
|
||||
val result = TootApiResult.makeWithCaption(this.instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
|
@ -641,7 +952,12 @@ class TootApiClient(
|
|||
// クライアントIDがアプリ上に保存されているか?
|
||||
val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
|
||||
val client_info = ClientInfo.load(instance, client_name)
|
||||
if(client_info != null) {
|
||||
|
||||
// スコープ一覧を取得する
|
||||
|
||||
val scope_string = getScopeString(ti)
|
||||
|
||||
if(client_info != null && ! client_info.optBoolean(KEY_IS_MISSKEY)) {
|
||||
|
||||
var client_credential = client_info.parseString(KEY_CLIENT_CREDENTIAL)
|
||||
val old_scope = client_info.parseString(KEY_CLIENT_SCOPE)
|
||||
|
@ -672,7 +988,7 @@ class TootApiClient(
|
|||
// client credential をタンスから消去する
|
||||
revokeClientCredential(client_info, client_credential)
|
||||
|
||||
// FIXME クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
|
||||
// XXX クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
|
||||
} else {
|
||||
// クライアント情報を再利用する
|
||||
result.data = prepareBrowserUrl(scope_string, client_info)
|
||||
|
@ -694,6 +1010,26 @@ class TootApiClient(
|
|||
return result
|
||||
}
|
||||
|
||||
// クライアントを登録してブラウザで開くURLを生成する
|
||||
fun authentication1(clientNameArg : String) : TootApiResult? {
|
||||
|
||||
// マストドンのインスタンス情報
|
||||
var ri = getInstanceInformation2Mastodon()
|
||||
var ti = ri?.data as? TootInstance
|
||||
if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) {
|
||||
return authentication1Mastodon(clientNameArg, ti)
|
||||
}
|
||||
|
||||
// misskeyのインスタンス情報
|
||||
ri = getInstanceInformation2Misskey()
|
||||
ti = ri?.data as? TootInstance
|
||||
if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) {
|
||||
return authentication1Misskey(clientNameArg, ti)
|
||||
}
|
||||
|
||||
return ri
|
||||
}
|
||||
|
||||
// oAuth2認証の続きを行う
|
||||
fun authentication2(clientNameArg : String, code : String) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
|
@ -819,7 +1155,7 @@ class TootApiClient(
|
|||
.append("?apikey=").append(mspApiKey.encodePercent())
|
||||
.append("&utoken=").append(user_token.encodePercent())
|
||||
.append("&q=").append(query.encodePercent())
|
||||
.append("&max=").append(max_id?.encodePercent() ?:"")
|
||||
.append("&max=").append(max_id?.encodePercent() ?: "")
|
||||
|
||||
Request.Builder().url(url.toString()).build()
|
||||
}) return result
|
||||
|
@ -882,14 +1218,14 @@ class TootApiClient(
|
|||
return result
|
||||
}
|
||||
|
||||
fun requestJson(req : Request) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(req.url().host())
|
||||
if(result.error != null) return result
|
||||
if(sendRequest(result, progressPath = null) { req }) {
|
||||
parseJson(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
// fun requestJson(req : Request) : TootApiResult? {
|
||||
// val result = TootApiResult.makeWithCaption(req.url().host())
|
||||
// if(result.error != null) return result
|
||||
// if(sendRequest(result, progressPath = null) { req }) {
|
||||
// parseJson(result)
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
|
||||
// 疑似アカウントでステータスURLからステータスIDを取得するためにHTMLを取得する
|
||||
fun getHttp(url : String) : TootApiResult? {
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import org.json.JSONObject
|
||||
|
||||
abstract class EntityId : Comparable<EntityId> {
|
||||
|
||||
abstract fun toLong() : Long
|
||||
abstract fun putMisskeyUntil(dst : JSONObject) : JSONObject
|
||||
abstract fun putMisskeySince(dst : JSONObject) : JSONObject
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(x : Long) = EntityIdLong(x)
|
||||
|
||||
fun mayNull(x : Long?) = when(x) {
|
||||
null -> null
|
||||
else -> EntityIdLong(x)
|
||||
}
|
||||
|
||||
fun from(x : String) = EntityIdString(x)
|
||||
|
||||
fun mayNull(x : String?) = when(x) {
|
||||
null -> null
|
||||
else -> EntityIdString(x)
|
||||
}
|
||||
|
||||
fun String.decode():EntityId?{
|
||||
if(this.isEmpty()) return null
|
||||
if(this[0]=='L') return from(this.substring(1).toLong())
|
||||
if(this[0]=='S') return from(this.substring(1))
|
||||
return null
|
||||
}
|
||||
|
||||
fun from(intent: Intent, key:String)=
|
||||
intent.getStringExtra(key)?.decode()
|
||||
|
||||
fun from(bundle: Bundle, key:String)=
|
||||
bundle.getString(key)?.decode()
|
||||
|
||||
fun from(data : JSONObject, key : String): EntityId?{
|
||||
val o = data.opt(key)
|
||||
if(o is Long) return EntityIdLong(o)
|
||||
return (o as? String)?.decode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun encode():String{
|
||||
val prefix = when(this){
|
||||
is EntityIdLong ->'L'
|
||||
is EntityIdString->'S'
|
||||
else -> error("unknown type")
|
||||
}
|
||||
return "$prefix$this"
|
||||
}
|
||||
|
||||
fun putTo(data : Intent, key : String) :Intent = data.putExtra( key,encode())
|
||||
|
||||
fun putTo(bundle:Bundle, key : String) = bundle.putString( key,encode())
|
||||
|
||||
fun putTo(data:JSONObject, key : String):JSONObject = data.put( key,encode())
|
||||
|
||||
}
|
||||
|
||||
class EntityIdLong(val x : Long) : EntityId() {
|
||||
|
||||
override fun compareTo(other : EntityId) = when(other) {
|
||||
this -> 0
|
||||
is EntityIdLong -> x.compareTo(other.x)
|
||||
else -> error("EntityIdLong: compare with ${other::javaClass.name}")
|
||||
}
|
||||
|
||||
override fun equals(other : Any?) =when(other) {
|
||||
is EntityIdLong -> x == other.x
|
||||
is EntityIdString -> x.toString() == other.x
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun hashCode() = (x xor x.ushr(32)).toInt()
|
||||
|
||||
override fun toString() = x.toString()
|
||||
|
||||
override fun toLong() = x
|
||||
|
||||
override fun putMisskeyUntil(dst : JSONObject) : JSONObject = dst.put("untilDate", x)
|
||||
override fun putMisskeySince(dst : JSONObject) : JSONObject = dst.put("sinceDate", x)
|
||||
|
||||
}
|
||||
|
||||
class EntityIdString(val x : String) : EntityId() {
|
||||
|
||||
override fun compareTo(other : EntityId) = when(other) {
|
||||
is EntityIdString -> x.compareTo(other.x)
|
||||
else -> error("EntityIdLong: compare with ${other::javaClass.name}")
|
||||
}
|
||||
|
||||
override fun equals(other : Any?) =when(other) {
|
||||
is EntityIdString -> x == other.x
|
||||
is EntityIdLong -> x == other.x.toString()
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun hashCode() = x.hashCode()
|
||||
|
||||
override fun toString() = x
|
||||
|
||||
override fun toLong() = TootStatus.INVALID_ID // error("can't convert string ID to long")
|
||||
|
||||
override fun putMisskeyUntil(dst : JSONObject) : JSONObject = dst.put("untilId", x)
|
||||
override fun putMisskeySince(dst : JSONObject) : JSONObject = dst.put("sinceId", x)
|
||||
|
||||
}
|
|
@ -31,7 +31,8 @@ class TootInstance(parser : TootParser, src : JSONObject) {
|
|||
// A description for the instance
|
||||
val description : String?
|
||||
|
||||
// An email address which can be used to contact the instance administrator
|
||||
// An email address which can be used to contact the instance administrator
|
||||
// misskeyの場合はURLらしい
|
||||
val email : String?
|
||||
|
||||
val version : String?
|
||||
|
@ -57,40 +58,59 @@ class TootInstance(parser : TootParser, src : JSONObject) {
|
|||
enum class InstanceType {
|
||||
|
||||
Mastodon,
|
||||
Pleroma
|
||||
Pleroma,
|
||||
Misskey
|
||||
}
|
||||
|
||||
private val instanceType : InstanceType
|
||||
val instanceType : InstanceType
|
||||
|
||||
// XXX: urls をパースしてない。使ってないから…
|
||||
|
||||
init {
|
||||
this.uri = src.parseString("uri")
|
||||
this.title = src.parseString("title")
|
||||
this.description = src.parseString("description")
|
||||
this.email = src.parseString("email")
|
||||
this.version = src.parseString("version")
|
||||
this.decoded_version = VersionString(version)
|
||||
this.stats = parseItem(::Stats, src.optJSONObject("stats"))
|
||||
this.thumbnail = src.parseString("thumbnail")
|
||||
|
||||
this.max_toot_chars = src.parseInt("max_toot_chars")
|
||||
|
||||
this.instanceType = when {
|
||||
rePleroma.matcher(version ?: "").find() -> InstanceType.Pleroma
|
||||
else -> InstanceType.Mastodon
|
||||
}
|
||||
|
||||
languages = src.optJSONArray("languages")?.toStringArrayList()
|
||||
|
||||
val parser2 = TootParser(
|
||||
parser.context,
|
||||
object : LinkHelper {
|
||||
override val host : String
|
||||
get() = uri ?: "?"
|
||||
if(parser.serviceType == ServiceType.MISSKEY){
|
||||
|
||||
this.uri = parser.linkHelper.host
|
||||
this.title = parser.linkHelper.host
|
||||
this.description = "(Misskey instance)"
|
||||
this.email = src.optJSONObject("maintainer")?.parseString("url")
|
||||
this.version = src.parseString("version")
|
||||
this.decoded_version = VersionString(version)
|
||||
this.stats = null
|
||||
this.thumbnail = null
|
||||
this.max_toot_chars = 1000
|
||||
this.instanceType = InstanceType.Misskey
|
||||
this.languages = ArrayList<String>().also{ it.add("?")}
|
||||
this.contact_account = null
|
||||
|
||||
}else {
|
||||
this.uri = src.parseString("uri")
|
||||
this.title = src.parseString("title")
|
||||
this.description = src.parseString("description")
|
||||
this.email = src.parseString("email")
|
||||
this.version = src.parseString("version")
|
||||
this.decoded_version = VersionString(version)
|
||||
this.stats = parseItem(::Stats, src.optJSONObject("stats"))
|
||||
this.thumbnail = src.parseString("thumbnail")
|
||||
|
||||
this.max_toot_chars = src.parseInt("max_toot_chars")
|
||||
|
||||
this.instanceType = when {
|
||||
rePleroma.matcher(version ?: "").find() -> InstanceType.Pleroma
|
||||
else -> InstanceType.Mastodon
|
||||
}
|
||||
)
|
||||
contact_account = parseItem(::TootAccount, parser2, src.optJSONObject("contact_account"))
|
||||
|
||||
languages = src.optJSONArray("languages")?.toStringArrayList()
|
||||
|
||||
val parser2 = TootParser(
|
||||
parser.context,
|
||||
object : LinkHelper {
|
||||
override val host : String
|
||||
get() = uri ?: "?"
|
||||
}
|
||||
)
|
||||
contact_account =
|
||||
parseItem(::TootAccount, parser2, src.optJSONObject("contact_account"))
|
||||
}
|
||||
}
|
||||
|
||||
class Stats(src : JSONObject) {
|
||||
|
|
|
@ -100,7 +100,7 @@ object LoginForm {
|
|||
activity, R.layout.lv_spinner_dropdown, ArrayList()
|
||||
) {
|
||||
|
||||
internal val nameFilter : Filter = object : Filter() {
|
||||
val nameFilter : Filter = object : Filter() {
|
||||
override fun convertResultToString(value : Any) : CharSequence {
|
||||
return value as String
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.ContentValues
|
|||
import android.database.sqlite.SQLiteDatabase
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.api.entity.EntityIdString
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
|
||||
|
@ -42,6 +43,9 @@ object ContentWarning :TableCompanion{
|
|||
}
|
||||
|
||||
fun isShown(status : TootStatus, default_value : Boolean) : Boolean {
|
||||
|
||||
if( status.idAccessOrOriginal is EntityIdString) return ContentWarningMisskey.isShown(status,default_value)
|
||||
|
||||
try {
|
||||
App1.database.query(
|
||||
table,
|
||||
|
@ -69,6 +73,10 @@ object ContentWarning :TableCompanion{
|
|||
}
|
||||
|
||||
fun save(status : TootStatus, is_shown : Boolean) {
|
||||
|
||||
if( status.idAccessOrOriginal is EntityIdString) return ContentWarningMisskey.save(status,is_shown)
|
||||
|
||||
|
||||
try {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package jp.juggler.subwaytooter.table
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
|
||||
object ContentWarningMisskey :TableCompanion{
|
||||
private val log = LogCategory("ContentWarning")
|
||||
|
||||
private const val table = "content_warning"
|
||||
private const val COL_HOST = "h"
|
||||
private const val COL_STATUS_ID = "si"
|
||||
private const val COL_SHOWN = "sh"
|
||||
private const val COL_TIME_SAVE = "time_save"
|
||||
|
||||
private val projection_shown = arrayOf(COL_SHOWN)
|
||||
|
||||
override fun onDBCreate(db : SQLiteDatabase) {
|
||||
log.d("onDBCreate!")
|
||||
db.execSQL(
|
||||
"create table if not exists $table"
|
||||
+ "(_id INTEGER PRIMARY KEY"
|
||||
+ ",$COL_HOST text not null"
|
||||
+ ",$COL_STATUS_ID text not null"
|
||||
+ ",$COL_SHOWN integer not null"
|
||||
+ ",$COL_TIME_SAVE integer default 0"
|
||||
+ ")"
|
||||
)
|
||||
db.execSQL(
|
||||
"create unique index if not exists ${table}_status_id on $table($COL_HOST,$COL_STATUS_ID)"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDBUpgrade(db : SQLiteDatabase, oldVersion : Int, newVersion : Int) {
|
||||
if(oldVersion < 29 && newVersion >= 29 ) {
|
||||
db.execSQL("drop table if exists $table")
|
||||
onDBCreate(db)
|
||||
}
|
||||
}
|
||||
|
||||
fun isShown(status : TootStatus, default_value : Boolean) : Boolean {
|
||||
try {
|
||||
App1.database.query(
|
||||
table,
|
||||
projection_shown,
|
||||
"h=? and si=?",
|
||||
arrayOf(
|
||||
status.hostAccessOrOriginal,
|
||||
status.idAccessOrOriginal.toString()
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use { cursor ->
|
||||
if(cursor.moveToFirst()) {
|
||||
val iv = cursor.getInt(cursor.getColumnIndex(COL_SHOWN))
|
||||
return 0 != iv
|
||||
}
|
||||
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "load failed.")
|
||||
}
|
||||
|
||||
return default_value
|
||||
}
|
||||
|
||||
fun save(status : TootStatus, is_shown : Boolean) {
|
||||
try {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_HOST, status.hostAccessOrOriginal)
|
||||
cv.put(COL_STATUS_ID, status.idAccessOrOriginal.toString())
|
||||
cv.put(COL_SHOWN, is_shown.b2i())
|
||||
cv.put(COL_TIME_SAVE, now)
|
||||
App1.database.replace(table, null, cv)
|
||||
|
||||
// 古いデータを掃除する
|
||||
val expire = now - 86400000L * 365
|
||||
App1.database.delete(table, "$COL_TIME_SAVE<?", arrayOf(expire.toString()))
|
||||
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "save failed.")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import android.content.ContentValues
|
|||
import android.database.sqlite.SQLiteDatabase
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.api.entity.EntityIdString
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
|
||||
|
@ -42,6 +43,7 @@ object MediaShown:TableCompanion {
|
|||
}
|
||||
|
||||
fun isShown(status : TootStatus, default_value : Boolean) : Boolean {
|
||||
if( status.idAccessOrOriginal is EntityIdString) return MediaShownMisskey.isShown(status,default_value)
|
||||
try {
|
||||
App1.database.query(
|
||||
table,
|
||||
|
@ -68,6 +70,7 @@ object MediaShown:TableCompanion {
|
|||
}
|
||||
|
||||
fun save(status : TootStatus, is_shown : Boolean) {
|
||||
if( status.idAccessOrOriginal is EntityIdString) return MediaShownMisskey.save(status,is_shown)
|
||||
try {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package jp.juggler.subwaytooter.table
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
|
||||
object MediaShownMisskey:TableCompanion {
|
||||
private val log = LogCategory("MediaShown")
|
||||
|
||||
private const val table = "media_shown"
|
||||
private const val COL_HOST = "h"
|
||||
private const val COL_STATUS_ID = "si"
|
||||
private const val COL_SHOWN = "sh"
|
||||
private const val COL_TIME_SAVE = "time_save"
|
||||
|
||||
private val projection_shown = arrayOf(COL_SHOWN)
|
||||
|
||||
override fun onDBCreate(db : SQLiteDatabase) {
|
||||
log.d("onDBCreate!")
|
||||
db.execSQL(
|
||||
"create table if not exists $table"
|
||||
+ "(_id INTEGER PRIMARY KEY"
|
||||
+ ",$COL_HOST text not null"
|
||||
+ ",$COL_STATUS_ID text not null"
|
||||
+ ",$COL_SHOWN integer not null"
|
||||
+ ",$COL_TIME_SAVE integer default 0"
|
||||
+ ")"
|
||||
)
|
||||
db.execSQL(
|
||||
"create unique index if not exists ${table}_status_id on $table($COL_HOST,$COL_STATUS_ID)"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDBUpgrade(db : SQLiteDatabase, oldVersion : Int, newVersion : Int) {
|
||||
if(oldVersion < 29 && newVersion >= 29) {
|
||||
db.execSQL("drop table if exists $table")
|
||||
onDBCreate(db)
|
||||
}
|
||||
}
|
||||
|
||||
fun isShown(status : TootStatus, default_value : Boolean) : Boolean {
|
||||
try {
|
||||
App1.database.query(
|
||||
table,
|
||||
projection_shown,
|
||||
"h=? and si=?",
|
||||
arrayOf(
|
||||
status.hostAccessOrOriginal,
|
||||
status.idAccessOrOriginal.toString()
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use { cursor ->
|
||||
if(cursor.moveToFirst()) {
|
||||
return 0 != cursor.getInt(cursor.getColumnIndex(COL_SHOWN))
|
||||
}
|
||||
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "load failed.")
|
||||
}
|
||||
|
||||
return default_value
|
||||
}
|
||||
|
||||
fun save(status : TootStatus, is_shown : Boolean) {
|
||||
try {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_HOST, status.hostAccessOrOriginal)
|
||||
cv.put(COL_STATUS_ID, status.idAccessOrOriginal.toString())
|
||||
cv.put(COL_SHOWN, is_shown.b2i())
|
||||
cv.put(COL_TIME_SAVE, now)
|
||||
App1.database.replace(table, null, cv)
|
||||
|
||||
// 古いデータを掃除する
|
||||
val expire = now - 86400000L * 365
|
||||
App1.database.delete(table, "$COL_TIME_SAVE<?", arrayOf(expire.toString()))
|
||||
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "save failed.")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -4,12 +4,10 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
|
@ -32,7 +30,6 @@ import java.io.File
|
|||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.lang.reflect.Field
|
||||
import java.security.MessageDigest
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
|
@ -48,7 +45,7 @@ object Utils {
|
|||
|
||||
val log = LogCategory("Utils")
|
||||
|
||||
val hex =
|
||||
val hexLower =
|
||||
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
|
||||
|
||||
/////////////////////////////////////////////
|
||||
|
@ -430,11 +427,22 @@ fun String.encodeUTF8() = this.toByteArray(charsetUTF8)
|
|||
fun ByteArray.decodeUTF8() = this.toString(charsetUTF8)
|
||||
|
||||
fun StringBuilder.appendHex2(value : Int) : StringBuilder {
|
||||
this.append(Utils.hex[(value shr 4) and 15])
|
||||
this.append(Utils.hex[value and 15])
|
||||
this.append(Utils.hexLower[(value shr 4) and 15])
|
||||
this.append(Utils.hexLower[value and 15])
|
||||
return this
|
||||
}
|
||||
|
||||
fun ByteArray.encodeHexLower() : String {
|
||||
val size = this.size
|
||||
val sb = StringBuilder(size *2)
|
||||
for( i in 0 until size){
|
||||
val value = this[i].toInt()
|
||||
sb.append(Utils.hexLower[(value shr 4) and 15])
|
||||
sb.append(Utils.hexLower[value and 15])
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun String?.optInt() : Int? {
|
||||
return try {
|
||||
this?.toInt(10)
|
||||
|
|
Loading…
Reference in New Issue