package jp.juggler.subwaytooter.column import android.os.SystemClock import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.finder.* import jp.juggler.subwaytooter.auth.authRepo 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 import jp.juggler.util.data.JsonObject import jp.juggler.util.log.LogCategory import jp.juggler.util.log.withCaption import jp.juggler.util.network.toPostRequestBuilder import java.util.* @Suppress("ClassNaming") class ColumnTask_Loading( columnArg: Column, ) : ColumnTask(columnArg, ColumnTaskType.LOADING) { companion object { private val log = LogCategory("CT_Loading") } internal var listPinned: ArrayList? = null override suspend fun background(): TootApiResult? { ctStarted.set(true) if (PrefB.bpOpenSticker.value) { OpenSticker.loadAndWait() } val client = TootApiClient(context, callback = object : TootApiCallback { override suspend fun isApiCancelled() = isCancelled || column.isDispose.get() override suspend fun publishApiProgress(s: String) { runOnMainLooper { if (isCancelled) return@runOnMainLooper column.taskProgress = s column.fireShowContent(reason = "loading progress", changeList = ArrayList()) } } }) client.account = accessInfo try { context.authRepo.checkConfirmed(accessInfo, client) column.keywordFilterTrees = column.encodeFilterTree(column.loadFilter2(client)) if (!accessInfo.isNA) { val (instance, instanceResult) = TootInstance.get(client) instance ?: return instanceResult if (instance.instanceType == InstanceType.Pixelfed) { return TootApiResult("currently Pixelfed instance is not supported.") } } return column.type.loading(this, client) } catch (ex: Throwable) { return TootApiResult(ex.withCaption("loading failed.")) } finally { try { column.updateRelation(client, listTmp, column.whoAccount, parser) } catch (ex: Throwable) { log.e(ex, "updateRelation failed.") } ctClosed.set(true) runOnMainLooperDelayed(333L) { if (!isCancelled) column.fireShowColumnStatus() } } } override suspend fun handleResult(result: TootApiResult?) { if (column.isDispose.get()) return if (isCancelled || result == null) { return } column.bInitialLoading = false column.lastTask = null if (result.error != null) { column.mInitialLoadingError = "${result.error} ${result.requestInfo}".trim() } else { column.duplicateMap.clear() column.listData.clear() val listTmp = this.listTmp if (listTmp != null) { val listPinned = this.listPinned if (listPinned?.isNotEmpty() == true) { val listNew = column.duplicateMap.filterDuplicate(listPinned) column.listData.addAll(listNew) } val listNew = when (column.type) { // 検索カラムはIDによる重複排除が不可能 ColumnType.SEARCH -> listTmp // 編集履歴は投稿日時で重複排除する ColumnType.STATUS_HISTORY -> column.duplicateMap.filterDuplicateByCreatedAt( listTmp ) // 他のカラムは重複排除してから追加 else -> column.duplicateMap.filterDuplicate(listTmp) } column.listData.addAll(listNew) } // 初回ロード完了時はストリーミングを開始させる場合がある column.appState.streamManager.updateStreamingColumns() } column.fireShowContent(reason = "loading updated", reset = true) // 初期ロードの直後は先頭に移動する column.viewHolder?.scrollToTop() column.updateMisskeyCapture() } ///////////////////////////////////////////////////////////////// private fun addEmptyMessage(emptyMessage: String? = null) { if (emptyMessage != null && listTmp?.isEmpty() == true) { // フォロー/フォロワー一覧には警告の表示が必要だった val who = column.whoAccount?.get() if (!accessInfo.isMe(who)) { if (who != null && accessInfo.isRemoteUser(who)) { listTmp?.add( TootMessageHolder( context.getString(R.string.follow_follower_list_may_restrict) ) ) } listTmp?.add(TootMessageHolder(emptyMessage)) } } } private suspend fun loadMisskeyMaxId( logCaption: String, requester: suspend (EntityId?, EntityId?) -> TootApiResult?, arrayFinder: (JsonObject) -> JsonArray?, emptyMessage: String? = null, listParser: (parser: TootParser, jsonArray: JsonArray) -> List, adder: (List, Boolean) -> Unit, initialMaxId: EntityId? = null, ): TootApiResult? { val addToHead = false fun parseResult(result: TootApiResult?): Boolean { val first = listTmp?.isEmpty() != false result ?: return log.d("$logCaption: cancelled.") result.jsonObject?.let { if (column.pagingType == ColumnPagingType.Cursor) { column.idOld = EntityId.mayNull(it.string("next")) } result.data = arrayFinder(it) } val array = result.jsonArray ?: return log.w("$logCaption: missing item list") val src = listParser(parser, array) if (listTmp == null) listTmp = ArrayList(src.size) adder(src, addToHead) if (first) addEmptyMessage(emptyMessage) val more = when (column.pagingType) { ColumnPagingType.Default -> if (first) { column.saveRange(bBottom = true, bTop = true, result = result, list = src) } else { column.saveRangeBottom(result, src) } ColumnPagingType.Offset -> { column.offsetNext += src.size true } else -> true } return when { !more -> log.d("$logCaption: no more items") src.isEmpty() -> log.d("$logCaption: empty list") else -> true } } val timeStart = SystemClock.elapsedRealtime() // 初回の取得 val firstResult = requester(initialMaxId, null) var more = parseResult(firstResult) // フィルタなどが有効な場合は2回目以降の取得 while (more) more = when { isCancelled -> log.d("$logCaption: cancelled.") !column.isFilterEnabled -> log.d("$logCaption: isFiltered is false.") column.idOld == null -> log.d("$logCaption: idOld is empty.") (listTmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH -> log.d("$logCaption: read enough data.") SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT -> log.d("$logCaption: timeout.") else -> parseResult(requester(column.idOld, null)) } return firstResult } private suspend fun loadMisskeyMinId( logCaption: String, requester: suspend (EntityId?, EntityId?) -> TootApiResult?, arrayFinder: (JsonObject) -> JsonArray?, emptyMessage: String? = null, listParser: (parser: TootParser, jsonArray: JsonArray) -> List, adder: (List, Boolean) -> Unit, initialMinId: EntityId? = null, ): TootApiResult? { val addToHead = true fun parseResult(result: TootApiResult?): Boolean { val first = listTmp?.isEmpty() != false result ?: return log.d("$logCaption: cancelled") result.jsonObject?.let { result.data = arrayFinder(it) } val array = result.jsonArray ?: return log.w("$logCaption: missing item list") val src = listParser(parser, array) if (listTmp == null) listTmp = ArrayList(src.size) adder(src, addToHead) if (first && emptyMessage != null && listTmp?.isEmpty() == true) { // フォロー/フォロワー一覧には警告の表示が必要だった val who = column.whoAccount?.get() if (!accessInfo.isMe(who)) { if (who != null && accessInfo.isRemoteUser(who)) { listTmp?.add( TootMessageHolder( context.getString(R.string.follow_follower_list_may_restrict) ) ) } listTmp?.add(TootMessageHolder(emptyMessage)) } } val more = if (first) { column.saveRange(bBottom = true, bTop = true, result = result, list = src) } else { column.saveRangeTop(result, src) true } return when { !more -> log.d("$logCaption: no more items.") src.isEmpty() -> log.d("$logCaption: empty item list.") else -> true } } val timeStart = SystemClock.elapsedRealtime() // 初回の取得 val firstResult = requester(null, initialMinId) var more = parseResult(firstResult) // フィルタなどが有効な場合は2回目以降の取得 while (more) more = when { isCancelled -> log.d("$logCaption: cancelled.") !column.isFilterEnabled -> log.d("$logCaption: isFiltered is false.") column.idRecent == null -> log.d("$logCaption: idRecent is empty.") (listTmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH -> log.d("$logCaption: read enough data.") SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT -> log.d("$logCaption: timeout.") else -> parseResult(requester(null, column.idRecent)) } listTmp?.sortByDescending { it.getOrderId() } return firstResult } private suspend fun loadMastodonMaxId( logCaption: String, requester: suspend (maxId: EntityId?, minId: EntityId?) -> TootApiResult?, arrayFinder: (JsonObject) -> JsonArray?, emptyMessage: String? = null, listParser: (parser: TootParser, jsonArray: JsonArray) -> List, adder: (List, Boolean) -> Unit, initialMaxId: EntityId? = null, ): TootApiResult? { val addToHead = false fun parseResult(result: TootApiResult?): Boolean { val first = listTmp?.isEmpty() != false result ?: return log.d("$logCaption: cancelled.") result.jsonObject?.let { if (column.pagingType == ColumnPagingType.Cursor) { column.idOld = EntityId.mayNull(it.string("next")) } result.data = arrayFinder(it) } val array = result.jsonArray ?: return log.w("$logCaption: missing item list") val src = listParser(parser, array) if (listTmp == null) listTmp = ArrayList(src.size) adder(src, addToHead) if (first) addEmptyMessage(emptyMessage) val more = when (column.pagingType) { ColumnPagingType.Default -> if (first) { column.saveRange(bBottom = true, bTop = true, result = result, list = src) } else { column.saveRangeBottom(result, src) } ColumnPagingType.Offset -> { // idOldがないので2回目以降は発生しない column.offsetNext += src.size true } else -> true } return when { !more -> log.d("$logCaption: no more items.") src.isEmpty() -> log.d("$logCaption: empty list.") else -> true } } // 初回の取得 val timeStart = SystemClock.elapsedRealtime() val firstResult = requester(initialMaxId, null) var more = parseResult(firstResult) // フィルタなどが有効な場合は2回目以降の取得 while (more) more = when { isCancelled -> log.d("$logCaption: cancelled.") !column.isFilterEnabled -> log.d("$logCaption: isFiltered is false.") column.idOld == null -> log.d("$logCaption: idOld is empty.") (listTmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH -> log.d("$logCaption: read enough data.") SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT -> log.d("$logCaption: timeout.") else -> parseResult(requester(column.idOld, null)) } return firstResult } private suspend fun loadMastodonMinId( logCaption: String, requester: suspend (maxId: EntityId?, minId: EntityId?) -> TootApiResult?, arrayFinder: (JsonObject) -> JsonArray?, emptyMessage: String? = null, listParser: (parser: TootParser, jsonArray: JsonArray) -> List, adder: (List, Boolean) -> Unit, initialMinId: EntityId? = null, ): TootApiResult? { val addToHead = true fun parseResult(result: TootApiResult?): Boolean { val first = listTmp?.isEmpty() != false result ?: return log.d("cancelled.") result.jsonObject?.let { result.data = arrayFinder(it) } val array = result.jsonArray ?: return log.d("$logCaption: missing item list") val src = listParser(parser, array) if (listTmp == null) listTmp = ArrayList(src.size) adder(src, addToHead) // フォロー/フォロワー一覧には警告の表示が必要だった if (first && emptyMessage != null && listTmp?.isEmpty() == true) { val who = column.whoAccount?.get() if (!accessInfo.isMe(who)) { if (who != null && accessInfo.isRemoteUser(who)) { listTmp?.add( TootMessageHolder( context.getString(R.string.follow_follower_list_may_restrict) ) ) } listTmp?.add(TootMessageHolder(emptyMessage)) } } val more = if (first) { column.saveRange(bBottom = true, bTop = true, result = result, list = src) } else { column.saveRangeTop(result, src) true } return when { !more -> log.d("$logCaption: no more items.") src.isEmpty() -> log.d("$logCaption: empty item list.") else -> true } } val timeStart = SystemClock.elapsedRealtime() // 初回の取得 val firstResult = requester(null, initialMinId) var more = parseResult(firstResult) // フィルタなどが有効な場合は2回目以降の取得 while (more) more = when { isCancelled -> log.d("$logCaption: cancelled.") !column.isFilterEnabled -> log.d("$logCaption: isFiltered is false.") column.idRecent == null -> log.d("$logCaption: idRecent is empty.") (listTmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH -> log.d("$logCaption: read enough data.") SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT -> log.d("$logCaption: timeout.") else -> parseResult(requester(null, column.idRecent)) } return firstResult } ///////////////////////////////////////////////////////////////// // functions that called from ColumnType.loading lambda. suspend fun getStatusesPinned(client: TootApiClient, pathBase: String) { val result = client.request(pathBase) val jsonArray = result?.jsonArray if (jsonArray != null) { // val src = TootParser( context, accessInfo, pinned = true, highlightTrie = highlightTrie ).statusList(jsonArray) this.listPinned = addWithFilterStatus(null, src) // pinned tootにはページングの概念はない } log.d("getStatusesPinned: list size=${listPinned?.size ?: -1}") } suspend fun getStatusList( client: TootApiClient, pathBase: String?, initialMinId: EntityId? = null, initialMaxId: EntityId? = null, misskeyParams: JsonObject? = null, arrayFinder: (JsonObject) -> JsonArray? = nullArrayFinder, listParser: (parser: TootParser, jsonArray: JsonArray) -> List = defaultStatusListParser, ): TootApiResult? { pathBase ?: return null // cancelled. val logCaption = "getStatusList" val adder: (List, Boolean) -> Unit = { src, head -> this.listTmp = addWithFilterStatus(listTmp, src, head = head) } return if (isMisskey) { val params = misskeyParams ?: column.makeMisskeyTimelineParameter(parser) val requester: suspend (EntityId?, EntityId?) -> TootApiResult? = { maxId, minId -> client.request( pathBase, params.apply { when { maxId != null -> putMisskeyUntil(maxId) minId != null -> putMisskeySince(minId) } }.toPostRequestBuilder() ) } when { initialMinId != null -> loadMisskeyMinId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMinId = initialMinId ) else -> loadMisskeyMaxId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMaxId = initialMaxId ) } } else { val delimiter = if (-1 == pathBase.indexOf('?')) '?' else '&' val requester: suspend (maxId: EntityId?, minId: EntityId?) -> TootApiResult? = { maxId, minId -> client.request( when { maxId != null -> "$pathBase${delimiter}max_id=$maxId" minId != null -> "$pathBase${delimiter}min_id=$minId" else -> pathBase } ) } when { initialMinId != null -> loadMastodonMinId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMinId = initialMinId ) else -> loadMastodonMaxId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMaxId = initialMaxId ) } } } suspend fun getNotificationList( client: TootApiClient, fromAcct: String? = null, initialMinId: EntityId? = null, initialMaxId: EntityId? = null, ): TootApiResult? { val logCaption = "getNotificationList" val pathBase = column.makeNotificationUrl(client, fromAcct) val arrayFinder = nullArrayFinder val listParser: (TootParser, JsonArray) -> List = defaultNotificationListParser val adder: (List, Boolean) -> Unit = { src, head -> addWithFilterNotification(listTmp, src, head = head) } return if (isMisskey) { val params = column .makeMisskeyBaseParameter(parser) .addMisskeyNotificationFilter() val requester: suspend (EntityId?, EntityId?) -> TootApiResult? = { maxId, minId -> client.request( pathBase, params.apply { when { maxId != null -> putMisskeyUntil(maxId) minId != null -> putMisskeySince(minId) } }.toPostRequestBuilder() ) } when { initialMinId != null -> loadMisskeyMinId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMinId = initialMinId ) else -> loadMisskeyMaxId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMaxId = initialMaxId ) } } else { val delimiter = if (-1 == pathBase.indexOf('?')) '?' else '&' val requester: suspend (EntityId?, EntityId?) -> TootApiResult? = { maxId, minId -> client.request( when { maxId != null -> "$pathBase${delimiter}max_id=$maxId" minId != null -> "$pathBase${delimiter}min_id=$minId" else -> pathBase } ) } when { initialMinId != null -> loadMastodonMinId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMinId = initialMinId ) else -> loadMastodonMaxId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMaxId = initialMaxId ) } }.also { listTmp?.mapNotNull { it as? TootNotification }?.let { injectData(context, accessInfo, it) } } } suspend fun getAccountList( client: TootApiClient, pathBase: String, emptyMessage: String? = null, misskeyParams: JsonObject? = null, arrayFinder: (JsonObject) -> JsonArray? = nullArrayFinder, listParser: (parser: TootParser, jsonArray: JsonArray) -> List = defaultAccountListParser, initialMinId: EntityId? = null, initialMaxId: EntityId? = null, ): TootApiResult? { val logCaption = "getAccountList" val adder: (List, Boolean) -> Unit = { src, head -> addAll(listTmp, src, head = head) } return if (isMisskey) { val params = misskeyParams ?: column.makeMisskeyBaseParameter(parser) val requester: suspend (EntityId?, EntityId?) -> TootApiResult? = { maxId, minId -> client.request( pathBase, params.apply { when { maxId != null -> putMisskeyUntil(maxId) minId != null -> putMisskeySince(minId) } }.toPostRequestBuilder() ) } when { initialMinId != null -> loadMisskeyMinId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, emptyMessage = emptyMessage, listParser = listParser, adder = adder, initialMinId = initialMinId ) else -> loadMisskeyMaxId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, emptyMessage = emptyMessage, listParser = listParser, adder = adder, initialMaxId = initialMaxId ) } } else { val delimiter = if (-1 == pathBase.indexOf('?')) '?' else '&' val requester: suspend (EntityId?, EntityId?) -> TootApiResult? = { maxId, minId -> client.request( when { maxId != null -> "$pathBase${delimiter}max_id=$maxId" minId != null -> "$pathBase${delimiter}min_id=$minId" else -> pathBase } ) } when { initialMinId != null -> loadMastodonMinId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, emptyMessage = emptyMessage, listParser = listParser, adder = adder, initialMinId = initialMinId ) else -> loadMastodonMaxId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, emptyMessage = emptyMessage, listParser = listParser, adder = adder, initialMaxId = initialMaxId ) } } } suspend fun getConversationSummary( client: TootApiClient, pathBase: String, initialMinId: EntityId? = null, initialMaxId: EntityId? = null, misskeyParams: JsonObject? = null, listParser: (parser: TootParser, jsonArray: JsonArray) -> List = defaultConversationSummaryListParser, ): TootApiResult? { val logCaption = "getConversationSummary" val arrayFinder = nullArrayFinder val adder: (List, Boolean) -> Unit = { src, head -> addWithFilterConversationSummary(listTmp, src, head = head) } return if (isMisskey) { val params = misskeyParams ?: column.makeMisskeyTimelineParameter(parser) val requester: suspend (EntityId?, EntityId?) -> TootApiResult? = { maxId, minId -> client.request( pathBase, params.apply { when { maxId != null -> putMisskeyUntil(maxId) minId != null -> putMisskeySince(minId) } }.toPostRequestBuilder() ) } when { initialMinId != null -> loadMisskeyMinId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMinId = initialMinId ) else -> loadMisskeyMaxId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMaxId = initialMaxId ) } } else { val delimiter = if (-1 == pathBase.indexOf('?')) '?' else '&' val requester: suspend (EntityId?, EntityId?) -> TootApiResult? = { maxId, minId -> client.request( when { maxId != null -> "$pathBase${delimiter}max_id=$maxId" minId != null -> "$pathBase${delimiter}min_id=$minId" else -> pathBase } ) } when { initialMinId != null -> loadMastodonMinId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMinId = initialMinId ) else -> loadMastodonMaxId( logCaption = logCaption, requester = requester, arrayFinder = arrayFinder, listParser = listParser, adder = adder, initialMaxId = initialMaxId ) } } } suspend fun getDomainBlockList( client: TootApiClient, pathBase: String, ) = client.request(pathBase)?.also { result -> val src = TootDomainBlock.parseList(result.jsonArray) column.saveRange(bBottom = true, bTop = true, result = result, list = src) listTmp = addAll(null, src) } suspend fun getReportList( client: TootApiClient, pathBase: String, ) = client.request(pathBase)?.also { result -> val src = parseList(::TootReport, result.jsonArray) column.saveRange(bBottom = true, bTop = true, result = result, list = src) listTmp = addAll(null, src) } suspend fun getScheduledStatuses(client: TootApiClient): TootApiResult? { val result = client.request(ApiPath.PATH_SCHEDULED_STATUSES) val src = parseList(::TootScheduled, parser, result?.jsonArray) listTmp = addAll(listTmp, src) column.saveRange(bBottom = true, bTop = true, result = result, list = src) return result } suspend fun getEditHistory(client: TootApiClient): TootApiResult? { // ページングなし val result = client.request("/api/v1/statuses/${column.statusId}/history") // TootStatusとしては不足している情報があるのを補う TootStatus.supplyEditHistory(result?.jsonArray, column.originalStatus) val src = parser.statusList(result?.jsonArray).reversed() listTmp = addAll(listTmp, src) column.saveRange(bBottom = true, bTop = true, result = result, list = src) return result } suspend fun getFollowedHashtags(client: TootApiClient): TootApiResult? { val result = client.request("/api/v1/followed_tags") val src = parser.tagList(result?.jsonArray) listTmp = addAll(listTmp, src) column.saveRange(bBottom = true, bTop = true, result = result, list = src) return result } suspend fun getListList( client: TootApiClient, pathBase: String, misskeyParams: JsonObject? = null, ): TootApiResult? { val result = if (misskeyParams != null) { client.request(pathBase, misskeyParams.toPostRequestBuilder()) } else { client.request(pathBase) } if (result != null) { val src = parseList(::TootList, parser, result.jsonArray) src.sort() column.saveRange(bBottom = true, bTop = true, result = result, list = src) this.listTmp = addAll(null, src) } return result } 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) } if (result != null) { val src = TootFilter.parseList(result.jsonArray) this.listTmp = addAll(null, src ?: emptyList()) } return result } suspend fun getAntennaList( client: TootApiClient, pathBase: String, misskeyParams: JsonObject? = null, ): TootApiResult? { val result = if (misskeyParams != null) { client.request(pathBase, misskeyParams.toPostRequestBuilder()) } else { client.request(pathBase) } if (result != null) { val src = parseList(::MisskeyAntenna, result.jsonArray) column.saveRange(bBottom = true, bTop = true, result = result, list = src) this.listTmp = addAll(null, src) } return result } suspend fun getPublicTlAroundTime( client: TootApiClient, url: String, ): TootApiResult? { // (Mastodonのみ対応) val (instance, instanceResult) = TootInstance.get(client) instance ?: return instanceResult // ステータスIDに該当するトゥート // タンスをまたいだりすると存在しないかもしれないが、エラーは出さない var result: TootApiResult? = client.request(String.format(Locale.JAPAN, ApiPath.PATH_STATUSES, column.statusId)) val targetStatus = parser.status(result?.jsonObject) if (targetStatus != null) { listTmp = addOne(listTmp, targetStatus) } column.idOld = null column.idRecent = null var bInstanceTooOld = false if (instance.versionGE(TootInstance.VERSION_2_6_0)) { // 指定より新しいトゥート result = getStatusList(client, url, initialMinId = column.statusId) if (result == null || result.error != null) return result } else { bInstanceTooOld = true } // 指定位置より古いトゥート result = getStatusList(client, url, initialMaxId = column.statusId) if (result == null || result.error != null) return result listTmp?.sortByDescending { it.getOrderId() } if (bInstanceTooOld) { listTmp?.add( 0, TootMessageHolder(context.getString(R.string.around_toot_limitation_warning)) ) } return result } suspend fun getAccountTlAroundTime(client: TootApiClient): TootApiResult? { // (Mastodonのみ対応) val (instance, instanceResult) = TootInstance.get(client) instance ?: return instanceResult // ステータスIDに該当するトゥート // タンスをまたいだりすると存在しないかもしれない var result: TootApiResult? = client.request(String.format(Locale.JAPAN, ApiPath.PATH_STATUSES, column.statusId)) val targetStatus = parser.status(result?.jsonObject) ?: return result listTmp = addOne(listTmp, targetStatus) // ↑のトゥートのアカウントのID column.profileId = targetStatus.account.id val path = column.makeProfileStatusesUrl(column.profileId) column.idOld = null column.idRecent = null var bInstanceTooOld = false if (instance.versionGE(TootInstance.VERSION_2_6_0)) { // 指定より新しいトゥート result = getStatusList(client, path, initialMinId = column.statusId) if (result == null || result.error != null) return result } else { bInstanceTooOld = true } // 指定位置より古いトゥート result = getStatusList(client, path, initialMaxId = column.statusId) if (result == null || result.error != null) return result listTmp?.sortByDescending { it.getOrderId() } if (bInstanceTooOld) { listTmp?.add( 0, TootMessageHolder(context.getString(R.string.around_toot_limitation_warning)) ) } return result } suspend fun getConversation( client: TootApiClient, withReference: Boolean = false, ): TootApiResult? { return if (isMisskey) { // 指定された発言そのもの val queryParams = column.makeMisskeyBaseParameter(parser).apply { put("noteId", column.statusId) } var result = client.request( "/api/notes/show", queryParams.toPostRequestBuilder() ) val jsonObject = result?.jsonObject ?: return result val targetStatus = parser.status(jsonObject) ?: return TootApiResult("TootStatus parse failed.") targetStatus.conversation_main = true // 祖先 val listAsc = ArrayList() while (true) { if (client.isApiCancelled()) return null queryParams["offset"] = listAsc.size result = client.request( "/api/notes/conversation", queryParams.toPostRequestBuilder() ) val jsonArray = result?.jsonArray ?: return result val src = parser.statusList(jsonArray) if (src.isEmpty()) break listAsc.addAll(src) } // 直接の子リプライ。(子孫をたどることまではしない) val listDesc = ArrayList() val idSet = HashSet() var untilId: EntityId? = null while (true) { if (client.isApiCancelled()) return null when { untilId == null -> { queryParams.remove("untilId") queryParams.remove("offset") } misskeyVersion >= 11 -> { queryParams["untilId"] = untilId.toString() } else -> queryParams["offset"] = listDesc.size } result = client.request( "/api/notes/replies", queryParams.toPostRequestBuilder() ) val jsonArray = result?.jsonArray ?: return result val src = parser.statusList(jsonArray) untilId = null for (status in src) { if (idSet.contains(status.id)) continue idSet.add(status.id) listDesc.add(status) untilId = status.id } if (untilId == null) break } // 一つのリストにまとめる this.listTmp = ArrayList( listAsc.size + listDesc.size + 2 ).apply { addAll(listAsc.sortedBy { it.time_created_at }) add(targetStatus) addAll(listDesc.sortedBy { it.time_created_at }) add(TootMessageHolder(context.getString(R.string.misskey_cant_show_all_descendants))) } // result } else { // 指定された発言そのもの var result = client.request( String.format(Locale.JAPAN, ApiPath.PATH_STATUSES, column.statusId) ) var jsonObject = result?.jsonObject ?: return result val targetStatus = parser.status(jsonObject) ?: return TootApiResult("TootStatus parse failed.") // 前後の会話 result = client.request( "/api/v1/statuses/${column.statusId}/context${ when (withReference) { true -> "?with_reference=true" else -> "" } }" ) jsonObject = result?.jsonObject ?: return result val conversationContext = parseItem(::TootContext, parser, jsonObject) // 一つのリストにまとめる targetStatus.conversation_main = true if (conversationContext != null) { this.listTmp = ArrayList( (conversationContext.ancestors?.size ?: 0) + (conversationContext.descendants?.size ?: 0) + 1 ) if (conversationContext.references != null) { addWithFilterStatus(this.listTmp, conversationContext.references) } if (conversationContext.ancestors != null) { addWithFilterStatus(this.listTmp, conversationContext.ancestors) } addOne(listTmp, targetStatus) if (conversationContext.descendants != null) { addWithFilterStatus(this.listTmp, conversationContext.descendants) } } else { this.listTmp = addOne(this.listTmp, targetStatus) this.listTmp = addOne( this.listTmp, TootMessageHolder(context.getString(R.string.toot_context_parse_failed)) ) } result } } suspend fun getSearch(client: TootApiClient): TootApiResult? { return if (isMisskey) { var result: TootApiResult? = TootApiResult() val parser = TootParser(context, accessInfo) listTmp = ArrayList() val queryAccount = column.searchQuery.trim().replace("^@".toRegex(), "") if (queryAccount.isNotEmpty()) { result = client.request( "/api/users/search", accessInfo.putMisskeyApiToken().apply { put("query", queryAccount) put("localOnly", !column.searchResolve) }.toPostRequestBuilder() ) val jsonArray = result?.jsonArray if (jsonArray != null) { val src = TootParser(context, accessInfo).accountList(jsonArray) listTmp = addAll(listTmp, src) } } val queryTag = column.searchQuery.trim().replace("^#".toRegex(), "") if (queryTag.isNotEmpty()) { result = client.request( "/api/hashtags/search", accessInfo.putMisskeyApiToken().apply { put("query", queryTag) }.toPostRequestBuilder() ) val jsonArray = result?.jsonArray if (jsonArray != null) { val src = TootTag.parseList(parser, jsonArray) listTmp = addAll(listTmp, src) } } if (column.searchQuery.isNotEmpty()) { result = client.request( "/api/notes/search", accessInfo.putMisskeyApiToken().apply { put("query", column.searchQuery) } .toPostRequestBuilder() ) val jsonArray = result?.jsonArray if (jsonArray != null) { val src = parser.statusList(jsonArray) listTmp = addWithFilterStatus(listTmp, src) if (src.isNotEmpty()) { val (ti, _) = TootInstance.get(client) if (ti?.versionGE(TootInstance.MISSKEY_VERSION_12) == true) { addOne(listTmp, TootSearchGap(TootSearchGap.SearchType.Status)) } } } } // 検索機能が無効だとsearch_query が 400を返すが、他のAPIがデータを返したら成功したことにする if (listTmp?.isNotEmpty() == true) { TootApiResult() } else { result } } else { if (accessInfo.isPseudo) { // 1.5.0rc からマストドンの検索APIは認証を要求するようになった return TootApiResult(context.getString(R.string.search_is_not_available_on_pseudo_account)) } val (instance, instanceResult) = TootInstance.get(client) instance ?: return instanceResult val (apiResult, searchResult) = client.requestMastodonSearch( parser, q = column.searchQuery, resolve = column.searchResolve, ) if (searchResult != null) { listTmp = ArrayList() addAll(listTmp, searchResult.hashtags) if (searchResult.searchApiVersion >= 2 && searchResult.hashtags.isNotEmpty()) { addOne(listTmp, TootSearchGap(TootSearchGap.SearchType.Hashtag)) } addAll(listTmp, searchResult.accounts) if (searchResult.searchApiVersion >= 2 && searchResult.accounts.isNotEmpty()) { addOne(listTmp, TootSearchGap(TootSearchGap.SearchType.Account)) } addAll(listTmp, searchResult.statuses) if (searchResult.searchApiVersion >= 2 && searchResult.statuses.isNotEmpty()) { addOne(listTmp, TootSearchGap(TootSearchGap.SearchType.Status)) } } return apiResult } } }