Mastodon 4.0以降で単語フィルタの削除に失敗していた
This commit is contained in:
parent
77d780881c
commit
2ed3ebe0f1
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue