diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt index ae52e247..02b5602e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt @@ -27,6 +27,7 @@ import android.widget.CompoundButton import android.widget.EditText import android.widget.Switch import android.widget.TextView +import jp.juggler.subwaytooter.api.* import java.io.File import java.io.FileInputStream @@ -34,12 +35,8 @@ import java.io.FileOutputStream import java.io.IOException import java.io.InputStream -import jp.juggler.subwaytooter.api.TootApiClient -import jp.juggler.subwaytooter.api.TootApiResult -import jp.juggler.subwaytooter.api.TootParser -import jp.juggler.subwaytooter.api.TootTask -import jp.juggler.subwaytooter.api.TootTaskRunner import jp.juggler.subwaytooter.api.entity.TootAccount +import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.ProgressDialogEx @@ -52,6 +49,7 @@ import okhttp3.MultipartBody import okhttp3.Request import okhttp3.RequestBody import okio.BufferedSink +import org.json.JSONObject class ActAccountSetting : AppCompatActivity(), View.OnClickListener, CompoundButton.OnCheckedChangeListener { @@ -103,6 +101,7 @@ class ActAccountSetting private lateinit var swNSFWOpen : Switch private lateinit var swDontShowTimeout : Switch private lateinit var btnOpenBrowser : Button + private lateinit var btnPushTest : Button private lateinit var cbNotificationMention : CheckBox private lateinit var cbNotificationBoost : CheckBox private lateinit var cbNotificationFavourite : CheckBox @@ -274,6 +273,7 @@ class ActAccountSetting swNSFWOpen = findViewById(R.id.swNSFWOpen) swDontShowTimeout = findViewById(R.id.swDontShowTimeout) btnOpenBrowser = findViewById(R.id.btnOpenBrowser) + btnPushTest= findViewById(R.id.btnPushTest) cbNotificationMention = findViewById(R.id.cbNotificationMention) cbNotificationBoost = findViewById(R.id.cbNotificationBoost) cbNotificationFavourite = findViewById(R.id.cbNotificationFavourite) @@ -318,6 +318,7 @@ class ActAccountSetting btnFields = findViewById(R.id.btnFields) btnOpenBrowser.setOnClickListener(this) + btnPushTest.setOnClickListener(this) btnAccessToken.setOnClickListener(this) btnInputAccessToken.setOnClickListener(this) btnAccountRemove.setOnClickListener(this) @@ -488,7 +489,8 @@ class ActAccountSetting R.id.btnAccountRemove -> performAccountRemove() R.id.btnVisibility -> performVisibility() R.id.btnOpenBrowser -> open_browser("https://" + account.host + "/") - + R.id.btnPushTest-> startTest() + R.id.btnUserCustom -> ActNickname.open( this, full_acct, @@ -895,11 +897,11 @@ class ActAccountSetting } } - internal fun updateCredential(key : String, value : Any) { + private fun updateCredential(key : String, value : Any) { updateCredential(listOf(Pair(key, value))) } - internal fun updateCredential(args : List>) { + private fun updateCredential(args : List>) { TootTaskRunner(this).run(account, object : TootTask { @@ -1324,5 +1326,75 @@ class ActAccountSetting task.executeOnExecutor(App1.task_executor) } + @SuppressLint("StaticFieldLeak") + private fun startTest() { + TootTaskRunner(this).run(account,object:TootTask{ + val sb = StringBuilder() + + private fun addLog(s:String) { + if(sb.isNotEmpty()) sb.append('\n') + sb.append(s) + } + + override fun background(client : TootApiClient) : TootApiResult? { // TODO + // インスタンスバージョンの確認 + var r = client.getInstanceInformation2() + val ti = r?.data as? TootInstance ?: return r + if(!ti.isEnoughVersion(TootInstance.VERSION_2_4)){ + addLog("Too old instance version ${ti.version} that does not support Push API.") + return r + } + + // プッシュ通知の登録 + var json :JSONObject? = JSONObject().also{ + it.put("subscription",JSONObject().also { + it.put("endpoint","${PollingWorker.APP_SERVER}/webpushcallback") + it.put("keys",JSONObject().also { + it.put( + "p256dh", + "BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=" + ) + it.put("auth", "eH_C8rq2raXqlcBVDa1gLg==") + }) + }) + it.put("data","<>") + } + var req = Request.Builder().post( + RequestBody.create(TootApiClient.MEDIA_TYPE_JSON,json.toString()) + ) + r = client.request("/api/v1/push/subscription",req) + var response = r?.response + if( response != null ){ + when(response.code()){ + 404 ->{ + addLog("this instance has no API endpoint 'POST /api/v1/push/subscription'. instance version is ${ti.version}") + return r + } + 403 ->{ + addLog("Your access token does not contains push scope. updating access token is recommended.") + return r + } + } + + addLog( "${response.request()}" ) + addLog("${response.code()} ${response.message()}") + json = r?.jsonObject + if(json != null) { + addLog(json.toString()) + } + } + return r + } + override fun handleResult(result : TootApiResult?) { + val e = result?.error + if(e != null) addLog(e) + AlertDialog.Builder(this@ActAccountSetting) + .setMessage(sb) + .setPositiveButton(R.string.close,null) + .show() + } + }) + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt index 4548a375..fb5fdd19 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt @@ -1609,7 +1609,10 @@ class ActMain : AppCompatActivity() this.host = instance val client_name = Pref.spClientName(this@ActMain) val result = client.authentication2(client_name, code) - this.ta = TootParser(this@ActMain, object : LinkHelper {}) + this.ta = TootParser(this@ActMain, object : LinkHelper { + override val host : String? + get() = instance + }) .account(result?.jsonObject) return result } @@ -1752,8 +1755,10 @@ class ActMain : AppCompatActivity() override fun background(client : TootApiClient) : TootApiResult? { val result = client.getUserCredential(access_token) - this.ta = - TootParser(this@ActMain, object : LinkHelper {}).account(result?.jsonObject) + this.ta = TootParser(this@ActMain, object : LinkHelper { + override val host : String? + get() = host + }).account(result?.jsonObject) return result } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index fd0a2b43..142ca423 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -2152,7 +2152,10 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba val viewRoot = layoutInflater.inflate(R.layout.dlg_plugin_missing, null, false) val tvText = viewRoot.findViewById(R.id.tvText) - val lcc = object : LinkHelper {} + val lcc = object : LinkHelper { + override val host : String? + get() = null + } val sv = DecodeOptions(this@ActPost, lcc).decodeHTML(text) tvText.text = sv tvText.movementMethod = LinkMovementMethod.getInstance() diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt index d8162f84..56d07fc6 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -11,6 +11,7 @@ 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.TootInstance import jp.juggler.subwaytooter.put import jp.juggler.subwaytooter.util.* import okhttp3.* @@ -58,9 +59,10 @@ class TootApiClient( private const val DEFAULT_CLIENT_NAME = "SubwayTooter" internal const val KEY_CLIENT_CREDENTIAL = "SubwayTooterClientCredential" + internal const val KEY_CLIENT_SCOPE = "SubwayTooterClientScope" private const val KEY_AUTH_VERSION = "SubwayTooterAuthVersion" - private const val AUTH_VERSION = 1 + private const val AUTH_VERSION = 3 private const val REDIRECT_URL = "subwaytooter://oauth/" private const val NO_INFORMATION = "(no information)" @@ -177,6 +179,11 @@ class TootApiClient( return sb.toString().replace("\n+".toRegex(), "\n") } + fun getScopeString(ti : TootInstance) = when { + ti.isEnoughVersion(TootInstance.VERSION_2_4) -> "read+write+follow+push" + else -> "read+write+follow" + } + } @Suppress("unused") @@ -278,7 +285,7 @@ class TootApiClient( // レスポンスがエラーかボディがカラならエラー状態を設定する // 例外を出すかも - internal fun readBodyBytes( + private fun readBodyBytes( result : TootApiResult, progressPath : String? = null, jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER @@ -321,7 +328,7 @@ class TootApiClient( } } - internal fun parseBytes( + private fun parseBytes( result : TootApiResult, progressPath : String? = null, jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER @@ -416,11 +423,11 @@ class TootApiClient( log.d("request: $path") - request_builder.url("https://" + instance + path) + request_builder.url("https://$instance$path") val access_token = account.getAccessToken() if(access_token?.isNotEmpty() == true) { - request_builder.header("Authorization", "Bearer " + access_token) + request_builder.header("Authorization", "Bearer $access_token") } request_builder.build() @@ -444,12 +451,32 @@ class TootApiClient( return parseJson(result) } + // インスタンス情報を取得する + internal fun getInstanceInformation2() : TootApiResult? { + val r = getInstanceInformation() + 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 + } + // クライアントをタンスに登録 - internal fun registerClient(clientName : String) : TootApiResult? { + internal 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 - // OAuth2 クライアント登録 if(! sendRequest(result) { Request.Builder() @@ -459,7 +486,7 @@ class TootApiClient( MEDIA_TYPE_FORM_URL_ENCODED, "client_name=" + clientName.encodePercent() + "&redirect_uris=" + REDIRECT_URL.encodePercent() - + "&scopes=read write follow" + + "&scopes=$scope_string" ) ) .build() @@ -525,8 +552,39 @@ class TootApiClient( return parseJson(result) } + // // client_credentialを無効にする + internal fun revokeClientCredential( + client_info : JSONObject, + client_credential : String + ) : TootApiResult? { + val result = TootApiResult.makeWithCaption(this.instance) + if(result.error != null) return result + + val client_id = client_info.parseString("client_id") + ?: return result.setError("missing client_id") + + val client_secret = client_info.parseString("client_secret") + ?: return result.setError("missing client_secret") + + if(! sendRequest(result) { + Request.Builder() + .url("https://$instance/oauth/revoke") + .post( + RequestBody.create( + TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED, + "token=" + client_credential.encodePercent() + + "&client_id=" + client_id.encodePercent() + + "&client_secret=" + client_secret.encodePercent() + ) + ) + .build() + }) return result + + return parseJson(result) + } + // 認証ページURLを作る - internal fun prepareBrowserUrl(client_info : JSONObject) : String? { + internal fun prepareBrowserUrl(scope_string : String, client_info : JSONObject) : String? { val account = this.account val client_id = client_info.parseString("client_id") ?: return null @@ -534,8 +592,8 @@ class TootApiClient( + "?client_id=" + client_id.encodePercent() + "&response_type=code" + "&redirect_uri=" + REDIRECT_URL.encodePercent() - + "&scope=read+write+follow" - + "&scopes=read+write+follow" + + "&scope=$scope_string" + + "&scopes=$scope_string" + "&state=" + (if(account != null) "db:" + account.db_id else "host:" + instance) + "&grant_type=authorization_code" + "&approval_prompt=force" @@ -545,6 +603,12 @@ class TootApiClient( // クライアントを登録してブラウザで開くURLを生成する fun authentication1(clientNameArg : String) : TootApiResult? { + + // インスタンス情報の取得 + val ri = getInstanceInformation2() + val ti = ri?.data as? TootInstance ?: return ri + val scope_string = getScopeString(ti) + val result = TootApiResult.makeWithCaption(this.instance) if(result.error != null) return result val instance = result.caption // same to instance @@ -555,6 +619,7 @@ class TootApiClient( if(client_info != null) { var client_credential = client_info.parseString(KEY_CLIENT_CREDENTIAL) + val old_scope = client_info.parseString(KEY_CLIENT_SCOPE) // client_credential をまだ取得していないなら取得する if(client_credential?.isEmpty() != false) { @@ -573,19 +638,33 @@ class TootApiClient( if(client_credential?.isNotEmpty() == true) { val resultSub = verifyClientCredential(client_credential) if(resultSub?.jsonObject != null) { - result.data = prepareBrowserUrl(client_info) - return result + + if(old_scope != scope_string) { + // マストドン2.4でスコープが追加された + // 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない + ClientInfo.delete(instance, client_name) + + // client credential をタンスから消去する + revokeClientCredential(client_info, client_credential) + + // FIXME クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない + } else { + // クライアント情報を再利用する + result.data = prepareBrowserUrl(scope_string, client_info) + return result + } } } } - val r2 = registerClient(client_name) + val r2 = registerClient(scope_string, client_name) val jsonObject = r2?.jsonObject ?: return r2 // {"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"******","client_secret":"******"} jsonObject.put(KEY_AUTH_VERSION, AUTH_VERSION) + jsonObject.put(KEY_CLIENT_SCOPE, scope_string) ClientInfo.save(instance, client_name, jsonObject.toString()) - result.data = prepareBrowserUrl(jsonObject) + result.data = prepareBrowserUrl(scope_string, jsonObject) return result } @@ -602,6 +681,8 @@ class TootApiClient( if(! sendRequest(result) { + val scope_string = client_info.optString(KEY_CLIENT_SCOPE) + val client_id = client_info.parseString("client_id") val client_secret = client_info.parseString("client_secret") if(client_id == null) return result.setError("missing client_id ") @@ -612,8 +693,8 @@ class TootApiClient( + "&client_id=" + client_id.encodePercent() + "&redirect_uri=" + REDIRECT_URL.encodePercent() + "&client_secret=" + client_secret.encodePercent() - + "&scope=read+write+follow" - + "&scopes=read+write+follow") + + "&scope=$scope_string" + + "&scopes=$scope_string") Request.Builder() .url("https://$instance/oauth/token") diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt index d47051ae..24636d7c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt @@ -9,6 +9,9 @@ class TootInstance(parser:TootParser,src : JSONObject) { companion object { val rePleroma = Pattern.compile("\\bpleroma\\b",Pattern.CASE_INSENSITIVE) + + val VERSION_2_4 = VersionString("2.4") + } // いつ取得したか(内部利用) diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.kt b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.kt index dca4b8d6..3c352bce 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.kt @@ -66,9 +66,9 @@ object ClientInfo :TableCompanion { } // 単体テスト用。インスタンス名を指定して削除する - internal fun delete(instance : String) { + fun delete(instance : String,client_name : String ) { try { - App1.database.delete(table, "$COL_HOST=?", arrayOf(instance)) + App1.database.delete(table, "$COL_HOST=? and $COL_CLIENT_NAME=?", arrayOf(instance, client_name)) } catch(ex : Throwable) { log.e(ex, "delete failed.") } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/LinkHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/util/LinkHelper.kt index 44f32e41..f59b2bcf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/LinkHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/LinkHelper.kt @@ -6,7 +6,6 @@ interface LinkHelper { // SavedAccountのロード時にhostを供給する必要があった val host : String? - get() = null fun findAcctColor(url : String?) : AcctColor? = null diff --git a/app/src/main/res/layout/act_account_setting.xml b/app/src/main/res/layout/act_account_setting.xml index 9cc13968..0032b54c 100644 --- a/app/src/main/res/layout/act_account_setting.xml +++ b/app/src/main/res/layout/act_account_setting.xml @@ -359,7 +359,17 @@ /> + +