diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt index 08627b1c..d860848b 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt @@ -1051,7 +1051,7 @@ class TestTootApiClient { println(url) // ブラウザからコールバックで受け取ったcodeを処理する - result = client.authentication2(clientName, "DUMMY_CODE") + result = client.authentication2Mastodon(clientName, "DUMMY_CODE") jsonObject = result?.jsonObject assertNotNull(jsonObject) if (jsonObject == null) return@runBlocking diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt index 8ba9817e..2b32e696 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt @@ -49,6 +49,7 @@ import java.io.FileOutputStream import java.io.InputStreamReader import java.lang.ref.WeakReference import java.util.* +import java.util.concurrent.atomic.AtomicReference import java.util.zip.ZipInputStream import kotlin.math.abs import kotlin.math.max @@ -579,7 +580,11 @@ class ActMain : AsyncActivity(), View.OnClickListener, // アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない val new_order = app_state.columnList .mapIndexedNotNull { index, column -> - if (!column.access_info.isNA && null == SavedAccount.loadAccount(this@ActMain, column.access_info.db_id)) { + if (!column.access_info.isNA && null == SavedAccount.loadAccount( + this@ActMain, + column.access_info.db_id + ) + ) { null } else { index @@ -639,7 +644,12 @@ class ActMain : AsyncActivity(), View.OnClickListener, if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :updateColumnStripSelection") ts = SystemClock.elapsedRealtime() - app_state.columnList.forEach { it.fireShowContent(reason = "ActMain onStart", reset = true) } + app_state.columnList.forEach { + it.fireShowContent( + reason = "ActMain onStart", + reset = true + ) + } te = SystemClock.elapsedRealtime() if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireShowContent") @@ -823,7 +833,7 @@ class ActMain : AsyncActivity(), View.OnClickListener, // 新しいカラムをどこに挿入するか // カラムの次の位置か、現在のページの次の位置か、終端 fun nextPosition(column: Column?): Int = - app_state.columnIndex(column)?.let{ it+1 } ?: defaultInsertPosition + app_state.columnIndex(column)?.let { it + 1 } ?: defaultInsertPosition private fun showQuickTootVisibility() { btnQuickTootMenu.imageResource = @@ -868,7 +878,7 @@ class ActMain : AsyncActivity(), View.OnClickListener, val refresh_after_toot = Pref.ipRefreshAfterToot(pref) if (refresh_after_toot != Pref.RAT_DONT_REFRESH) { app_state.columnList - .filter{ it.access_info.acct == posted_acct} + .filter { it.access_info.acct == posted_acct } .forEach { it.startRefreshForPost( refresh_after_toot, @@ -970,7 +980,7 @@ class ActMain : AsyncActivity(), View.OnClickListener, private fun isOrderChanged(new_order: List): Boolean { if (new_order.size != app_state.columnCount) return true - for(i in new_order.indices){ + for (i in new_order.indices) { if (new_order[i] != i) return true } return false @@ -1027,7 +1037,7 @@ class ActMain : AsyncActivity(), View.OnClickListener, REQUEST_CODE_COLUMN_COLOR -> if (data != null) { app_state.saveColumnList() val idx = data.getIntExtra(ActColumnCustomize.EXTRA_COLUMN_INDEX, 0) - app_state.column(idx)?.let{ + app_state.column(idx)?.let { it.fireColumnColor() it.fireShowContent( reason = "ActMain column color changed", @@ -1074,7 +1084,10 @@ class ActMain : AsyncActivity(), View.OnClickListener, REQUEST_CODE_TEXT -> when (resultCode) { ActText.RESULT_SEARCH_MSP -> searchFromActivityResult(data, ColumnType.SEARCH_MSP) ActText.RESULT_SEARCH_TS -> searchFromActivityResult(data, ColumnType.SEARCH_TS) - ActText.RESULT_SEARCH_NOTESTOCK -> searchFromActivityResult(data, ColumnType.SEARCH_NOTESTOCK) + ActText.RESULT_SEARCH_NOTESTOCK -> searchFromActivityResult( + data, + ColumnType.SEARCH_NOTESTOCK + ) } } @@ -1090,7 +1103,7 @@ class ActMain : AsyncActivity(), View.OnClickListener, } // カラムが0個ならアプリを終了する - if (app_state.columnCount == 0 ) { + if (app_state.columnCount == 0) { finish() return } @@ -1504,12 +1517,12 @@ class ActMain : AsyncActivity(), View.OnClickListener, val c = env.pager.currentItem c == idx }, { env -> - idx >= 0 && idx in env.visibleColumnsIndices - } + idx >= 0 && idx in env.visibleColumnsIndices + } ) private fun updateColumnStrip() { - llEmpty.vg(app_state.columnCount==0) + llEmpty.vg(app_state.columnCount == 0) val iconSize = stripIconSize val rootW = (iconSize * 1.25f + 0.5f).toInt() @@ -1588,7 +1601,7 @@ class ActMain : AsyncActivity(), View.OnClickListener, handler.post(Runnable { if (isFinishing) return@Runnable - if (app_state.columnCount == 0 ) { + if (app_state.columnCount == 0) { llColumnStrip.setVisibleRange(-1, -1, 0f) } else { phoneTab({ env -> @@ -1788,7 +1801,7 @@ class ActMain : AsyncActivity(), View.OnClickListener, PollingWorker.queueNotificationClicked(this, uri) val columnList = app_state.columnList - val column =columnList.firstOrNull { + val column = columnList.firstOrNull { it.type == ColumnType.NOTIFICATIONS && it.access_info == account && !it.system_notification_not_related @@ -1883,8 +1896,10 @@ class ActMain : AsyncActivity(), View.OnClickListener, val error = uri.getQueryParameter("error") val error_description = uri.getQueryParameter("error_description") if (error != null || error_description != null) - return TootApiResult(error_description.notBlank() ?: error.notBlank() - ?: "?") + return TootApiResult( + error_description.notBlank() ?: error.notBlank() + ?: "?" + ) // subwaytooter://oauth(\d*)/ // ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2 @@ -1923,22 +1938,26 @@ class ActMain : AsyncActivity(), View.OnClickListener, val instance = client.apiHost ?: return TootApiResult("missing instance in callback url.") - - val (ti, r2) = TootInstance.get(client) - ti ?: return r2 - - this.ti = ti this.host = instance + val parser = TootParser( this@ActMain, linkHelper = LinkHelper.create(instance) ) - return client.authentication2( + val refToken = AtomicReference(null) + return client.authentication2Mastodon( Pref.spClientName(this@ActMain), - code - )?.also { this.ta = parser.account(it.jsonObject) } + code, + outAccessToken = refToken + )?.also { + this.ta = parser.account(it.jsonObject) + if( ta != null){ + val (ti, r2) = TootInstance.get(client, forceAccessToken = refToken.get()) + this.ti = ti ?: return r2 + } + } } } @@ -2105,7 +2124,11 @@ class ActMain : AsyncActivity(), View.OnClickListener, override suspend fun background(client: TootApiClient): TootApiResult? { - val (instance, instanceResult) = TootInstance.get(client, apiHost) + val (instance, instanceResult) = TootInstance.get( + client, + apiHost, + forceAccessToken = access_token + ) instance ?: return instanceResult this.ti = instance @@ -2193,19 +2216,19 @@ class ActMain : AsyncActivity(), View.OnClickListener, return } - app_state.columnIndex(column)?.let{ page_delete -> + app_state.columnIndex(column)?.let { page_delete -> phoneTab({ env -> val page_showing = env.pager.currentItem removeColumn(column) - if ( page_showing == page_delete) { - scrollAndLoad(page_showing-1) + if (page_showing == page_delete) { + scrollAndLoad(page_showing - 1) } }, { removeColumn(column) - scrollAndLoad( page_delete - 1) + scrollAndLoad(page_delete - 1) }) } } @@ -2253,8 +2276,8 @@ class ActMain : AsyncActivity(), View.OnClickListener, scrollAndLoad(lastColumnIndex) } - private fun scrollAndLoad(idx:Int){ - val c = app_state.column(idx) ?:return + private fun scrollAndLoad(idx: Int) { + val c = app_state.column(idx) ?: return scrollToColumn(idx) if (!c.bFirstInitialized) c.startLoading() } 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 f5795500..6663f011 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -9,6 +9,7 @@ import jp.juggler.subwaytooter.util.* import jp.juggler.util.* import okhttp3.* import java.util.* +import java.util.concurrent.atomic.AtomicReference class TootApiClient( val context: Context, @@ -117,10 +118,14 @@ class TootApiClient( return sb.toString().replace("\n+".toRegex(), "\n") } - fun getScopeString(ti: TootInstance) = when { + fun getScopeString(ti: TootInstance?) = when { + // 古いサーバ + ti?.versionGE(TootInstance.VERSION_2_4_0_rc1) == false ->"read+write+follow" + // 新しいサーバか、AUTHORIZED_FETCH(3.0.0以降)によりサーバ情報を取得できなかった + else -> "read+write+follow+push" + + // 過去の試行錯誤かな // ti.versionGE(TootInstance.VERSION_2_7_0_rc1) -> "read+write+follow+push+create" - ti.versionGE(TootInstance.VERSION_2_4_0_rc1) -> "read+write+follow+push" - else -> "read+write+follow" } fun getScopeArrayMisskey(@Suppress("UNUSED_PARAMETER") ti: TootInstance) = @@ -547,6 +552,7 @@ class TootApiClient( path: String, request_builder: Request.Builder = Request.Builder(), withoutToken: Boolean = false, + forceAccessToken:String? =null, ): TootApiResult? { val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result @@ -556,19 +562,19 @@ class TootApiClient( try { if (!sendRequest(result) { - val url = "https://${apiHost?.ascii}$path" + val url = "https://${apiHost?.ascii}$path" request_builder.url(url) - if(!withoutToken){ - val access_token = account?.getAccessToken() + if (!withoutToken) { + val access_token = forceAccessToken ?: account?.getAccessToken() if (access_token?.isNotEmpty() == true) { request_builder.header("Authorization", "Bearer $access_token") } } request_builder.build() - .also{ log.d("request: ${it.method} $url") } + .also { log.d("request: ${it.method} $url") } }) return result @@ -684,7 +690,10 @@ class TootApiClient( return result } - private suspend fun authentication1Misskey(clientNameArg: String, ti: TootInstance): TootApiResult? { + private suspend fun authentication1Misskey( + clientNameArg: String, + ti: TootInstance + ): TootApiResult? { val result = TootApiResult.makeWithCaption(this.apiHost?.pretty) if (result.error != null) return result val instance = result.caption // same to instance @@ -954,7 +963,7 @@ class TootApiClient( private suspend fun prepareClientMastodon( clientNameArg: String, - ti: TootInstance, + ti: TootInstance?, forceUpdateClient: Boolean = false ): TootApiResult? { // 前準備 @@ -1046,11 +1055,11 @@ class TootApiClient( private suspend fun authentication1Mastodon( clientNameArg: String, - ti: TootInstance, + ti: TootInstance?, forceUpdateClient: Boolean = false ): TootApiResult? { - if (ti.instanceType == InstanceType.Pixelfed) { + if (ti?.instanceType == InstanceType.Pixelfed) { return TootApiResult("currently Pixelfed instance is not supported.") } @@ -1069,15 +1078,24 @@ class TootApiClient( ): TootApiResult? { val (ti, ri) = TootInstance.get(this) - ti ?: return ri - return when { + log.i("authentication1: instance info version=${ti?.version} misskeyVersion=${ti?.misskeyVersion} responseCode=${ri?.response?.code}") + return if (ti == null) when (ri?.response?.code) { + // https://github.com/tateisu/SubwayTooter/issues/155 + // Mastodon's WHITELIST_MODE + 401 -> authentication1Mastodon(clientNameArg, null, forceUpdateClient) + else -> ri + } else when { ti.misskeyVersion > 0 -> authentication1Misskey(clientNameArg, ti) else -> authentication1Mastodon(clientNameArg, ti, forceUpdateClient) } } // oAuth2認証の続きを行う - suspend fun authentication2(clientNameArg: String, code: String): TootApiResult? { + suspend fun authentication2Mastodon( + clientNameArg: String, + code: String, + outAccessToken: AtomicReference + ): TootApiResult? { val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result @@ -1116,8 +1134,8 @@ class TootApiClient( if (access_token?.isEmpty() != false) { return result.setError("missing access_token in the response.") } + outAccessToken.set(access_token) return getUserCredential(access_token, token_info) - } // アクセストークン手動入力でアカウントを更新する場合、アカウントの情報を取得する @@ -1282,13 +1300,13 @@ class TootApiClient( val request_builder = Request.Builder() - if( account.isMisskey){ + if (account.isMisskey) { val accessToken = account.misskeyApiToken if (accessToken?.isNotEmpty() == true) { val delm = if (-1 != url.indexOf('?')) '&' else '?' url = "$url${delm}i=${accessToken.encodePercent()}" } - }else { + } else { val access_token = account.getAccessToken() if (access_token?.isNotEmpty() == true) { val delm = if (-1 != url.indexOf('?')) '&' else '?' 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 ecced2e7..8cdea4aa 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 @@ -30,30 +30,34 @@ enum class InstanceType { Pleroma } -enum class CapabilitySource{ +enum class CapabilitySource { Fedibird, } -enum class InstanceCapability(private val capabilitySource:CapabilitySource,private val id: String) { - FavouriteHashtag(CapabilitySource.Fedibird,"favourite_hashtag"), - FavouriteDomain(CapabilitySource.Fedibird,"favourite_domain"), - StatusExpire(CapabilitySource.Fedibird,"status_expire"), - FollowNoDelivery(CapabilitySource.Fedibird,"follow_no_delivery"), - FollowHashtag(CapabilitySource.Fedibird,"follow_hashtag"), - SubscribeAccount(CapabilitySource.Fedibird,"subscribe_account"), - SubscribeDomain(CapabilitySource.Fedibird,"subscribe_domain"), - SubscribeKeyword(CapabilitySource.Fedibird,"subscribe_keyword"), - TimelineNoLocal(CapabilitySource.Fedibird,"timeline_no_local"), - TimelineDomain(CapabilitySource.Fedibird,"timeline_domain"), - TimelineGroup(CapabilitySource.Fedibird,"timeline_group"), - TimelineGroupDirectory(CapabilitySource.Fedibird,"timeline_group_directory"), - VisibilityMutual(CapabilitySource.Fedibird,"visibility_mutual"), - VisibilityLimited(CapabilitySource.Fedibird,"visibility_limited"), + +enum class InstanceCapability( + private val capabilitySource: CapabilitySource, + private val id: String +) { + FavouriteHashtag(CapabilitySource.Fedibird, "favourite_hashtag"), + FavouriteDomain(CapabilitySource.Fedibird, "favourite_domain"), + StatusExpire(CapabilitySource.Fedibird, "status_expire"), + FollowNoDelivery(CapabilitySource.Fedibird, "follow_no_delivery"), + FollowHashtag(CapabilitySource.Fedibird, "follow_hashtag"), + SubscribeAccount(CapabilitySource.Fedibird, "subscribe_account"), + SubscribeDomain(CapabilitySource.Fedibird, "subscribe_domain"), + SubscribeKeyword(CapabilitySource.Fedibird, "subscribe_keyword"), + TimelineNoLocal(CapabilitySource.Fedibird, "timeline_no_local"), + TimelineDomain(CapabilitySource.Fedibird, "timeline_domain"), + TimelineGroup(CapabilitySource.Fedibird, "timeline_group"), + TimelineGroupDirectory(CapabilitySource.Fedibird, "timeline_group_directory"), + VisibilityMutual(CapabilitySource.Fedibird, "visibility_mutual"), + VisibilityLimited(CapabilitySource.Fedibird, "visibility_limited"), ; - fun hasCapability(instance:TootInstance):Boolean{ - when(capabilitySource){ - CapabilitySource.Fedibird->{ - if( instance.fedibird_capabilities?.any{ it == id} ==true ) return true + fun hasCapability(instance: TootInstance): Boolean { + when (capabilitySource) { + CapabilitySource.Fedibird -> { + if (instance.fedibird_capabilities?.any { it == id } == true) return true } } // XXX: もし機能がMastodon公式に取り込まれたならバージョン番号で判断できるはず @@ -220,7 +224,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { return i >= 0 } - fun hasCapability(cap:InstanceCapability) = cap.hasCapability(this) + fun hasCapability(cap: InstanceCapability) = cap.hasCapability(this) companion object { @@ -320,6 +324,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { val account: SavedAccount?, val allowPixelfed: Boolean, val forceUpdate: Boolean, + val forceAccessToken: String?, ) { val result = Channel>() } @@ -333,7 +338,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { var item: TootInstance? - if (!ri.forceUpdate) { + if (!ri.forceUpdate && ri.forceAccessToken == null) { // re-use cached item. val now = SystemClock.elapsedRealtime() item = cacheData @@ -353,9 +358,15 @@ class TootInstance(parser: TootParser, src: JsonObject) { // get new information val result = when { - ri.account == null -> { + // マストドンのホワイトリストモード用 + ri.forceAccessToken != null -> + ri.client.request( + "/api/v1/instance", + forceAccessToken = ri.forceAccessToken + ) + + ri.account == null -> ri.client.getInstanceInformation() - } ri.account.isMisskey -> ri.client.request( @@ -409,11 +420,16 @@ class TootInstance(parser: TootParser, src: JsonObject) { private suspend fun loop() { while (true) { requestQueue.receive().let { req -> - req.result.send(try { - getImpl(req) - } catch (ex: Throwable) { - Pair(null, TootApiResult(ex.withCaption("can't get server information."))) - }) + req.result.send( + try { + getImpl(req) + } catch (ex: Throwable) { + Pair( + null, + TootApiResult(ex.withCaption("can't get server information.")) + ) + } + ) } } } @@ -454,7 +470,8 @@ class TootInstance(parser: TootParser, src: JsonObject) { hostArg: Host? = null, account: SavedAccount? = if (hostArg == client.apiHost) client.account else null, allowPixelfed: Boolean = false, - forceUpdate: Boolean = false + forceUpdate: Boolean = false, + forceAccessToken: String? = null, // マストドンのwhitelist modeでアカウント追加時に必要 ): Pair { val tmpInstance = client.apiHost @@ -465,11 +482,18 @@ class TootInstance(parser: TootParser, src: JsonObject) { // update client.apiHost if (hostArg != null) client.apiHost = hostArg - if (client.apiHost == null) throw NullPointerException("missing host to get server information.") + val host = client.apiHost + ?: throw NullPointerException("missing host to get server information.") // ホスト名ごとに用意したオブジェクトで同期する - return RequestInfo(client, account, allowPixelfed, forceUpdate) - .also { client.apiHost!!.getCacheEntry().requestQueue.send(it) } + return RequestInfo( + client = client, + account = account, + allowPixelfed = allowPixelfed, + forceUpdate = forceUpdate, + forceAccessToken = forceAccessToken + ) + .also { host.getCacheEntry().requestQueue.send(it) } .result.receive() } finally {