2021-06-21 05:03:09 +02:00
|
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
|
|
|
|
import android.os.SystemClock
|
|
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
|
|
|
import jp.juggler.subwaytooter.R
|
2024-01-04 02:40:09 +01:00
|
|
|
|
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
|
2022-05-29 15:38:21 +02:00
|
|
|
|
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
2021-06-21 05:03:09 +02:00
|
|
|
|
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
2023-01-13 13:22:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.getVisibilityString
|
2021-11-20 01:36:43 +01:00
|
|
|
|
import jp.juggler.subwaytooter.pref.PrefB
|
2021-06-21 05:03:09 +02:00
|
|
|
|
import jp.juggler.subwaytooter.span.MyClickableSpan
|
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
2023-02-04 21:52:26 +01:00
|
|
|
|
import jp.juggler.subwaytooter.table.daoAcctColor
|
|
|
|
|
import jp.juggler.subwaytooter.table.daoSavedAccount
|
|
|
|
|
import jp.juggler.subwaytooter.table.daoTagHistory
|
2023-01-13 13:22:25 +01:00
|
|
|
|
import jp.juggler.util.coroutine.AppDispatchers
|
2024-01-04 02:40:09 +01:00
|
|
|
|
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
|
2023-01-13 13:22:25 +01:00
|
|
|
|
import jp.juggler.util.network.MEDIA_TYPE_JSON
|
|
|
|
|
import jp.juggler.util.network.toPostRequestBuilder
|
2022-06-03 17:03:10 +02:00
|
|
|
|
import kotlinx.coroutines.CancellationException
|
|
|
|
|
import kotlinx.coroutines.delay
|
|
|
|
|
import kotlinx.coroutines.withContext
|
2021-06-21 05:03:09 +02:00
|
|
|
|
import okhttp3.Request
|
|
|
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
2024-01-04 02:40:09 +01:00
|
|
|
|
import java.util.Calendar
|
|
|
|
|
import java.util.GregorianCalendar
|
|
|
|
|
import java.util.Locale
|
|
|
|
|
import java.util.TimeZone
|
2022-06-03 17:03:10 +02:00
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
2022-05-29 15:38:21 +02:00
|
|
|
|
sealed class PostResult {
|
|
|
|
|
class Normal(
|
|
|
|
|
val targetAccount: SavedAccount,
|
|
|
|
|
val status: TootStatus,
|
|
|
|
|
) : PostResult()
|
|
|
|
|
|
|
|
|
|
class Scheduled(
|
|
|
|
|
val targetAccount: SavedAccount,
|
|
|
|
|
) : PostResult()
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-11 00:11:49 +01:00
|
|
|
|
@Suppress("LongParameterList")
|
2021-06-21 05:03:09 +02:00
|
|
|
|
class PostImpl(
|
|
|
|
|
val activity: AppCompatActivity,
|
|
|
|
|
val account: SavedAccount,
|
|
|
|
|
val content: String,
|
|
|
|
|
|
|
|
|
|
// nullはCWチェックなしを示す // nullじゃなくてカラならエラー
|
|
|
|
|
val spoilerText: String?,
|
|
|
|
|
|
|
|
|
|
val visibilityArg: TootVisibility,
|
|
|
|
|
val bNSFW: Boolean,
|
|
|
|
|
val inReplyToId: EntityId?,
|
|
|
|
|
val attachmentListArg: List<PostAttachment>?,
|
|
|
|
|
enqueteItemsArg: List<String>?,
|
|
|
|
|
val pollType: TootPollsType?,
|
|
|
|
|
val pollExpireSeconds: Int,
|
|
|
|
|
val pollHideTotals: Boolean,
|
|
|
|
|
val pollMultipleChoice: Boolean,
|
|
|
|
|
val scheduledAt: Long,
|
|
|
|
|
val scheduledId: EntityId?,
|
|
|
|
|
val redraftStatusId: EntityId?,
|
2022-03-15 12:39:37 +01:00
|
|
|
|
val editStatusId: EntityId?,
|
2021-06-21 05:03:09 +02:00
|
|
|
|
val emojiMapCustom: HashMap<String, CustomEmoji>?,
|
|
|
|
|
var useQuoteToot: Boolean,
|
2022-06-03 16:50:49 +02:00
|
|
|
|
var lang: String,
|
2021-06-21 05:03:09 +02:00
|
|
|
|
) {
|
|
|
|
|
companion object {
|
|
|
|
|
private val log = LogCategory("PostImpl")
|
2023-09-03 13:21:28 +02:00
|
|
|
|
|
|
|
|
|
// ハッシュタグ内部の、半角数字以外のASCII文字にマッチする正規表現
|
|
|
|
|
// 単体テストで使うのでpublig
|
|
|
|
|
val reTagAsciiNotNumber = """[\x00-\x2f\x3a-\x7f]""".toRegex()
|
|
|
|
|
|
|
|
|
|
// ハッシュタグ内部の、非ASCII文字にマッチする正規表現
|
|
|
|
|
// 単体テストで使うのでpublig
|
|
|
|
|
val reTagNonAscii = """[^\x00-\x7f]""".toRegex()
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
|
|
|
|
private var lastPostTapped: Long = 0L
|
2022-06-03 17:03:10 +02:00
|
|
|
|
|
|
|
|
|
private val isPosting = AtomicBoolean(false)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val attachmentList = attachmentListArg?.mapNotNull { it.attachment }?.notEmpty()
|
|
|
|
|
|
|
|
|
|
// null だと投票を作成しない。空リストはエラー
|
|
|
|
|
private val enqueteItems = enqueteItemsArg?.filter { it.isNotEmpty() }
|
|
|
|
|
|
|
|
|
|
private var visibilityChecked: TootVisibility? = null
|
|
|
|
|
|
|
|
|
|
private val choiceMaxChars = when {
|
|
|
|
|
account.isMisskey -> 15
|
|
|
|
|
pollType == TootPollsType.FriendsNico -> 15
|
2022-06-03 16:50:49 +02:00
|
|
|
|
else -> 40 // TootPollsType.Mastodon
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-30 14:32:12 +02:00
|
|
|
|
private fun preCheckPollItemOne(list: List<String>, idx: Int, item: String) {
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
|
|
|
|
// 選択肢が長すぎる
|
|
|
|
|
val cpCount = item.codePointCount(0, item.length)
|
|
|
|
|
if (cpCount > choiceMaxChars) {
|
|
|
|
|
val over = cpCount - choiceMaxChars
|
2022-05-30 14:32:12 +02:00
|
|
|
|
activity.errorString(R.string.enquete_item_too_long, idx + 1, over)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 他の項目と重複している
|
|
|
|
|
if ((0 until idx).any { list[it] == item }) {
|
2022-05-30 14:32:12 +02:00
|
|
|
|
activity.errorString(R.string.enquete_item_duplicate, idx + 1)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
// may null, not error
|
2021-06-21 05:03:09 +02:00
|
|
|
|
private suspend fun getWebVisibility(
|
|
|
|
|
client: TootApiClient,
|
|
|
|
|
parser: TootParser,
|
|
|
|
|
instance: TootInstance,
|
2024-01-04 02:40:09 +01:00
|
|
|
|
): 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)
|
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun checkServerHasVisibility(
|
|
|
|
|
actual: TootVisibility?,
|
|
|
|
|
extra: TootVisibility,
|
|
|
|
|
instance: TootInstance,
|
|
|
|
|
checkFun: (TootInstance) -> Boolean,
|
|
|
|
|
) {
|
|
|
|
|
if (actual != extra || checkFun(instance)) return
|
2023-02-04 21:52:26 +01:00
|
|
|
|
val strVisibility = extra.getVisibilityString(account.isMisskey)
|
2024-01-04 02:40:09 +01:00
|
|
|
|
activity.errorApiResult(R.string.server_has_no_support_of_visibility, strVisibility)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun checkVisibility(
|
|
|
|
|
client: TootApiClient,
|
|
|
|
|
parser: TootParser,
|
|
|
|
|
instance: TootInstance,
|
|
|
|
|
): TootVisibility? {
|
|
|
|
|
val v = when (visibilityArg) {
|
|
|
|
|
TootVisibility.WebSetting -> getWebVisibility(client, parser, instance)
|
|
|
|
|
else -> visibilityArg
|
|
|
|
|
}
|
2021-11-20 01:36:43 +01:00
|
|
|
|
checkServerHasVisibility(
|
|
|
|
|
v,
|
|
|
|
|
TootVisibility.Mutual,
|
|
|
|
|
instance,
|
|
|
|
|
InstanceCapability::visibilityMutual
|
|
|
|
|
)
|
|
|
|
|
checkServerHasVisibility(
|
|
|
|
|
v,
|
|
|
|
|
TootVisibility.Limited,
|
|
|
|
|
instance,
|
|
|
|
|
InstanceCapability::visibilityLimited
|
|
|
|
|
)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun deleteStatus(client: TootApiClient) {
|
|
|
|
|
val result = if (account.isMisskey) {
|
|
|
|
|
client.request(
|
|
|
|
|
"/api/notes/delete",
|
|
|
|
|
account.putMisskeyApiToken(JsonObject()).apply {
|
|
|
|
|
put("noteId", redraftStatusId)
|
|
|
|
|
}.toPostRequestBuilder()
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
client.request(
|
|
|
|
|
"/api/v1/statuses/$redraftStatusId",
|
|
|
|
|
Request.Builder().delete()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
log.d("delete redraft. result=$result")
|
|
|
|
|
delay(2000L)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun deleteScheduledStatus(client: TootApiClient) {
|
|
|
|
|
val result = client.request(
|
|
|
|
|
"/api/v1/scheduled_statuses/$scheduledId",
|
|
|
|
|
Request.Builder().delete()
|
|
|
|
|
)
|
|
|
|
|
log.d("delete old scheduled status. result=$result")
|
|
|
|
|
delay(2000L)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun encodeParamsMisskey(json: JsonObject, client: TootApiClient) {
|
|
|
|
|
account.putMisskeyApiToken(json)
|
|
|
|
|
|
|
|
|
|
json["viaMobile"] = true
|
|
|
|
|
|
|
|
|
|
json["text"] = EmojiDecoder.decodeShortCode(content, emojiMapCustom = emojiMapCustom)
|
|
|
|
|
|
|
|
|
|
spoilerText?.notEmpty()?.let {
|
|
|
|
|
json["cw"] = EmojiDecoder.decodeShortCode(it, emojiMapCustom = emojiMapCustom)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inReplyToId?.toString()?.let {
|
|
|
|
|
json[if (useQuoteToot) "renoteId" else "replyId"] = it
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
when (val visibility = visibilityChecked) {
|
|
|
|
|
null -> Unit
|
|
|
|
|
TootVisibility.DirectSpecified, TootVisibility.DirectPrivate -> {
|
|
|
|
|
val userIds = JsonArray()
|
|
|
|
|
val m = TootAccount.reMisskeyMentionPost.matcher(content)
|
|
|
|
|
while (m.find()) {
|
|
|
|
|
val username = m.groupEx(1)
|
|
|
|
|
val host = m.groupEx(2) // may null
|
|
|
|
|
|
|
|
|
|
client.request(
|
|
|
|
|
"/api/users/show",
|
|
|
|
|
account.putMisskeyApiToken().apply {
|
|
|
|
|
username.notEmpty()?.let { put("username", it) }
|
|
|
|
|
host.notEmpty()?.let { put("host", it) }
|
|
|
|
|
}.toPostRequestBuilder()
|
|
|
|
|
)?.let { result ->
|
|
|
|
|
result.jsonObject?.string("id").notEmpty()?.let { userIds.add(it) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
json["visibility"] = when {
|
|
|
|
|
userIds.isNotEmpty() -> {
|
|
|
|
|
json["visibleUserIds"] = userIds
|
|
|
|
|
"specified"
|
|
|
|
|
}
|
2023-04-22 20:01:21 +02:00
|
|
|
|
|
2021-06-21 05:03:09 +02:00
|
|
|
|
account.misskeyVersion >= 11 -> "specified"
|
|
|
|
|
else -> "private"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
val localVis = visibility.strMisskey.replace("^local-".toRegex(), "")
|
|
|
|
|
if (localVis != visibility.strMisskey) json["localOnly"] = true
|
|
|
|
|
json["visibility"] = localVis
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (attachmentList != null) {
|
|
|
|
|
val array = JsonArray()
|
|
|
|
|
for (a in attachmentList) {
|
|
|
|
|
|
|
|
|
|
// Misskeyは画像の再利用に問題がないので redraftとバージョンのチェックは行わない
|
|
|
|
|
array.add(a.id.toString())
|
|
|
|
|
|
|
|
|
|
// Misskeyの場合、NSFWするにはアップロード済みの画像を drive/files/update で更新する
|
|
|
|
|
if (bNSFW) {
|
2024-01-04 02:40:09 +01:00
|
|
|
|
client.requestOrThrow(
|
2021-06-21 05:03:09 +02:00
|
|
|
|
"/api/drive/files/update",
|
|
|
|
|
account.putMisskeyApiToken().apply {
|
|
|
|
|
put("fileId", a.id.toString())
|
|
|
|
|
put("isSensitive", true)
|
2024-01-04 02:40:09 +01:00
|
|
|
|
}.toPostRequestBuilder()
|
2021-06-21 05:03:09 +02:00
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (array.isNotEmpty()) json["mediaIds"] = array
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (enqueteItems?.isNotEmpty() == true) {
|
|
|
|
|
val choices = JsonArray().apply {
|
|
|
|
|
for (item in enqueteItems) {
|
|
|
|
|
val text = EmojiDecoder.decodeShortCode(item, emojiMapCustom = emojiMapCustom)
|
|
|
|
|
if (text.isEmpty()) continue
|
|
|
|
|
add(text)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (choices.isNotEmpty()) {
|
|
|
|
|
json["poll"] = jsonObjectOf("choices" to choices)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun encodeParamsMastodon(json: JsonObject, instance: TootInstance) {
|
2022-03-16 09:04:19 +01:00
|
|
|
|
|
2022-06-03 16:50:49 +02:00
|
|
|
|
when (val lang = lang.trim()) {
|
2022-05-31 16:39:22 +02:00
|
|
|
|
// Web設定に従うなら指定しない
|
2022-06-03 16:50:49 +02:00
|
|
|
|
SavedAccount.LANG_WEB, "" -> Unit
|
2022-05-31 16:39:22 +02:00
|
|
|
|
// 端末の言語コード
|
2022-06-03 16:50:49 +02:00
|
|
|
|
SavedAccount.LANG_DEVICE ->
|
2022-05-31 16:39:22 +02:00
|
|
|
|
json["language"] = Locale.getDefault().language
|
|
|
|
|
// その他
|
2022-06-03 16:50:49 +02:00
|
|
|
|
else ->
|
2022-05-31 16:39:22 +02:00
|
|
|
|
json["language"] = lang
|
|
|
|
|
}
|
2022-03-16 09:04:19 +01:00
|
|
|
|
|
2021-06-21 05:03:09 +02:00
|
|
|
|
visibilityChecked?.let { json["visibility"] = it.strMastodon }
|
|
|
|
|
|
|
|
|
|
json["status"] = EmojiDecoder.decodeShortCode(content, emojiMapCustom = emojiMapCustom)
|
|
|
|
|
json["sensitive"] = bNSFW
|
2021-11-20 01:36:43 +01:00
|
|
|
|
json["spoiler_text"] =
|
|
|
|
|
EmojiDecoder.decodeShortCode(spoilerText ?: "", emojiMapCustom = emojiMapCustom)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
2021-11-20 01:36:43 +01:00
|
|
|
|
inReplyToId?.toString()
|
|
|
|
|
?.let { json[if (useQuoteToot) "quote_id" else "in_reply_to_id"] = it }
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
|
|
|
|
if (attachmentList != null) {
|
2023-01-13 13:22:25 +01:00
|
|
|
|
json["media_ids"] = buildJsonArray {
|
2021-06-21 05:03:09 +02:00
|
|
|
|
for (a in attachmentList) {
|
|
|
|
|
if (a.redraft && !instance.versionGE(TootInstance.VERSION_2_4_1)) continue
|
|
|
|
|
add(a.id.toString())
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-22 20:01:21 +02:00
|
|
|
|
attachmentList.mapNotNull { a ->
|
2023-04-22 08:41:17 +02:00
|
|
|
|
buildJsonObject {
|
2023-04-22 20:01:21 +02:00
|
|
|
|
put("id", a.id.toString())
|
|
|
|
|
a.updateDescription?.let { put("description", it) }
|
|
|
|
|
a.updateThumbnail?.let { put("thumbnail", it) }
|
|
|
|
|
a.updateFocus?.let { put("focus", it) }
|
|
|
|
|
}.takeIf { it.keys.size >= 2 }
|
|
|
|
|
}.notEmpty()?.toJsonArray()?.let {
|
|
|
|
|
json["media_attributes"] = it
|
2023-04-22 08:41:17 +02:00
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (enqueteItems != null) {
|
|
|
|
|
if (pollType == TootPollsType.Mastodon) {
|
2023-01-13 13:22:25 +01:00
|
|
|
|
json["poll"] = buildJsonObject {
|
2021-06-21 05:03:09 +02:00
|
|
|
|
put("multiple", pollMultipleChoice)
|
|
|
|
|
put("hide_totals", pollHideTotals)
|
|
|
|
|
put("expires_in", pollExpireSeconds)
|
|
|
|
|
put("options", enqueteItems.map {
|
|
|
|
|
EmojiDecoder.decodeShortCode(it, emojiMapCustom = emojiMapCustom)
|
|
|
|
|
}.toJsonArray())
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
json["isEnquete"] = true
|
|
|
|
|
json["enquete_items"] = enqueteItems.map {
|
|
|
|
|
EmojiDecoder.decodeShortCode(it, emojiMapCustom = emojiMapCustom)
|
|
|
|
|
}.toJsonArray()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scheduledAt != 0L) {
|
|
|
|
|
if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) {
|
2024-01-04 02:40:09 +01:00
|
|
|
|
activity.errorApiResult(R.string.scheduled_status_requires_mastodon_2_7_0)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
// UTCの日時を渡す
|
|
|
|
|
val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"))
|
|
|
|
|
c.timeInMillis = scheduledAt
|
|
|
|
|
val sv = String.format(
|
|
|
|
|
Locale.JAPANESE,
|
2021-08-24 01:03:03 +02:00
|
|
|
|
"%d-%02d-%02dT%02d:%02d:%02d.%03dZ",
|
2021-06-21 05:03:09 +02:00
|
|
|
|
c.get(Calendar.YEAR),
|
|
|
|
|
c.get(Calendar.MONTH) + 1,
|
|
|
|
|
c.get(Calendar.DAY_OF_MONTH),
|
|
|
|
|
c.get(Calendar.HOUR_OF_DAY),
|
|
|
|
|
c.get(Calendar.MINUTE),
|
2021-08-24 01:03:03 +02:00
|
|
|
|
c.get(Calendar.SECOND),
|
|
|
|
|
c.get(Calendar.MILLISECOND)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
)
|
|
|
|
|
json["scheduled_at"] = sv
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun saveStatusTag(status: TootStatus?) {
|
|
|
|
|
status ?: return
|
|
|
|
|
|
|
|
|
|
val s = status.decoded_content
|
|
|
|
|
val spanList = s.getSpans(0, s.length, MyClickableSpan::class.java)
|
|
|
|
|
if (spanList != null) {
|
|
|
|
|
val tagList = ArrayList<String?>(spanList.size)
|
|
|
|
|
for (span in spanList) {
|
|
|
|
|
val start = s.getSpanStart(span)
|
|
|
|
|
val end = s.getSpanEnd(span)
|
|
|
|
|
val text = s.subSequence(start, end).toString()
|
|
|
|
|
if (text.startsWith("#")) {
|
|
|
|
|
tagList.add(text.substring(1))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
val count = tagList.size
|
|
|
|
|
if (count > 0) {
|
2023-02-04 21:52:26 +01:00
|
|
|
|
daoTagHistory.saveList(System.currentTimeMillis(), tagList, 0, count)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-29 15:38:21 +02:00
|
|
|
|
suspend fun runSuspend(): PostResult {
|
2022-05-30 14:32:12 +02:00
|
|
|
|
|
2022-06-03 16:50:49 +02:00
|
|
|
|
if (account.isMisskey) {
|
2022-06-04 20:15:58 +02:00
|
|
|
|
attachmentList
|
|
|
|
|
?.notEmpty()
|
|
|
|
|
?.map { it.id.toString() }
|
|
|
|
|
?.takeIf { it != it.distinct() }
|
|
|
|
|
?.let {
|
|
|
|
|
activity.errorString(R.string.post_error_attachments_duplicated)
|
2022-05-30 14:32:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (content.isEmpty() && attachmentList == null) {
|
|
|
|
|
activity.errorString(R.string.post_error_contents_empty)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// nullはCWチェックなしを示す // nullじゃなくてカラならエラー
|
|
|
|
|
if (spoilerText != null && spoilerText.isEmpty()) {
|
|
|
|
|
activity.errorString(R.string.post_error_contents_warning_empty)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!enqueteItems.isNullOrEmpty()) {
|
|
|
|
|
if (enqueteItems.size < 2) {
|
|
|
|
|
activity.errorString(R.string.enquete_item_is_empty, enqueteItems.size + 1)
|
|
|
|
|
}
|
|
|
|
|
enqueteItems.forEachIndexed { i, v ->
|
|
|
|
|
preCheckPollItemOne(enqueteItems, i, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scheduledAt != 0L && account.isMisskey) {
|
|
|
|
|
error("misskey has no scheduled status API")
|
|
|
|
|
}
|
2022-05-29 15:38:21 +02:00
|
|
|
|
|
2023-02-04 21:52:26 +01:00
|
|
|
|
if (PrefB.bpWarnHashtagAsciiAndNonAscii.value) {
|
2022-05-29 15:38:21 +02:00
|
|
|
|
TootTag.findHashtags(content, account.isMisskey)
|
|
|
|
|
?.filter {
|
2023-09-03 13:21:28 +02:00
|
|
|
|
// タグがASCII文字(半角数字を除く)と非ASCII文字の両方を含むか?
|
|
|
|
|
val hasAscii = reTagAsciiNotNumber.containsMatchIn(it)
|
|
|
|
|
val hasNonAscii = reTagNonAscii.containsMatchIn(it)
|
|
|
|
|
hasAscii && hasNonAscii
|
2022-05-29 15:38:21 +02:00
|
|
|
|
}?.map { "#$it" }
|
|
|
|
|
?.notEmpty()
|
|
|
|
|
?.let { badTags ->
|
|
|
|
|
activity.confirm(
|
|
|
|
|
R.string.hashtag_contains_ascii_and_not_ascii,
|
|
|
|
|
badTags.joinToString(", ")
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val isMisskey = account.isMisskey
|
|
|
|
|
if (!visibilityArg.isTagAllowed(isMisskey)) {
|
|
|
|
|
TootTag.findHashtags(content, isMisskey)
|
|
|
|
|
?.notEmpty()
|
|
|
|
|
?.let { tags ->
|
|
|
|
|
log.d("findHashtags ${tags.joinToString(",")}")
|
|
|
|
|
activity.confirm(
|
|
|
|
|
R.string.hashtag_and_visibility_not_match
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (redraftStatusId != null) {
|
|
|
|
|
activity.confirm(R.string.delete_base_status_before_toot)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scheduledId != null) {
|
|
|
|
|
activity.confirm(R.string.delete_scheduled_status_before_update)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
activity.confirm(
|
2023-02-04 21:52:26 +01:00
|
|
|
|
activity.getString(
|
|
|
|
|
R.string.confirm_post_from,
|
|
|
|
|
daoAcctColor.getNickname(account)
|
|
|
|
|
),
|
2023-02-06 03:10:24 +01:00
|
|
|
|
account.confirmPost
|
2022-05-29 15:38:21 +02:00
|
|
|
|
) { newConfirmEnabled ->
|
2023-02-06 03:10:24 +01:00
|
|
|
|
account.confirmPost = newConfirmEnabled
|
|
|
|
|
daoSavedAccount.save(account)
|
2022-05-29 15:38:21 +02:00
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
|
|
|
|
// ボタン連打判定
|
|
|
|
|
val now = SystemClock.elapsedRealtime()
|
|
|
|
|
val delta = now - lastPostTapped
|
|
|
|
|
lastPostTapped = now
|
|
|
|
|
if (delta < 1000L) {
|
2022-05-30 14:32:12 +02:00
|
|
|
|
log.e("lastPostTapped within 1 sec!")
|
2021-06-21 05:03:09 +02:00
|
|
|
|
activity.showToast(false, R.string.post_button_tapped_repeatly)
|
2022-05-29 15:38:21 +02:00
|
|
|
|
throw CancellationException("post_button_tapped_repeatly")
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-03 17:03:10 +02:00
|
|
|
|
// 投稿中に再度投稿ボタンが押された
|
|
|
|
|
if (isPosting.get()) {
|
|
|
|
|
log.e("other postJob is active!")
|
|
|
|
|
activity.showToast(false, R.string.post_button_tapped_repeatly)
|
|
|
|
|
throw CancellationException("preCheck failed.")
|
|
|
|
|
}
|
|
|
|
|
// 全ての確認を終えたらバックグラウンドでの処理を開始する
|
|
|
|
|
isPosting.set(true)
|
|
|
|
|
return try {
|
2023-01-17 13:42:47 +01:00
|
|
|
|
withContext(AppDispatchers.MainImmediate) {
|
2024-01-04 02:40:09 +01:00
|
|
|
|
val (status, scheduled) = activity.runApiTask2(
|
|
|
|
|
accessInfo = account,
|
2022-06-03 17:03:10 +02:00
|
|
|
|
progressSetup = { it.setCanceledOnTouchOutside(false) },
|
|
|
|
|
) { client ->
|
2024-01-04 02:40:09 +01:00
|
|
|
|
val instance = TootInstance.getOrThrow(client)
|
2022-06-03 17:03:10 +02:00
|
|
|
|
|
|
|
|
|
if (instance.instanceType == InstanceType.Pixelfed) {
|
|
|
|
|
// Pixelfedは返信に画像を添付できない
|
|
|
|
|
if (inReplyToId != null && attachmentList != null) {
|
2024-01-04 02:40:09 +01:00
|
|
|
|
error(R.string.pixelfed_does_not_allow_reply_with_media)
|
2022-06-03 17:03:10 +02:00
|
|
|
|
}
|
2022-05-29 15:38:21 +02:00
|
|
|
|
|
2022-06-03 17:03:10 +02:00
|
|
|
|
// Pixelfedの返信ではない投稿は画像添付が必須
|
|
|
|
|
if (inReplyToId == null && attachmentList == null) {
|
2024-01-04 02:40:09 +01:00
|
|
|
|
error(R.string.pixelfed_does_not_allow_post_without_media)
|
2022-06-03 17:03:10 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
2022-06-03 17:03:10 +02:00
|
|
|
|
val parser = TootParser(this, account)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
// may null
|
|
|
|
|
this@PostImpl.visibilityChecked = checkVisibility(client, parser, instance)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
2022-06-03 17:03:10 +02:00
|
|
|
|
if (redraftStatusId != null) {
|
|
|
|
|
// 元の投稿を削除する
|
|
|
|
|
deleteStatus(client)
|
|
|
|
|
} else if (scheduledId != null) {
|
|
|
|
|
deleteScheduledStatus(client)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-03 17:03:10 +02:00
|
|
|
|
val json = JsonObject()
|
|
|
|
|
try {
|
|
|
|
|
if (account.isMisskey) {
|
|
|
|
|
encodeParamsMisskey(json, client)
|
|
|
|
|
} else {
|
|
|
|
|
encodeParamsMastodon(json, instance)
|
|
|
|
|
}
|
|
|
|
|
} catch (ex: JsonException) {
|
2024-01-04 02:40:09 +01:00
|
|
|
|
throw IllegalStateException("encoding status failed.", ex)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-03 17:03:10 +02:00
|
|
|
|
val bodyString = json.toString()
|
|
|
|
|
|
|
|
|
|
fun createRequestBuilder(isPut: Boolean = false): Request.Builder {
|
|
|
|
|
val requestBody = bodyString.toRequestBody(MEDIA_TYPE_JSON)
|
|
|
|
|
return when {
|
|
|
|
|
isPut -> Request.Builder().put(requestBody)
|
|
|
|
|
else -> Request.Builder().post(requestBody)
|
|
|
|
|
}.also {
|
2023-02-04 21:52:26 +01:00
|
|
|
|
if (!PrefB.bpDontDuplicationCheck.value) {
|
2022-06-03 17:03:10 +02:00
|
|
|
|
val digest = (bodyString + account.acct.ascii).digestSHA256Hex()
|
|
|
|
|
it.header("Idempotency-Key", digest)
|
|
|
|
|
}
|
2022-03-15 12:39:37 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
try {
|
|
|
|
|
val result = when {
|
|
|
|
|
account.isMisskey -> client.requestOrThrow(
|
|
|
|
|
"/api/notes/create",
|
|
|
|
|
createRequestBuilder()
|
|
|
|
|
)
|
2022-06-03 17:03:10 +02:00
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
editStatusId != null -> client.requestOrThrow(
|
|
|
|
|
"/api/v1/statuses/$editStatusId",
|
|
|
|
|
createRequestBuilder(isPut = true)
|
|
|
|
|
)
|
2023-04-22 20:01:21 +02:00
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
else -> client.requestOrThrow(
|
|
|
|
|
"/api/v1/statuses",
|
|
|
|
|
createRequestBuilder()
|
2022-06-03 17:03:10 +02:00
|
|
|
|
)
|
|
|
|
|
}
|
2024-01-04 02:40:09 +01:00
|
|
|
|
val jsonObject = result.jsonObject
|
|
|
|
|
when {
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
// 予約投稿完了
|
|
|
|
|
scheduledAt != 0L && jsonObject != null -> {
|
|
|
|
|
// {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]}
|
|
|
|
|
Pair(null, true)
|
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
// 通常投稿完了
|
|
|
|
|
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)")
|
2023-07-11 02:09:43 +02:00
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
errorMessage.contains("HTTP 404") ->
|
|
|
|
|
error("$ex\n${activity.getString(R.string.post_404_desc)}")
|
2023-07-11 02:09:43 +02:00
|
|
|
|
|
2024-01-04 02:40:09 +01:00
|
|
|
|
else -> throw ex
|
2023-07-11 02:09:43 +02:00
|
|
|
|
}
|
2022-06-03 17:03:10 +02:00
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
2024-01-04 02:40:09 +01:00
|
|
|
|
when {
|
|
|
|
|
scheduled -> PostResult.Scheduled(account)
|
|
|
|
|
|
|
|
|
|
status == null ->
|
|
|
|
|
error("can't parse status in API result.")
|
|
|
|
|
|
|
|
|
|
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る
|
|
|
|
|
else -> PostResult.Normal(account, status)
|
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
2022-06-03 17:03:10 +02:00
|
|
|
|
} finally {
|
|
|
|
|
isPosting.set(false)
|
2022-05-29 15:38:21 +02:00
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
}
|
|
|
|
|
}
|