From 2ed3ebe0f14b908c4bc06b0869681adc109d9a16 Mon Sep 17 00:00:00 2001 From: tateisu Date: Thu, 4 Jan 2024 10:40:09 +0900 Subject: [PATCH] =?UTF-8?q?Mastodon=204.0=E4=BB=A5=E9=99=8D=E3=81=A7?= =?UTF-8?q?=E5=8D=98=E8=AA=9E=E3=83=95=E3=82=A3=E3=83=AB=E3=82=BF=E3=81=AE?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=AB=E5=A4=B1=E6=95=97=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 29 +-- .../jp/juggler/subwaytooter/ActCallback.kt | 8 +- .../juggler/subwaytooter/ActKeywordFilter.kt | 6 +- .../subwaytooter/action/Action_Boost.kt | 166 +++++++------- .../subwaytooter/action/Action_Filter.kt | 89 ++++--- .../jp/juggler/subwaytooter/api/ApiPath.kt | 2 +- .../juggler/subwaytooter/api/TootApiClient.kt | 56 ++++- .../api/TootApiResultException.kt | 8 +- .../subwaytooter/column/ColumnFilters.kt | 5 +- .../subwaytooter/column/ColumnTask_Loading.kt | 3 +- .../jp/juggler/subwaytooter/util/PostImpl.kt | 217 +++++++++--------- .../jp/juggler/util/coroutine/EmptyScope.kt | 8 +- .../java/jp/juggler/util/data/StringUtils.kt | 3 +- .../java/jp/juggler/util/os/ContextUtils.kt | 11 +- .../main/java/jp/juggler/util/ui/UiUtils.kt | 27 ++- 15 files changed, 362 insertions(+), 276 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d51e95e7..df403376 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -164,7 +164,7 @@ dependencies { implementation(project(":anko")) implementation(fileTree(mapOf("dir" to "src/main/libs", "include" to arrayOf("*.aar")))) - "fcmImplementation"("com.google.firebase:firebase-messaging:23.3.1") + "fcmImplementation"("com.google.firebase:firebase-messaging:23.4.0") "fcmImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:${Vers.kotlinxCoroutinesVersion}") // implementation "org.conscrypt:conscrypt-android:$conscryptVersion" @@ -272,25 +272,26 @@ tasks.register("detektAll") { ) ) -// val kotlinFiles = "**/*.kt" -// include(kotlinFiles) - val resourceFiles = "**/resources/**" val buildFiles = "**/build/**" exclude(resourceFiles, buildFiles) reports { - val buildDir = layout.buildDirectory - - xml.required.set(false) - xml.outputLocation.set(file("$buildDir/reports/detekt/st-${name}.xml")) - - html.required.set(true) - html.outputLocation.set(file("$buildDir/reports/detekt/st-${name}.html")) + fun reportLocationByExt(ext: String) = + layout.buildDirectory + .file("reports/detekt/st-${name}.$ext") + .get() + .asFile txt.required.set(true) - txt.outputLocation.set(file("$buildDir/reports/detekt/st-${name}.txt")) + txt.outputLocation.set(reportLocationByExt("txt")) - sarif.required.set(true) - sarif.outputLocation.set(file("$buildDir/reports/detekt/st-${name}.sarif")) + html.required.set(true) + html.outputLocation.set(reportLocationByExt("html")) + + xml.required.set(false) + xml.outputLocation.set(reportLocationByExt("xml")) + + sarif.required.set(false) + sarif.outputLocation.set(reportLocationByExt("sarif")) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActCallback.kt b/app/src/main/java/jp/juggler/subwaytooter/ActCallback.kt index 9defc90c..73044fbb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActCallback.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActCallback.kt @@ -79,12 +79,14 @@ class ActCallback : AppCompatActivity() { sharedIntent.set(intent) } } + forbidUriFromApp(intent) -> { // last_uriをクリアする lastUri.set(null) // ダイアログを閉じるまで画面遷移しない return } + else -> { val uri = intent.data if (uri != null) { @@ -123,7 +125,7 @@ class ActCallback : AppCompatActivity() { val type = src.type if (type.isMediaMimeType()) { - when (action){ + when (action) { Intent.ACTION_VIEW -> { src.data?.let { uriOriginal -> try { @@ -137,6 +139,7 @@ class ActCallback : AppCompatActivity() { } } } + Intent.ACTION_SEND -> { var uri = src.getStreamUriExtra() ?: return src // text/plainの場合 @@ -152,7 +155,8 @@ class ActCallback : AppCompatActivity() { log.e(ex, "remake failed. src=$src") } } - Intent.ACTION_SEND_MULTIPLE -> { + + Intent.ACTION_SEND_MULTIPLE -> { val listUri = src.getStreamUriListExtra() ?: return null val listDst = ArrayList() diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActKeywordFilter.kt b/app/src/main/java/jp/juggler/subwaytooter/ActKeywordFilter.kt index e529b3b1..21611dca 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActKeywordFilter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActKeywordFilter.kt @@ -247,7 +247,7 @@ class ActKeywordFilter : AppCompatActivity() { if (result?.response?.code == 404) { // try v1 - result = client.request("${ApiPath.PATH_FILTERS}/$filterId") + result = client.request("${ApiPath.PATH_FILTERS_V1}/$filterId") result?.jsonObject?.let { try { resultFilter = TootFilter(it) @@ -391,12 +391,12 @@ class ActKeywordFilter : AppCompatActivity() { return runApiTask(account) { client -> if (filterId == null) { client.request( - ApiPath.PATH_FILTERS, + ApiPath.PATH_FILTERS_V1, params.toPostRequestBuilder() ) } else { client.request( - "${ApiPath.PATH_FILTERS}/$filterId", + "${ApiPath.PATH_FILTERS_V1}/$filterId", params.toRequestBody().toPut() ) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt index 30554426..d69bf8d3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt @@ -7,11 +7,17 @@ import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.actmain.addColumn import jp.juggler.subwaytooter.actmain.reloadAccountSetting import jp.juggler.subwaytooter.actmain.showColumnMatchAccount -import jp.juggler.subwaytooter.api.* +import jp.juggler.subwaytooter.api.ApiTask +import jp.juggler.subwaytooter.api.TootApiClient +import jp.juggler.subwaytooter.api.TootApiResult +import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.entity.Acct import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootVisibility +import jp.juggler.subwaytooter.api.errorApiResult +import jp.juggler.subwaytooter.api.runApiTask2 +import jp.juggler.subwaytooter.api.syncStatus import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.findStatus import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm @@ -27,6 +33,7 @@ import jp.juggler.util.coroutine.launchMain import jp.juggler.util.data.JsonObject import jp.juggler.util.log.showToast import jp.juggler.util.network.toPostRequestBuilder +import kotlinx.coroutines.CancellationException import kotlin.math.max private class BoostImpl( @@ -47,8 +54,6 @@ private class BoostImpl( private val isPrivateToot = accessInfo.isMastodon && statusArg.visibility == TootVisibility.PrivateFollowers - private var bConfirmed = false - private fun preCheck(): Boolean { // アカウントからステータスにブースト操作を行っているなら、何もしない @@ -73,15 +78,15 @@ private class BoostImpl( } else { val (result, status) = client.syncStatus(accessInfo, statusArg) when { + result == null -> throw CancellationException() status == null -> errorApiResult(result) - status.reblogged -> errorApiResult(getString(R.string.already_boosted)) + status.reblogged -> errorApiResult(R.string.already_boosted) else -> status } } // ブースト結果をUIに反映させる - private fun after(result: TootApiResult?, newStatus: TootStatus?, unrenoteId: EntityId?) { - result ?: return // cancelled. + private fun after(newStatus: TootStatus?, unrenoteId: EntityId?) { when { // Misskeyでunrenoteに成功した unrenoteId != null -> { @@ -143,36 +148,20 @@ private class BoostImpl( } callback() } - - else -> activity.showToast(true, result.error) } } - suspend fun boostApi(client: TootApiClient, targetStatus: TootStatus): TootApiResult? = - if (accessInfo.isMisskey) { - if (!bSet) { - val myRenoteId = targetStatus.myRenoteId ?: errorApiResult("missing renote id.") - - client.request( - "/api/notes/delete", - accessInfo.putMisskeyApiToken().apply { - put("noteId", myRenoteId.toString()) - put("renoteId", targetStatus.id.toString()) - }.toPostRequestBuilder() - )?.also { - if (it.response?.code == 204) { - resultUnrenoteId = myRenoteId - } - } - } else { - client.request( + suspend fun boostApi(client: TootApiClient, targetStatus: TootStatus): TootApiResult = + when { + accessInfo.isMisskey -> when { + // misskey, create renote + bSet -> client.requestOrThrow( "/api/notes/create", accessInfo.putMisskeyApiToken().apply { put("renoteId", targetStatus.id.toString()) }.toPostRequestBuilder() - )?.also { result -> - val jsonObject = result.jsonObject - if (jsonObject != null) { + ).apply { + jsonObject?.let { jsonObject -> val outerStatus = parser.status(jsonObject.jsonObject("createdNote") ?: jsonObject) val innerStatus = outerStatus?.reblog ?: outerStatus @@ -184,73 +173,84 @@ private class BoostImpl( resultStatus = innerStatus } } - } - } else { - val b = JsonObject().apply { - if (visibility != null) put("visibility", visibility.strMastodon) - }.toPostRequestBuilder() + // misskey, delete renote + else -> { + val myRenoteId = targetStatus.myRenoteId + ?: error("missing renote id.") - client.request( + client.requestOrThrow( + "/api/notes/delete", + accessInfo.putMisskeyApiToken().apply { + put("noteId", myRenoteId.toString()) + put("renoteId", targetStatus.id.toString()) + }.toPostRequestBuilder() + ).apply { + if (response?.code == 204) { + resultUnrenoteId = myRenoteId + } + } + } + } + // mastodon, reblog or unreblog + else -> client.requestOrThrow( "/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}", - b - )?.also { result -> + requestBuilder = JsonObject().apply { + if (visibility != null) put("visibility", visibility.strMastodon) + }.toPostRequestBuilder() + ).apply { // reblogはreblogを表すStatusを返す // unreblogはreblogしたStatusを返す - val s = parser.status(result.jsonObject) + val s = parser.status(jsonObject) resultStatus = s?.reblog ?: s } } - fun run() { - activity.launchAndShowError { - if (!preCheck()) return@launchAndShowError + fun run() = activity.launchAndShowError { + if (!preCheck()) return@launchAndShowError - if (!bConfirmed) { - activity.confirm( - activity.getString( - when { - !bSet -> R.string.confirm_unboost_from - isPrivateToot -> R.string.confirm_boost_private_from - visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from - else -> R.string.confirm_boost_from - }, - daoAcctColor.getNickname(accessInfo) - ), - when (bSet) { - true -> accessInfo.confirmBoost - else -> accessInfo.confirmUnboost - } - ) { newConfirmEnabled -> - when (bSet) { - true -> accessInfo.confirmBoost = newConfirmEnabled - else -> accessInfo.confirmUnboost = newConfirmEnabled - } - daoSavedAccount.save(accessInfo) - activity.reloadAccountSetting(accessInfo) - } + val confirmMessage = activity.getString( + when { + !bSet -> R.string.confirm_unboost_from + isPrivateToot -> R.string.confirm_boost_private_from + visibility == TootVisibility.PrivateFollowers -> + R.string.confirm_private_boost_from + + else -> R.string.confirm_boost_from + }, + daoAcctColor.getNickname(accessInfo) + ) + + activity.confirm( + message = confirmMessage, + isConfirmEnabled = when (bSet) { + true -> accessInfo.confirmBoost + else -> accessInfo.confirmUnboost } + ) { newConfirmEnabled -> + when (bSet) { + true -> accessInfo.confirmBoost = newConfirmEnabled + else -> accessInfo.confirmUnboost = newConfirmEnabled + } + daoSavedAccount.save(accessInfo) + activity.reloadAccountSetting(accessInfo) + } - // ブースト表示を更新中にする - activity.appState.setBusyBoost(accessInfo, statusArg) - activity.showColumnMatchAccount(accessInfo) - - val result = - activity.runApiTask( - accessInfo, - progressStyle = ApiTask.PROGRESS_NONE - ) { client -> - try { - val targetStatus = syncStatus(client) - boostApi(client, targetStatus) - } catch (ex: TootApiResultException) { - ex.result - } - } + // ブースト表示を更新中にする + activity.appState.setBusyBoost(accessInfo, statusArg) + activity.showColumnMatchAccount(accessInfo) + try { + activity.runApiTask2( + accessInfo = accessInfo, + progressStyle = ApiTask.PROGRESS_NONE + ) { client -> + boostApi(client, syncStatus(client)) + } + // カラムデータの書き換え + after(resultStatus, resultUnrenoteId) + } finally { // 更新中状態をリセット activity.appState.resetBusyBoost(accessInfo, statusArg) - // カラムデータの書き換え - after(result, resultStatus, resultUnrenoteId) - // result == null の場合でも更新中表示の解除が必要になる + // 失敗やキャンセルの場合でも表示を更新する activity.showColumnMatchAccount(accessInfo) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Filter.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Filter.kt index c67f60e8..ca48a5b1 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Filter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Filter.kt @@ -3,15 +3,19 @@ package jp.juggler.subwaytooter.action import jp.juggler.subwaytooter.ActKeywordFilter import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.R +import jp.juggler.subwaytooter.api.ApiPath +import jp.juggler.subwaytooter.api.TootApiClient +import jp.juggler.subwaytooter.api.TootApiResult +import jp.juggler.subwaytooter.api.TootApiResultException +import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.subwaytooter.api.entity.TootFilter -import jp.juggler.subwaytooter.api.runApiTask +import jp.juggler.subwaytooter.api.runApiTask2 import jp.juggler.subwaytooter.column.onFilterDeleted import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.log.showToast -import okhttp3.Request // private val log = LogCategory("Action_Filter") @@ -30,39 +34,56 @@ fun ActMain.openFilterMenu(accessInfo: SavedAccount, item: TootFilter?) { } } -fun ActMain.filterDelete( - accessInfo: SavedAccount, - filter: TootFilter, - bConfirmed: Boolean = false, -) { - launchAndShowError { - if (!bConfirmed) { - confirm(R.string.filter_delete_confirm, filter.displayString) - } - - var resultFilterList: List? = null - runApiTask(accessInfo) { client -> - var result = - client.request("/api/v1/filters/${filter.id}", Request.Builder().delete()) - if (result != null && result.error == null) { - result = client.request("/api/v1/filters") - val jsonArray = result?.jsonArray - if (jsonArray != null) resultFilterList = TootFilter.parseList(jsonArray) - } - result - }?.let { result -> - when (val filterList = resultFilterList) { - null -> showToast(false, result.error) - - else -> { - showToast(false, R.string.delete_succeeded) - for (column in appState.columnList) { - if (column.accessInfo == accessInfo) { - column.onFilterDeleted(filter, filterList) - } - } - } +suspend fun TootApiClient.filterDelete(filterId: EntityId): TootApiResult { + for (path in arrayOf( + "/api/v2/filters/${filterId}", + "/api/v1/filters/${filterId}", + )) { + try { + return requestOrThrow(path = path) + } catch (ex: TootApiResultException) { + when (ex.result?.response?.code) { + 404 -> continue + else -> throw ex } } } + error("missing filter APIs.") +} + +suspend fun TootApiClient.filterLoad(): List { + for (path in arrayOf( + ApiPath.PATH_FILTERS_V2, + ApiPath.PATH_FILTERS_V1, + )) { + try { + val jsonArray = requestOrThrow(path).jsonArray + ?: error("API response has no jsonArray.") + return TootFilter.parseList(jsonArray) + ?: error("TootFilter.parseList returns null.") + } catch (ex: TootApiResultException) { + when (ex.result?.response?.code) { + 404 -> continue + else -> throw ex + } + } + } + error("missing filter APIs.") +} + +fun ActMain.filterDelete( + accessInfo: SavedAccount, + filter: TootFilter, +) = launchAndShowError { + confirm(R.string.filter_delete_confirm, filter.displayString) + val newFilters = runApiTask2(accessInfo) { client -> + client.filterDelete(filter.id) + client.filterLoad() + } + showToast(false, R.string.delete_succeeded) + for (column in appState.columnList) { + if (column.accessInfo == accessInfo) { + column.onFilterDeleted(filter, newFilters) + } + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt b/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt index c70acc93..cd689342 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt @@ -41,7 +41,7 @@ object ApiPath { // リストではなくオブジェクトを返すAPI const val PATH_STATUSES = "/api/v1/statuses/%s" // 1:status_id - const val PATH_FILTERS = "/api/v1/filters" + const val PATH_FILTERS_V1 = "/api/v1/filters" const val PATH_FILTERS_V2 = "/api/v2/filters" const val PATH_MISSKEY_PROFILE_FOLLOWING = "/api/users/following" 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 15a91da9..5b3b7c7c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -5,16 +5,42 @@ import android.net.Uri import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.auth.AuthBase -import jp.juggler.subwaytooter.api.entity.* +import jp.juggler.subwaytooter.api.entity.Acct +import jp.juggler.subwaytooter.api.entity.Host +import jp.juggler.subwaytooter.api.entity.TootAccount +import jp.juggler.subwaytooter.api.entity.TootAccountRef import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull +import jp.juggler.subwaytooter.api.entity.TootInstance +import jp.juggler.subwaytooter.api.entity.TootResults +import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.table.SavedAccount -import jp.juggler.subwaytooter.util.* -import jp.juggler.util.data.* +import jp.juggler.subwaytooter.util.DecodeOptions +import jp.juggler.subwaytooter.util.LinkHelper +import jp.juggler.subwaytooter.util.SimpleHttpClient +import jp.juggler.subwaytooter.util.SimpleHttpClientImpl +import jp.juggler.subwaytooter.util.matchHost +import jp.juggler.util.data.CharacterGroup +import jp.juggler.util.data.JsonObject +import jp.juggler.util.data.asciiRegex +import jp.juggler.util.data.decodeJsonArray +import jp.juggler.util.data.decodeJsonObject +import jp.juggler.util.data.decodePercent +import jp.juggler.util.data.decodeUTF8 +import jp.juggler.util.data.encodePercent +import jp.juggler.util.data.groupEx +import jp.juggler.util.data.letNotEmpty +import jp.juggler.util.data.notEmpty import jp.juggler.util.log.LogCategory import jp.juggler.util.log.withCaption import jp.juggler.util.network.toPostRequestBuilder -import okhttp3.* +import kotlinx.coroutines.CancellationException +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener import okhttp3.internal.closeQuietly class TootApiClient( @@ -358,7 +384,7 @@ class TootApiClient( ////////////////////////////////////////////////////////////////////// -// fun request( + // fun request( // path: String, // request_builder: Request.Builder = Request.Builder() // ): TootApiResult? { @@ -391,6 +417,26 @@ class TootApiClient( // } // + /** + * requestと同じだがキャンセルやエラー発生時に例外を投げる + */ + suspend fun requestOrThrow( + path: String, + requestBuilder: Request.Builder = Request.Builder(), + forceAccessToken: String? = null, + ): TootApiResult { + val result = request( + path = path, + requestBuilder = requestBuilder, + forceAccessToken = forceAccessToken, + ) + when { + result == null -> throw CancellationException() + !result.error.isNullOrBlank() -> errorApiResult(result) + else -> return result + } + } + suspend fun request( path: String, requestBuilder: Request.Builder = Request.Builder(), diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResultException.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResultException.kt index 101eddcc..bb7b9b87 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResultException.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResultException.kt @@ -1,12 +1,18 @@ package jp.juggler.subwaytooter.api +import android.content.Context +import androidx.annotation.StringRes + class TootApiResultException(val result: TootApiResult?) : Exception(result?.error ?: "cancelled.") { constructor(error: String) : this(TootApiResult(error)) } -fun errorApiResult(result: TootApiResult?): Nothing = +fun errorApiResult(result: TootApiResult): Nothing = throw TootApiResultException(result) fun errorApiResult(error: String): Nothing = throw TootApiResultException(error) + +fun Context.errorApiResult(@StringRes stringId: Int, vararg args: Any?): Nothing = + errorApiResult(getString(stringId, *args)) diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt index fea8a0d8..b6aaa1cb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt @@ -8,7 +8,6 @@ import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.table.* -import jp.juggler.util.* import jp.juggler.util.coroutine.launchMain import jp.juggler.util.data.WordTrieTree import jp.juggler.util.log.LogCategory @@ -433,7 +432,7 @@ suspend fun Column.loadFilter2(client: TootApiClient): List? { if (getFilterContext() == null) return null var result = client.request(ApiPath.PATH_FILTERS_V2) if (result?.response?.code == 404) { - result = client.request(ApiPath.PATH_FILTERS) + result = client.request(ApiPath.PATH_FILTERS_V1) } val jsonArray = result?.jsonArray ?: return null @@ -511,7 +510,7 @@ fun reloadFilter(context: Context, accessInfo: SavedAccount) { ) { client -> var result = client.request(ApiPath.PATH_FILTERS_V2) if (result?.response?.code == 404) { - result = client.request(ApiPath.PATH_FILTERS) + result = client.request(ApiPath.PATH_FILTERS_V1) } result?.jsonArray?.let { resultList = TootFilter.parseList(it) diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt index 41ee3028..e86b7048 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt @@ -10,7 +10,6 @@ import jp.juggler.subwaytooter.columnviewholder.scrollToTop import jp.juggler.subwaytooter.notification.injectData import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.util.OpenSticker -import jp.juggler.util.* import jp.juggler.util.coroutine.runOnMainLooper import jp.juggler.util.coroutine.runOnMainLooperDelayed import jp.juggler.util.data.JsonArray @@ -902,7 +901,7 @@ class ColumnTask_Loading( suspend fun getFilterList(client: TootApiClient): TootApiResult? { var result = client.request(ApiPath.PATH_FILTERS_V2) if (result?.response?.code == 404) { - result = client.request(ApiPath.PATH_FILTERS) + result = client.request(ApiPath.PATH_FILTERS_V1) } if (result != null) { val src = TootFilter.parseList(result.jsonArray) diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt index 85913dff..58e5bc76 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt @@ -3,8 +3,20 @@ package jp.juggler.subwaytooter.util import android.os.SystemClock import androidx.appcompat.app.AppCompatActivity import jp.juggler.subwaytooter.R -import jp.juggler.subwaytooter.api.* -import jp.juggler.subwaytooter.api.entity.* +import jp.juggler.subwaytooter.api.TootApiClient +import jp.juggler.subwaytooter.api.TootApiResultException +import jp.juggler.subwaytooter.api.TootParser +import jp.juggler.subwaytooter.api.entity.EntityId +import jp.juggler.subwaytooter.api.entity.InstanceCapability +import jp.juggler.subwaytooter.api.entity.InstanceType +import jp.juggler.subwaytooter.api.entity.TootAccount +import jp.juggler.subwaytooter.api.entity.TootInstance +import jp.juggler.subwaytooter.api.entity.TootPollsType +import jp.juggler.subwaytooter.api.entity.TootStatus +import jp.juggler.subwaytooter.api.entity.TootTag +import jp.juggler.subwaytooter.api.entity.TootVisibility +import jp.juggler.subwaytooter.api.errorApiResult +import jp.juggler.subwaytooter.api.runApiTask2 import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.getVisibilityString @@ -14,19 +26,31 @@ import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.daoAcctColor import jp.juggler.subwaytooter.table.daoSavedAccount import jp.juggler.subwaytooter.table.daoTagHistory -import jp.juggler.util.* import jp.juggler.util.coroutine.AppDispatchers -import jp.juggler.util.data.* -import jp.juggler.util.log.* +import jp.juggler.util.data.JsonArray +import jp.juggler.util.data.JsonException +import jp.juggler.util.data.JsonObject +import jp.juggler.util.data.buildJsonArray +import jp.juggler.util.data.buildJsonObject +import jp.juggler.util.data.digestSHA256Hex +import jp.juggler.util.data.groupEx +import jp.juggler.util.data.jsonObjectOf +import jp.juggler.util.data.notEmpty +import jp.juggler.util.data.toJsonArray +import jp.juggler.util.log.LogCategory +import jp.juggler.util.log.errorString +import jp.juggler.util.log.showToast import jp.juggler.util.network.MEDIA_TYPE_JSON import jp.juggler.util.network.toPostRequestBuilder -import jp.juggler.util.ui.* import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody -import java.util.* +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.Locale +import java.util.TimeZone import java.util.concurrent.atomic.AtomicBoolean sealed class PostResult { @@ -110,36 +134,22 @@ class PostImpl( } } - private var resultStatus: TootStatus? = null - private var resultCredentialTmp: TootAccount? = null - private var resultScheduledStatusSucceeded = false - - private suspend fun getCredential( - client: TootApiClient, - parser: TootParser, - ): TootApiResult? { - return client.request("/api/v1/accounts/verify_credentials")?.also { result -> - resultCredentialTmp = parser.account(result.jsonObject) - } - } - + // may null, not error private suspend fun getWebVisibility( client: TootApiClient, parser: TootParser, instance: TootInstance, - ): TootVisibility? { - if (account.isMisskey || instance.versionGE(TootInstance.VERSION_1_6)) return null - - val r2 = getCredential(client, parser) - - val credentialTmp = resultCredentialTmp - ?: errorApiResult(r2) - - val privacy = credentialTmp.source?.privacy - ?: errorApiResult(activity.getString(R.string.cant_get_web_setting_visibility)) - - return TootVisibility.parseMastodon(privacy) - // may null, not error + ): TootVisibility? = when { + account.isMisskey -> null + instance.versionGE(TootInstance.VERSION_1_6) -> null + else -> { + val privacy = parser.account( + client.requestOrThrow("/api/v1/accounts/verify_credentials") + .jsonObject + )?.source?.privacy + ?: error(R.string.cant_get_web_setting_visibility) + TootVisibility.parseMastodon(privacy) + } } private fun checkServerHasVisibility( @@ -150,12 +160,7 @@ class PostImpl( ) { if (actual != extra || checkFun(instance)) return val strVisibility = extra.getVisibilityString(account.isMisskey) - errorApiResult( - activity.getString( - R.string.server_has_no_support_of_visibility, - strVisibility - ) - ) + activity.errorApiResult(R.string.server_has_no_support_of_visibility, strVisibility) } private suspend fun checkVisibility( @@ -205,7 +210,6 @@ class PostImpl( "/api/v1/scheduled_statuses/$scheduledId", Request.Builder().delete() ) - log.d("delete old scheduled status. result=$result") delay(2000L) } @@ -271,15 +275,13 @@ class PostImpl( // Misskeyの場合、NSFWするにはアップロード済みの画像を drive/files/update で更新する if (bNSFW) { - val r = client.request( + client.requestOrThrow( "/api/drive/files/update", account.putMisskeyApiToken().apply { put("fileId", a.id.toString()) put("isSensitive", true) - } - .toPostRequestBuilder() + }.toPostRequestBuilder() ) - if (r == null || r.error != null) errorApiResult(r) } } if (array.isNotEmpty()) json["mediaIds"] = array @@ -361,7 +363,7 @@ class PostImpl( if (scheduledAt != 0L) { if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) { - errorApiResult(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0)) + activity.errorApiResult(R.string.scheduled_status_requires_mastodon_2_7_0) } // UTCの日時を渡す val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC")) @@ -505,32 +507,28 @@ class PostImpl( isPosting.set(true) return try { withContext(AppDispatchers.MainImmediate) { - activity.runApiTask( - account, + val (status, scheduled) = activity.runApiTask2( + accessInfo = account, progressSetup = { it.setCanceledOnTouchOutside(false) }, ) { client -> - val (instance, ri) = TootInstance.get(client) - instance ?: return@runApiTask ri + val instance = TootInstance.getOrThrow(client) if (instance.instanceType == InstanceType.Pixelfed) { // Pixelfedは返信に画像を添付できない if (inReplyToId != null && attachmentList != null) { - return@runApiTask TootApiResult(getString(R.string.pixelfed_does_not_allow_reply_with_media)) + error(R.string.pixelfed_does_not_allow_reply_with_media) } // Pixelfedの返信ではない投稿は画像添付が必須 if (inReplyToId == null && attachmentList == null) { - return@runApiTask TootApiResult(getString(R.string.pixelfed_does_not_allow_post_without_media)) + error(R.string.pixelfed_does_not_allow_post_without_media) } } val parser = TootParser(this, account) - this@PostImpl.visibilityChecked = try { - checkVisibility(client, parser, instance) // may null - } catch (ex: TootApiResultException) { - return@runApiTask ex.result - } + // may null + this@PostImpl.visibilityChecked = checkVisibility(client, parser, instance) if (redraftStatusId != null) { // 元の投稿を削除する @@ -546,10 +544,8 @@ class PostImpl( } else { encodeParamsMastodon(json, instance) } - } catch (ex: TootApiResultException) { - return@runApiTask ex.result } catch (ex: JsonException) { - log.e(ex, "status encoding failed.") + throw IllegalStateException("encoding status failed.", ex) } val bodyString = json.toString() @@ -567,67 +563,66 @@ class PostImpl( } } - when { - account.isMisskey -> client.request( - "/api/notes/create", - createRequestBuilder() - ) + try { + val result = when { + account.isMisskey -> client.requestOrThrow( + "/api/notes/create", + createRequestBuilder() + ) - editStatusId != null -> client.request( - "/api/v1/statuses/$editStatusId", - createRequestBuilder(isPut = true) - ) + editStatusId != null -> client.requestOrThrow( + "/api/v1/statuses/$editStatusId", + createRequestBuilder(isPut = true) + ) - else -> client.request( - "/api/v1/statuses", - createRequestBuilder() - ) - }?.also { result -> + else -> client.requestOrThrow( + "/api/v1/statuses", + createRequestBuilder() + ) + } val jsonObject = result.jsonObject + when { - if (scheduledAt != 0L && jsonObject != null) { - // {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]} - resultScheduledStatusSucceeded = true - return@runApiTask result - } else { - val status = parser.status( - when { - account.isMisskey -> jsonObject?.jsonObject("createdNote") - ?: jsonObject + // 予約投稿完了 + scheduledAt != 0L && jsonObject != null -> { + // {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]} + Pair(null, true) + } - else -> jsonObject - } - ) - resultStatus = status - saveStatusTag(status) + // 通常投稿完了 + else -> { + val status = parser.status( + when { + account.isMisskey -> + jsonObject?.jsonObject("createdNote") + ?: jsonObject + + else -> jsonObject + } + )?.also { saveStatusTag(it) } + Pair(status, false) + } + } + } catch (ex: TootApiResultException) { + val errorMessage = ex.result?.error + when { + errorMessage.isNullOrBlank() -> error("(missing error detail)") + + errorMessage.contains("HTTP 404") -> + error("$ex\n${activity.getString(R.string.post_404_desc)}") + + else -> throw ex } } - }.let { result -> - if (result == null) throw CancellationException() + } + when { + scheduled -> PostResult.Scheduled(account) - val status = resultStatus - when { - resultScheduledStatusSucceeded -> - PostResult.Scheduled(account) + status == null -> + error("can't parse status in API result.") - // 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る - status != null -> - PostResult.Normal(account, status) - - else -> { - val e = result.error - error( - when { - e.isNullOrBlank() -> "(missing error detail)" - - e.contains("HTTP 404") -> - "$e\n${activity.getString(R.string.post_404_desc)}" - - else -> e - } - ) - } - } + // 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る + else -> PostResult.Normal(account, status) } } } finally { diff --git a/base/src/main/java/jp/juggler/util/coroutine/EmptyScope.kt b/base/src/main/java/jp/juggler/util/coroutine/EmptyScope.kt index 8d842de4..0eb60349 100644 --- a/base/src/main/java/jp/juggler/util/coroutine/EmptyScope.kt +++ b/base/src/main/java/jp/juggler/util/coroutine/EmptyScope.kt @@ -7,7 +7,12 @@ import jp.juggler.util.log.showError import jp.juggler.util.log.showToast import jp.juggler.util.ui.ProgressDialogEx import jp.juggler.util.ui.dismissSafe -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -68,6 +73,7 @@ fun AppCompatActivity.launchAndShowError( is CancellationException -> { log.w(errorCaption ?: "launchAndShowError cancelled.") } + else -> { log.e(ex, errorCaption ?: "launchAndShowError failed.") showError(ex, errorCaption) diff --git a/base/src/main/java/jp/juggler/util/data/StringUtils.kt b/base/src/main/java/jp/juggler/util/data/StringUtils.kt index fd57fa60..b043c7ff 100644 --- a/base/src/main/java/jp/juggler/util/data/StringUtils.kt +++ b/base/src/main/java/jp/juggler/util/data/StringUtils.kt @@ -9,7 +9,8 @@ import android.util.Base64 import androidx.core.net.toUri import jp.juggler.util.log.LogCategory import java.security.MessageDigest -import java.util.* +import java.util.LinkedList +import java.util.Locale import java.util.regex.Matcher import java.util.regex.Pattern diff --git a/base/src/main/java/jp/juggler/util/os/ContextUtils.kt b/base/src/main/java/jp/juggler/util/os/ContextUtils.kt index 44832db8..db244530 100644 --- a/base/src/main/java/jp/juggler/util/os/ContextUtils.kt +++ b/base/src/main/java/jp/juggler/util/os/ContextUtils.kt @@ -1,6 +1,7 @@ package jp.juggler.util.os import android.content.Context +import androidx.annotation.StringRes /** * インストゥルメントテストのContextは @@ -8,4 +9,12 @@ import android.content.Context * この場合は元のcontextを補うのがベストだろう。 */ val Context.applicationContextSafe: Context - get() = applicationContext ?: this + get() = try { + applicationContext ?: this + } catch (ex: Throwable) { + // applicationContextへのアクセスは例外を出すことがある + this + } + +fun Context.error(@StringRes resId: Int, vararg args: Any?): Nothing = + error(getString(resId, *args)) diff --git a/base/src/main/java/jp/juggler/util/ui/UiUtils.kt b/base/src/main/java/jp/juggler/util/ui/UiUtils.kt index 29ac282a..3c6b93dc 100644 --- a/base/src/main/java/jp/juggler/util/ui/UiUtils.kt +++ b/base/src/main/java/jp/juggler/util/ui/UiUtils.kt @@ -392,18 +392,17 @@ fun AppCompatActivity.setNavigationBack(toolbar: Toolbar) = onBackPressedDispatcher.onBackPressed() } +val Float.roundPixels get() = (this + 0.5f).toInt() +fun DisplayMetrics.dpFloat(src: Float) = (density * src) +fun DisplayMetrics.dpFloat(src: Int) = (density * src.toFloat()) +fun Resources.dpFloat(src: Float) = displayMetrics.dpFloat(src) +fun Resources.dpFloat(src: Int) = displayMetrics.dpFloat(src) +fun Context.dpFloat(src: Float) = resources.dpFloat(src) +fun Context.dpFloat(src: Int) = resources.dpFloat(src) -val Float.roundPixels get()= (this+0.5f).toInt() -fun DisplayMetrics.dpFloat(src:Float) = (density* src) -fun DisplayMetrics.dpFloat(src:Int) = (density*src.toFloat()) -fun Resources.dpFloat(src:Float) = displayMetrics.dpFloat(src) -fun Resources.dpFloat(src:Int) = displayMetrics.dpFloat(src) -fun Context.dpFloat(src:Float) = resources.dpFloat(src) -fun Context.dpFloat(src:Int) = resources.dpFloat(src) - -fun DisplayMetrics.dp(src:Float) = (density* src).roundPixels -fun DisplayMetrics.dp(src:Int) = (density*src.toFloat()).roundPixels -fun Resources.dp(src:Float) = displayMetrics.dp(src) -fun Resources.dp(src:Int) = displayMetrics.dp(src) -fun Context.dp(src:Float) = resources.dp(src) -fun Context.dp(src:Int) = resources.dp(src) +fun DisplayMetrics.dp(src: Float) = (density * src).roundPixels +fun DisplayMetrics.dp(src: Int) = (density * src.toFloat()).roundPixels +fun Resources.dp(src: Float) = displayMetrics.dp(src) +fun Resources.dp(src: Int) = displayMetrics.dp(src) +fun Context.dp(src: Float) = resources.dp(src) +fun Context.dp(src: Int) = resources.dp(src)