2018-01-04 19:52:25 +01:00
|
|
|
|
package jp.juggler.subwaytooter.api.entity
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.text.Spannable
|
2019-08-30 06:02:08 +02:00
|
|
|
|
import android.text.SpannableStringBuilder
|
2019-08-30 05:05:19 +02:00
|
|
|
|
import android.widget.TextView
|
|
|
|
|
import jp.juggler.subwaytooter.R
|
2018-11-06 02:29:33 +01:00
|
|
|
|
import jp.juggler.subwaytooter.api.MisskeyAccountDetailMap
|
2018-01-21 17:47:13 +01:00
|
|
|
|
import jp.juggler.subwaytooter.api.TootParser
|
2021-05-19 22:14:30 +02:00
|
|
|
|
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
2021-11-20 01:36:43 +01:00
|
|
|
|
import jp.juggler.subwaytooter.pref.PrefB
|
2019-08-30 06:02:08 +02:00
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
2019-01-28 19:02:09 +01:00
|
|
|
|
import jp.juggler.subwaytooter.table.UserRelation
|
2023-02-04 21:52:26 +01:00
|
|
|
|
import jp.juggler.subwaytooter.table.daoUserRelation
|
2019-08-30 06:02:08 +02:00
|
|
|
|
import jp.juggler.subwaytooter.util.DecodeOptions
|
2020-09-07 14:20:11 +02:00
|
|
|
|
import jp.juggler.subwaytooter.util.LinkHelper
|
2019-12-15 14:01:42 +01:00
|
|
|
|
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
|
2020-09-08 01:55:15 +02:00
|
|
|
|
import jp.juggler.subwaytooter.util.matchHost
|
2019-08-30 06:02:08 +02:00
|
|
|
|
import jp.juggler.subwaytooter.view.MyLinkMovementMethod
|
2018-12-01 00:02:18 +01:00
|
|
|
|
import jp.juggler.util.*
|
2023-01-13 13:22:25 +01:00
|
|
|
|
import jp.juggler.util.data.*
|
|
|
|
|
import jp.juggler.util.log.LogCategory
|
|
|
|
|
import jp.juggler.util.ui.vg
|
2018-11-06 02:29:33 +01:00
|
|
|
|
import java.util.*
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import java.util.regex.Pattern
|
|
|
|
|
|
2020-12-07 13:23:14 +01:00
|
|
|
|
open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
|
|
|
|
|
|
|
|
|
|
//URL of the user's profile page (can be remote)
|
|
|
|
|
// https://mastodon.juggler.jp/@tateisu
|
|
|
|
|
// 疑似アカウントではnullになります
|
|
|
|
|
val url: String?
|
|
|
|
|
|
|
|
|
|
// The ID of the account
|
|
|
|
|
val id: EntityId
|
|
|
|
|
|
|
|
|
|
// The username of the account /[A-Za-z0-9_]{1,30}/
|
|
|
|
|
val username: String
|
|
|
|
|
|
|
|
|
|
final override val apiHost: Host
|
|
|
|
|
final override val apDomain: Host
|
|
|
|
|
|
|
|
|
|
// Equals username for local users, includes @domain for remote ones
|
|
|
|
|
val acct: Acct
|
|
|
|
|
|
|
|
|
|
// The account's display name
|
|
|
|
|
val display_name: String
|
|
|
|
|
|
|
|
|
|
//Boolean for when the account cannot be followed without waiting for approval first
|
|
|
|
|
val locked: Boolean
|
|
|
|
|
|
|
|
|
|
// The time the account was created
|
|
|
|
|
// ex: "2017-04-13T11:06:08.289Z"
|
|
|
|
|
val created_at: String?
|
|
|
|
|
val time_created_at: Long
|
|
|
|
|
|
|
|
|
|
// The number of followers for the account
|
|
|
|
|
var followers_count: Long? = null
|
|
|
|
|
|
|
|
|
|
//The number of accounts the given account is following
|
|
|
|
|
var following_count: Long? = null
|
|
|
|
|
|
|
|
|
|
// The number of statuses the account has made
|
|
|
|
|
var statuses_count: Long? = null
|
|
|
|
|
|
|
|
|
|
// Biography of user
|
|
|
|
|
// 説明文。改行は\r\n。リンクなどはHTMLタグで書かれている
|
|
|
|
|
val note: String?
|
|
|
|
|
|
|
|
|
|
// URL to the avatar image
|
|
|
|
|
val avatar: String?
|
|
|
|
|
|
|
|
|
|
// URL to the avatar static image (gif)
|
|
|
|
|
val avatar_static: String?
|
|
|
|
|
|
|
|
|
|
//URL to the header image
|
|
|
|
|
val header: String?
|
|
|
|
|
|
|
|
|
|
// URL to the header static image (gif)
|
|
|
|
|
val header_static: String?
|
|
|
|
|
|
|
|
|
|
val source: Source?
|
|
|
|
|
|
|
|
|
|
val profile_emojis: HashMap<String, NicoProfileEmoji>?
|
|
|
|
|
|
|
|
|
|
val movedRef: TootAccountRef?
|
|
|
|
|
|
|
|
|
|
val moved: TootAccount?
|
|
|
|
|
get() = movedRef?.get()
|
|
|
|
|
|
|
|
|
|
class Field(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val name: String,
|
|
|
|
|
val value: String,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val verified_at: Long, // 0L if not verified
|
2020-12-19 12:39:36 +01:00
|
|
|
|
)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
|
|
|
|
val fields: ArrayList<Field>?
|
|
|
|
|
|
|
|
|
|
val custom_emojis: HashMap<String, CustomEmoji>?
|
|
|
|
|
|
|
|
|
|
val bot: Boolean
|
|
|
|
|
val isCat: Boolean
|
|
|
|
|
val isAdmin: Boolean
|
|
|
|
|
val isPro: Boolean
|
|
|
|
|
|
|
|
|
|
@Suppress("unused")
|
|
|
|
|
val isLocal: Boolean
|
|
|
|
|
get() = acct.host == null
|
|
|
|
|
|
|
|
|
|
val isRemote: Boolean
|
|
|
|
|
get() = acct.host != null
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
fun getUserUrl() = url ?: "https://${apDomain.pretty}/@$username"
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
|
|
|
|
// user_hides_network is preference, not exposed in API
|
|
|
|
|
// val user_hides_network : Boolean
|
|
|
|
|
|
|
|
|
|
var pinnedNotes: ArrayList<TootStatus>? = null
|
|
|
|
|
private var pinnedNoteIds: ArrayList<String>? = null
|
|
|
|
|
|
|
|
|
|
// misskey (only /api/users/show)
|
|
|
|
|
var location: String? = null
|
|
|
|
|
var birthday: String? = null
|
|
|
|
|
|
2020-12-19 12:16:28 +01:00
|
|
|
|
// mastodon 3.0.0-dev // last_status_at : "2019-08-29T12:42:08.838Z" or null
|
|
|
|
|
// mastodon 3.1 // last_status_at : "2019-08-29" or null
|
2020-12-19 12:39:36 +01:00
|
|
|
|
private var last_status_at = 0L
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
|
|
|
|
// mastodon 3.3.0
|
|
|
|
|
var suspended = false
|
|
|
|
|
|
|
|
|
|
val json: JsonObject
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
this.json = src
|
|
|
|
|
src["_fromStream"] = parser.fromStream
|
|
|
|
|
|
|
|
|
|
when (parser.serviceType) {
|
2020-12-19 12:39:36 +01:00
|
|
|
|
ServiceType.MISSKEY -> {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.custom_emojis =
|
2021-11-20 01:36:43 +01:00
|
|
|
|
parseMapOrNull(
|
|
|
|
|
CustomEmoji.decodeMisskey,
|
|
|
|
|
parser.apDomain,
|
2023-01-10 17:40:26 +01:00
|
|
|
|
parser.apiHost,
|
2021-11-20 01:36:43 +01:00
|
|
|
|
src.jsonArray("emojis")
|
|
|
|
|
)
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.profile_emojis = null
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.username = src.stringOrThrow("username")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.apiHost = src.string("host")?.let { Host.parse(it) } ?: parser.apiHost
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.url = "https://${apiHost.ascii}/@$username"
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.apDomain = apiHost // FIXME apiHostとapDomainが異なる場合はMisskeyだとどうなの…?
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
@Suppress("LeakingThis")
|
|
|
|
|
this.acct = when {
|
|
|
|
|
// アクセス元から見て内部ユーザなら short acct
|
|
|
|
|
parser.linkHelper.matchHost(this) -> Acct.parse(username)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
// アクセス元から見て外部ユーザならfull acct
|
|
|
|
|
else -> Acct.parse(username, apDomain)
|
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
//
|
|
|
|
|
val sv = src.string("name")
|
|
|
|
|
this.display_name = if (sv?.isNotEmpty() == true) sv.sanitizeBDI() else username
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
//
|
|
|
|
|
this.note = src.string("description")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.source = null
|
|
|
|
|
this.movedRef = null
|
|
|
|
|
this.locked = src.optBoolean("isLocked")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.bot = src.optBoolean("isBot", false)
|
|
|
|
|
this.isCat = src.optBoolean("isCat", false)
|
|
|
|
|
this.isAdmin = src.optBoolean("isAdmin", false)
|
|
|
|
|
this.isPro = src.optBoolean("isPro", false)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
// this.user_hides_network = src.optBoolean("user_hides_network")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.id = EntityId.mayDefault(src.string("id"))
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.followers_count = src.long("followersCount") ?: -1L
|
|
|
|
|
this.following_count = src.long("followingCount") ?: -1L
|
|
|
|
|
this.statuses_count = src.long("notesCount") ?: -1L
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.created_at = src.string("createdAt")
|
|
|
|
|
this.time_created_at = TootStatus.parseTime(this.created_at)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
// https://github.com/syuilo/misskey/blob/develop/src/client/scripts/get-static-image-url.ts
|
|
|
|
|
fun String.getStaticImageUrl(): String? {
|
|
|
|
|
val uri = this.mayUri() ?: return null
|
|
|
|
|
val dummy = "${uri.encodedAuthority}${uri.encodedPath}"
|
|
|
|
|
return "https://${parser.linkHelper.apiHost.ascii}/proxy/$dummy?url=${encodePercent()}&static=1"
|
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.avatar = src.string("avatarUrl")
|
|
|
|
|
this.avatar_static = src.string("avatarUrl")?.getStaticImageUrl()
|
|
|
|
|
this.header = src.string("bannerUrl")
|
|
|
|
|
this.header_static = src.string("bannerUrl")?.getStaticImageUrl()
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.pinnedNoteIds = src.stringArrayList("pinnedNoteIds")
|
|
|
|
|
if (parser.misskeyDecodeProfilePin) {
|
|
|
|
|
val list = parseList(::TootStatus, parser, src.jsonArray("pinnedNotes"))
|
|
|
|
|
list.forEach { it.pinned = true }
|
2023-01-13 13:22:25 +01:00
|
|
|
|
this.pinnedNotes = list.ifEmpty { null }
|
2020-12-19 12:39:36 +01:00
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val profile = src.jsonObject("profile")
|
|
|
|
|
this.location = profile?.string("location")
|
|
|
|
|
this.birthday = profile?.string("birthday")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.fields = parseMisskeyFields(src)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2023-02-04 21:52:26 +01:00
|
|
|
|
daoUserRelation.fromAccount(parser, src, id)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
@Suppress("LeakingThis")
|
|
|
|
|
MisskeyAccountDetailMap.fromAccount(parser, this, id)
|
|
|
|
|
}
|
|
|
|
|
ServiceType.NOTESTOCK -> {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
// notestock はActivityPub 準拠のサービスなので、サーバ内IDというのは特にない
|
|
|
|
|
this.id = EntityId.DEFAULT
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2021-11-20 01:36:43 +01:00
|
|
|
|
this.username =
|
|
|
|
|
src.stringOrThrow("display_name") // notestockはdisplay_nameとusernameが入れ替わってる?
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.display_name = src.stringOrThrow("username")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val tmpAcct = src.string("subject")?.let { Acct.parse(it) }
|
|
|
|
|
val apDomain = tmpAcct?.takeIf { it.isValidFull }?.host
|
|
|
|
|
?: Host.parse(
|
|
|
|
|
src.string("id").mayUri()?.authority?.notEmpty()
|
|
|
|
|
?: error("can't get apDomain from account's AP id.")
|
|
|
|
|
)
|
|
|
|
|
this.url = src.string("url")
|
|
|
|
|
val apiHost = Host.parse(
|
|
|
|
|
url.mayUri()?.authority?.notEmpty()
|
|
|
|
|
?: error("can't get apiHost from account's AP url.")
|
|
|
|
|
)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.apiHost = apiHost
|
|
|
|
|
this.apDomain = apDomain
|
|
|
|
|
this.acct = Acct.parse(this.username, apDomain)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.avatar = src.string("avatar")
|
|
|
|
|
this.avatar_static = src.string("avatar_static")
|
|
|
|
|
this.header = src.string("header")
|
|
|
|
|
this.header_static = src.string("header_static")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.locked = src.boolean("manuallyApprovesFollowers") ?: false
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.note = src.string("note")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val apTag = APTag(parser, src.jsonArray("tag"))
|
|
|
|
|
this.custom_emojis = apTag.emojiList.notEmpty()
|
|
|
|
|
this.profile_emojis = apTag.profileEmojiList.notEmpty()
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
// APだと attachment にデータはあるが、検索結果に表示しないので読まない
|
|
|
|
|
this.fields = null
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.source = null
|
|
|
|
|
this.movedRef = null
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.followers_count = null
|
|
|
|
|
this.following_count = null
|
|
|
|
|
this.statuses_count = null
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.created_at = null
|
|
|
|
|
this.time_created_at = 0L
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.bot = false
|
|
|
|
|
this.isCat = false
|
|
|
|
|
this.isAdmin = false
|
|
|
|
|
this.isPro = false
|
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
|
|
|
|
|
// 絵文字データは先に読んでおく
|
2023-01-10 17:40:26 +01:00
|
|
|
|
this.custom_emojis = parseMapOrNull(
|
|
|
|
|
CustomEmoji.decode,
|
|
|
|
|
parser.apDomain,
|
|
|
|
|
parser.apiHost,
|
|
|
|
|
src.jsonArray("emojis")
|
|
|
|
|
)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
|
|
|
|
this.profile_emojis = when (val o = src["profile_emojis"]) {
|
2020-12-19 12:39:36 +01:00
|
|
|
|
is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, TootStatus.log)
|
|
|
|
|
is JsonObject -> parseProfileEmoji2(::NicoProfileEmoji, o, TootStatus.log)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 疑似アカウントにacctとusernameだけ
|
|
|
|
|
this.url = src.string("url")
|
|
|
|
|
this.username = src.stringOrThrow("username")
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
val sv = src.string("display_name")
|
|
|
|
|
this.display_name = if (sv?.isNotEmpty() == true) sv.sanitizeBDI() else username
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
this.note = src.string("note")
|
|
|
|
|
|
|
|
|
|
this.source = parseSource(src.jsonObject("source"))
|
|
|
|
|
this.movedRef = TootAccountRef.mayNull(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
parser,
|
|
|
|
|
src.jsonObject("moved")?.let {
|
|
|
|
|
TootAccount(parser, it)
|
|
|
|
|
}
|
|
|
|
|
)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
this.locked = src.optBoolean("locked")
|
|
|
|
|
|
|
|
|
|
this.fields = parseFields(src.jsonArray("fields"))
|
|
|
|
|
|
|
|
|
|
this.bot = src.optBoolean("bot", false)
|
|
|
|
|
this.suspended = src.optBoolean("suspended", false)
|
|
|
|
|
this.isAdmin = false
|
|
|
|
|
this.isCat = false
|
|
|
|
|
this.isPro = false
|
|
|
|
|
// this.user_hides_network = src.optBoolean("user_hides_network")
|
|
|
|
|
|
|
|
|
|
this.last_status_at = TootStatus.parseTime(src.string("last_status_at"))
|
|
|
|
|
|
|
|
|
|
when (parser.serviceType) {
|
2020-12-19 12:39:36 +01:00
|
|
|
|
ServiceType.MASTODON -> {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.id = EntityId.mayDefault(src.string("id"))
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val tmpAcct = src.stringOrThrow("acct")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val (apiHost, apDomain) = findHostFromUrl(tmpAcct, parser.linkHelper, url)
|
|
|
|
|
apiHost ?: error("can't get apiHost from acct or url")
|
|
|
|
|
apDomain ?: error("can't get apDomain from acct or url")
|
|
|
|
|
this.apiHost = apiHost
|
|
|
|
|
this.apDomain = apDomain
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2021-11-20 01:36:43 +01:00
|
|
|
|
this.acct =
|
|
|
|
|
Acct.parse(username, if (tmpAcct.contains('@')) apDomain else null)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.followers_count = src.long("followers_count")
|
|
|
|
|
this.following_count = src.long("following_count")
|
|
|
|
|
this.statuses_count = src.long("statuses_count")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.created_at = src.string("created_at")
|
|
|
|
|
this.time_created_at = TootStatus.parseTime(this.created_at)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.avatar = src.string("avatar")
|
|
|
|
|
this.avatar_static = src.string("avatar_static")
|
|
|
|
|
this.header = src.string("header")
|
|
|
|
|
this.header_static = src.string("header_static")
|
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
ServiceType.TOOTSEARCH -> {
|
|
|
|
|
// tootsearch のアカウントのIDはどのタンス上のものか分からないので役に立たない
|
|
|
|
|
this.id = EntityId.DEFAULT
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val tmpAcct = src.stringOrThrow("acct")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val (apiHost, apDomain) = findHostFromUrl(tmpAcct, null, url)
|
|
|
|
|
apiHost ?: error("can't get apiHost from acct or url")
|
|
|
|
|
apDomain ?: error("can't get apDomain from acct or url")
|
|
|
|
|
this.apiHost = apiHost
|
|
|
|
|
this.apDomain = apDomain
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.acct = Acct.parse(this.username, this.apDomain)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.followers_count = src.long("followers_count")
|
|
|
|
|
this.following_count = src.long("following_count")
|
|
|
|
|
this.statuses_count = src.long("statuses_count")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.created_at = src.string("created_at")
|
|
|
|
|
this.time_created_at = TootStatus.parseTime(this.created_at)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.avatar = src.string("avatar")
|
|
|
|
|
this.avatar_static = src.string("avatar_static")
|
|
|
|
|
this.header = src.string("header")
|
|
|
|
|
this.header_static = src.string("header_static")
|
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
ServiceType.MSP -> {
|
|
|
|
|
this.id = EntityId.mayDefault(src.string("id"))
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
// MSPはLTLの情報しか持ってないのでacctは常にホスト名部分を持たない
|
|
|
|
|
val (apiHost, apDomain) = findHostFromUrl(null, null, url)
|
|
|
|
|
apiHost ?: error("can't get apiHost from acct or url")
|
|
|
|
|
apDomain ?: error("can't get apDomain from acct or url")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.apiHost = apiHost
|
|
|
|
|
this.apDomain = apiHost
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.acct = Acct.parse(this.username, this.apDomain)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.followers_count = null
|
|
|
|
|
this.following_count = null
|
|
|
|
|
this.statuses_count = null
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
this.created_at = null
|
|
|
|
|
this.time_created_at = 0L
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
val avatar = src.string("avatar")
|
|
|
|
|
this.avatar = avatar
|
|
|
|
|
this.avatar_static = avatar
|
|
|
|
|
this.header = null
|
|
|
|
|
this.header_static = null
|
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2020-12-19 12:39:36 +01:00
|
|
|
|
ServiceType.MISSKEY, ServiceType.NOTESTOCK -> error("will not happen")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Source(src: JsonObject) {
|
|
|
|
|
|
|
|
|
|
// デフォルト公開範囲
|
|
|
|
|
val privacy: String?
|
|
|
|
|
|
|
|
|
|
// 添付画像をデフォルトでNSFWにする設定
|
|
|
|
|
private val sensitive: Boolean
|
|
|
|
|
|
|
|
|
|
// HTMLエンコードされていない、生のnote
|
|
|
|
|
val note: String?
|
|
|
|
|
|
|
|
|
|
// 2.4.0 から?
|
|
|
|
|
val fields: ArrayList<Field>?
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
this.privacy = src.string("privacy")
|
|
|
|
|
this.note = src.string("note")
|
|
|
|
|
// nullになることがあるが、falseと同じ扱いでよい
|
|
|
|
|
this.sensitive = src.optBoolean("sensitive", false)
|
|
|
|
|
//
|
|
|
|
|
this.fields = parseFields(src.jsonArray("fields"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// リストメンバーダイアログや引っ越し先ユーザなど、TL以外の部分に名前を表示する場合は
|
|
|
|
|
// Invalidator の都合でSpannableを別途生成する必要がある
|
|
|
|
|
fun decodeDisplayName(context: Context): Spannable {
|
|
|
|
|
|
|
|
|
|
// remove white spaces
|
|
|
|
|
val sv = reWhitespace.matcher(display_name).replaceAll(" ")
|
|
|
|
|
|
|
|
|
|
// decode emoji code
|
|
|
|
|
return DecodeOptions(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
context,
|
|
|
|
|
emojiMapProfile = profile_emojis,
|
|
|
|
|
emojiMapCustom = custom_emojis,
|
2022-05-30 22:30:52 +02:00
|
|
|
|
authorDomain = this
|
2020-12-19 12:39:36 +01:00
|
|
|
|
).decodeEmoji(sv)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun SpannableStringBuilder.replaceAllEx(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
pattern: Pattern,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
replacement: String,
|
2020-12-19 12:39:36 +01:00
|
|
|
|
): SpannableStringBuilder {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
val m = pattern.matcher(this)
|
|
|
|
|
var buffer: SpannableStringBuilder? = null
|
|
|
|
|
var lastEnd = 0
|
|
|
|
|
while (m.find()) {
|
|
|
|
|
val dst = buffer ?: SpannableStringBuilder().also { buffer = it }
|
|
|
|
|
dst
|
|
|
|
|
.append(this.subSequence(lastEnd, m.start()))
|
|
|
|
|
.append(replacement) // 変数展開には未対応
|
|
|
|
|
lastEnd = m.end()
|
|
|
|
|
}
|
|
|
|
|
return buffer
|
|
|
|
|
?.also { if (lastEnd < length) it.append(subSequence(lastEnd, length)) }
|
|
|
|
|
?: this
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun SpannableStringBuilder.trimEx(isSpace: (c: Char) -> Boolean = { it <= ' ' }): CharSequence {
|
|
|
|
|
var start = 0
|
|
|
|
|
var end = length
|
|
|
|
|
while (start < end && isSpace(this[start])) ++start
|
|
|
|
|
while (end > start && isSpace(this[end - 1])) --end
|
|
|
|
|
return when {
|
|
|
|
|
start >= end -> ""
|
|
|
|
|
start == 0 && end == length -> this
|
|
|
|
|
else -> subSequence(start, end)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setAccountExtra(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
accessInfo: SavedAccount,
|
|
|
|
|
tv: TextView,
|
|
|
|
|
invalidator: NetworkEmojiInvalidator?,
|
2021-05-21 13:56:22 +02:00
|
|
|
|
fromProfileHeader: Boolean = false,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
suggestionSource: String? = null,
|
2020-12-19 12:39:36 +01:00
|
|
|
|
): SpannableStringBuilder? {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
val context = tv.context
|
|
|
|
|
|
|
|
|
|
var sb: SpannableStringBuilder? = null
|
|
|
|
|
fun prepareSb() = sb?.apply { append('\n') } ?: SpannableStringBuilder().also { sb = it }
|
|
|
|
|
val delm = ": "
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
if (suggestionSource?.isNotEmpty() == true) {
|
2021-05-21 13:56:22 +02:00
|
|
|
|
prepareSb()
|
|
|
|
|
.append(context.getString(R.string.suggestion_source))
|
|
|
|
|
.append(delm)
|
|
|
|
|
.append(suggestionSource)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-04 21:52:26 +01:00
|
|
|
|
if (PrefB.bpDirectoryLastActive.value && last_status_at > 0L) {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
prepareSb()
|
|
|
|
|
.append(context.getString(R.string.last_active))
|
|
|
|
|
.append(delm)
|
2021-11-20 01:36:43 +01:00
|
|
|
|
.append(
|
|
|
|
|
TootStatus.formatTime(
|
|
|
|
|
context,
|
|
|
|
|
last_status_at,
|
|
|
|
|
bAllowRelative = true,
|
|
|
|
|
onlyDate = true
|
|
|
|
|
)
|
|
|
|
|
)
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
|
|
|
|
if (!fromProfileHeader) {
|
2023-02-04 21:52:26 +01:00
|
|
|
|
if (PrefB.bpDirectoryTootCount.value &&
|
2021-06-20 15:12:25 +02:00
|
|
|
|
(statuses_count ?: 0L) > 0L
|
|
|
|
|
) {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
prepareSb()
|
|
|
|
|
.append(context.getString(R.string.toot_count))
|
|
|
|
|
.append(delm)
|
|
|
|
|
.append(statuses_count.toString())
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2023-02-04 21:52:26 +01:00
|
|
|
|
if (PrefB.bpDirectoryFollowers.value &&
|
|
|
|
|
!PrefB.bpHideFollowCount.value &&
|
2021-06-20 15:12:25 +02:00
|
|
|
|
(followers_count ?: 0L) > 0L
|
|
|
|
|
) {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
prepareSb()
|
|
|
|
|
.append(context.getString(R.string.followers))
|
|
|
|
|
.append(delm)
|
|
|
|
|
.append(followers_count.toString())
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
2020-12-07 13:23:14 +01:00
|
|
|
|
|
2023-02-04 21:52:26 +01:00
|
|
|
|
if (PrefB.bpDirectoryNote.value && note?.isNotEmpty() == true) {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
val decodedNote = DecodeOptions(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
context,
|
|
|
|
|
accessInfo,
|
|
|
|
|
short = true,
|
|
|
|
|
decodeEmoji = true,
|
|
|
|
|
emojiMapProfile = profile_emojis,
|
|
|
|
|
emojiMapCustom = custom_emojis,
|
|
|
|
|
unwrapEmojiImageTag = true,
|
2022-05-30 22:30:52 +02:00
|
|
|
|
authorDomain = this,
|
2020-12-19 12:39:36 +01:00
|
|
|
|
).decodeHTML(note)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
.replaceAllEx(reNoteLineFeed, " ")
|
|
|
|
|
.trimEx()
|
|
|
|
|
if (decodedNote.isNotBlank()) {
|
|
|
|
|
prepareSb().append(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
if (decodedNote is SpannableStringBuilder && decodedNote.length > 200) {
|
|
|
|
|
decodedNote.replace(200, decodedNote.length, "…")
|
|
|
|
|
} else {
|
|
|
|
|
decodedNote
|
|
|
|
|
}
|
|
|
|
|
)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tv.vg(sb != null)
|
|
|
|
|
?.apply {
|
|
|
|
|
text = sb
|
|
|
|
|
movementMethod = MyLinkMovementMethod
|
|
|
|
|
invalidator?.register(sb)
|
|
|
|
|
}
|
|
|
|
|
?: invalidator?.clear()
|
|
|
|
|
|
|
|
|
|
return sb
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
|
|
private val log = LogCategory("TootAccount")
|
|
|
|
|
|
|
|
|
|
internal val reWhitespace = "[\\s\\t\\x0d\\x0a]+".asciiPattern()
|
|
|
|
|
|
|
|
|
|
// noteをディレクトリに表示する際、制御文字や空白を変換する
|
|
|
|
|
private val reNoteLineFeed: Pattern = """[\x00-\x20\x7f ]+""".asciiPattern()
|
|
|
|
|
|
|
|
|
|
// IDNドメインを含むホスト名の正規表現
|
|
|
|
|
private const val reHostIdn = """(?:(?:[\p{L}\p{N}][\p{L}\p{N}-_]*\.)+[\p{L}\p{N}]{2,})"""
|
|
|
|
|
|
|
|
|
|
internal val reHostInUrl: Pattern = """\Ahttps://($reHostIdn)/"""
|
|
|
|
|
.asciiPattern()
|
|
|
|
|
|
|
|
|
|
// 文字数カウントに使う正規表現
|
|
|
|
|
private val reCountLink = """(https?://$reHostIdn[\w/:%#@${'$'}&?!()\[\]~.=+\-]*)"""
|
|
|
|
|
.asciiPattern()
|
|
|
|
|
|
|
|
|
|
// 投稿中のURLは23文字として扱う
|
|
|
|
|
private val strUrlReplacement = (1..23).joinToString(transform = { " " })
|
|
|
|
|
|
|
|
|
|
// \p{L} : アルファベット (Letter)。
|
|
|
|
|
// Ll(小文字)、Lm(擬似文字)、Lo(その他の文字)、Lt(タイトル文字)、Lu(大文字アルファベット)を含む
|
|
|
|
|
// \p{M} : 記号 (Mark)
|
|
|
|
|
// \p{Nd} : 10 進数字 (Decimal number)
|
|
|
|
|
// \p{Pc} : 連結用句読記号 (Connector punctuation)
|
|
|
|
|
|
|
|
|
|
// rubyの [:word:] : 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
|
|
|
|
|
const val reRubyWord = """\p{L}\p{M}\p{Nd}\p{Pc}"""
|
|
|
|
|
|
|
|
|
|
// rubyの [:alpha:] : 英字 (Letter | Mark)
|
|
|
|
|
const val reRubyAlpha = """\p{L}\p{M}"""
|
|
|
|
|
|
|
|
|
|
private const val reMastodonUserName = """[A-Za-z0-9_]+(?:[A-Za-z0-9_.-]+[A-Za-z0-9_]+)?"""
|
|
|
|
|
private const val reMastodonMention =
|
|
|
|
|
"""(?<=^|[^/$reRubyWord])@(($reMastodonUserName)(?:@[$reRubyWord.-]+[A-Za-z0-9]+)?)"""
|
|
|
|
|
|
|
|
|
|
val reCountMention = reMastodonMention.asciiPattern()
|
|
|
|
|
|
|
|
|
|
fun countText(s: String): Int {
|
|
|
|
|
return s
|
|
|
|
|
.replaceAll(reCountLink, strUrlReplacement)
|
|
|
|
|
.replaceAll(reCountMention, "@$2")
|
|
|
|
|
.codePointCount()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MisskeyのMFMのメンションのドメイン部分はIDN非対応
|
|
|
|
|
private const val reMisskeyHost = """\w[\w.-]*\w"""
|
|
|
|
|
|
|
|
|
|
// https://misskey.io/@tateisu@%E3%83%9E%E3%82%B9%E3%83%88%E3%83%89%E3%83%B33.juggler.jp
|
|
|
|
|
// のようなURLがMisskeyのメンションから生成されることがある
|
|
|
|
|
// %エンコーディングのデコードが必要
|
|
|
|
|
private const val reMisskeyHostEncoded = """[\w%][\w.%-]*[\w%]"""
|
|
|
|
|
|
|
|
|
|
// MFMのメンション @username @username@host
|
|
|
|
|
// (Mastodonのカラムでは使われていない)
|
|
|
|
|
// MisskeyのMFMはIDNをサポートしていない
|
|
|
|
|
private val reMisskeyMentionBase = """@(\w+(?:[\w-]*\w)?)(?:@($reMisskeyHost))?"""
|
|
|
|
|
.asciiPattern()
|
|
|
|
|
|
|
|
|
|
// MFMパース時に使う
|
|
|
|
|
internal val reMisskeyMentionMFM = """\A$reMisskeyMentionBase"""
|
|
|
|
|
.asciiPattern()
|
|
|
|
|
|
|
|
|
|
// 投稿送信時にメンションを見つけてuserIdを調べるために使う
|
|
|
|
|
internal val reMisskeyMentionPost = """(?:\A|\s)$reMisskeyMentionBase"""
|
|
|
|
|
.asciiPattern()
|
|
|
|
|
|
|
|
|
|
// host, user ,(instance)
|
|
|
|
|
// Misskeyだけではないのでusernameの定義が違う
|
|
|
|
|
internal val reAccountUrl =
|
|
|
|
|
"""\Ahttps://($reHostIdn)/@(\w+[\w-]*)(?:@($reMisskeyHostEncoded))?(?=\z|[?#])"""
|
|
|
|
|
.asciiPattern()
|
|
|
|
|
|
|
|
|
|
// host,user
|
|
|
|
|
internal val reAccountUrl2 =
|
|
|
|
|
"""\Ahttps://($reHostIdn)/users/(\w|\w+[\w-]*\w)(?=\z|[?#])"""
|
|
|
|
|
.asciiPattern()
|
|
|
|
|
|
|
|
|
|
// notestockはaccountのnotag先頭に
|
|
|
|
|
|
|
|
|
|
fun getAcctFromUrl(url: String?): Acct? {
|
|
|
|
|
|
|
|
|
|
url ?: return null
|
|
|
|
|
|
|
|
|
|
var m = reAccountUrl.matcher(url)
|
|
|
|
|
if (m.find()) {
|
|
|
|
|
val host = m.groupEx(1)
|
|
|
|
|
val user = m.groupEx(2)!!
|
|
|
|
|
val instance = m.groupEx(3)?.decodePercent()
|
|
|
|
|
return Acct.parse(user, instance?.notEmpty() ?: host)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m = reAccountUrl2.matcher(url)
|
|
|
|
|
if (m.find()) {
|
|
|
|
|
val host = m.groupEx(1)
|
|
|
|
|
val user = m.groupEx(2)!!
|
|
|
|
|
return Acct.parse(user, host)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-14 11:19:01 +01:00
|
|
|
|
private fun parseSource(src: JsonObject?): Source? =
|
|
|
|
|
try {
|
|
|
|
|
src?.let { Source(it) }
|
2020-12-07 13:23:14 +01:00
|
|
|
|
} catch (ex: Throwable) {
|
2023-01-14 11:19:01 +01:00
|
|
|
|
log.e(ex, "parseSource failed.")
|
2020-12-07 13:23:14 +01:00
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun findApDomain(acctArg: String?, linkHelper: LinkHelper?): Host? {
|
|
|
|
|
// acctから調べる
|
|
|
|
|
if (acctArg != null) {
|
|
|
|
|
val acct = Acct.parse(acctArg)
|
|
|
|
|
if (acct.host != null) return acct.host
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Acctはnullか、hostを含まない
|
|
|
|
|
if (linkHelper != null) return linkHelper.apDomain
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun findApiHost(url: String?): Host? {
|
|
|
|
|
// URLから調べる
|
|
|
|
|
// たぶんどんなURLでもauthorityの部分にホスト名が来るだろう(慢心)
|
|
|
|
|
url.mayUri()?.authority?.notEmpty()?.let { return Host.parse(it) }
|
|
|
|
|
|
|
|
|
|
log.e("findApiHost: can't parse host from URL $url")
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tootsearch用。URLやUriを使ってアカウントのインスタンス名を調べる
|
|
|
|
|
fun findHostFromUrl(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
acctArg: String?,
|
|
|
|
|
linkHelper: LinkHelper?,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
url: String?,
|
2020-12-19 12:39:36 +01:00
|
|
|
|
): Pair<Host?, Host?> {
|
2020-12-07 13:23:14 +01:00
|
|
|
|
val apDomain = findApDomain(acctArg, linkHelper)
|
|
|
|
|
val apiHost = findApiHost(url)
|
|
|
|
|
return Pair(apiHost ?: apDomain, apDomain ?: apiHost)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun parseFields(src: JsonArray?): ArrayList<Field>? {
|
|
|
|
|
src ?: return null
|
|
|
|
|
val dst = ArrayList<Field>()
|
|
|
|
|
for (item in src) {
|
|
|
|
|
if (item !is JsonObject) continue
|
|
|
|
|
val name = item.string("name") ?: continue
|
|
|
|
|
val value = item.string("value") ?: continue
|
|
|
|
|
val verifiedAt = when (val svVerifiedAt = item.string("verified_at")) {
|
2020-12-19 12:39:36 +01:00
|
|
|
|
null -> 0L
|
2020-12-07 13:23:14 +01:00
|
|
|
|
else -> TootStatus.parseTime(svVerifiedAt)
|
|
|
|
|
}
|
|
|
|
|
dst.add(Field(name, value, verifiedAt))
|
|
|
|
|
}
|
|
|
|
|
return dst.notEmpty()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun parseMisskeyFields(src: JsonObject): ArrayList<Field>? {
|
|
|
|
|
|
|
|
|
|
var dst: ArrayList<Field>? = null
|
|
|
|
|
|
|
|
|
|
// リモートユーザーはAP経由のフィールドが表示される
|
|
|
|
|
// https://github.com/syuilo/misskey/pull/3590
|
|
|
|
|
// https://github.com/syuilo/misskey/pull/3596
|
|
|
|
|
src.jsonArray("fields")?.forEach { o ->
|
|
|
|
|
if (o !is JsonObject) return@forEach
|
|
|
|
|
//plain text
|
|
|
|
|
val n = o.string("name") ?: return@forEach
|
|
|
|
|
// mfm
|
|
|
|
|
val v = o.string("value") ?: ""
|
|
|
|
|
dst = (dst ?: ArrayList()).apply { add(Field(n, v, 0L)) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// misskeyローカルユーザーはTwitter等の連携をフィールドに表示する
|
|
|
|
|
// https://github.com/syuilo/misskey/pull/3499
|
|
|
|
|
// https://github.com/syuilo/misskey/pull/3586
|
|
|
|
|
|
|
|
|
|
fun appendField(name: String, caption: String, url: String) {
|
|
|
|
|
val value = """[$caption]($url)"""
|
|
|
|
|
dst = (dst ?: ArrayList()).apply { add(Field(name, value, 0L)) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runCatching {
|
|
|
|
|
src.jsonObject("twitter")?.let {
|
|
|
|
|
appendField(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
"Twitter",
|
|
|
|
|
"@${it.string("screenName")}",
|
|
|
|
|
"https://twitter.com/intent/user?user_id=${it.string("userId")}"
|
|
|
|
|
)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runCatching {
|
|
|
|
|
src.jsonObject("github")?.string("login")?.let {
|
|
|
|
|
appendField(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
"GitHub",
|
|
|
|
|
it,
|
|
|
|
|
"https://github.com/$it"
|
|
|
|
|
)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runCatching {
|
|
|
|
|
src.jsonObject("discord")?.let {
|
|
|
|
|
appendField(
|
2020-12-19 12:39:36 +01:00
|
|
|
|
"Discord",
|
|
|
|
|
"${it.string("username")}#${it.string("discriminator")}",
|
|
|
|
|
"https://discordapp.com/users/${it.string("id")}"
|
|
|
|
|
)
|
2020-12-07 13:23:14 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return if (dst?.isNotEmpty() == true) dst else null
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|