SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/ColumnUtil.kt

536 lines
16 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package jp.juggler.subwaytooter
import jp.juggler.subwaytooter.Column.Companion.READ_LIMIT
import jp.juggler.subwaytooter.Column.Companion.log
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.syncAccountByAcct
import jp.juggler.util.*
import java.util.*
internal inline fun <reified T : TimelineItem> addAll(
dstArg : ArrayList<TimelineItem>?,
src : List<T>,
head : Boolean = false
) : ArrayList<TimelineItem> =
(dstArg ?: ArrayList(src.size)).apply {
if(head) {
addAll(0, src)
} else {
addAll(src)
}
}
internal fun addOne(
dstArg : ArrayList<TimelineItem>?,
item : TimelineItem?,
head : Boolean = false
) : ArrayList<TimelineItem> =
(dstArg ?: ArrayList()).apply {
if(item != null) {
if(head) {
add(0, item)
} else {
add(item)
}
}
}
internal fun ColumnTask.addWithFilterStatus(
dstArg : ArrayList<TimelineItem>?,
srcArg : List<TootStatus>,
head : Boolean = false
) : ArrayList<TimelineItem> =
(dstArg ?: ArrayList(srcArg.size)).apply {
val src = srcArg.filter { ! column.isFiltered(it) }
if(head) {
addAll(0, src)
} else {
addAll(src)
}
}
internal fun ColumnTask.addWithFilterConversationSummary(
dstArg : ArrayList<TimelineItem>?,
srcArg : List<TootConversationSummary>,
head : Boolean = false
) : ArrayList<TimelineItem> =
(dstArg ?: ArrayList(srcArg.size)).apply {
val src = srcArg.filter { ! column.isFiltered(it.last_status) }
if(head) {
addAll(0, src)
} else {
addAll(src)
}
}
internal fun ColumnTask.addWithFilterNotification(
dstArg : ArrayList<TimelineItem>?,
srcArg : List<TootNotification>,
head : Boolean = false
) : ArrayList<TimelineItem> =
(dstArg ?: ArrayList(srcArg.size)).apply {
val src = srcArg.filter { ! column.isFiltered(it) }
if(head) {
addAll(0, src)
} else {
addAll(src)
}
}
internal fun Column.dispatchProfileTabStatus() =
when {
isMisskey -> ColumnType.ProfileStatusMisskey
else -> ColumnType.ProfileStatusMastodon
}
internal fun Column.dispatchProfileTabFollowing() =
when {
misskeyVersion >= 11 -> ColumnType.FollowingMisskey11
isMisskey -> ColumnType.FollowingMisskey10
access_info.isPseudo -> ColumnType.FollowingMastodonPseudo
else -> ColumnType.FollowingMastodon
}
internal fun Column.dispatchProfileTabFollowers() =
when {
misskeyVersion >= 11 -> ColumnType.FollowersMisskey11
isMisskey -> ColumnType.FollowersMisskey10
access_info.isPseudo -> ColumnType.FollowersMastodonPseudo
else -> ColumnType.FollowersMastodon
}
internal fun ColumnTask.dispatchProfileTabStatus() =
column.dispatchProfileTabStatus()
internal fun ColumnTask.dispatchProfileTabFollowing() =
column.dispatchProfileTabFollowing()
internal fun ColumnTask.dispatchProfileTabFollowers() =
column.dispatchProfileTabFollowers()
internal fun Column.loadListInfo(client : TootApiClient, bForceReload : Boolean) {
val parser = TootParser(context, access_info)
if(bForceReload || this.list_info == null) {
val result = if(isMisskey) {
client.request(
"/api/users/lists/show",
makeMisskeyBaseParameter(parser).apply {
put("listId", profile_id)
}.toPostRequestBuilder()
)
} else {
client.request("/api/v1/lists/${profile_id.toString()}")
}
val jsonObject = result?.jsonObject
if(jsonObject != null) {
val data = parseItem(::TootList, parser, jsonObject)
if(data != null) {
this.list_info = data
client.publishApiProgress("") // カラムヘッダの再表示
}
}
}
}
internal fun Column.loadAntennaInfo(client : TootApiClient, bForceReload : Boolean) {
val parser = TootParser(context, access_info)
if(bForceReload || this.antenna_info == null) {
val result = if(isMisskey) {
client.request(
"/api/antennas/show",
makeMisskeyBaseParameter(parser).apply {
put("antennaId", profile_id)
}.toPostRequestBuilder()
)
} else {
TootApiResult("antenna feature is not supported on Mastodon")
}
val jsonObject = result?.jsonObject
if(jsonObject != null) {
val data = parseItem(::MisskeyAntenna, jsonObject)
if(data != null) {
this.antenna_info = data
client.publishApiProgress("") // カラムヘッダの再表示
}
}
}
}
internal fun JsonObject.putMisskeyUntil(id : EntityId?) : JsonObject {
if(id != null) put("untilId", id.toString())
return this
}
internal fun JsonObject.putMisskeySince(id : EntityId?) : JsonObject {
if(id != null) put("sinceId", id.toString())
return this
}
internal fun JsonObject.addRangeMisskey(column : Column, bBottom : Boolean) : JsonObject {
if(bBottom) {
putMisskeyUntil(column.idOld)
} else {
putMisskeySince(column.idRecent)
}
return this
}
internal fun JsonObject.addMisskeyNotificationFilter(column : Column) : JsonObject {
when(column.quick_filter) {
Column.QUICK_FILTER_ALL -> {
val excludeList = jsonArray {
// Misskeyのお気に入りは通知されない
// if(dont_show_favourite) ...
if(column.dont_show_boost) {
add("renote")
add("quote")
}
if(column.dont_show_follow) {
add("follow")
add("receiveFollowRequest")
}
if(column.dont_show_reply) {
add("mention")
add("reply")
}
if(column.dont_show_reaction) {
add("reaction")
}
if(column.dont_show_vote) {
add("poll_vote")
}
if(column.dont_show_normal_toot) {
// FIXME Misskeyには特定フォロー者からの投稿を通知する機能があるのか
}
}
if(excludeList.isNotEmpty()) put("excludeTypes", excludeList)
}
// QUICK_FILTER_FAVOURITE // misskeyはお気に入りの通知はない
Column.QUICK_FILTER_BOOST -> put(
"includeTypes",
jsonArray("renote", "quote")
)
Column.QUICK_FILTER_FOLLOW -> put(
"includeTypes",
jsonArray("follow", "receiveFollowRequest")
)
Column.QUICK_FILTER_MENTION -> put(
"includeTypes",
jsonArray("mention", "reply")
)
Column.QUICK_FILTER_REACTION -> put("includeTypes", jp.juggler.util.jsonArray("reaction"))
Column.QUICK_FILTER_VOTE -> put("includeTypes", jp.juggler.util.jsonArray("poll_vote"))
Column.QUICK_FILTER_POST -> {
// FIXME Misskeyには特定フォロー者からの投稿を通知する機能があるのか
}
}
return this
}
internal fun JsonObject.putMisskeyParamsTimeline(column : Column) : JsonObject {
if(column.with_attachment && ! column.with_highlight) {
put("mediaOnly", true)
put("withMedia", true)
put("withFiles", true)
put("media", true)
}
return this
}
internal fun Column.makeHashtagAcctUrl(client : TootApiClient) : String? {
return if(isMisskey) {
// currently not supported
null
} else {
if(profile_id == null) {
val (result, whoRef) = client.syncAccountByAcct(access_info, hashtag_acct)
result ?: return null // cancelled.
if(whoRef == null) {
log.w("makeHashtagAcctUrl: ${result.error ?: "?"}")
return null
}
profile_id = whoRef.get().id
}
val sb = StringBuilder("/api/v1/accounts/${profile_id}/statuses")
.append("?limit=").append(READ_LIMIT)
.append("&tagged=").append(hashtag.encodePercent())
if(with_attachment) sb.append("&only_media=true")
if(instance_local) sb.append("&local=true")
sb.append(makeHashtagExtraQuery())
sb.toString()
}
}
internal fun Column.makeMisskeyBaseParameter(parser : TootParser?) =
access_info.putMisskeyApiToken().apply {
if(access_info.isMisskey) {
if(parser != null) parser.serviceType = ServiceType.MISSKEY
put("limit", 40)
}
}
internal fun Column.makeMisskeyParamsUserId(parser : TootParser) =
makeMisskeyBaseParameter(parser).apply {
put("userId", profile_id.toString())
}
internal fun Column.makeMisskeyTimelineParameter(parser : TootParser) =
makeMisskeyBaseParameter(parser).apply {
putMisskeyParamsTimeline(this@makeMisskeyTimelineParameter)
}
internal fun Column.makeMisskeyParamsProfileStatuses(parser : TootParser) =
makeMisskeyParamsUserId(parser).apply {
putMisskeyParamsTimeline(this@makeMisskeyParamsProfileStatuses)
if(! dont_show_reply) put("includeReplies", true)
if(! dont_show_boost) put("includeMyRenotes", true)
}
const val PATH_LOCAL = "/api/v1/timelines/public?local=true&limit=$READ_LIMIT"
internal fun Column.makePublicLocalUrl() : String {
return when {
access_info.isMisskey -> "/api/notes/local-timeline"
with_attachment -> "${PATH_LOCAL}&only_media=true" // mastodon 2.3 or later
else -> PATH_LOCAL
}
}
internal fun Column.makeMisskeyHybridTlUrl() : String {
return when {
access_info.isMisskey -> "/api/notes/hybrid-timeline"
else -> makePublicLocalUrl()
}
}
internal fun Column.makeDomainTimelineUrl() : String {
val base = "/api/v1/timelines/public?domain=$instance_uri&limit=$READ_LIMIT"
return when {
access_info.isMisskey -> "/api/notes/local-timeline"
with_attachment -> "$base&only_media=true"
else -> base
}
}
internal fun Column.makePublicFederateUrl() : String {
return if(access_info.isMisskey) {
"/api/notes/global-timeline"
} else {
val sb = StringBuilder("/api/v1/timelines/public?limit=$READ_LIMIT")
if(with_attachment) sb.append("&only_media=true")
if(remote_only) sb.append("&remote=true")
sb.toString()
}
}
const val PATH_HOME = "/api/v1/timelines/home?limit=$READ_LIMIT"
internal fun Column.makeHomeTlUrl() : String {
return when {
access_info.isMisskey -> "/api/notes/timeline"
with_attachment -> "$PATH_HOME&only_media=true"
else -> PATH_HOME
}
}
internal fun Column.makeNotificationUrl(
client : TootApiClient,
fromAcct : String? = null
) : String {
return when {
access_info.isMisskey -> "/api/i/notifications"
else -> {
val sb = StringBuilder(Column.PATH_NOTIFICATIONS) // always contain "?limit=XX"
when(val quick_filter = quick_filter) {
Column.QUICK_FILTER_ALL -> {
if(dont_show_favourite) sb.append("&exclude_types[]=favourite")
if(dont_show_boost) sb.append("&exclude_types[]=reblog")
if(dont_show_follow) sb.append("&exclude_types[]=follow")
if(dont_show_reply) sb.append("&exclude_types[]=mention")
if(dont_show_vote) sb.append("&exclude_types[]=poll")
if(dont_show_normal_toot) sb.append("&exclude_types[]=status")
}
else -> {
if(quick_filter != Column.QUICK_FILTER_FAVOURITE) sb.append("&exclude_types[]=favourite")
if(quick_filter != Column.QUICK_FILTER_BOOST) sb.append("&exclude_types[]=reblog")
if(quick_filter != Column.QUICK_FILTER_FOLLOW) sb.append("&exclude_types[]=follow")
if(quick_filter != Column.QUICK_FILTER_MENTION) sb.append("&exclude_types[]=mention")
if(quick_filter != Column.QUICK_FILTER_POST) sb.append("&exclude_types[]=status")
}
}
if(fromAcct?.isNotEmpty() == true) {
if(profile_id == null) {
val (result, whoRef) = client.syncAccountByAcct(access_info, hashtag_acct)
if(result != null) {
whoRef ?: error(result.error ?: "unknown error")
profile_id = whoRef.get().id
}
}
if(profile_id != null) {
sb.append("&account_id=").append(profile_id.toString())
}
}
// reaction,voteはmastodonにはない
sb.toString()
}
}
}
internal fun Column.makeListTlUrl() : String {
return if(isMisskey) {
"/api/notes/user-list-timeline"
} else {
"/api/v1/timelines/list/${profile_id}?limit=$READ_LIMIT"
}
}
internal fun Column.makeAntennaTlUrl() : String {
return if(isMisskey) {
"/api/antennas/notes"
} else {
"/nonexistent" // Mastodonにはアンテナ機能はない
}
}
internal fun Column.makeHashtagExtraQuery() : String {
val sb = StringBuilder()
hashtag_any.split(" ").filter { it.isNotEmpty() }.forEach {
sb.append("&any[]=").append(it.encodePercent())
}
hashtag_all.split(" ").filter { it.isNotEmpty() }.forEach {
sb.append("&all[]=").append(it.encodePercent())
}
hashtag_none.split(" ").filter { it.isNotEmpty() }.forEach {
sb.append("&none[]=").append(it.encodePercent())
}
return sb.toString()
}
internal fun Column.makeHashtagUrl() : String {
return if(isMisskey) {
"/api/notes/search_by_tag"
} else {
// hashtag : String // 先頭の#を含まない
val sb = StringBuilder("/api/v1/timelines/tag/")
.append(hashtag.encodePercent())
.append("?limit=").append(READ_LIMIT)
if(with_attachment) sb.append("&only_media=true")
if(instance_local) sb.append("&local=true")
sb
.append(makeHashtagExtraQuery())
.toString()
}
}
internal fun Column.makeHashtagParams(parser : TootParser) =
makeMisskeyTimelineParameter(parser).apply {
put("tag", hashtag)
put("limit", Column.MISSKEY_HASHTAG_LIMIT)
}
// mastodon用
internal fun Column.makeProfileStatusesUrl(profile_id : EntityId?) : String {
var path = "/api/v1/accounts/$profile_id/statuses?limit=$READ_LIMIT"
if(with_attachment && ! with_highlight) path += "&only_media=1"
if(dont_show_boost) path += "&exclude_reblogs=1"
if(dont_show_reply) path += "&exclude_replies=1"
return path
}
internal val misskeyArrayFinderUsers = { it : JsonObject ->
it.jsonArray("users")
}
////////////////////////////////////////////////////////////////////////////////
// account list parser
internal val nullArrayFinder : (JsonObject) -> JsonArray? =
{ null }
internal val defaultAccountListParser : (parser : TootParser, jsonArray : JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> parser.accountList(jsonArray) }
private fun misskeyUnwrapRelationAccount(parser : TootParser, srcList : JsonArray, key : String) =
srcList.objectList().mapNotNull {
when(val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> TootAccountRef.mayNull(parser, parser.account(it.jsonObject(key)))
?.apply { _orderId = relationId }
}
}
internal val misskey11FollowingParser : (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "followee") }
internal val misskey11FollowersParser : (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
internal val misskeyCustomParserFollowRequest : (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
internal val misskeyCustomParserMutes : (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "mutee") }
internal val misskeyCustomParserBlocks : (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "blockee") }
////////////////////////////////////////////////////////////////////////////////
// status list parser
internal val defaultStatusListParser : (parser : TootParser, jsonArray : JsonArray) -> List<TootStatus> =
{ parser, jsonArray -> parser.statusList(jsonArray) }
internal val misskeyCustomParserFavorites : (TootParser, JsonArray) -> List<TootStatus> =
{ parser, jsonArray ->
jsonArray.objectList().mapNotNull {
when(val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> parser.status(it.jsonObject("note"))?.apply {
favourited = true
_orderId = relationId
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// notification list parser
val defaultNotificationListParser : (parser : TootParser, jsonArray : JsonArray) -> List<TootNotification> =
{ parser, jsonArray -> parser.notificationList(jsonArray) }
val defaultDomainBlockListParser : (parser : TootParser, jsonArray : JsonArray) -> List<TootDomainBlock> =
{ _, jsonArray -> TootDomainBlock.parseList(jsonArray) }
val defaultReportListParser : (parser : TootParser, jsonArray : JsonArray) -> List<TootReport> =
{ _, jsonArray -> parseList(::TootReport, jsonArray) }
val defaultConversationSummaryListParser : (parser : TootParser, jsonArray : JsonArray) -> List<TootConversationSummary> =
{ parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) }