アカウントの追加/更新に失敗するバグの修正

This commit is contained in:
tateisu 2018-05-11 22:42:54 +09:00
parent e4df54479c
commit 3f3279243f
11 changed files with 209 additions and 33 deletions

View File

@ -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<Pair<String, Any>>) {
private fun updateCredential(args : List<Pair<String, Any>>) {
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","<<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()
}
})
}
}

View File

@ -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
}

View File

@ -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<TextView>(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()

View File

@ -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")

View File

@ -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")
}
// いつ取得したか(内部利用)

View File

@ -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.")
}

View File

@ -6,7 +6,6 @@ interface LinkHelper {
// SavedAccountのロード時にhostを供給する必要があった
val host : String?
get() = null
fun findAcctColor(url : String?) : AcctColor? = null

View File

@ -359,7 +359,17 @@
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnPushTest"
style="@style/setting_horizontal_stretch"
android:ellipsize="start"
android:text="@string/push_notification_test"
android:textAllCaps="false"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<Button

View File

@ -659,8 +659,9 @@
<string name="available_mastodon_2_4_later">(available in Mastodon 2.4 or later)</string>
<string name="confirm_boost_private_from">Boost private status from %1$s ? It\'s shown to all followers.</string>
<string name="boost_private_toot_not_allowed">You can\'t boost private toot by another person.</string>
<string name="push_notification_test">Push Notification Test (Mastodon 2.4 or later)</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>-->
<!--<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>-->
<!--<string name="abc_action_bar_up_description">Revenir en haut de la page</string>-->

View File

@ -938,5 +938,6 @@
<string name="available_mastodon_2_4_later">(マストドン2.4以降で利用可能)</string>
<string name="confirm_boost_private_from">非公開トゥートを %1$s からブーストしますか? 全てのフォロワーに公開されます</string>
<string name="boost_private_toot_not_allowed">非公開トゥートをブーストできるのは本人だけです</string>
<string name="push_notification_test">Push Notification Test (Mastodon 2.4 or later)</string>
</resources>

View File

@ -645,4 +645,5 @@
<string name="available_mastodon_2_4_later">(available in Mastodon 2.4 or later)</string>
<string name="confirm_boost_private_from">Boost private status from %1$s ? It\'s shown to all followers.</string>
<string name="boost_private_toot_not_allowed">You can\'t boost private toot by another person.</string>
<string name="push_notification_test">Push Notification Test (Mastodon 2.4 or later)</string>
</resources>