Mastodon 4.0以降で単語フィルタの削除に失敗していた

This commit is contained in:
tateisu 2024-01-04 10:40:09 +09:00
parent 77d780881c
commit 2ed3ebe0f1
15 changed files with 362 additions and 276 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<TootFilter>? = 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<TootFilter> {
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)
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<TootFilter>? {
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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