APIのEntityがクラスイニシャライザ内部で複雑な処理をするのを減らす。それはsuspendにできない…

This commit is contained in:
tateisu 2023-02-07 21:49:45 +09:00
parent 60bacaac64
commit ecbed39f5b
80 changed files with 2597 additions and 2370 deletions

View File

@ -22,6 +22,8 @@
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<!-- Chrome Custom Tabs が明示指定するChromeパッケージ -->
<package android:name="com.android.chrome" />
<!-- カスタム共有ボタンのアプリ選択 -->
<intent>

View File

@ -18,8 +18,9 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.auth.authRepo
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.notification.*
@ -129,10 +130,6 @@ class ActAccountSetting : AppCompatActivity(),
ActAccountSettingBinding.inflate(layoutInflater, null, false)
}
private val authRepo by lazy {
AuthRepo(this)
}
private lateinit var nameInvalidator: NetworkEmojiInvalidator
private lateinit var noteInvalidator: NetworkEmojiInvalidator
private lateinit var defaultTextInvalidator: NetworkEmojiInvalidator
@ -205,7 +202,7 @@ class ActAccountSetting : AppCompatActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
backPressed {handleBackPressed()}
backPressed { handleBackPressed() }
prPickAvater.register(this)
prPickHeader.register(this)
@ -543,7 +540,7 @@ class ActAccountSetting : AppCompatActivity(),
private fun showPushSetting() {
views.run {
run{
run {
val usePush = swNotificationPushEnabled.isChecked
tvPushPolicyDesc.vg(usePush)
spPushPolicy.vg(usePush)
@ -552,7 +549,7 @@ class ActAccountSetting : AppCompatActivity(),
btnPushSubscriptionNotForce.vg(usePush)
}
run{
run {
val usePull = swNotificationPullEnabled.isChecked
tvDontShowTimeout.vg(usePull)
swDontShowTimeout.vg(usePull)
@ -625,13 +622,12 @@ class ActAccountSetting : AppCompatActivity(),
}
}
private fun handleBackPressed(){
private fun handleBackPressed() {
checkNotificationImmediateAll(this, onlyEnqueue = true)
checkNotificationImmediate(this, account.db_id)
finish()
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
when (buttonView) {
views.cbLocked -> {
@ -908,9 +904,7 @@ class ActAccountSetting : AppCompatActivity(),
}
private fun showProfile(src: TootAccount) {
if (isDestroyed) return
profileBusy = true
try {
views.ivProfileAvatar.setImageUrl(
@ -1095,11 +1089,8 @@ class ActAccountSetting : AppCompatActivity(),
"/api/drive/files/create",
multipartBuilder.build().toPost()
)?.also { result ->
val jsonObject = result.jsonObject
if (jsonObject != null) {
ta = parseItem(::TootAttachment, ServiceType.MISSKEY, jsonObject)
if (ta == null) result.error = "TootAttachment.parse failed"
}
ta = parseItem(result.jsonObject) { tootAttachment(ServiceType.MISSKEY, it) }
if (ta == null) result.error = "TootAttachment.parse failed"
}
return Pair(result, ta)

View File

@ -32,7 +32,6 @@ import jp.juggler.subwaytooter.appsetting.AppDataExporter
import jp.juggler.subwaytooter.appsetting.AppSettingItem
import jp.juggler.subwaytooter.appsetting.SettingType
import jp.juggler.subwaytooter.appsetting.appSettingRoot
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.databinding.ActAppSettingBinding
import jp.juggler.subwaytooter.databinding.LvSettingItemBinding
import jp.juggler.subwaytooter.dialog.DlgAppPicker
@ -100,10 +99,6 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
MyAdapter()
}
val authRepo by lazy {
AuthRepo(this)
}
private val arNoop = ActivityResultHandler(log) { }
private val arImportAppData = ActivityResultHandler(log) { r ->

View File

@ -10,9 +10,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.auth.AuthRepo
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.databinding.ActKeywordFilterBinding
import jp.juggler.subwaytooter.databinding.LvKeywordFilterBinding
@ -114,7 +114,7 @@ class ActKeywordFilter : AppCompatActivity() {
launchAndShowError {
// filter ID の有無はUIに影響するのでinitUIより先に初期化する
filterId = EntityId.from(intent, EXTRA_FILTER_ID)
filterId = EntityId.entityId(intent, EXTRA_FILTER_ID)
val a = intent.long(EXTRA_ACCOUNT_DB_ID)
?.let { daoSavedAccount.loadAccount(it) }

View File

@ -24,6 +24,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener
import com.google.android.exoplayer2.util.RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachmentJson
import jp.juggler.subwaytooter.databinding.ActMediaViewerBinding
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable
@ -78,7 +79,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
internal fun decodeMediaList(src: String?) =
ArrayList<TootAttachment>().apply {
src?.decodeJsonArray()?.objectList()
?.map { TootAttachment.decodeJson(it) }
?.map { tootAttachmentJson(it) }
?.let { addAll(it) }
}

View File

@ -237,7 +237,7 @@ class ActPost : AppCompatActivity(),
showMediaAttachment()
showVisibility()
updateTextCount()
showReplyTo()
launchAndShowError { showReplyTo() }
showPoll()
showQuotedRenote()
}
@ -274,9 +274,9 @@ class ActPost : AppCompatActivity(),
R.id.ivMedia3 -> performAttachmentClick(2)
R.id.ivMedia4 -> performAttachmentClick(3)
R.id.btnPost -> performPost()
R.id.btnRemoveReply -> removeReply()
R.id.btnRemoveReply -> launchAndShowError { removeReply() }
R.id.btnMore -> performMore()
R.id.btnPlugin -> openMushroom()
R.id.btnPlugin -> launchAndShowError { openMushroom() }
R.id.btnEmojiPicker -> completionHelper.openEmojiPickerFromMore()
R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList(
featuredTagCache[account?.acct?.ascii ?: ""]?.list

View File

@ -16,13 +16,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import jp.juggler.subwaytooter.api.dialogOrToast
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.auth.authRepo
import jp.juggler.subwaytooter.databinding.ActPushMessageListBinding
import jp.juggler.subwaytooter.databinding.LvPushMessageBinding
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.runInProgress
import jp.juggler.subwaytooter.notification.PushMessageIconColor
import jp.juggler.subwaytooter.notification.iconColor
import jp.juggler.subwaytooter.push.PushMessageIconColor
import jp.juggler.subwaytooter.push.iconColor
import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.table.PushMessage
import jp.juggler.subwaytooter.table.daoAccountNotificationStatus
@ -61,14 +60,6 @@ class ActPushMessageList : AppCompatActivity() {
// 特に何もしない
}
private val authRepo by lazy {
applicationContext.authRepo
}
private val pushRepo by lazy {
applicationContext.pushRepo
}
override fun onCreate(savedInstanceState: Bundle?) {
prNotification.register(this)
prNotification.checkOrLaunch()
@ -209,7 +200,8 @@ class ActPushMessageList : AppCompatActivity() {
"type: ${pm.notificationType}",
"id: ${pm.notificationId}",
"dataSize: ${pm.rawBody?.size}",
pm.textExpand
pm.textExpand,
pm.formatError?.let { "error: $it" },
).mapNotNull { it.notBlank() }.joinToString("\n")
}
}

View File

@ -9,7 +9,6 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.databinding.ActTextBinding
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.SavedAccount
@ -117,10 +116,6 @@ class ActText : AppCompatActivity() {
return super.onCreateOptionsMenu(menu)
}
private val authRepo by lazy {
AuthRepo(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
App1.setActivityTheme(this)

View File

@ -446,6 +446,7 @@ class App1 : Application() {
.build()
suspend fun getHttpCached(url: String): ByteArray? {
val caller = RuntimeException("caller's stackTrace.")
val response: Response
try {
@ -461,7 +462,7 @@ class App1 : Application() {
}
if (!response.isSuccessful) {
log.e(TootApiClient.formatResponse(response, "getHttp response error. $url"))
log.e(caller,TootApiClient.formatResponse(response, "getHttp response error. $url"))
return null
}

View File

@ -299,8 +299,7 @@ class AppState(
if (list != null) editColumnList(save = false) { it.addAll(list) }
// ミュートデータのロード
TootStatus.muted_app = daoMutedApp.nameSet()
TootStatus.muted_word = daoMutedWord.nameSet()
TootStatus.updateMuteData(force = true)
// 背景フォルダの掃除
try {
@ -595,8 +594,7 @@ class AppState(
}
fun onMuteUpdated() {
TootStatus.muted_app = daoMutedApp.nameSet()
TootStatus.muted_word = daoMutedWord.nameSet()
TootStatus.updateMuteData(force=true)
columnList.forEach { it.onMuteUpdated() }
}
}

View File

@ -261,7 +261,8 @@ fun ActMain.follow(
"/api/v1/accounts/$userId/${if (bFollow) "follow" else "unfollow"}",
"".toFormRequestBody().toPost()
)?.also { result ->
val newRelation = parseItem(::TootRelationShip, parser, result.jsonObject)
val newRelation =
parseItem(result.jsonObject) { TootRelationShip(parser, it) }
resultRelation = daoUserRelation.saveUserRelation(accessInfo, newRelation)
}
}
@ -373,7 +374,8 @@ private fun ActMain.followRemote(
"/api/v1/accounts/$userId/follow",
"".toFormRequestBody().toPost()
)?.also { result ->
parseItem(::TootRelationShip, parser, result.jsonObject)?.let {
parseItem(result.jsonObject) { TootRelationShip(parser, it) }
?.let {
resultRelation = daoUserRelation.saveUserRelation(accessInfo, it)
}
}
@ -486,7 +488,8 @@ fun ActMain.followRequestAuthorize(
)?.also { result ->
// Mastodon 3.0.0 から更新されたリレーションを返す
// https//github.com/tootsuite/mastodon/pull/11800
val newRelation = parseItem(::TootRelationShip, parser, result.jsonObject)
val newRelation =
parseItem(result.jsonObject) { TootRelationShip(parser, it) }
daoUserRelation.saveUserRelation(accessInfo, newRelation)
// 読めなくてもエラー処理は行わない
}

View File

@ -97,11 +97,12 @@ fun ActMain.listCreate(
)
}?.also { result ->
client.publishApiProgress(getString(R.string.parsing_response))
resultList = parseItem(
::TootList,
TootParser(this, accessInfo),
result.jsonObject
)
resultList = parseItem(result.jsonObject) {
TootList(
TootParser(this, accessInfo),
it
)
}
}
}?.let { result ->
when (val list = resultList) {
@ -196,11 +197,12 @@ fun ActMain.listRename(
)
}?.also { result ->
client.publishApiProgress(getString(R.string.parsing_response))
resultList = parseItem(
::TootList,
TootParser(this, accessInfo),
result.jsonObject
)
resultList = parseItem(result.jsonObject) {
TootList(
TootParser(this, accessInfo),
it
)
}
}
}?.let { result ->
when (val list = resultList) {

View File

@ -74,7 +74,7 @@ fun ActMain.listMemberAdd(
val relation = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject)
parseItem(result.jsonObject) { TootRelationShip(parser, it) }
) ?: return@runApiTask TootApiResult("parse error.")
if (!relation.following) {

View File

@ -179,7 +179,7 @@ private fun ActMain.userMute(
if (jsonObject != null) {
resultRelation = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, jsonObject)
parseItem(jsonObject) { TootRelationShip(parser, it) }
)
}
}
@ -430,7 +430,7 @@ fun ActMain.userBlock(
val parser = TootParser(this, accessInfo)
relationResult = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject)
parseItem(result.jsonObject) { TootRelationShip(parser, it) }
)
}
}
@ -793,7 +793,7 @@ fun ActMain.userSetShowBoosts(
val parser = TootParser(this, accessInfo)
resultRelation = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject)
parseItem(result.jsonObject) { TootRelationShip(parser, it) }
)
}
}?.let { result ->
@ -844,11 +844,12 @@ fun ActMain.userSetStatusNotification(
jsonObjectOf("notify" to enabled)
.toPostRequestBuilder()
)?.also { result ->
val relation = parseItem(
::TootRelationShip,
TootParser(this, accessInfo),
result.jsonObject
)
val relation = parseItem(result.jsonObject) {
TootRelationShip(
TootParser(this, accessInfo),
it
)
}
if (relation != null) {
daoUserRelation.save1Mastodon(
System.currentTimeMillis(),
@ -893,7 +894,9 @@ fun ActMain.userEndorsement(
val parser = TootParser(this, accessInfo)
resultRelation = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject)
parseItem(result.jsonObject) {
TootRelationShip(parser, it)
}
)
}
}?.let { result ->

View File

@ -18,9 +18,9 @@ fun ActMain.onCompleteActPost(data: Intent) {
this.postedAcct = data.string(ActPost.EXTRA_POSTED_ACCT)?.let { Acct.parse(it) }
if (data.extras?.containsKey(ActPost.EXTRA_POSTED_STATUS_ID) == true) {
postedStatusId = EntityId.from(data, ActPost.EXTRA_POSTED_STATUS_ID)
postedReplyId = EntityId.from(data, ActPost.EXTRA_POSTED_REPLY_ID)
postedRedraftId = EntityId.from(data, ActPost.EXTRA_POSTED_REDRAFT_ID)
postedStatusId = EntityId.entityId(data, ActPost.EXTRA_POSTED_STATUS_ID)
postedReplyId = EntityId.entityId(data, ActPost.EXTRA_POSTED_REPLY_ID)
postedRedraftId = EntityId.entityId(data, ActPost.EXTRA_POSTED_REDRAFT_ID)
val postedStatusId = postedStatusId
val statusJson = data.string(ActPost.KEY_EDIT_STATUS)

View File

@ -13,13 +13,13 @@ import jp.juggler.subwaytooter.action.openActPostImpl
import jp.juggler.subwaytooter.action.userProfile
import jp.juggler.subwaytooter.api.auth.Auth2Result
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.auth.authRepo
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.findStatusIdFromUrl
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.api.showApiError
import jp.juggler.subwaytooter.auth.authRepo
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.startLoading
import jp.juggler.subwaytooter.dialog.actionsDialog
@ -52,7 +52,7 @@ private val log = LogCategory("ActMainIntent")
// ActOAuthCallbackで受け取ったUriを処理する
fun ActMain.handleIntentUri(uri: Uri) {
try {
log.d("handleIntentUri $uri")
log.i("handleIntentUri $uri")
when (uri.scheme) {
"subwaytooter", "misskeyclientproto" -> handleCustomSchemaUri(uri)
else -> handleOtherUri(uri)

View File

@ -9,6 +9,8 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachmentJson
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.defaultColorIcon
@ -41,7 +43,7 @@ fun ActPost.decodeAttachments(sv: String) {
try {
sv.decodeJsonArray().objectList().forEach {
try {
attachmentList.add(PostAttachment(TootAttachment.decodeJson(it)))
attachmentList.add(PostAttachment(tootAttachmentJson(it)))
} catch (ex: Throwable) {
log.e(ex, "can't parse TootAttachment.")
}
@ -282,8 +284,9 @@ fun ActPost.sendFocusPoint(pa: PostAttachment, attachment: TootAttachment, x: Fl
put("focus", "%.2f,%.2f".format(x, y))
}.toPutRequestBuilder()
)?.also { result ->
resultAttachment =
parseItem(::TootAttachment, ServiceType.MASTODON, result.jsonObject)
resultAttachment = parseItem(result.jsonObject) {
tootAttachment(ServiceType.MASTODON, it)
}
}
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("set focus point failed."))

View File

@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachmentJson
import jp.juggler.subwaytooter.dialog.DlgDraftPicker
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoPostDraft
@ -160,7 +161,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
if (tmpAttachmentList != null) {
// 本文からURLを除去する
tmpAttachmentList.forEach {
val textUrl = TootAttachment.decodeJson(it).text_url
val textUrl = tootAttachmentJson(it).text_url
if (textUrl?.isNotEmpty() == true) {
content = content.replace(textUrl, "")
}
@ -192,7 +193,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
apiClient.account = account
// 返信ステータスが存在するかどうか
EntityId.from(draft, DRAFT_REPLY_ID)?.let { inReplyToId ->
EntityId.entityId(draft, DRAFT_REPLY_ID)?.let { inReplyToId ->
val result = apiClient.request("/api/v1/statuses/$inReplyToId")
if (isTaskCancelled()) return@launchProgress null
if (result?.jsonObject == null) {
@ -210,7 +211,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
val it = tmpAttachmentList.iterator()
while (it.hasNext()) {
if (isTaskCancelled()) return@launchProgress null
val ta = TootAttachment.decodeJson(it.next())
val ta = tootAttachmentJson(it.next())
if (checkExist(ta.url)) continue
it.remove()
isSomeAttachmentRemoved = true
@ -241,7 +242,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
val contentWarningChecked = draft.optBoolean(DRAFT_CONTENT_WARNING_CHECK)
val nsfwChecked = draft.optBoolean(DRAFT_NSFW_CHECK)
val tmpAttachmentList = draft.jsonArray(DRAFT_ATTACHMENT_LIST)
val replyId = EntityId.from(draft, DRAFT_REPLY_ID)
val replyId = EntityId.entityId(draft, DRAFT_REPLY_ID)
val draftVisibility =
TootVisibility.parseSavedVisibility(draft.string(DRAFT_VISIBILITY))
@ -293,7 +294,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
attachmentList.clear()
tmpAttachmentList.forEach {
if (it !is JsonObject) return@forEach
val pa = PostAttachment(TootAttachment.decodeJson(it))
val pa = PostAttachment(tootAttachmentJson(it))
attachmentList.add(pa)
}
}
@ -328,7 +329,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
)
}
fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String) {
suspend fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String) {
try {
val baseStatus =
TootParser(this, account).status(jsonText.decodeJsonObject())
@ -423,7 +424,7 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
}
}
fun ActPost.initializeFromEditStatus(account: SavedAccount, jsonText: String) {
suspend fun ActPost.initializeFromEditStatus(account: SavedAccount, jsonText: String) {
try {
val baseStatus =
TootParser(this, account).status(jsonText.decodeJsonObject())

View File

@ -30,7 +30,7 @@ import jp.juggler.util.ui.vg
private val log = LogCategory("ActPostExtra")
fun ActPost.appendContentText(
suspend fun ActPost.appendContentText(
src: String?,
selectBefore: Boolean = false,
) {
@ -77,7 +77,7 @@ fun ActPost.appendContentText(
}
}
fun ActPost.appendContentText(src: Intent) {
suspend fun ActPost.appendContentText(src: Intent) {
val list = ArrayList<String>()
var sv: String?
@ -128,7 +128,7 @@ suspend fun ActPost.resetText() {
}
}
fun ActPost.afterUpdateText() {
suspend fun ActPost.afterUpdateText() {
// 2017/9/13 VISIBILITY_WEB_SETTING から VISIBILITY_PUBLICに変更した
// VISIBILITY_WEB_SETTING だと 1.5未満のタンスでトラブルになるので…
states.visibility = states.visibility ?: account?.visibility ?: TootVisibility.Public
@ -211,7 +211,7 @@ suspend fun ActPost.updateText(
afterUpdateText()
}
fun ActPost.initializeFromSharedIntent(sharedIntent: Intent) {
suspend fun ActPost.initializeFromSharedIntent(sharedIntent: Intent) {
try {
val hasUri = when (sharedIntent.action) {
Intent.ACTION_VIEW -> {

View File

@ -25,7 +25,7 @@ fun ActPost.resetMushroom() {
}
@SuppressLint("InflateParams")
fun ActPost.showRecommendedPlugin(title: String?) {
suspend fun ActPost.showRecommendedPlugin(title: String?) {
@RawRes val resId = when (getString(R.string.language_code)) {
"ja" -> R.raw.recommended_plugin_ja
@ -59,7 +59,7 @@ fun ActPost.showRecommendedPlugin(title: String?) {
}
}
fun ActPost.openMushroom() {
suspend fun ActPost.openMushroom() {
try {
var text: String? = null
when {

View File

@ -31,7 +31,7 @@ fun ActPost.showQuotedRenote() {
views.cbQuote.vg(states.inReplyToId != null)
}
fun ActPost.showReplyTo() {
suspend fun ActPost.showReplyTo() {
views.llReply.vg(states.inReplyToId != null)?.let {
views.tvReplyTo.text = DecodeOptions(
this,
@ -46,7 +46,7 @@ fun ActPost.showReplyTo() {
}
}
fun ActPost.removeReply() {
suspend fun ActPost.removeReply() {
states.inReplyToId = null
states.inReplyToText = null
states.inReplyToImage = null
@ -55,7 +55,7 @@ fun ActPost.removeReply() {
showQuotedRenote()
}
fun ActPost.initializeFromReplyStatus(account: SavedAccount, jsonText: String) {
suspend fun ActPost.initializeFromReplyStatus(account: SavedAccount, jsonText: String) {
try {
val replyStatus =
TootParser(this, account).status(jsonText.decodeJsonObject())

View File

@ -3,10 +3,7 @@ package jp.juggler.subwaytooter.actpost
import jp.juggler.subwaytooter.ActPost
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.api.entity.TootScheduled
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.parseItem
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.dialog.DlgDateTime
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.PostAttachment
@ -36,14 +33,12 @@ fun ActPost.resetSchedule() {
showSchedule()
}
fun ActPost.initializeFromScheduledStatus(account: SavedAccount, jsonText: String) {
suspend fun ActPost.initializeFromScheduledStatus(account: SavedAccount, jsonText: String) {
try {
val item = parseItem(
::TootScheduled,
TootParser(this, account),
jsonText.decodeJsonObject(),
log
) ?: error("initializeFromScheduledStatus: parse failed.")
val item = parseItem(jsonText.decodeJsonObject()){
val parser =TootParser(this, account)
TootScheduled(parser,it)
} ?: error("initializeFromScheduledStatus: parse failed.")
scheduledStatus = item

View File

@ -82,12 +82,9 @@ suspend fun ActPost.restoreState(savedInstanceState: Bundle) {
account?.let { a ->
states.scheduledStatusEncoded?.let { jsonText ->
scheduledStatus = parseItem(
::TootScheduled,
TootParser(this, a),
jsonText.decodeJsonObject(),
log
)
scheduledStatus = parseItem(jsonText.decodeJsonObject()) {
TootScheduled(TootParser(this, a), it)
}
}
}
val stateAttachmentList = appState.attachmentList

View File

@ -26,6 +26,7 @@ import jp.juggler.util.data.asciiRegex
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.showKeyboard
import kotlinx.coroutines.yield
import kotlin.math.min
// 入力補完機能
@ -406,12 +407,12 @@ class CompletionHelper(
// et.setCustomSelectionActionModeCallback( action_mode_callback );
}
private fun SpannableStringBuilder.appendEmoji(
private suspend fun SpannableStringBuilder.appendEmoji(
emoji: EmojiBase,
bInstanceHasCustomEmoji: Boolean,
) = appendEmoji(bInstanceHasCustomEmoji, emoji)
private fun SpannableStringBuilder.appendEmoji(
private suspend fun SpannableStringBuilder.appendEmoji(
bInstanceHasCustomEmoji: Boolean,
emoji: EmojiBase,
): SpannableStringBuilder {
@ -468,10 +469,8 @@ class CompletionHelper(
procTextChanged.run()
// キーボードを再度表示する
App1.getAppState(
activity,
"PostHelper/EmojiPicker/cb"
).handler.post { et.showKeyboard() }
yield()
et.showKeyboard()
}
}

View File

@ -38,7 +38,7 @@ abstract class ResponseWithBase {
/**
* 応答ボディのHTMLやテキストを整形する
*/
private fun simplifyErrorHtml(body: String): String {
private suspend fun simplifyErrorHtml(body: String): String {
// JsonObjectとして解釈できるならエラーメッセージを検出する
try {
val json = body.decodeJsonObject()
@ -71,7 +71,7 @@ abstract class ResponseWithBase {
/**
* エラー応答のステータス部分や本文を文字列にする
*/
fun parseErrorResponse(body: String? = null): String =
suspend fun parseErrorResponse(body: String? = null): String =
try {
StringBuilder().apply {
// 応答ボディのテキストがあれば追加

View File

@ -2,6 +2,8 @@ package jp.juggler.subwaytooter.api
import android.content.Context
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAccount.Companion.tootAccount
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.JsonArray
@ -33,29 +35,32 @@ class TootParser(
fun getFullAcct(acct: Acct?) = linkHelper.getFullAcct(acct)
fun account(src: JsonObject?) = parseItem(::TootAccount, this, src)
fun accountList(array: JsonArray?) =
TootAccountRef.wrapList(this, parseList(::TootAccount, this, array))
fun account(src: JsonObject?) =
parseItem(src) { tootAccount(this, it) }
fun status(src: JsonObject?) = parseItem(::TootStatus, this, src)
fun statusList(array: JsonArray?) = parseList(::TootStatus, this, array)
fun accountRefList(array: JsonArray?) =
TootAccountRef.wrapList(this, parseList(array) { tootAccount(this, it) })
fun notification(src: JsonObject?) = parseItem(::TootNotification, this, src)
fun notificationList(src: JsonArray?) = parseList(::TootNotification, this, src)
fun status(src: JsonObject?) =
parseItem(src) { tootStatus(this, it) }
fun statusList(array: JsonArray?) = parseList(array) { tootStatus(this, it) }
fun tag(src: JsonObject?) =
src?.let { TootTag.parse(this, it) }
fun notification(src: JsonObject?) = parseItem(src) { TootNotification.tootNotification(this, it) }
fun notificationList(array: JsonArray?) =
parseList(array) { TootNotification.tootNotification(this, it) }
fun tagList(array: JsonArray?) =
TootTag.parseList(this, array)
fun tag(src: JsonObject?) = src?.let { TootTag.parse(this, it) }
fun tagList(array: JsonArray?) = TootTag.parseList(this, array)
fun results(src: JsonObject?) = parseItem(::TootResults, this, src)
fun instance(src: JsonObject?) = parseItem(::TootInstance, this, src)
fun results(src: JsonObject?) =
parseItem(src) { TootResults(this, it) }
fun instance(src: JsonObject?) =
parseItem(src) { TootInstance(this, it) }
fun getMisskeyUserRelation(whoId: EntityId) = misskeyUserRelationMap[whoId]
fun parseMisskeyApShow(jsonObject: JsonObject?): Any? {
// ap/show の戻り値はActivityPubオブジェクトではなく、Misskeyのエンティティです。
// ap/show の戻り値はActivityPubオブジェクトではなく、Misskeyのエンティティ
suspend fun parseMisskeyApShow(jsonObject: JsonObject?): Any? {
return when (jsonObject?.string("type")) {
"Note" -> status(jsonObject.jsonObject("object"))
"User" -> account(jsonObject.jsonObject("object"))

View File

@ -40,7 +40,10 @@ abstract class AuthBase {
fun findAuthForVerifyAccount(client: TootApiClient, misskeyVersionMajor: Int) =
when {
misskeyVersionMajor >= 13 -> MisskeyAuth13(client)
// https://mastodon.juggler.jp/@tateisu/109819635248751031
// https://github.com/misskey-dev/misskey/issues/9825
// https://github.com/misskey-dev/misskey/commit/788ae2f6ca37d297e912bfba02821543e8566522
// misskeyVersionMajor >= 13 -> MisskeyAuth13(client)
misskeyVersionMajor > 0 -> MisskeyAuth10(client)
else -> MastodonAuth(client)
}

View File

@ -1,9 +1,8 @@
package jp.juggler.subwaytooter.auth
package jp.juggler.subwaytooter.api.auth
import android.content.Context
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.auth.Auth2Result
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
import jp.juggler.util.data.JsonArray
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.cast
@ -18,10 +19,9 @@ class APAttachment(jsonArray: JsonArray?) {
?.mapNotNull { it.cast<JsonObject>() }
?.forEach { it ->
try {
when (it.string("type")) {
"Document" -> {
mediaAttachments.add(TootAttachment(ServiceType.NOTESTOCK, it))
}
if (it.string("type") == "Document") {
tootAttachment(ServiceType.NOTESTOCK, it)
.let { mediaAttachments.add(it) }
}
} catch (ex: Throwable) {
log.e(ex, "APAttachment ctor failed.")

View File

@ -36,20 +36,20 @@ class EntityId(val x: String) : Comparable<EntityId> {
return EntityId(this.substring(1))
}
fun from(intent: Intent?, key: String) =
fun entityId(intent: Intent?, key: String) =
intent?.string(key)?.decodeEntityId()
fun from(bundle: Bundle?, key: String) =
fun entityId(bundle: Bundle?, key: String) =
bundle?.string(key)?.decodeEntityId()
// 内部保存データのデコード用。APIレスポンスのパースに使ってはいけない
fun from(data: JsonObject?, key: String): EntityId? {
fun entityId(data: JsonObject?, key: String): EntityId? {
val o = data?.get(key)
if (o is Long) return EntityId(o.toString())
return (o as? String)?.decodeEntityId()
}
fun from(cursor: Cursor, key: String) =
fun entityId(cursor: Cursor, key: String) =
cursor.getStringOrNull(key)?.decodeEntityId()
}

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.util.data.JsonArray
import jp.juggler.util.data.JsonException
import jp.juggler.util.data.JsonObject
@ -12,273 +11,125 @@ object EntityUtil {
////////////////////////////////////////
// JSONObjectを渡してEntityを生成するコードのnullチェックと例外補足
// creator()を呼び出して例外チェックを行う
inline fun <reified T> parseItem(
factory: (src: JsonObject) -> T,
src: JsonObject?,
log: LogCategory = EntityUtil.log,
): T? {
if (src == null) return null
return try {
factory(src)
} catch (ex: Throwable) {
log.e(ex, "${T::class.simpleName} parse failed.")
null
}
creator: () -> T?,
): T? = try {
creator()
} catch (ex: Throwable) {
EntityUtil.log.e(ex, "parseItem failed. ${T::class.simpleName}")
null
}
inline fun <P1 : Any, reified T> parseItem(
p1: P1?,
creator: (P1) -> T?,
): T? = try {
p1?.let { creator(it) }
} catch (ex: Throwable) {
EntityUtil.log.e(ex, "parseItemP1 failed. ${T::class.simpleName}")
null
}
// creator(JsonObject)を呼び出して例外チェックを行う
inline fun <reified T> parseList(
factory: (src: JsonObject) -> T,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
creator: (JsonObject) -> T,
): ArrayList<T> {
val dst = ArrayList<T>()
val dstList = ArrayList<T>()
if (src != null) {
val src_length = src.size
if (src_length > 0) {
dst.ensureCapacity(src_length)
for (i in 0 until src_length) {
val item = parseItem(factory, src.jsonObject(i), log)
if (item != null) dst.add(item)
val srcSize = src.size
for (i in 0 until srcSize) {
try {
val dst = src.jsonObject(i)?.let { creator(it) }
?: continue
dstList.add(dst)
} catch (ex: Throwable) {
EntityUtil.log.w("parseList failed. ${T::class.simpleName}")
}
}
}
return dst
}
inline fun <S, reified T> parseList(
factory: (serviceType: S, src: JsonObject) -> T,
serviceType: S,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
): ArrayList<T> {
val dst = ArrayList<T>()
if (src != null) {
val src_length = src.size
if (src_length > 0) {
dst.ensureCapacity(src_length)
for (i in 0 until src_length) {
val item = parseItem(factory, serviceType, src.jsonObject(i), log)
if (item != null) dst.add(item)
}
}
}
return dst
return dstList
}
inline fun <reified T> parseListOrNull(
factory: (src: JsonObject) -> T,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
creator: (JsonObject) -> T?,
): ArrayList<T>? {
var dstList: ArrayList<T>? = null
if (src != null) {
val src_length = src.size
if (src_length > 0) {
val dst = ArrayList<T>(src_length)
for (i in 0 until src.size) {
val item = parseItem(factory, src.jsonObject(i), log)
if (item != null) dst.add(item)
val srcSize = src.size
for (i in src.indices) {
try {
val dst = src.jsonObject(i)?.let { creator(it) }
?: continue
(dstList ?: ArrayList<T>(srcSize).also { dstList = it }).add(dst)
} catch (ex: Throwable) {
EntityUtil.log.w("parseListOrNull failed. ${T::class.simpleName}")
}
if (dst.isNotEmpty()) return dst
}
}
return null
return dstList
}
@Suppress("unused")
inline fun <reified K, reified V> parseMap(
factory: (src: JsonObject) -> V,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
creator: (JsonObject) -> V?,
): HashMap<K, V> where V : Mappable<K> {
val dst = HashMap<K, V>()
val dstMap = HashMap<K, V>()
if (src != null) {
for (i in src.indices) {
val item = parseItem(factory, src.jsonObject(i), log)
if (item != null) dst[item.mapKey] = item
try {
val dst = src.jsonObject(i)?.let { creator(it) } ?: continue
dstMap[dst.mapKey] = dst
} catch (ex: Throwable) {
EntityUtil.log.w("parseMap failed. ${V::class.simpleName}")
}
}
}
return dst
return dstMap
}
inline fun <reified K, reified V> parseMapOrNull(
factory: (src: JsonObject) -> V,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
creator: (src: JsonObject) -> V?,
): HashMap<K, V>? where V : Mappable<K> {
var dstMap: HashMap<K, V>? = null
if (src != null) {
val size = src.size
if (size > 0) {
val dst = HashMap<K, V>()
for (i in 0 until size) {
val item = parseItem(factory, src.jsonObject(i), log)
if (item != null) dst[item.mapKey] = item
for (i in src.indices) {
try {
val dst = src.jsonObject(i)?.let { creator(it) } ?: continue
(dstMap ?: HashMap<K, V>().also { dstMap = it })[dst.mapKey] = dst
} catch (ex: Throwable) {
EntityUtil.log.w("parseMapOrNull failed. ${V::class.simpleName}")
}
if (dst.isNotEmpty()) return dst
}
}
return null
}
inline fun <reified K, reified V> parseMapOrNull(
factory: (apDomain: Host, apiHost: Host, src: JsonObject) -> V,
apDomain: Host,
apiHost: Host,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
): HashMap<K, V>? where V : Mappable<K> {
if (src != null) {
val size = src.size
if (size > 0) {
val dst = HashMap<K, V>()
for (i in 0 until size) {
val item = parseItem(factory, apDomain, apiHost, src.jsonObject(i), log)
if (item != null) dst[item.mapKey] = item
}
if (dst.isNotEmpty()) return dst
}
}
return null
return dstMap
}
inline fun <reified V> parseProfileEmoji2(
factory: (src: JsonObject, shortcode: String) -> V,
src: JsonObject?,
log: LogCategory = EntityUtil.log,
srcMap: JsonObject?,
creator: (src: JsonObject, shortcode: String) -> V,
): HashMap<String, V>? {
if (src != null) {
val size = src.size
if (size > 0) {
val dst = HashMap<String, V>()
for (key in src.keys) {
val v = src.jsonObject(key) ?: continue
val item = try {
factory(v, key)
} catch (ex: Throwable) {
log.e(ex, "parseProfileEmoji2 failed.")
null
}
if (item != null) dst[key] = item
}
if (dst.isNotEmpty()) return dst
}
}
return null
}
////////////////////////////////////////
inline fun <P, reified T> parseItem(
factory: (parser: P, src: JsonObject) -> T,
parser: P,
src: JsonObject?,
log: LogCategory = EntityUtil.log,
): T? {
if (src == null) return null
return try {
factory(parser, src)
} catch (ex: Throwable) {
log.e(ex, "${T::class.simpleName} parse failed.")
null
}
}
inline fun <P1, P2, reified T> parseItem(
factory: (p1: P1, p2: P2, src: JsonObject) -> T,
p1: P1,
p2: P2,
src: JsonObject?,
log: LogCategory = EntityUtil.log,
): T? {
if (src == null) return null
return try {
factory(p1, p2, src)
} catch (ex: Throwable) {
log.e(ex, "${T::class.simpleName} parse failed.")
null
}
}
inline fun <reified T> parseItem(
factory: (serviceType: ServiceType, src: JsonObject) -> T,
serviceType: ServiceType,
src: JsonObject?,
log: LogCategory = EntityUtil.log,
): T? {
if (src == null) return null
return try {
factory(serviceType, src)
} catch (ex: Throwable) {
log.e(ex, "${T::class.simpleName} parse failed.")
null
}
}
inline fun <P1, reified T> parseListP1(
factory: (p1: P1, src: JsonObject) -> T,
p1: P1,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
): ArrayList<T> {
val dst = ArrayList<T>()
if (src != null) {
val src_length = src.size
if (src_length > 0) {
dst.ensureCapacity(src_length)
for (i in src.indices) {
val item = parseItem(factory, p1, src.jsonObject(i), log)
if (item != null) dst.add(item)
var dstMap: HashMap<String, V>? = null
if (srcMap != null) {
for (key in srcMap.keys) {
try {
val dst = srcMap.jsonObject(key)
?.let { creator(it, key) }
?: continue
(dstMap ?: HashMap<String, V>().also { dstMap = it })[key] = dst
} catch (ex: Throwable) {
EntityUtil.log.w("parseProfileEmoji2 failed. ${V::class.simpleName}")
}
}
}
return dst
return dstMap
}
inline fun <P1, P2, reified T> parseListP2(
factory: (p1: P1, p2: P2, src: JsonObject) -> T,
p1: P1,
p2: P2,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
): ArrayList<T> {
val dst = ArrayList<T>()
if (src != null) {
val src_length = src.size
if (src_length > 0) {
dst.ensureCapacity(src_length)
for (i in src.indices) {
val item = parseItem(factory, p1, p2, src.jsonObject(i), log)
if (item != null) dst.add(item)
}
}
}
return dst
}
@Suppress("unused")
inline fun <reified T> parseListOrNull(
factory: (parser: TootParser, src: JsonObject) -> T,
parser: TootParser,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
): ArrayList<T>? {
if (src != null) {
val src_length = src.size
if (src_length > 0) {
val dst = ArrayList<T>()
dst.ensureCapacity(src_length)
for (i in src.indices) {
val item = parseItem(factory, parser, src.jsonObject(i), log)
if (item != null) dst.add(item)
}
if (dst.isNotEmpty()) return dst
}
}
return null
}
////////////////////////////////////////
// 添付データのJSON表現のリストを作る
fun <T : TootAttachmentLike> ArrayList<T>.encodeJson(): JsonArray {
val a = JsonArray()
forEach { ta ->

View File

@ -7,10 +7,10 @@ import android.widget.TextView
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.MisskeyAccountDetailMap
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.LinkHelper
@ -24,69 +24,94 @@ import jp.juggler.util.ui.vg
import java.util.*
import java.util.regex.Pattern
open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
open class TootAccount(
//URL of the user's profile page (can be remote)
// https://mastodon.juggler.jp/@tateisu
// 疑似アカウントではnullになります
val url: String?
val url: String?,
// The ID of the account
val id: EntityId
val id: EntityId,
// The username of the account /[A-Za-z0-9_]{1,30}/
val username: String
val username: String,
final override val apiHost: Host
final override val apDomain: Host
final override val apiHost: Host,
final override val apDomain: Host,
// Equals username for local users, includes @domain for remote ones
val acct: Acct
val acct: Acct,
// The account's display name
val display_name: String
val display_name: String,
//Boolean for when the account cannot be followed without waiting for approval first
val locked: Boolean
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
val created_at: String?,
val time_created_at: Long,
// The number of followers for the account
var followers_count: Long? = null
var followers_count: Long? = null,
//The number of accounts the given account is following
var following_count: Long? = null
var following_count: Long? = null,
// The number of statuses the account has made
var statuses_count: Long? = null
var statuses_count: Long? = null,
// Biography of user
// 説明文。改行は\r\n。リンクなどはHTMLタグで書かれている
val note: String?
val note: String?,
// URL to the avatar image
val avatar: String?
val avatar: String?,
// URL to the avatar static image (gif)
val avatar_static: String?
val avatar_static: String?,
//URL to the header image
val header: String?
val header: String?,
// URL to the header static image (gif)
val header_static: String?
val header_static: String?,
val source: Source?
val source: Source?,
val profile_emojis: HashMap<String, NicoProfileEmoji>?
val profile_emojis: HashMap<String, NicoProfileEmoji>?,
val movedRef: TootAccountRef?
val movedRef: TootAccountRef?,
val moved: TootAccount?
get() = movedRef?.get()
val fields: ArrayList<Field>?,
val custom_emojis: HashMap<String, CustomEmoji>?,
val bot: Boolean,
val isCat: Boolean,
val isAdmin: Boolean,
val isPro: Boolean,
// 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,
// 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
private var last_status_at: Long = 0L,
// mastodon 3.3.0
var suspended: Boolean = false,
val json: JsonObject,
) : HostAndDomain {
class Field(
val name: String,
@ -94,14 +119,8 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
val verified_at: Long, // 0L if not verified
)
val fields: ArrayList<Field>?
val custom_emojis: HashMap<String, CustomEmoji>?
val bot: Boolean
val isCat: Boolean
val isAdmin: Boolean
val isPro: Boolean
val moved: TootAccount?
get() = movedRef?.get()
@Suppress("unused")
val isLocal: Boolean
@ -112,308 +131,6 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
fun getUserUrl() = url ?: "https://${apDomain.pretty}/@$username"
// 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
// 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
private var last_status_at = 0L
// mastodon 3.3.0
var suspended = false
val json: JsonObject
init {
this.json = src
src["_fromStream"] = parser.fromStream
when (parser.serviceType) {
ServiceType.MISSKEY -> {
this.custom_emojis =
parseMapOrNull(
CustomEmoji.decodeMisskey,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis")
)
this.profile_emojis = null
this.username = src.stringOrThrow("username")
this.apiHost = src.string("host")?.let { Host.parse(it) } ?: parser.apiHost
this.url = "https://${apiHost.ascii}/@$username"
this.apDomain = apiHost // FIXME apiHostとapDomainが異なる場合はMisskeyだとどうなの…
@Suppress("LeakingThis")
this.acct = when {
// アクセス元から見て内部ユーザなら short acct
parser.linkHelper.matchHost(this) -> Acct.parse(username)
// アクセス元から見て外部ユーザならfull acct
else -> Acct.parse(username, apDomain)
}
//
val sv = src.string("name")
this.display_name = if (sv?.isNotEmpty() == true) sv.sanitizeBDI() else username
//
this.note = src.string("description")
this.source = null
this.movedRef = null
this.locked = src.optBoolean("isLocked")
this.bot = src.optBoolean("isBot", false)
this.isCat = src.optBoolean("isCat", false)
this.isAdmin = src.optBoolean("isAdmin", false)
this.isPro = src.optBoolean("isPro", false)
// this.user_hides_network = src.optBoolean("user_hides_network")
this.id = EntityId.mayDefault(src.string("id"))
this.followers_count = src.long("followersCount") ?: -1L
this.following_count = src.long("followingCount") ?: -1L
this.statuses_count = src.long("notesCount") ?: -1L
this.created_at = src.string("createdAt")
this.time_created_at = TootStatus.parseTime(this.created_at)
// 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"
}
this.avatar = src.string("avatarUrl")
this.avatar_static = src.string("avatarUrl")?.getStaticImageUrl()
this.header = src.string("bannerUrl")
this.header_static = src.string("bannerUrl")?.getStaticImageUrl()
this.pinnedNoteIds = src.stringArrayList("pinnedNoteIds")
if (parser.misskeyDecodeProfilePin) {
val list = parseList(::TootStatus, parser, src.jsonArray("pinnedNotes"))
list.forEach { it.pinned = true }
this.pinnedNotes = list.ifEmpty { null }
}
val profile = src.jsonObject("profile")
this.location = profile?.string("location")
this.birthday = profile?.string("birthday")
this.fields = parseMisskeyFields(src)
daoUserRelation.fromAccount(parser, src, id)
@Suppress("LeakingThis")
MisskeyAccountDetailMap.fromAccount(parser, this, id)
}
ServiceType.NOTESTOCK -> {
// notestock はActivityPub 準拠のサービスなので、サーバ内IDというのは特にない
this.id = EntityId.DEFAULT
this.username =
src.stringOrThrow("display_name") // notestockはdisplay_nameとusernameが入れ替わってる
this.display_name = src.stringOrThrow("username")
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.")
)
this.apiHost = apiHost
this.apDomain = apDomain
this.acct = Acct.parse(this.username, apDomain)
this.avatar = src.string("avatar")
this.avatar_static = src.string("avatar_static")
this.header = src.string("header")
this.header_static = src.string("header_static")
this.locked = src.boolean("manuallyApprovesFollowers") ?: false
this.note = src.string("note")
val apTag = APTag(parser, src.jsonArray("tag"))
this.custom_emojis = apTag.emojiList.notEmpty()
this.profile_emojis = apTag.profileEmojiList.notEmpty()
// APだと attachment にデータはあるが、検索結果に表示しないので読まない
this.fields = null
this.source = null
this.movedRef = null
this.followers_count = null
this.following_count = null
this.statuses_count = null
this.created_at = null
this.time_created_at = 0L
this.bot = false
this.isCat = false
this.isAdmin = false
this.isPro = false
}
else -> {
// 絵文字データは先に読んでおく
this.custom_emojis = parseMapOrNull(
CustomEmoji.decode,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis")
)
this.profile_emojis = when (val o = src["profile_emojis"]) {
is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, TootStatus.log)
is JsonObject -> parseProfileEmoji2(::NicoProfileEmoji, o, TootStatus.log)
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(
parser,
src.jsonObject("moved")?.let {
TootAccount(parser, it)
}
)
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) {
ServiceType.MASTODON -> {
this.id = EntityId.mayDefault(src.string("id"))
val tmpAcct = src.stringOrThrow("acct")
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
this.acct =
Acct.parse(username, if (tmpAcct.contains('@')) apDomain else null)
this.followers_count = src.long("followers_count")
this.following_count = src.long("following_count")
this.statuses_count = src.long("statuses_count")
this.created_at = src.string("created_at")
this.time_created_at = TootStatus.parseTime(this.created_at)
this.avatar = src.string("avatar")
this.avatar_static = src.string("avatar_static")
this.header = src.string("header")
this.header_static = src.string("header_static")
}
ServiceType.TOOTSEARCH -> {
// tootsearch のアカウントのIDはどのタンス上のものか分からないので役に立たない
this.id = EntityId.DEFAULT
val tmpAcct = src.stringOrThrow("acct")
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
this.acct = Acct.parse(this.username, this.apDomain)
this.followers_count = src.long("followers_count")
this.following_count = src.long("following_count")
this.statuses_count = src.long("statuses_count")
this.created_at = src.string("created_at")
this.time_created_at = TootStatus.parseTime(this.created_at)
this.avatar = src.string("avatar")
this.avatar_static = src.string("avatar_static")
this.header = src.string("header")
this.header_static = src.string("header_static")
}
ServiceType.MSP -> {
this.id = EntityId.mayDefault(src.string("id"))
// 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")
this.apiHost = apiHost
this.apDomain = apiHost
this.acct = Acct.parse(this.username, this.apDomain)
this.followers_count = null
this.following_count = null
this.statuses_count = null
this.created_at = null
this.time_created_at = 0L
val avatar = src.string("avatar")
this.avatar = avatar
this.avatar_static = avatar
this.header = null
this.header_static = null
}
ServiceType.MISSKEY, ServiceType.NOTESTOCK -> error("will not happen")
}
}
}
}
class Source(src: JsonObject) {
// デフォルト公開範囲
@ -441,7 +158,21 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
// リストメンバーダイアログや引っ越し先ユーザなど、TL以外の部分に名前を表示する場合は
// Invalidator の都合でSpannableを別途生成する必要がある
fun decodeDisplayName(context: Context): Spannable {
// remove white spaces
val sv = reWhitespace.matcher(display_name).replaceAll(" ")
// decode emoji code
return DecodeOptions(
context,
emojiMapProfile = profile_emojis,
emojiMapCustom = custom_emojis,
authorDomain = this
).decodeEmoji(sv)
}
// リストメンバーダイアログや引っ越し先ユーザなど、TL以外の部分に名前を表示する場合は
// Invalidator の都合でSpannableを別途生成する必要がある
fun decodeDisplayNameCached(context: Context): Spannable {
// remove white spaces
val sv = reWhitespace.matcher(display_name).replaceAll(" ")
@ -655,8 +386,355 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
"""\Ahttps://($reHostIdn)/users/(\w|\w+[\w-]*\w)(?=\z|[?#])"""
.asciiPattern()
// notestockはaccountのnotag先頭に
fun tootAccount(parser: TootParser, src: JsonObject): TootAccount {
src["_fromStream"] = parser.fromStream
val acct: Acct
val apDomain: Host
val apiHost: Host
var avatar: String? = null
var avatar_static: String? = null
var birthday: String? = null
val bot: Boolean
val created_at: String?
val custom_emojis: HashMap<String, CustomEmoji>?
val display_name: String
val fields: ArrayList<Field>?
val followers_count: Long?
val following_count: Long?
val header: String?
val header_static: String?
val id: EntityId
val isAdmin: Boolean
val isCat: Boolean
val isPro: Boolean
var location: String? = null
val locked: Boolean
val movedRef: TootAccountRef?
val note: String?
var pinnedNoteIds: ArrayList<String>? = null
var pinnedNotes: ArrayList<TootStatus>? = null
val profile_emojis: HashMap<String, NicoProfileEmoji>?
val source: Source?
val statuses_count: Long?
val time_created_at: Long
val url: String?
val username: String
var suspended = false
var last_status_at = 0L
when (parser.serviceType) {
ServiceType.MISSKEY -> {
custom_emojis =
parseMapOrNull(src.jsonArray("emojis")) {
CustomEmoji.decodeMisskey(parser.apDomain, parser.apiHost, it)
}
profile_emojis = null
username = src.stringOrThrow("username")
apiHost = src.string("host")?.let { Host.parse(it) } ?: parser.apiHost
url = "https://${apiHost.ascii}/@$username"
apDomain = apiHost // FIXME apiHostとapDomainが異なる場合はMisskeyだとどうなの…
@Suppress("LeakingThis")
acct = when {
// アクセス元から見て内部ユーザなら short acct
parser.linkHelper.matchHost(apiHost, apDomain) -> Acct.parse(username)
// アクセス元から見て外部ユーザならfull acct
else -> Acct.parse(username, apDomain)
}
//
val sv = src.string("name")
display_name = if (sv?.isNotEmpty() == true) sv.sanitizeBDI() else username
//
note = src.string("description")
source = null
movedRef = null
locked = src.optBoolean("isLocked")
bot = src.optBoolean("isBot", false)
isCat = src.optBoolean("isCat", false)
isAdmin = src.optBoolean("isAdmin", false)
isPro = src.optBoolean("isPro", false)
// this.user_hides_network = src.optBoolean("user_hides_network")
id = EntityId.mayDefault(src.string("id"))
followers_count = src.long("followersCount") ?: -1L
following_count = src.long("followingCount") ?: -1L
statuses_count = src.long("notesCount") ?: -1L
created_at = src.string("createdAt")
time_created_at = TootStatus.parseTime(created_at)
// 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"
}
avatar = src.string("avatarUrl")
avatar_static = src.string("avatarUrl")?.getStaticImageUrl()
header = src.string("bannerUrl")
header_static = src.string("bannerUrl")?.getStaticImageUrl()
pinnedNoteIds = src.stringArrayList("pinnedNoteIds")
if (parser.misskeyDecodeProfilePin) {
val list =
parseList(src.jsonArray("pinnedNotes")) { tootStatus(parser, it) }
list.forEach { it.pinned = true }
pinnedNotes = list.ifEmpty { null }
}
val profile = src.jsonObject("profile")
location = profile?.string("location")
birthday = profile?.string("birthday")
fields = parseMisskeyFields(src)
daoUserRelation.fromAccount(parser, src, id)
}
ServiceType.NOTESTOCK -> {
// notestock はActivityPub 準拠のサービスなので、サーバ内IDというのは特にない
id = EntityId.DEFAULT
username =
src.stringOrThrow("display_name") // notestockはdisplay_nameとusernameが入れ替わってる
display_name = src.stringOrThrow("username")
val tmpAcct = src.string("subject")?.let { Acct.parse(it) }
apDomain = tmpAcct?.takeIf { it.isValidFull }?.host
?: Host.parse(
src.string("id").mayUri()?.authority?.notEmpty()
?: error("can't get apDomain from account's AP id.")
)
url = src.string("url")
apiHost = Host.parse(
url.mayUri()?.authority?.notEmpty()
?: error("can't get apiHost from account's AP url.")
)
acct = Acct.parse(username, apDomain)
avatar = src.string("avatar")
avatar_static = src.string("avatar_static")
header = src.string("header")
header_static = src.string("header_static")
locked = src.boolean("manuallyApprovesFollowers") ?: false
note = src.string("note")
val apTag = APTag(parser, src.jsonArray("tag"))
custom_emojis = apTag.emojiList.notEmpty()
profile_emojis = apTag.profileEmojiList.notEmpty()
// APだと attachment にデータはあるが、検索結果に表示しないので読まない
fields = null
source = null
movedRef = null
followers_count = null
following_count = null
statuses_count = null
created_at = null
time_created_at = 0L
bot = false
isCat = false
isAdmin = false
isPro = false
}
else -> {
// 絵文字データは先に読んでおく
custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
CustomEmoji.decode(
parser.apDomain,
parser.apiHost,
it
)
}
profile_emojis = when (val o = src["profile_emojis"]) {
is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) }
is JsonObject -> parseProfileEmoji2(o) { j, k -> NicoProfileEmoji(j, k) }
else -> null
}
// 疑似アカウントにacctとusernameだけ
url = src.string("url")
username = src.stringOrThrow("username")
//
val sv = src.string("display_name")
display_name = if (sv?.isNotEmpty() == true) sv.sanitizeBDI() else username
//
note = src.string("note")
source = parseSource(src.jsonObject("source"))
movedRef = TootAccountRef.mayNull(
parser,
src.jsonObject("moved")?.let {
tootAccount(parser, it)
}
)
locked = src.optBoolean("locked")
fields = parseFields(src.jsonArray("fields"))
bot = src.optBoolean("bot", false)
suspended = src.optBoolean("suspended", false)
isAdmin = false
isCat = false
isPro = false
// this.user_hides_network = src.optBoolean("user_hides_network")
last_status_at = TootStatus.parseTime(src.string("last_status_at"))
when (parser.serviceType) {
ServiceType.MASTODON -> {
id = EntityId.mayDefault(src.string("id"))
val tmpAcct = src.stringOrThrow("acct")
val pair = findHostFromUrl(
tmpAcct,
parser.linkHelper,
url
)
apiHost = pair.first ?: error("can't get apiHost from acct or url")
apDomain = pair.second ?: error("can't get apDomain from acct or url")
acct =
Acct.parse(username, if (tmpAcct.contains('@')) apDomain else null)
followers_count = src.long("followers_count")
following_count = src.long("following_count")
statuses_count = src.long("statuses_count")
created_at = src.string("created_at")
time_created_at = TootStatus.parseTime(created_at)
avatar = src.string("avatar")
avatar_static = src.string("avatar_static")
header = src.string("header")
header_static = src.string("header_static")
}
ServiceType.TOOTSEARCH -> {
// tootsearch のアカウントのIDはどのタンス上のものか分からないので役に立たない
id = EntityId.DEFAULT
val tmpAcct = src.stringOrThrow("acct")
val pair = findHostFromUrl(tmpAcct, null, url)
apiHost = pair.first ?: error("can't get apiHost from acct or url")
apDomain = pair.second ?: error("can't get apDomain from acct or url")
acct = Acct.parse(username, apDomain)
followers_count = src.long("followers_count")
following_count = src.long("following_count")
statuses_count = src.long("statuses_count")
created_at = src.string("created_at")
time_created_at = TootStatus.parseTime(created_at)
avatar = src.string("avatar")
avatar_static = src.string("avatar_static")
header = src.string("header")
header_static = src.string("header_static")
}
ServiceType.MSP -> {
id = EntityId.mayDefault(src.string("id"))
// MSPはLTLの情報しか持ってないのでacctは常にホスト名部分を持たない
val pair = findHostFromUrl(null, null, url)
apiHost = pair.first ?: error("can't get apiHost from acct or url")
apDomain = pair.second ?: error("can't get apDomain from acct or url")
acct = Acct.parse(username, apDomain)
followers_count = null
following_count = null
statuses_count = null
created_at = null
time_created_at = 0L
avatar = src.string("avatar")
avatar_static = avatar
header = null
header_static = null
}
ServiceType.MISSKEY, ServiceType.NOTESTOCK -> error("will not happen")
}
}
}
return TootAccount(
acct = acct,
apDomain = apDomain,
apiHost = apiHost,
avatar = avatar,
avatar_static = avatar_static,
birthday = birthday,
bot = bot,
created_at = created_at,
custom_emojis = custom_emojis,
display_name = display_name,
fields = fields,
followers_count = followers_count,
following_count = following_count,
header = header,
header_static = header_static,
id = id,
isAdmin = isAdmin,
isCat = isCat,
isPro = isPro,
json = src,
location = location,
locked = locked,
movedRef = movedRef,
note = note,
pinnedNoteIds = pinnedNoteIds,
pinnedNotes = pinnedNotes,
profile_emojis = profile_emojis,
source = source,
statuses_count = statuses_count,
time_created_at = time_created_at,
url = url,
username = username,
suspended = suspended,
last_status_at = last_status_at,
).apply {
when (parser.serviceType) {
ServiceType.MISSKEY -> {
@Suppress("LeakingThis")
MisskeyAccountDetailMap.fromAccount(parser, this, id)
}
else -> Unit
}
}
}
// notestockはaccountのnotag先頭に
fun getAcctFromUrl(url: String?): Acct? {
url ?: return null
@ -687,7 +765,10 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
null
}
private fun findApDomain(acctArg: String?, linkHelper: LinkHelper?): Host? {
private fun findApDomain(
acctArg: String?,
linkHelper: LinkHelper?,
): Host? {
// acctから調べる
if (acctArg != null) {
val acct = Acct.parse(acctArg)
@ -712,9 +793,12 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
// Tootsearch用。URLやUriを使ってアカウントのインスタンス名を調べる
fun findHostFromUrl(
acctArg: String?,
linkHelper: LinkHelper?,
url: String?,
): Pair<Host?, Host?> {
linkHelper: LinkHelper
?,
url: String
?,
)
: Pair<Host?, Host?> {
val apDomain = findApDomain(acctArg, linkHelper)
val apiHost = findApiHost(url)
return Pair(apiHost ?: apDomain, apDomain ?: apiHost)

View File

@ -5,51 +5,54 @@ import jp.juggler.subwaytooter.api.TootAccountMap
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.util.DecodeOptions
class TootAccountRef(parser: TootParser, account: TootAccount) : TimelineItem() {
val mapId: Int
class TootAccountRef private constructor(
val mapId: Int,
// The account's display name
val decoded_display_name: Spannable
val decoded_note: Spannable
val decoded_display_name: Spannable,
val decoded_note: Spannable,
) : TimelineItem() {
var _orderId: EntityId? = null
override fun getOrderId(): EntityId = _orderId ?: get().id
init {
this.mapId = TootAccountMap.register(parser, account)
this.decoded_display_name = account.decodeDisplayName(parser.context)
this.decoded_note = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
emojiMapProfile = account.profile_emojis,
emojiMapCustom = account.custom_emojis,
unwrapEmojiImageTag = true,
authorDomain = account,
).decodeHTML(account.note)
}
fun get() = TootAccountMap.find(this)
companion object {
fun notNull(parser: TootParser, account: TootAccount) =
tootAccountRef(parser, account)
fun mayNull(parser: TootParser, account: TootAccount?): TootAccountRef? {
return when (account) {
null -> null
else -> TootAccountRef(parser, account)
else -> tootAccountRef(parser, account)
}
}
fun notNull(parser: TootParser, account: TootAccount) = TootAccountRef(parser, account)
fun wrapList(parser: TootParser, src: Iterable<TootAccount>): ArrayList<TootAccountRef> {
fun wrapList(
parser: TootParser,
src: Iterable<TootAccount>,
): ArrayList<TootAccountRef> {
val dst = ArrayList<TootAccountRef>()
for (a in src) {
dst.add(TootAccountRef(parser, a))
dst.add(tootAccountRef(parser, a))
}
return dst
}
fun tootAccountRef(parser: TootParser, account: TootAccount) =
TootAccountRef(
mapId = TootAccountMap.register(parser, account),
decoded_display_name = account.decodeDisplayName(parser.context),
decoded_note = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
emojiMapProfile = account.profile_emojis,
emojiMapCustom = account.custom_emojis,
unwrapEmojiImageTag = true,
authorDomain = account,
).decodeHTML(account.note),
)
}
}

View File

@ -8,74 +8,73 @@ import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
class TootAnnouncement(parser: TootParser, src: JsonObject) {
// {"id":"1",
// "content":"\u003cp\u003e日本語\u003cbr /\u003eURL \u003ca href=\"https://www.youtube.com/watch?v=2n1fM2ItdL8\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003eyoutube.com/watch?v=2n1fM2ItdL\u003c/span\u003e\u003cspan class=\"invisible\"\u003e8\u003c/span\u003e\u003c/a\u003e\u003cbr /\u003eカスタム絵文字 :ct013: \u003cbr /\u003e普通の絵文字 🤹 \u003c/p\u003e\u003cp\u003e改行2つ\u003c/p\u003e",
// "starts_at":"2020-01-23T00:00:00.000Z",
// "ends_at":"2020-01-28T23:59:00.000Z",
// "all_day":true,
// "mentions":[],
// "tags":[],
// "emojis":[{"shortcode":"ct013","url":"https://m2j.zzz.ac/custom_emojis/images/000/004/116/original/ct013.png","static_url":"https://m2j.zzz.ac/custom_emojis/images/000/004/116/static/ct013.png","visible_in_picker":true}],
// "reactions":[]}]
val id = EntityId.mayDefault(src.string("id"))
val starts_at = TootStatus.parseTime(src.string("starts_at"))
val ends_at = TootStatus.parseTime(src.string("ends_at"))
val all_day = src.boolean("all_day") ?: false
val published_at = TootStatus.parseTime(src.string("published_at"))
val updated_at = TootStatus.parseTime(src.string("updated_at"))
private val custom_emojis: HashMap<String, CustomEmoji>?
// {"id":"1",
// "content":"\u003cp\u003e日本語\u003cbr /\u003eURL \u003ca href=\"https://www.youtube.com/watch?v=2n1fM2ItdL8\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003eyoutube.com/watch?v=2n1fM2ItdL\u003c/span\u003e\u003cspan class=\"invisible\"\u003e8\u003c/span\u003e\u003c/a\u003e\u003cbr /\u003eカスタム絵文字 :ct013: \u003cbr /\u003e普通の絵文字 🤹 \u003c/p\u003e\u003cp\u003e改行2つ\u003c/p\u003e",
// "starts_at":"2020-01-23T00:00:00.000Z",
// "ends_at":"2020-01-28T23:59:00.000Z",
// "all_day":true,
// "mentions":[],
// "tags":[],
// "emojis":[{"shortcode":"ct013","url":"https://m2j.zzz.ac/custom_emojis/images/000/004/116/original/ct013.png","static_url":"https://m2j.zzz.ac/custom_emojis/images/000/004/116/static/ct013.png","visible_in_picker":true}],
// "reactions":[]}]
class TootAnnouncement(
val id: EntityId,
val starts_at: Long,
val ends_at: Long,
val all_day: Boolean,
val published_at: Long,
val updated_at: Long,
private val custom_emojis: HashMap<String, CustomEmoji>?,
// Body of the status; this will contain HTML (remote HTML already sanitized)
val content: String
val decoded_content: Spannable
val content: String,
val decoded_content: Spannable,
//An array of Tags
val tags: List<TootTag>?
val tags: List<TootTag>?,
// An array of Mentions
val mentions: ArrayList<TootMention>?
var reactions: MutableList<TootReaction>? = null
init {
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis = parseMapOrNull(
CustomEmoji.decode,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis"),
log
)
this.tags = TootTag.parseListOrNull(parser, src.jsonArray("tags"))
this.mentions = parseListOrNull(::TootMention, src.jsonArray("mentions"), log)
val options = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
emojiMapCustom = custom_emojis,
// emojiMapProfile = profile_emojis,
// attachmentList = media_attachments,
highlightTrie = parser.highlightTrie,
mentions = mentions,
)
this.content = src.string("content") ?: ""
this.decoded_content = options.decodeHTML(content)
this.reactions = parseListOrNull(TootReaction::parseFedibird, src.jsonArray("reactions"))
}
val mentions: ArrayList<TootMention>?,
var reactions: MutableList<TootReaction>? = null,
) {
companion object {
private val log = LogCategory("TootAnnouncement")
fun tootAnnouncement(parser: TootParser, src: JsonObject): TootAnnouncement {
val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
CustomEmoji.decode(parser.apDomain, parser.apiHost, it)
}
val reactions = parseListOrNull(src.jsonArray("reactions")) {
TootReaction.parseFedibird(it)
}
val mentions = parseListOrNull(src.jsonArray("mentions")) {
TootMention(it)
}
val options = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
emojiMapCustom = custom_emojis,
// emojiMapProfile = profile_emojis,
// attachmentList = media_attachments,
highlightTrie = parser.highlightTrie,
mentions = mentions,
)
val content = src.string("content") ?: ""
return TootAnnouncement(
id = EntityId.mayDefault(src.string("id")),
starts_at = TootStatus.parseTime(src.string("starts_at")),
ends_at = TootStatus.parseTime(src.string("ends_at")),
all_day = src.boolean("all_day") ?: false,
published_at = TootStatus.parseTime(src.string("published_at")),
updated_at = TootStatus.parseTime(src.string("updated_at")),
custom_emojis = custom_emojis,
tags = TootTag.parseListOrNull(parser, src.jsonArray("tags")),
mentions = mentions,
content = content,
decoded_content = options.decodeHTML(content),
reactions = reactions,
)
}
// return null if list is empty
fun filterShown(src: List<TootAnnouncement>?): List<TootAnnouncement>? {
val now = System.currentTimeMillis()

View File

@ -2,10 +2,49 @@ package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.util.*
import jp.juggler.util.data.*
class TootAttachment : TootAttachmentLike {
class TootAttachment private constructor(
// ID of the attachment
val id: EntityId,
//One of: "image", "video", "gifv". or may null ? may "unknown" ?
override val type: TootAttachmentType,
//URL of the locally hosted version of the image
val url: String?,
//For remote images, the remote URL of the original image
val remote_url: String?,
// URL of the preview image
// (Mastodon 2.9.2) audioのpreview_url は .mpga のURL
// (Misskey v11) audioのpreview_url は null
val preview_url: String?,
val preview_remote_url: String?,
// Shorter URL for the image, for insertion into text (only present on local images)
val text_url: String?,
// ALT text (Mastodon 2.0.0 or later)
override val description: String?,
override val focusX: Float,
override val focusY: Float,
// MisskeyはメディアごとにNSFWフラグがある
val isSensitive: Boolean,
// Mastodon 2.9.0 or later
val blurhash: String?,
) : TootAttachmentLike {
// 内部フラグ: 再編集で引き継いだ添付メディアなら真
var redraft: Boolean = false
override val urlForDescription: String?
get() = remote_url.notEmpty() ?: url
companion object {
private fun parseFocusValue(parent: JsonObject?, key: String): Float {
@ -35,10 +74,11 @@ class TootAttachment : TootAttachmentLike {
private const val KEY_Y = "y"
private const val KEY_BLURHASH = "blurhash"
fun decodeJson(src: JsonObject) = TootAttachment(src, decode = true)
private val ext_audio = arrayOf(".mpga", ".mp3", ".aac", ".ogg")
private fun parseType(src: String?) =
TootAttachmentType.values().find { it.id == src }
private fun guessMediaTypeByUrl(src: String?): TootAttachmentType? {
val uri = src.mayUri() ?: return null
@ -48,47 +88,136 @@ class TootAttachment : TootAttachmentLike {
return null
}
fun tootAttachmentJson(
src: JsonObject,
): TootAttachment {
val url: String? = src.string(KEY_URL)
val remote_url: String? = src.string(KEY_REMOTE_URL)
val type: TootAttachmentType = when (val tmpType = parseType(src.string(KEY_TYPE))) {
null, TootAttachmentType.Unknown -> {
guessMediaTypeByUrl(remote_url ?: url) ?: TootAttachmentType.Unknown
}
else -> tmpType
}
val focus = src.jsonObject(KEY_META)?.jsonObject(KEY_FOCUS)
return TootAttachment(
blurhash = src.string(KEY_BLURHASH),
description = src.string(KEY_DESCRIPTION),
focusX = parseFocusValue(focus, KEY_X),
focusY = parseFocusValue(focus, KEY_Y),
id = EntityId.mayDefault(src.string(KEY_ID)),
isSensitive = src.optBoolean(KEY_IS_SENSITIVE),
preview_remote_url = src.string(KEY_PREVIEW_REMOTE_URL),
preview_url = src.string(KEY_PREVIEW_URL),
remote_url = remote_url,
text_url = src.string(KEY_TEXT_URL),
type = type,
url = url,
)
}
private fun tootAttachmentMisskey(src: JsonObject): TootAttachment {
val mimeType = src.string("type") ?: "?"
val type: TootAttachmentType = when {
mimeType.startsWith("image/") -> TootAttachmentType.Image
mimeType.startsWith("video/") -> TootAttachmentType.Video
mimeType.startsWith("audio/") -> TootAttachmentType.Audio
else -> TootAttachmentType.Unknown
}
val url = src.string("url")
val description = src.string("comment")?.notBlank()
?: src.string("name")?.notBlank()
return TootAttachment(
blurhash = null,
description = description,
focusX = 0f,
focusY = 0f,
id = EntityId.mayDefault(src.string("id")),
isSensitive = src.optBoolean("isSensitive", false),
preview_remote_url = null,
preview_url = src.string("thumbnailUrl"),
remote_url = url,
text_url = url,
type = type,
url = url,
)
}
private fun tootAttachmentNoteStock(src: JsonObject): TootAttachment {
val url: String? = src.string("url")
val preview_url: String? = src.string("img_hash")
?.let { "https://img.osa-p.net/proxy/500x,q100,s$it/$url" }
val mediaType = src.string("mediaType")
val type: TootAttachmentType = when {
mediaType?.startsWith("image") == true -> TootAttachmentType.Image
mediaType?.startsWith("video") == true -> TootAttachmentType.Video
mediaType?.startsWith("audio") == true -> TootAttachmentType.Audio
else -> guessMediaTypeByUrl(url) ?: TootAttachmentType.Unknown
}
val focus = null
return TootAttachment(
blurhash = src.string("blurhash"),
description = src.string("name"),
focusX = parseFocusValue(focus, "x"),
focusY = parseFocusValue(focus, "y"),
id = EntityId.DEFAULT,
isSensitive = false,
preview_remote_url = null,
preview_url = preview_url,
remote_url = url,
text_url = url,
type = type,
url = url,
)
}
private fun tootAttachmentMastodon(src: JsonObject): TootAttachment {
val url: String? = src.string("url")
val remote_url: String? = src.string("remote_url")
val type: TootAttachmentType = when (val tmpType = parseType(src.string("type"))) {
null, TootAttachmentType.Unknown -> {
guessMediaTypeByUrl(remote_url ?: url) ?: TootAttachmentType.Unknown
}
else -> tmpType
}
val focus = src.jsonObject("meta")?.jsonObject("focus")
return TootAttachment(
blurhash = src.string("blurhash"),
description = src.string("description"),
focusX = parseFocusValue(focus, "x"),
focusY = parseFocusValue(focus, "y"),
id = EntityId.mayDefault(src.string("id")),
isSensitive = false,
preview_remote_url = src.string("preview_remote_url"),
preview_url = src.string("preview_url"),
remote_url = remote_url,
text_url = src.string("text_url"),
type = type,
url = url,
)
}
fun tootAttachment(serviceType: ServiceType, src: JsonObject) =
when (serviceType) {
ServiceType.MISSKEY -> tootAttachmentMisskey(src)
ServiceType.NOTESTOCK -> tootAttachmentNoteStock(src)
else -> tootAttachmentMastodon(src)
}
fun tootAttachment(parser: TootParser, src: JsonObject) =
tootAttachment(parser.serviceType, src)
}
constructor(parser: TootParser, src: JsonObject) : this(parser.serviceType, src)
// ID of the attachment
val id: EntityId
//One of: "image", "video", "gifv". or may null ? may "unknown" ?
override val type: TootAttachmentType
//URL of the locally hosted version of the image
val url: String?
//For remote images, the remote URL of the original image
val remote_url: String?
// URL of the preview image
// (Mastodon 2.9.2) audioのpreview_url は .mpga のURL
// (Misskey v11) audioのpreview_url は null
val preview_url: String?
val preview_remote_url: String?
// Shorter URL for the image, for insertion into text (only present on local images)
val text_url: String?
// ALT text (Mastodon 2.0.0 or later)
override val description: String?
override val focusX: Float
override val focusY: Float
// 内部フラグ: 再編集で引き継いだ添付メディアなら真
var redraft: Boolean = false
// MisskeyはメディアごとにNSFWフラグがある
val isSensitive: Boolean
// Mastodon 2.9.0 or later
val blurhash: String?
///////////////////////////////
override fun hasUrl(url: String): Boolean = when (url) {
@ -96,100 +225,6 @@ class TootAttachment : TootAttachmentLike {
else -> false
}
override val urlForDescription: String?
get() = remote_url.notEmpty() ?: url
constructor(serviceType: ServiceType, src: JsonObject) {
when (serviceType) {
ServiceType.MISSKEY -> {
id = EntityId.mayDefault(src.string("id"))
val mimeType = src.string("type") ?: "?"
this.type = when {
mimeType.startsWith("image/") -> TootAttachmentType.Image
mimeType.startsWith("video/") -> TootAttachmentType.Video
mimeType.startsWith("audio/") -> TootAttachmentType.Audio
else -> TootAttachmentType.Unknown
}
url = src.string("url")
preview_url = src.string("thumbnailUrl")
preview_remote_url = null
remote_url = url
text_url = url
description = src.string("comment")?.notBlank()
?: src.string("name")?.notBlank()
focusX = 0f
focusY = 0f
isSensitive = src.optBoolean("isSensitive", false)
blurhash = null
}
ServiceType.NOTESTOCK -> {
id = EntityId.DEFAULT
url = src.string("url")
remote_url = url
preview_url = src.string("img_hash")
?.let { "https://img.osa-p.net/proxy/500x,q100,s$it/$url" }
preview_remote_url = null
text_url = url
description = src.string("name")
isSensitive = false // Misskey用のパラメータなので、マストドンでは適当な値を使ってOK
val mediaType = src.string("mediaType")
type = when {
mediaType?.startsWith("image") == true -> TootAttachmentType.Image
mediaType?.startsWith("video") == true -> TootAttachmentType.Video
mediaType?.startsWith("audio") == true -> TootAttachmentType.Audio
else -> guessMediaTypeByUrl(remote_url ?: url)
?: TootAttachmentType.Unknown
// TODO GIFVかどうかの判定はどうするの
}
val focus = null // TODO focus指定はどうなるの
focusX = parseFocusValue(focus, "x")
focusY = parseFocusValue(focus, "y")
blurhash = src.string("blurhash")
}
else -> {
id = EntityId.mayDefault(src.string("id"))
url = src.string("url")
remote_url = src.string("remote_url")
preview_url = src.string("preview_url")
preview_remote_url = src.string("preview_remote_url")
text_url = src.string("text_url")
description = src.string("description")
isSensitive = false // Misskey用のパラメータなので、マストドンでは適当な値を使ってOK
type = when (val tmpType = parseType(src.string("type"))) {
null, TootAttachmentType.Unknown -> {
guessMediaTypeByUrl(remote_url ?: url) ?: TootAttachmentType.Unknown
}
else -> tmpType
}
val focus = src.jsonObject("meta")?.jsonObject("focus")
focusX = parseFocusValue(focus, "x")
focusY = parseFocusValue(focus, "y")
blurhash = src.string("blurhash")
}
}
}
private fun parseType(src: String?) =
TootAttachmentType.values().find { it.id == src }
override fun urlForThumbnail() =
if (PrefB.bpPriorLocalURL.value) {
preview_url.notEmpty() ?: preview_remote_url.notEmpty()
@ -240,35 +275,6 @@ class TootAttachment : TootAttachmentLike {
})
}
}
constructor(
src: JsonObject,
@Suppress("UNUSED_PARAMETER") decode: Boolean, // dummy parameter for choosing this ctor.
) {
id = EntityId.mayDefault(src.string(KEY_ID))
url = src.string(KEY_URL)
remote_url = src.string(KEY_REMOTE_URL)
preview_url = src.string(KEY_PREVIEW_URL)
preview_remote_url = src.string(KEY_PREVIEW_REMOTE_URL)
text_url = src.string(KEY_TEXT_URL)
type = when (val tmpType = parseType(src.string(KEY_TYPE))) {
null, TootAttachmentType.Unknown -> {
guessMediaTypeByUrl(remote_url ?: url) ?: TootAttachmentType.Unknown
}
else -> tmpType
}
description = src.string(KEY_DESCRIPTION)
isSensitive = src.optBoolean(KEY_IS_SENSITIVE)
val focus = src.jsonObject(KEY_META)?.jsonObject(KEY_FOCUS)
focusX = parseFocusValue(focus, KEY_X)
focusY = parseFocusValue(focus, KEY_Y)
blurhash = src.string(KEY_BLURHASH)
}
}
// v1.3 から 添付ファイルの画像のピクセルサイズが取得できるようになった

View File

@ -29,39 +29,42 @@ class TootCard(
val originalStatus: TootStatus? = null,
) {
companion object {
fun tootCard(src: JsonObject) =
TootCard(
url = src.string("url"),
title = src.string("title"),
description = src.string("description"),
image = src.string("image"),
constructor(src: JsonObject) : this(
url = src.string("url"),
title = src.string("title"),
description = src.string("description"),
image = src.string("image"),
type = src.string("type"),
author_name = src.string("author_name"),
author_url = src.string("author_url"),
provider_name = src.string("provider_name"),
provider_url = src.string("provider_url"),
blurhash = src.string("blurhash")
)
type = src.string("type"),
author_name = src.string("author_name"),
author_url = src.string("author_url"),
provider_name = src.string("provider_name"),
provider_url = src.string("provider_url"),
blurhash = src.string("blurhash")
)
constructor(parser: TootParser, src: TootStatus) : this(
originalStatus = src,
url = src.url,
title = "${src.account.display_name} @${parser.getFullAcct(src.account.acct).pretty}",
description = src.spoiler_text.filterNotEmpty()
?: if (parser.serviceType == ServiceType.MISSKEY) {
src.content
} else {
DecodeOptions(
context = parser.context,
decodeEmoji = true,
authorDomain = src.account,
).decodeHTML(src.content ?: "").toString()
},
image = src.media_attachments
?.firstOrNull()
?.urlForThumbnail()
?: src.account.avatar_static,
type = "photo"
)
fun tootCard(parser: TootParser, src: TootStatus) =
TootCard(
originalStatus = src,
url = src.url,
title = "${src.account.display_name} @${parser.getFullAcct(src.account.acct).pretty}",
description = src.spoiler_text.filterNotEmpty()
?: if (parser.serviceType == ServiceType.MISSKEY) {
src.content
} else {
DecodeOptions(
context = parser.context,
decodeEmoji = true,
authorDomain = src.account,
).decodeHTML(src.content ?: "").toString()
},
image = src.media_attachments
?.firstOrNull()
?.urlForThumbnail()
?: src.account.avatar_static,
type = "photo"
)
}
}

View File

@ -1,6 +1,7 @@
package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
import jp.juggler.util.data.JsonObject
class TootContext(
@ -12,8 +13,8 @@ class TootContext(
val references: ArrayList<TootStatus>?,
) {
constructor(parser: TootParser, src: JsonObject) : this(
ancestors = parseListOrNull(::TootStatus, parser, src.jsonArray("ancestors")),
descendants = parseListOrNull(::TootStatus, parser, src.jsonArray("descendants")),
references = parseListOrNull(::TootStatus, parser, src.jsonArray("references")),
ancestors = parseList(src.jsonArray("ancestors")) { tootStatus(parser, it) },
descendants = parseList(src.jsonArray("descendants")) { tootStatus(parser, it) },
references = parseList(src.jsonArray("references")) { tootStatus(parser, it) },
)
}

View File

@ -12,7 +12,7 @@ class TootConversationSummary(parser: TootParser, src: JsonObject) : TimelineIte
init {
this.id = EntityId.mayDefault(src.string("id"))
this.accounts = parser.accountList(src.jsonArray("accounts"))
this.accounts = parser.accountRefList(src.jsonArray("accounts"))
this.last_status = parser.status(src.jsonObject("last_status"))!!
this.unread = src.optBoolean("unread")

View File

@ -5,6 +5,7 @@ import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.TootAccount.Companion.tootAccount
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
@ -62,6 +63,7 @@ object InstanceCapability {
ti?.fedibirdCapabilities?.contains("emoji_reaction") == true ||
ti?.pleromaFeatures?.contains("pleroma_emoji_reactions") == true
}
fun statusReference(ai: SavedAccount, ti: TootInstance?) =
when {
ai.isPseudo -> false
@ -74,7 +76,7 @@ object InstanceCapability {
ai.isPseudo -> false
ai.isMisskey -> false
// 予約投稿自体はMastodonに2.7.0からある。通知はFedibird拡張
else -> ti?.fedibirdCapabilities !=null && ti.versionGE(TootInstance.VERSION_2_7_0_rc1)
else -> ti?.fedibirdCapabilities != null && ti.versionGE(TootInstance.VERSION_2_7_0_rc1)
}
fun canMultipleReaction(ai: SavedAccount, ti: TootInstance? = TootInstance.getCached(ai)) =
@ -208,7 +210,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
this.version = src.string("version")
this.decoded_version = VersionString(version)
this.stats = parseItem(::Stats, src.jsonObject("stats"))
this.stats = parseItem(src.jsonObject("stats")) { Stats(it) }
this.thumbnail = src.string("thumbnail")
this.max_toot_chars = src.int("max_toot_chars")
@ -221,18 +223,19 @@ class TootInstance(parser: TootParser, src: JsonObject) {
languages = src.jsonArray("languages")?.stringArrayList()
contact_account = parseItem(
::TootAccount,
TootParser(
parser.context,
LinkHelper.create(
apiHostArg = apiHost,
apDomainArg = apDomain,
misskeyVersion = 0,
)
),
src.jsonObject("contact_account")
)
contact_account = parseItem(src.jsonObject("contact_account")) {
tootAccount(
TootParser(
parser.context,
LinkHelper.create(
apiHostArg = apiHost,
apDomainArg = apDomain,
misskeyVersion = 0,
)
),
it
)
}
this.description = src.string("description")
this.short_description = src.string("short_description")
@ -592,17 +595,18 @@ class TootInstance(parser: TootParser, src: JsonObject) {
val json = result?.jsonObject
?: return@QueuedRequest Pair(null, result)
val item = parseItem(
::TootInstance,
TootParser(
client.context,
linkHelper = linkHelper ?: LinkHelper.create(
(hostArg ?: client.apiHost)!!,
misskeyVersion = parseMisskeyVersion(json)
)
),
json
) ?: return@QueuedRequest Pair(
val item = parseItem(json) {
TootInstance(
TootParser(
client.context,
linkHelper = linkHelper ?: LinkHelper.create(
(hostArg ?: client.apiHost)!!,
misskeyVersion = parseMisskeyVersion(json)
)
),
it
)
} ?: return@QueuedRequest Pair(
null,
result.setError("instance information parse error.")
)

View File

@ -1,11 +1,36 @@
package jp.juggler.subwaytooter.api.entity
import android.content.Context
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
class TootNotification(parser: TootParser, src: JsonObject) : TimelineItem() {
class TootNotification(
val json: JsonObject,
val id: EntityId,
// One of: "mention", "reblog", "favourite", "follow"
val type: String,
// The Account sending the notification to the user
val accountRef: TootAccountRef?,
// The Status associated with the notification, if applicable
// 投稿の更新により変更可能になる
var status: TootStatus?,
var reaction: TootReaction? = null,
val reblog_visibility: TootVisibility,
// The time the notification was created
private val created_at: String?,
val time_created_at: Long,
) : TimelineItem() {
val account: TootAccount?
get() = accountRef?.get()
companion object {
@Suppress("unused")
@ -55,83 +80,166 @@ class TootNotification(parser: TootParser, src: JsonObject) : TimelineItem() {
const val TYPE_EMOJI_REACTION = "emoji_reaction"
const val TYPE_STATUS_REFERENCE = "status_reference"
const val TYPE_SCHEDULED_STATUS = "scheduled_status"
fun tootNotification(parser: TootParser, src: JsonObject): TootNotification {
val id: EntityId
// One of: "mention", "reblog", "favourite", "follow"
val type: String
// The Account sending the notification to the user
val accountRef: TootAccountRef?
// The Status associated with the notification, if applicable
// 投稿の更新により変更可能になる
val status: TootStatus?
val reaction: TootReaction?
val reblog_visibility: TootVisibility
// The time the notification was created
val created_at: String?
val time_created_at: Long
if (parser.serviceType == ServiceType.MISSKEY) {
id = EntityId.mayDefault(src.string("id"))
type = src.stringOrThrow("type")
created_at = src.string("createdAt")
time_created_at = TootStatus.parseTime(created_at)
accountRef = TootAccountRef.mayNull(
parser,
parser.account(
src.jsonObject("user")
)
)
status = parser.status(
src.jsonObject("note")
)
reaction = src.string("reaction")
?.notEmpty()
?.let { TootReaction.parseMisskey(it) }
reblog_visibility = TootVisibility.Unknown
// Misskeyの通知APIはページネーションをIDでしか行えない
// これは改善される予定 https://github.com/syuilo/misskey/issues/2275
} else {
id = EntityId.mayDefault(src.string("id"))
type = src.stringOrThrow("type")
created_at = src.string("created_at")
time_created_at = TootStatus.parseTime(created_at)
accountRef =
TootAccountRef.mayNull(parser, parser.account(src.jsonObject("account")))
status = parser.status(src.jsonObject("status"))
reaction = src.jsonObject("emoji_reaction")
?.notEmpty()
?.let { TootReaction.parseFedibird(it) }
// pleroma unicode emoji
?: src.string("emoji")?.let { TootReaction(name = it) }
// fedibird
// https://github.com/fedibird/mastodon/blob/7974fd3c7ec11ea9f7bef4ad7f4009fff53f62af/app/serializers/rest/notification_serializer.rb#L9
val visibilityString = when {
src.boolean("limited") == true -> "limited"
else -> src.string("reblog_visibility")
}
reblog_visibility = TootVisibility.parseMastodon(visibilityString)
?: TootVisibility.Unknown
}
return TootNotification(
json = src,
id = id,
type = type,
accountRef = accountRef,
status = status,
reaction = reaction,
reblog_visibility = reblog_visibility,
created_at = created_at,
time_created_at = time_created_at,
)
}
}
val json: JsonObject
val id: EntityId
val type: String // One of: "mention", "reblog", "favourite", "follow"
val accountRef: TootAccountRef? // The Account sending the notification to the user
// The Status associated with the notification, if applicable
// 投稿の更新により変更可能になる
var status: TootStatus?
var reaction: TootReaction? = null
val reblog_visibility: TootVisibility
private val created_at: String? // The time the notification was created
val time_created_at: Long
val account: TootAccount?
get() = accountRef?.get()
override fun getOrderId() = id
init {
json = src
fun getNotificationLine(context: Context): String {
if (parser.serviceType == ServiceType.MISSKEY) {
id = EntityId.mayDefault(src.string("id"))
val name = when (PrefB.bpShowAcctInSystemNotification.value) {
false -> accountRef?.decoded_display_name
type = src.stringOrThrow("type")
created_at = src.string("createdAt")
time_created_at = TootStatus.parseTime(created_at)
accountRef = TootAccountRef.mayNull(
parser,
parser.account(
src.jsonObject("user")
)
)
status = parser.status(
src.jsonObject("note")
)
reaction = src.string("reaction")
?.notEmpty()
?.let { TootReaction.parseMisskey(it) }
reblog_visibility = TootVisibility.Unknown
// Misskeyの通知APIはページネーションをIDでしか行えない
// これは改善される予定 https://github.com/syuilo/misskey/issues/2275
} else {
id = EntityId.mayDefault(src.string("id"))
type = src.stringOrThrow("type")
created_at = src.string("created_at")
time_created_at = TootStatus.parseTime(created_at)
accountRef =
TootAccountRef.mayNull(parser, parser.account(src.jsonObject("account")))
status = parser.status(src.jsonObject("status"))
reaction = src.jsonObject("emoji_reaction")
?.notEmpty()
?.let { TootReaction.parseFedibird(it) }
// pleroma unicode emoji
?: src.string("emoji")?.let { TootReaction(name = it) }
// fedibird
// https://github.com/fedibird/mastodon/blob/7974fd3c7ec11ea9f7bef4ad7f4009fff53f62af/app/serializers/rest/notification_serializer.rb#L9
val visibilityString = when {
src.boolean("limited") == true -> "limited"
else -> src.string("reblog_visibility")
true -> {
val acctPretty = accountRef?.get()?.acct?.pretty
if (acctPretty?.isNotEmpty() == true) {
"@$acctPretty"
} else {
null
}
}
this.reblog_visibility = TootVisibility.parseMastodon(visibilityString)
?: TootVisibility.Unknown
} ?: "?"
return when (type) {
TYPE_MENTION,
TYPE_REPLY,
-> context.getString(R.string.display_name_replied_by, name)
TYPE_RENOTE,
TYPE_REBLOG,
-> context.getString(R.string.display_name_boosted_by, name)
TYPE_QUOTE,
-> context.getString(R.string.display_name_quoted_by, name)
TYPE_STATUS,
-> context.getString(R.string.display_name_posted_by, name)
TYPE_UPDATE,
-> context.getString(R.string.display_name_updates_post, name)
TYPE_STATUS_REFERENCE,
-> context.getString(R.string.display_name_references_post, name)
TYPE_FOLLOW,
-> context.getString(R.string.display_name_followed_by, name)
TYPE_UNFOLLOW,
-> context.getString(R.string.display_name_unfollowed_by, name)
TYPE_ADMIN_SIGNUP,
-> context.getString(R.string.display_name_signed_up, name)
TYPE_ADMIN_REPORT,
-> context.getString(R.string.display_name_report, name)
TYPE_FAVOURITE,
-> context.getString(R.string.display_name_favourited_by, name)
TYPE_EMOJI_REACTION_PLEROMA,
TYPE_EMOJI_REACTION,
TYPE_REACTION,
-> context.getString(R.string.display_name_reaction_by, name)
TYPE_VOTE,
TYPE_POLL_VOTE_MISSKEY,
-> context.getString(R.string.display_name_voted_by, name)
TYPE_FOLLOW_REQUEST,
TYPE_FOLLOW_REQUEST_MISSKEY,
-> context.getString(R.string.display_name_follow_request_by, name)
TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY,
-> context.getString(R.string.display_name_follow_request_accepted_by, name)
TYPE_POLL,
-> context.getString(R.string.end_of_polling_from, name)
else -> "?"
}
}
}

View File

@ -1,6 +1,9 @@
package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TootAnnouncement.Companion.tootAnnouncement
import jp.juggler.subwaytooter.api.entity.TootNotification.Companion.tootNotification
import jp.juggler.subwaytooter.api.entity.TootReaction.Companion.parseFedibird
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
@ -27,7 +30,7 @@ object TootPayload {
"update" -> parser.status(payload)
// ここを通るケースはまだ確認できていない
"notification" -> parser.notification(payload)
"notification" -> tootNotification(parser, payload)
// ここを通るケースはまだ確認できていない
else -> {
@ -58,15 +61,15 @@ object TootPayload {
-> parser.status(src)
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
"notification" -> parser.notification(src)
"notification" -> tootNotification(parser, src)
"conversation" -> parseItem(::TootConversationSummary, parser, src)
"conversation" -> parseItem(src) { TootConversationSummary(parser, it) }
"announcement" -> parseItem(::TootAnnouncement, parser, src)
"announcement" -> parseItem(src) { tootAnnouncement(parser, it) }
"emoji_reaction",
"announcement.reaction",
-> parseItem(TootReaction::parseFedibird, src)
-> parseItem(src) { parseFedibird(it) }
else -> {
log.e("unknown payload(2). message=$parentText")

View File

@ -10,6 +10,8 @@ import jp.juggler.util.*
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
private val log = LogCategory("TootPolls")
enum class TootPollsType {
Mastodon, // Mastodon 2.8
Misskey, // Misskey
@ -26,57 +28,64 @@ class TootPollsChoice(
)
class TootPolls(
parser: TootParser,
val pollType: TootPollsType,
status: TootStatus,
list_attachment: ArrayList<TootAttachmentLike>?,
src: JsonObject,
srcArray: JsonArray? = null,
) {
// one of enquete,enquete_result
val type: String?
val type: String?,
val question: String? // HTML text
// HTML text
val question: String?,
val decoded_question: Spannable // 表示用にデコードしてしまうのでNonNullになる
// 表示用にデコードしてしまうのでNonNullになる
val decoded_question: Spannable,
// array of text with emoji
val items: ArrayList<TootPollsChoice>?
val items: ArrayList<TootPollsChoice>?,
// 結果の数値 // null or array of number
var ratios: MutableList<Float>? = null
var ratios: MutableList<Float>? = null,
// 結果の数値のテキスト // null or array of string
private var ratios_text: MutableList<String>? = null
private var ratios_text: MutableList<String>? = null,
// 以下はJSONには存在しないが内部で使う
val time_start: Long
val status_id: EntityId
val time_start: Long,
val status_id: EntityId,
// Mastodon poll API
var expired_at = Long.MAX_VALUE
var expired = false
var multiple = false
var votes_count: Int? = null
var maxVotesCount: Int? = null
var pollId: EntityId? = null
var expired_at: Long = Long.MAX_VALUE,
var ownVoted: Boolean
var expired: Boolean = false,
init {
var multiple: Boolean = false,
this.time_start = status.time_created_at
this.status_id = status.id
var votes_count: Int? = null,
when (pollType) {
var maxVotesCount: Int? = null,
var pollId: EntityId? = null,
var ownVoted: Boolean,
) {
companion object {
const val ENQUETE_EXPIRE = 30000L
const val TYPE_ENQUETE = "enquete"
@Suppress("unused")
const val TYPE_ENQUETE_RESULT = "enquete_result"
@Suppress("HasPlatformType")
private val reWhitespace = """[\s\t\x0d\x0a]+""".asciiPattern()
fun tootPolls(
pollType: TootPollsType,
parser: TootParser,
status: TootStatus,
list_attachment: ArrayList<TootAttachmentLike>?,
src: JsonObject,
srcArray: JsonArray? = null,
): TootPolls = when (pollType) {
TootPollsType.Misskey -> {
this.items = parseChoiceListMisskey(
src.jsonArray("choices")
)
val items = parseChoiceListMisskey(src.jsonArray("choices"))
val votesList = ArrayList<Int>()
var votesMax = 1
var ownVoted = false
@ -86,68 +95,51 @@ class TootPolls(
votesList.add(votes)
if (votes > votesMax) votesMax = votes
}
this.ownVoted = ownVoted
var ratios: MutableList<Float>? = null
var ratios_text: MutableList<String>? = null
if (votesList.isNotEmpty()) {
this.ratios =
ratios =
votesList.map { (it.toFloat() / votesMax.toFloat()) }.toMutableList()
this.ratios_text =
ratios_text =
votesList.map { parser.context.getString(R.string.vote_count_text, it) }
.toMutableList()
} else {
this.ratios = null
this.ratios_text = null
}
this.type = TYPE_ENQUETE
this.question = status.content
this.decoded_question = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
attachmentList = list_attachment,
linkTag = status,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
).decodeHTML(this.question ?: "?")
val question = status.content
TootPolls(
pollType = pollType,
time_start = status.time_created_at,
status_id = status.id,
items = items,
ownVoted = ownVoted,
ratios = ratios,
ratios_text = ratios_text,
type = TYPE_ENQUETE,
question = question,
decoded_question = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
attachmentList = list_attachment,
linkTag = status,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
).decodeHTML(question ?: "?"),
)
}
TootPollsType.Mastodon -> {
this.type = "enquete"
this.question = status.content
this.decoded_question = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
attachmentList = list_attachment,
linkTag = status,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
).decodeHTML(this.question ?: "?")
this.items = parseChoiceListMastodon(
val question = status.content
val items = parseChoiceListMastodon(
parser.context,
status,
src.jsonArray("options")?.objectList()
)
this.pollId = EntityId.mayNull(src.string("id"))
this.expired_at =
TootStatus.parseTime(src.string("expires_at")).notZero() ?: Long.MAX_VALUE
this.expired = src.optBoolean("expired", false)
this.multiple = src.optBoolean("multiple", false)
this.votes_count = src.int("votes_count")
val multiple = src.optBoolean("multiple", false)
var ownVoted = src.optBoolean("voted", false)
src.jsonArray("own_votes")?.forEach {
if (it is Number) {
val i = it.toInt()
@ -156,103 +148,105 @@ class TootPolls(
}
}
this.ownVoted = ownVoted
when {
this.items == null -> maxVotesCount = null
this.multiple -> {
var max: Int? = null
for (item in items) {
val v = item.votes
if (v != null && (max == null || v > max)) max = v
TootPolls(
pollType = pollType,
type = "enquete",
question = question,
items = items,
multiple = multiple,
ownVoted = ownVoted,
maxVotesCount = when {
items == null -> null
multiple -> {
var max: Int? = null
for (item in items) {
val v = item.votes
if (v != null && (max == null || v > max)) max = v
}
max
}
maxVotesCount = max
}
else -> {
var sum: Int? = null
for (item in items) {
val v = item.votes
if (v != null) sum = (sum ?: 0) + v
else -> {
var sum: Int? = null
for (item in items) {
val v = item.votes
if (v != null) sum = (sum ?: 0) + v
}
sum
}
maxVotesCount = sum
}
}
},
time_start = status.time_created_at,
status_id = status.id,
decoded_question = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
attachmentList = list_attachment,
linkTag = status,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
).decodeHTML(question ?: "?"),
pollId = EntityId.mayNull(src.string("id")),
expired_at =
TootStatus.parseTime(src.string("expires_at")).notZero() ?: Long.MAX_VALUE,
expired = src.optBoolean("expired", false),
votes_count = src.int("votes_count"),
)
}
TootPollsType.FriendsNico -> {
this.type = src.string("type")
this.question = src.string("question")
this.decoded_question = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
attachmentList = list_attachment,
linkTag = status,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
).decodeHTML(this.question ?: "?")
this.items = parseChoiceListFriendsNico(
parser.context,
status,
src.stringArrayList("items")
val question = src.string("question")
TootPolls(
pollType = pollType,
question = question,
type = src.string("type"),
time_start = status.time_created_at,
status_id = status.id,
decoded_question = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
attachmentList = list_attachment,
linkTag = status,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
).decodeHTML(question ?: "?"),
items = parseChoiceListFriendsNico(
parser.context,
status,
src.stringArrayList("items")
),
ratios = src.floatArrayList("ratios"),
ratios_text = src.stringArrayList("ratios_text"),
ownVoted = false,
)
this.ratios = src.floatArrayList("ratios")
this.ratios_text = src.stringArrayList("ratios_text")
this.ownVoted = false
}
TootPollsType.Notestock -> {
this.type = "enquete"
this.question = status.content
this.decoded_question = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
attachmentList = list_attachment,
linkTag = status,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account,
unwrapEmojiImageTag = true, // notestockはカスタム絵文字がimageタグになってる
).decodeHTML(this.question ?: "?")
this.items = parseChoiceListNotestock(
val question = status.content
val expired_at =
TootStatus.parseTime(src.string("endTime")).notZero() ?: Long.MAX_VALUE
val items = parseChoiceListNotestock(
parser.context,
status,
srcArray?.objectList()
)
val multiple = src.containsKey("anyOf")
val maxVotesCount = when {
items == null -> null
this.pollId = EntityId.DEFAULT
this.expired_at =
TootStatus.parseTime(src.string("endTime")).notZero() ?: Long.MAX_VALUE
this.expired = expired_at >= System.currentTimeMillis()
this.multiple = src.containsKey("anyOf")
this.votes_count = items?.sumOf { it.votes ?: 0 }?.notZero()
this.ownVoted = false
when {
this.items == null -> maxVotesCount = null
this.multiple -> {
multiple -> {
var max: Int? = null
for (item in items) {
val v = item.votes
if (v != null && (max == null || v > max)) max = v
}
maxVotesCount = max
max
}
else -> {
@ -261,26 +255,40 @@ class TootPolls(
val v = item.votes
if (v != null) sum = (sum ?: 0) + v
}
maxVotesCount = sum
sum
}
}
TootPolls(
pollType = pollType,
type = "enquete",
status_id = status.id,
time_start = status.time_created_at,
question = question,
items = items,
expired_at = expired_at,
maxVotesCount = maxVotesCount,
multiple = multiple,
decoded_question = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
attachmentList = list_attachment,
linkTag = status,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account,
unwrapEmojiImageTag = true, // notestockはカスタム絵文字がimageタグになってる
).decodeHTML(question ?: "?"),
pollId = EntityId.DEFAULT,
expired = expired_at >= System.currentTimeMillis(),
votes_count = items?.sumOf { it.votes ?: 0 }?.notZero(),
ownVoted = false,
)
}
}
}
companion object {
internal val log = LogCategory("TootPolls")
const val ENQUETE_EXPIRE = 30000L
const val TYPE_ENQUETE = "enquete"
@Suppress("unused")
const val TYPE_ENQUETE_RESULT = "enquete_result"
@Suppress("HasPlatformType")
private val reWhitespace = """[\s\t\x0d\x0a]+""".asciiPattern()
fun parse(
parser: TootParser,
@ -291,9 +299,9 @@ class TootPolls(
): TootPolls? {
src ?: return null
return try {
TootPolls(
parser,
tootPolls(
pollType,
parser,
status,
listAttachment,
src

View File

@ -189,7 +189,7 @@ class TootReaction(
// 古い形式の絵文字はUnicode絵文字にする
misskeyOldReactions[code]?.let {
return EmojiDecoder.decodeEmoji(options, it)
return EmojiDecoder.decodeEmojiCached(options, it)
}
// カスタム絵文字
@ -225,7 +225,7 @@ class TootReaction(
}
// フォールバック
// unicode絵文字、もしくは :xxx: などのshortcode表現
return EmojiDecoder.decodeEmoji(options, code)
return EmojiDecoder.decodeEmojiCached(options, code)
}
// リアクションカラムの絵文字絞り込み用

View File

@ -15,7 +15,7 @@ class TootResults private constructor(
var searchApiVersion = 0 // 0 means not from search API. such as trend tags.
constructor(parser: TootParser, src: JsonObject) : this(
accounts = parser.accountList(src.jsonArray("accounts")),
accounts = parser.accountRefList(src.jsonArray("accounts")),
statuses = parser.statusList(src.jsonArray("statuses")),
hashtags = TootTag.parseList(parser, src.jsonArray("hashtags"))
)

View File

@ -1,6 +1,7 @@
package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.log.LogCategory
@ -27,12 +28,9 @@ class TootScheduled(parser: TootParser, val src: JsonObject) : TimelineItem() {
timeScheduledAt = TootStatus.parseTime(scheduledAt)
mediaAttachments =
parseListOrNull(
::TootAttachment,
parser,
src.jsonArray("media_attachments"),
log
)
parseList(src.jsonArray("media_attachments")) {
tootAttachment(parser, it)
}
val params = src.jsonObject("params")
text = params?.string("text")
visibility = TootVisibility.parseMastodon(params?.string("visibility"))

View File

@ -17,7 +17,7 @@ val misskeyArrayFinderUsers = { it: JsonObject ->
// account list parser
val defaultAccountListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> parser.accountList(jsonArray) }
{ parser, jsonArray -> parser.accountRefList(jsonArray) }
private fun misskeyUnwrapRelationAccount(parser: TootParser, srcList: JsonArray, key: String) =
srcList.objectList().mapNotNull {
@ -72,10 +72,12 @@ val defaultDomainBlockListParser: (parser: TootParser, jsonArray: JsonArray) ->
{ _, jsonArray -> TootDomainBlock.parseList(jsonArray) }
val defaultReportListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootReport> =
{ _, jsonArray -> parseList(::TootReport, jsonArray) }
{ _, jsonArray -> parseList(jsonArray) { TootReport(it) } }
val defaultConversationSummaryListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootConversationSummary> =
{ parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) }
{ parser, jsonArray ->
parseList(jsonArray) { TootConversationSummary(parser, it) }
}
///////////////////////////////////////////////////////////////////////

View File

@ -283,7 +283,7 @@ object ColumnEncoder {
enableSpeech = src.optBoolean(KEY_ENABLE_SPEECH)
useOldApi = src.optBoolean(KEY_USE_OLD_API)
lastViewingItemId = EntityId.from(src, KEY_LAST_VIEWING_ITEM)
lastViewingItemId = EntityId.entityId(src, KEY_LAST_VIEWING_ITEM)
regexText = src.string(KEY_REGEX_TEXT) ?: ""
languageFilter = src.jsonObject(KEY_LANGUAGE_FILTER)

View File

@ -423,7 +423,8 @@ suspend fun Column.loadListInfo(client: TootApiClient, bForceReload: Boolean) {
val jsonObject = result?.jsonObject
if (jsonObject != null) {
val data = parseItem(::TootList, parser, jsonObject)
val data =
parseItem(jsonObject) { TootList(parser, it) }
if (data != null) {
this.listInfo = data
client.publishApiProgress("") // カラムヘッダの再表示
@ -449,7 +450,7 @@ suspend fun Column.loadAntennaInfo(client: TootApiClient, bForceReload: Boolean)
val jsonObject = result?.jsonObject
if (jsonObject != null) {
val data = parseItem(::MisskeyAntenna, jsonObject)
val data = parseItem(jsonObject) { MisskeyAntenna(it) }
if (data != null) {
this.antennaInfo = data
client.publishApiProgress("") // カラムヘッダの再表示

View File

@ -374,7 +374,7 @@ fun Column.onMisskeyNoteUpdated(ev: MisskeyNoteUpdate) {
// userId が自分かどうか調べる
// アクセストークンの更新をして自分のuserIdが分かる状態でないとキャプチャ結果を反映させない
// でないとリアクションの2重カウントなどが発生してしまう)
val myId = EntityId.from(accessInfo.tokenJson, AuthBase.KEY_USER_ID)
val myId = EntityId.entityId(accessInfo.tokenJson, AuthBase.KEY_USER_ID)
if (myId == null) {
log.w("onNoteUpdated: missing my userId. updating access token is recommenced!!")
}

View File

@ -7,7 +7,7 @@ import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TimelineItem
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
import jp.juggler.subwaytooter.api.entity.TootAnnouncement.Companion.tootAnnouncement
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.parseList
import jp.juggler.subwaytooter.table.SavedAccount
@ -108,7 +108,7 @@ abstract class ColumnTask(
else -> {
column.announcements =
parseList(::TootAnnouncement, parser, result.jsonArray)
parseList(result.jsonArray) { tootAnnouncement(parser, it) }
.notEmpty()
column.announcementUpdated = SystemClock.elapsedRealtime()
client.publishApiProgress("announcements loaded")

View File

@ -3,9 +3,9 @@ package jp.juggler.subwaytooter.column
import android.os.SystemClock
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.auth.authRepo
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
@ -842,14 +842,14 @@ class ColumnTask_Loading(
client: TootApiClient,
pathBase: String,
) = client.request(pathBase)?.also { result ->
val src = parseList(::TootReport, result.jsonArray)
val src = parseList(result.jsonArray) { TootReport(it) }
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)
val src = parseList(result?.jsonArray) { TootScheduled(parser, it) }
listTmp = addAll(listTmp, src)
column.saveRange(bBottom = true, bTop = true, result = result, list = src)
@ -889,7 +889,7 @@ class ColumnTask_Loading(
client.request(pathBase)
}
if (result != null) {
val src = parseList(::TootList, parser, result.jsonArray)
val src = parseList(result.jsonArray) { TootList(parser, it) }
src.sort()
column.saveRange(bBottom = true, bTop = true, result = result, list = src)
this.listTmp = addAll(null, src)
@ -920,7 +920,7 @@ class ColumnTask_Loading(
client.request(pathBase)
}
if (result != null) {
val src = parseList(::MisskeyAntenna, result.jsonArray)
val src = parseList(result.jsonArray) { MisskeyAntenna(it) }
column.saveRange(bBottom = true, bTop = true, result = result, list = src)
this.listTmp = addAll(null, src)
}
@ -1116,7 +1116,7 @@ class ColumnTask_Loading(
)
jsonObject = result?.jsonObject ?: return result
val conversationContext =
parseItem(::TootContext, parser, jsonObject)
parseItem(jsonObject) { TootContext(parser, it) }
// 一つのリストにまとめる
targetStatus.conversation_main = true
@ -1171,8 +1171,7 @@ class ColumnTask_Loading(
)
val jsonArray = result?.jsonArray
if (jsonArray != null) {
val src =
TootParser(context, accessInfo).accountList(jsonArray)
val src = TootParser(context, accessInfo).accountRefList(jsonArray)
listTmp = addAll(listTmp, src)
}
}

View File

@ -1049,7 +1049,7 @@ class ColumnTask_Refresh(
{ src, head -> addAll(listTmp, src, head = head) }
val listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootReport> =
{ _, jsonArray -> parseList(::TootReport, jsonArray) }
{ _, jsonArray -> parseList(jsonArray) { TootReport(it) } }
return if (isMisskey) {
TootApiResult("Misskey has no API to list reports from you.")
@ -1163,7 +1163,7 @@ class ColumnTask_Refresh(
suspend fun getScheduledStatuses(client: TootApiClient): TootApiResult? {
val result = client.request(column.addRange(bBottom, ApiPath.PATH_SCHEDULED_STATUSES))
val src = parseList(::TootScheduled, parser, result?.jsonArray)
val src = parseList(result?.jsonArray) { TootScheduled(parser, it) }
listTmp = addAll(listTmp, src)
column.saveRange(bBottom, !bBottom, result, src)
return result

View File

@ -7,6 +7,7 @@ import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRef
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.search.MspHelper.loadingMSP
import jp.juggler.subwaytooter.search.MspHelper.refreshMSP
@ -1930,7 +1931,7 @@ enum class ColumnType(
if (a == null) {
TootApiResult("can't parse account information")
} else {
column.whoAccount = TootAccountRef(parser, a)
column.whoAccount = tootAccountRef(parser, a)
getScheduledStatuses(client)
}
}

View File

@ -110,7 +110,7 @@ class UserRelationLoader(val column: Column) {
if (result == null || result.response?.code in 400 until 500) break
val list = parseList(::TootRelationShip, parser, result.jsonArray)
val list = parseList(result.jsonArray) { TootRelationShip(parser, it) }
if (list.size == userIdList.size) {
for (i in 0 until list.size) {
list[i].id = userIdList[i]
@ -144,7 +144,7 @@ class UserRelationLoader(val column: Column) {
sb.append(whoList[n++].toString())
}
val result = client.request(sb.toString()) ?: break // cancelled.
val list = parseList(::TootRelationShip, parser, result.jsonArray)
val list = parseList(result.jsonArray) { TootRelationShip(parser, it) }
if (list.size > 0) daoUserRelation.saveListMastodon(
now,
column.accessInfo.db_id,

View File

@ -374,10 +374,10 @@ private fun ColumnViewHolder.showReactions(
btn.setPadding(paddingH, paddingV, paddingH, paddingV)
btn.text = if (url == null) {
EmojiDecoder.decodeEmoji(options, "${reaction.name} ${reaction.count}")
if (url == null) {
btn.text = EmojiDecoder.decodeEmojiCached(options, "${reaction.name} ${reaction.count}")
} else {
SpannableStringBuilder("${reaction.name} ${reaction.count}").also { sb ->
btn.text = SpannableStringBuilder("${reaction.name} ${reaction.count}").also { sb ->
sb.setSpan(
NetworkEmojiSpan(url, scale = 1.5f),
0,

View File

@ -183,11 +183,12 @@ class DlgListMember(
.putMisskeyApiToken()
.toPostRequestBuilder()
)?.also { result ->
resultList = parseList(
::TootList,
TootParser(activity, listOwner),
result.jsonArray ?: return@also
).apply {
resultList = parseList(result.jsonArray) {
TootList(
TootParser(activity, listOwner),
it
)
}.apply {
if (whoLocal != null) {
forEach { list ->
list.isRegistered =
@ -197,31 +198,30 @@ class DlgListMember(
}
}
} else {
val registeredSet = HashSet<EntityId>()
// メンバーを指定してリスト登録状況を取得
if (whoLocal != null) client.request(
"/api/v1/accounts/${whoLocal.id}/lists"
)?.also { result ->
val jsonArray = result.jsonArray
?: return@runApiTask result
parseList(
::TootList,
TootParser(activity, listOwner),
jsonArray
).forEach {
parseList(result.jsonArray) {
TootList(
TootParser(activity, listOwner),
it
)
}.forEach {
registeredSet.add(it.id)
}
}
// リスト一覧を取得
client.request("/api/v1/lists")?.also { result ->
resultList = parseList(
::TootList,
TootParser(activity, listOwner),
result.jsonArray ?: return@also
).apply {
resultList = parseList(result.jsonArray) {
TootList(
TootParser(activity, listOwner),
it
)
}.apply {
sort()
forEach {
it.isRegistered = registeredSet.contains(it.id)

View File

@ -42,7 +42,7 @@ private class EmojiPicker(
private val activity: AppCompatActivity,
private val accessInfo: SavedAccount?,
private val closeOnSelected: Boolean,
private val onPicked: (EmojiBase, bInstanceHasCustomEmoji: Boolean) -> Unit,
private val onPicked: suspend (EmojiBase, bInstanceHasCustomEmoji: Boolean) -> Unit,
) {
companion object {
private val log = LogCategory("EmojiPicker")
@ -452,7 +452,9 @@ private class EmojiPicker(
} else {
// この場合はビューの更新は不要で、タップ状態の表示を行える
}
onPicked(targetEmoji, bInstanceHasCustomEmoji)
activity.launchAndShowError {
onPicked(targetEmoji, bInstanceHasCustomEmoji)
}
}
private suspend fun createCustomEmojiCategories(): List<PickerItemCategory> {
@ -787,7 +789,7 @@ fun launchEmojiPicker(
activity: AppCompatActivity,
accessInfo: SavedAccount?,
closeOnSelected: Boolean,
onPicked: (EmojiBase, bInstanceHasCustomEmoji: Boolean) -> Unit,
onPicked: suspend (EmojiBase, bInstanceHasCustomEmoji: Boolean) -> Unit,
) = activity.launchAndShowError {
EmojiPicker(
activity = activity,

View File

@ -254,6 +254,7 @@ class LoginForm(
short = true,
).decodeHTML(it)
}.replace("""\n[\s\n]+""".toRegex(), "\n")
.trim()
}
}
}

View File

@ -23,11 +23,14 @@ import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.util.minWidthCompat
import jp.juggler.subwaytooter.util.startMargin
import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.getAdaptiveRippleDrawableRound
import org.jetbrains.anko.allCaps
import org.jetbrains.anko.dip
private val log = LogCategory("ItemViewHolderReaction")
fun ItemViewHolder.makeReactionsView(status: TootStatus) {
val reactionSet = status.reactionSet
if (reactionSet?.hasReaction() != true) {
@ -77,9 +80,6 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
if (reaction.count <= 0) return@forEachIndexed
val ssb = reaction.toSpannableStringBuilder(options, status)
.also { it.append(" ${reaction.count}") }
val b = AppCompatButton(act).apply {
layoutParams = FlexboxLayout.LayoutParams(
FlexboxLayout.LayoutParams.WRAP_CONTENT,
@ -109,10 +109,7 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
setTextColor(colorTextContent)
setPadding(paddingH, 0, paddingH, 0)
text = ssb
setTextSize(TypedValue.COMPLEX_UNIT_PX, textHeight)
allCaps = false
tag = reaction
setOnClickListener {
@ -123,7 +120,6 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
act.reactionAdd(column, status, taggedReaction?.name, taggedReaction?.staticUrl)
}
}
setOnLongClickListener {
val taggedReaction = it.tag as? TootReaction
act.reactionFromAnotherAccount(
@ -135,8 +131,17 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
}
// カスタム絵文字の場合、アニメーション等のコールバックを処理する必要がある
val invalidator = NetworkEmojiInvalidator(act.handler, this)
invalidator.register(ssb)
extraInvalidatorList.add(invalidator)
try {
val ssb =
reaction.toSpannableStringBuilder(options, status)
.also { it.append(" ${reaction.count}") }
text = ssb
invalidator.register(ssb)
} catch (ex: Throwable) {
log.e(ex, "can't decode reaction emoji.")
text = "${reaction.name} ${reaction.count}"
}
}
box.addView(b)
}

View File

@ -337,27 +337,6 @@ fun ItemViewHolder.showBoost(
accessInfo.supplyBaseUrl(who.avatar)
)
}
// フォローの場合 decoded_display_name が2箇所で表示に使われるのを避ける必要がある
val text: Spannable = if (reaction != null) {
val options = DecodeOptions(
activity,
accessInfo,
decodeEmoji = true,
enlargeEmoji = 1.5f,
enlargeCustomEmoji = 1.5f
)
val ssb = reaction.toSpannableStringBuilder(options, boostStatus)
ssb.append(" ")
ssb.append(
who.decodeDisplayName(activity)
.intoStringResource(activity, stringId)
)
} else {
who.decodeDisplayName(activity)
.intoStringResource(activity, stringId)
}
boostTime = time
llBoosted.visibility = View.VISIBLE
showStatusTime(
@ -368,9 +347,33 @@ fun ItemViewHolder.showBoost(
status = boostStatus,
reblogVisibility = reblogVisibility
)
tvBoosted.text = text
boostInvalidator.register(text)
setAcct(tvBoostedAcct, accessInfo, who)
// フォローの場合 decoded_display_name が2箇所で表示に使われるのを避ける必要がある
if (reaction != null) {
val options = DecodeOptions(
activity,
accessInfo,
decodeEmoji = true,
enlargeEmoji = 1.5f,
enlargeCustomEmoji = 1.5f
)
val ssb = reaction.toSpannableStringBuilder(options, boostStatus)
ssb.append(" ")
ssb.append(
who.decodeDisplayNameCached(activity)
.intoStringResource(activity, stringId)
)
val text: Spannable =ssb
tvBoosted.text = text
boostInvalidator.register(text)
} else {
val text: Spannable =
who.decodeDisplayNameCached(activity)
.intoStringResource(activity, stringId)
tvBoosted.text = text
boostInvalidator.register(text)
}
}
fun ItemViewHolder.showMessageHolder(item: TootMessageHolder) {
@ -730,7 +733,7 @@ fun ItemViewHolder.showScheduled(item: TootScheduled) {
showStatusTimeScheduled(activity, tvTime, item)
val who = column.whoAccount!!.get()
val whoRef = TootAccountRef(TootParser(activity, accessInfo), who)
val whoRef = TootAccountRef.tootAccountRef(TootParser(activity, accessInfo), who)
this.statusAccount = whoRef
setAcct(tvAcct, accessInfo, who)

View File

@ -6,7 +6,9 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.notification.CheckerWakeLocks.Companion.checkerWakeLocks
import jp.juggler.subwaytooter.notification.PullNotification.getMessageNotifications
import jp.juggler.subwaytooter.notification.PullNotification.removeMessageNotification
@ -94,80 +96,6 @@ class PollingChecker(
private val checkJob = Job()
private fun NotificationData.getNotificationLine(): String {
val name = when (PrefB.bpShowAcctInSystemNotification.value) {
false -> notification.accountRef?.decoded_display_name
true -> {
val acctPretty = notification.accountRef?.get()?.acct?.pretty
if (acctPretty?.isNotEmpty() == true) {
"@$acctPretty"
} else {
null
}
}
} ?: "?"
return "- " + when (notification.type) {
TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY,
-> context.getString(R.string.display_name_replied_by, name)
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_REBLOG,
-> context.getString(R.string.display_name_boosted_by, name)
TootNotification.TYPE_QUOTE,
-> context.getString(R.string.display_name_quoted_by, name)
TootNotification.TYPE_STATUS,
-> context.getString(R.string.display_name_posted_by, name)
TootNotification.TYPE_UPDATE,
-> context.getString(R.string.display_name_updates_post, name)
TootNotification.TYPE_STATUS_REFERENCE,
-> context.getString(R.string.display_name_references_post, name)
TootNotification.TYPE_FOLLOW,
-> context.getString(R.string.display_name_followed_by, name)
TootNotification.TYPE_UNFOLLOW,
-> context.getString(R.string.display_name_unfollowed_by, name)
TootNotification.TYPE_ADMIN_SIGNUP,
-> context.getString(R.string.display_name_signed_up, name)
TootNotification.TYPE_ADMIN_REPORT,
-> context.getString(R.string.display_name_report, name)
TootNotification.TYPE_FAVOURITE,
-> context.getString(R.string.display_name_favourited_by, name)
TootNotification.TYPE_EMOJI_REACTION_PLEROMA,
TootNotification.TYPE_EMOJI_REACTION,
TootNotification.TYPE_REACTION,
-> context.getString(R.string.display_name_reaction_by, name)
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL_VOTE_MISSKEY,
-> context.getString(R.string.display_name_voted_by, name)
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
-> context.getString(R.string.display_name_follow_request_by, name)
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY,
-> context.getString(R.string.display_name_follow_request_accepted_by, name)
TootNotification.TYPE_POLL,
-> context.getString(R.string.end_of_polling_from, name)
else -> "?"
}
}
private fun createPolicyFilter(
account: SavedAccount,
): (TootNotification) -> Boolean = when (account.pushPolicy) {
@ -240,22 +168,13 @@ class PollingChecker(
commonMutex.withLock {
// グローバル変数の暖気
if (TootStatus.muted_app == null) {
TootStatus.muted_app = daoMutedApp.nameSet()
}
if (TootStatus.muted_word == null) {
TootStatus.muted_word = daoMutedWord.nameSet()
}
TootStatus.updateMuteData()
}
// // installIdとデバイストークンの取得
// val deviceToken = loadFirebaseMessagingToken(context)
// loadInstallId(context, account, deviceToken, progress)
val favMuteSet = commonMutex.withLock {
daoFavMute.acctSet()
}
accountMutex(accountDbId).withLock {
if (!account.isRequiredPullCheck()) {
// 通知チェックの定期実行が不要なら
@ -308,7 +227,6 @@ class PollingChecker(
if (PrefB.bpSeparateReplyNotificationGroup.value) {
var tr = TrackingRunner(
account = account,
favMuteSet = favMuteSet,
trackingType = TrackingType.NotReply,
trackingName = PullNotification.TRACKING_NAME_DEFAULT
)
@ -318,7 +236,6 @@ class PollingChecker(
//
tr = TrackingRunner(
account = account,
favMuteSet = favMuteSet,
trackingType = TrackingType.Reply,
trackingName = PullNotification.TRACKING_NAME_REPLY
)
@ -328,7 +245,6 @@ class PollingChecker(
} else {
val tr = TrackingRunner(
account = account,
favMuteSet = favMuteSet,
trackingType = TrackingType.All,
trackingName = PullNotification.TRACKING_NAME_DEFAULT
)
@ -345,7 +261,6 @@ class PollingChecker(
inner class TrackingRunner(
val account: SavedAccount,
val favMuteSet: Set<Acct>,
var trackingType: TrackingType = TrackingType.All,
var trackingName: String = "",
) {
@ -424,9 +339,10 @@ class PollingChecker(
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
-> {
val who = notification.account
if (who != null && favMuteSet.contains(account.getFullAcct(who))) {
log.d("${account.getFullAcct(who)} is in favMuteSet.")
val whoAcct = notification.account
?.let { account.getFullAcct(it) }
if (whoAcct?.let { TootStatus.favMuteSet?.contains(it) } == true) {
log.d("${whoAcct.pretty} is in favMuteSet.")
return
}
}
@ -518,7 +434,7 @@ class PollingChecker(
notificationId = item.notification.id.toString()
) { builder ->
builder.setWhen(item.notification.time_created_at)
val summary = item.getNotificationLine()
val summary = item.notification.getNotificationLine(context)
builder.setContentTitle(summary)
when (val content = item.notification.status?.decoded_content?.notEmpty()) {
null -> builder.setContentText(item.accessInfo.acct.pretty)
@ -547,7 +463,7 @@ class PollingChecker(
notificationTag
) { builder ->
builder.setWhen(first.notification.time_created_at)
val a = first.getNotificationLine()
val a = first.notification.getNotificationLine(context)
val dataList = dstListData
if (dataList.size == 1) {
builder.setContentTitle(a)
@ -561,7 +477,7 @@ class PollingChecker(
.setSummaryText(account.acct.pretty)
for (i in 0 until min(4, dataList.size)) {
style.addLine(dataList[i].getNotificationLine())
style.addLine(dataList[i].notification.getNotificationLine(context))
}
builder.setStyle(style)

View File

@ -8,15 +8,11 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiError
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.InstanceCapability
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootPushSubscription
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.push.ApiPushMastodon
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.lazyContext
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.PushRepo.Companion.followDomain
import jp.juggler.subwaytooter.table.*
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
@ -224,7 +220,7 @@ class PushMastodon(
return if (alertsOld.joinToString(",") == alertsNew.joinToString(",")) {
log.i("${account.acct}: same alerts(2)")
true
}else {
} else {
log.i("${account.acct}: changed. old=${alertsOld.sorted()}, new=${alertsNew.sorted()}")
subLog.i("notification type set changed.")
false
@ -290,15 +286,18 @@ class PushMastodon(
a: SavedAccount,
pm: PushMessage,
) {
val json = pm.messageJson ?: return
val apiHost = a.apiHost
val json = pm.messageJson ?: error("missing messageJson")
pm.notificationType = json.string("notification_type")
pm.iconLarge = json.string("icon").followDomain(apiHost)
pm.iconLarge = a.supplyBaseUrl(json.string("icon"))
pm.text = arrayOf(
// あなたのトゥートが tateisu 🤹 さんにお気に入り登録されました
json.string("title"),
).mapNotNull { it?.trim()?.notBlank() }.joinToString("\n").ellipsizeDot3(400)
).mapNotNull { it?.trim()?.notBlank() }
.joinToString("\n")
.ellipsizeDot3(128)
pm.textExpand = arrayOf(
// あなたのトゥートが tateisu 🤹 さんにお気に入り登録されました
json.string("title"),
@ -306,7 +305,10 @@ class PushMastodon(
json.string("body"),
// 対象の投稿の本文? (古い
json.jsonObject("data")?.string("content"),
).mapNotNull { it?.trim()?.notBlank() }.joinToString("\n").ellipsizeDot3(400)
).mapNotNull { it?.trim()?.notBlank() }
.joinToString("\n")
.ellipsizeDot3(400)
when {
pm.notificationType.isNullOrEmpty() -> {
// old mastodon
@ -334,7 +336,7 @@ class PushMastodon(
// 重複排除は完全に諦める
pm.notificationId = pm.timestamp.toString()
pm.iconSmall = json.string("badge").followDomain(apiHost)
pm.iconSmall = a.supplyBaseUrl(json.string("badge"))
}
else -> {
// Mastodon 4.0
@ -354,5 +356,29 @@ class PushMastodon(
// - タイムスタンプ情報はない。
}
}
// 通知のミュートについて:
// - アプリ名がないのでアプリ名ミュートは使えない
// - notification.user のfull acct がないのでふぁぼ魔ミュートは行えない
// - テキスト本文のミュートは…部分的には可能
if(pm.textExpand?.let{TootStatus.muted_word?.matchShort(it)}==true){
error("muted by text word.")
}
// // ふぁぼ魔ミュート
// when ( pm.notificationType) {
// TootNotification.TYPE_REBLOG,
// TootNotification.TYPE_FAVOURITE,
// TootNotification.TYPE_FOLLOW,
// TootNotification.TYPE_FOLLOW_REQUEST,
// TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
// -> {
// val whoAcct = a.getFullAcct(user)
// if (TootStatus.favMuteSet?.contains(whoAcct) == true) {
// error("muted by favMuteSet ${whoAcct.pretty}")
// }
// }
// }
}
}

View File

@ -1,23 +1,27 @@
package jp.juggler.subwaytooter.push
import android.content.Context
import jp.juggler.crypt.defaultSecurityProvider
import jp.juggler.crypt.encodeP256Dh
import jp.juggler.crypt.generateKeyPair
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiError
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.push.ApiPushMisskey
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.lazyContext
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.PushRepo.Companion.followDomain
import jp.juggler.subwaytooter.table.*
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import java.security.Provider
import java.security.SecureRandom
import java.security.interfaces.ECPublicKey
class PushMisskey(
private val context: Context,
private val api: ApiPushMisskey,
private val provider: Provider =
defaultSecurityProvider,
@ -26,6 +30,9 @@ class PushMisskey(
override val daoStatus: AccountNotificationStatus.Access =
AccountNotificationStatus.Access(appDatabase),
) : PushBase() {
companion object {
private val log = LogCategory("PushMisskey")
}
override suspend fun updateSubscription(
subLog: SubscriptionLogger,
@ -161,50 +168,59 @@ class PushMisskey(
a: SavedAccount,
pm: PushMessage,
) {
val json = pm.messageJson ?: return
val apiHost = a.apiHost
pm.iconSmall = null // バッジ画像のURLはない。通知種別により決まる
json.long("dateTime")?.let {
pm.timestamp = it
}
val body = json.jsonObject("body")
val user = body?.jsonObject("user")
pm.iconLarge = user?.string("avatarUrl").followDomain(apiHost)
val json = pm.messageJson ?: error("missign messageJson")
when (val eventType = json.string("type")) {
"notification" -> {
val notificationType = body?.string("type")
val parser = TootParser(context, a)
val notification = parser.notification(json.jsonObject("body"))
?: error("can't parse notification. json=$json")
pm.notificationType = notificationType
val user = notification.account
// アプリミュートと単語ミュート
if (notification.status?.checkMuted() == true) {
error("this message is muted by app or word.")
}
// ふぁぼ魔ミュート
when (notification.type) {
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_FAVOURITE,
TootNotification.TYPE_FOLLOW,
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
-> {
val whoAcct = a.getFullAcct(user)
if (TootStatus.favMuteSet?.contains(whoAcct) == true) {
error("muted by favMuteSet ${whoAcct.pretty}")
}
}
}
// バッジ画像のURLはない。通知種別により決まる
pm.iconSmall = null
pm.iconLarge = a.supplyBaseUrl(user?.avatar_static)
pm.notificationType = notification.type
json.long("dateTime")?.let { pm.timestamp = it }
pm.text = arrayOf(
user?.string("username"),
notificationType,
body?.string("text")?.takeIf {
when (notificationType) {
"mention", "quote" -> true
else -> false
}
}
).mapNotNull { it?.trim()?.notBlank() }.joinToString("\n").ellipsizeDot3(128)
notification.getNotificationLine(context),
).mapNotNull { it.trim().notBlank() }
.joinToString("\n")
.ellipsizeDot3(128)
pm.textExpand = arrayOf(
user?.string("username"),
notificationType,
body?.string("text")?.takeIf {
when (notificationType) {
"mention", "quote" -> true
else -> false
}
}
).mapNotNull { it?.trim()?.notBlank() }.joinToString("\n").ellipsizeDot3(400)
pm.text,
notification.status?.decoded_content,
).mapNotNull { it?.trim()?.notBlank() }
.joinToString("\n")
.ellipsizeDot3(400)
}
// 通知以外のイベントは全部無視したい
else -> error("謎のイベント $eventType user=${user?.string("username")}")
else -> error("謎のイベント $eventType json=$json")
}
}
}

View File

@ -13,14 +13,13 @@ import jp.juggler.crypt.*
import jp.juggler.subwaytooter.ActCallback
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.push.ApiPushAppServer
import jp.juggler.subwaytooter.api.push.ApiPushMastodon
import jp.juggler.subwaytooter.api.push.ApiPushMisskey
import jp.juggler.subwaytooter.dialog.SuspendProgress
import jp.juggler.subwaytooter.notification.NotificationChannels
import jp.juggler.subwaytooter.notification.NotificationDeleteReceiver.Companion.intentNotificationDelete
import jp.juggler.subwaytooter.notification.iconColor
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.*
@ -82,35 +81,24 @@ class PushRepo(
private val fcmHandler: FcmHandler,
) {
companion object {
private val reHttp = """https?://""".toRegex()
@Suppress("RegExpSimplifiable")
private val reTailDigits = """([0-9]+)\z""".toRegex()
const val JSON_CAME_FROM = "<>cameFrom"
const val CAME_FROM_UNIFIED_PUSH = "unifiedPush"
const val CAME_FROM_FCM = "fcm"
private val ncPushMessage = NotificationChannels.PushMessage
var refReporter: WeakReference<SuspendProgress.Reporter>? = null
val ncPushMessage = NotificationChannels.PushMessage
fun String?.followDomain(apiHost: Host) = when {
isNullOrEmpty() -> null
reHttp.containsMatchIn(this) -> this
this[0] == '/' -> "https://$apiHost$this"
else -> "https://$apiHost/$this"
}
}
private val pushMisskey by lazy {
PushMisskey(
context = context,
api = apiPushMisskey,
provider = provider,
prefDevice = prefDevice,
daoStatus = daoStatus,
)
}
private val pushMastodon by lazy {
PushMastodon(
context = context,
@ -276,7 +264,7 @@ class PushRepo(
// アプリサーバにendpointを登録する
refReporter?.get()?.setMessage("アプリサーバにプッシュサービスの情報を送信しています")
if( !fcmHandler.hasFcm && prefDevice.pushDistributor ==PrefDevice.PUSH_DISTRIBUTOR_FCM){
if (!fcmHandler.hasFcm && prefDevice.pushDistributor == PrefDevice.PUSH_DISTRIBUTOR_FCM) {
log.w("fmc selected, but this is noFcm build. unset distributer.")
prefDevice.pushDistributor = null
}
@ -466,85 +454,86 @@ class PushRepo(
val pm = daoPushMessage.find(messageId)
?: error("missing pushMessage")
// rawBodyをBinPackMapにデコード
var map = pm.rawBody?.decodeBinPackMap()
?: error("binPack decode failed.")
// ペイロードがなくてURLが付与されたメッセージは
// アプリサーバから読み直す
if (map["b"] == null) {
map.string("l")?.let { largeObjectId ->
apiPushAppServer.getLargeObject(largeObjectId)
?.let {
map = it.decodeBinPack() as? BinPackMap
?: error("binPack decode failed.")
pm.rawBody = it
daoPushMessage.save(pm)
}
}
}
// acctHashがある
val acctHash = map.string("a") ?: error("missing a.")
val status = daoStatus.findByAcctHash(acctHash)
?: error("missing status for acctHash $acctHash")
val acct = status.acct.notEmpty()
?: error("empty acct.")
val account = daoSavedAccount.loadAccountByAcct(Acct.parse(acct))
?: error("missing account for acct ${status.acct}")
pm.loginAcct = status.acct
decodeMessageContent(status, pm, map)
val messageJson = pm.messageJson
if (messageJson == null) {
// デコード失敗
// 古い鍵で行った購読だろう。
// メッセージに含まれるappServerHashを指定してendpoint登録を削除する
// するとアプリサーバはSNSサーバに対してgoneを返すようになり掃除が適切に行われるはず
map.string("c").notEmpty()?.let {
val count = apiPushAppServer.endpointRemove(hashId = it)
.int("count")
log.w("endpointRemove $count hashId=$it")
}
error("can't decode WebPush message to JSON.")
}
// Mastodonはなぜかアクセストークンが書いてあるので危険…
val censored = messageJson.toString()
.replace(
""""access_token":"[^"]+"""".toRegex(),
""""access_token":"***""""
)
log.i("${status.acct} $censored")
// messageJsonを解釈して通知に出す内容を決める
try {
// rawBodyをBinPackMapにデコード
var map = pm.rawBody?.decodeBinPackMap()
?: error("binPack decode failed.")
// ペイロードがなくてURLが付与されたメッセージは
// アプリサーバから読み直す
if (map["b"] == null) {
map.string("l")?.let { largeObjectId ->
apiPushAppServer.getLargeObject(largeObjectId)
?.let {
map = it.decodeBinPack() as? BinPackMap
?: error("binPack decode failed.")
pm.rawBody = it
daoPushMessage.save(pm)
}
}
}
// acctHashがある
val acctHash = map.string("a") ?: error("missing a.")
val status = daoStatus.findByAcctHash(acctHash)
?: error("missing status for acctHash $acctHash")
val acct = status.acct.notEmpty()
?: error("empty acct.")
val account = daoSavedAccount.loadAccountByAcct(Acct.parse(acct))
?: error("missing account for acct ${status.acct}")
pm.loginAcct = status.acct
decodeMessageContent(status, pm, map)
val messageJson = pm.messageJson
if (messageJson == null) {
// デコード失敗
// 古い鍵で行った購読だろう。
// メッセージに含まれるappServerHashを指定してendpoint登録を削除する
// するとアプリサーバはSNSサーバに対してgoneを返すようになり掃除が適切に行われるはず
map.string("c").notEmpty()?.let {
val count = apiPushAppServer.endpointRemove(hashId = it).int("count")
log.w("endpointRemove $count hashId=$it")
}
error("can't decode WebPush message to JSON.")
}
// Mastodonはなぜかアクセストークンが書いてあるので危険…
val censored = messageJson.toString()
.replace(
""""access_token":"[^"]+"""".toRegex(),
""""access_token":"***""""
)
log.i("${status.acct} $censored")
// messageJsonを解釈して通知に出す内容を決める
TootStatus.updateMuteData()
pushBase(account).formatPushMessage(account, pm)
val notificationId = pm.notificationId
if (notificationId.isNullOrEmpty()) {
error("can't show notification. missing notificationId.")
}
if (!allowDupilicateNotification &&
daoNotificationShown.duplicateOrPut(acct, notificationId)
) {
error("can't show notification. it's duplicate. $acct $notificationId")
}
// 解読できた(例外が出なかった)なら通知を出す
showPushNotification(pm, account, notificationId)
} catch (ex: Throwable) {
log.e(ex, "formatPushMessage failed.")
return
log.e(ex, "updateMessage failed.")
pm.formatError = ex.withCaption()
} finally {
daoPushMessage.save(pm)
}
daoPushMessage.save(pm)
val notificationId = pm.notificationId
if (notificationId.isNullOrEmpty()) {
log.e("can't show notification. missing notificationId.")
return
}
if (!allowDupilicateNotification &&
daoNotificationShown.duplicateOrPut(acct, notificationId)
) {
log.w("can't show notification. it's duplicate. $acct $notificationId")
return
}
// 解読できた(例外が出なかった)なら通知を出す
showPushNotification(pm, account, notificationId)
}
/**
@ -687,7 +676,7 @@ class PushRepo(
// val piTap = PendingIntent.getActivity(this, nc.pircTap, iTap, PendingIntent.FLAG_IMMUTABLE)
ncPushMessage.notify(context, urlDelete) {
color = ContextCompat.getColor(context,iconAndColor.colorRes)
color = ContextCompat.getColor(context, iconAndColor.colorRes)
setSmallIcon(iconSmall)
iconBitmapLarge?.let { setLargeIcon(it) }
setContentTitle(pm.loginAcct)

View File

@ -7,6 +7,7 @@ import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.ServiceType
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
import jp.juggler.subwaytooter.column.ColumnTask_Loading
import jp.juggler.subwaytooter.column.ColumnTask_Refresh
import jp.juggler.subwaytooter.column.addWithFilterStatus
@ -59,7 +60,7 @@ object NotestockHelper {
for (src in array) {
try {
if (src !is JsonObject) continue
add(TootStatus(parser, src))
add(tootStatus(parser, src))
} catch (ex: Throwable) {
log.e(ex, "parse item failed.")
}

View File

@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.ServiceType
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
import jp.juggler.subwaytooter.column.ColumnTask_Loading
import jp.juggler.subwaytooter.column.ColumnTask_Refresh
import jp.juggler.subwaytooter.column.addWithFilterStatus
@ -66,7 +67,7 @@ object TootsearchHelper {
for (src in array) {
try {
val source = src.cast<JsonObject>()?.jsonObject("_source") ?: continue
add(TootStatus(parser, source))
add(tootStatus(parser, source))
} catch (ex: Throwable) {
log.e(ex, "parse item failed.")
}

View File

@ -76,7 +76,7 @@ class FavMute(
} ?: emptyList()
fun acctSet()= buildSet {
fun acctSet() :Set<Acct> = buildSet {
try {
db.query(table, null, null, null, null, null, null)
.use { cursor ->

View File

@ -193,16 +193,16 @@ class NotificationTracking {
if (cursor.moveToFirst()) {
dst.id = cursor.getLong(COL_ID)
dst.post_id = EntityId.from(cursor, COL_POST_ID)
dst.post_id = EntityId.entityId(cursor, COL_POST_ID)
dst.post_time = cursor.getLong(COL_POST_TIME)
val show = EntityId.from(cursor, COL_NID_SHOW)
val show = EntityId.entityId(cursor, COL_NID_SHOW)
if (show == null) {
dst.nid_show = null
dst.nid_read = null
} else {
dst.nid_show = show
val read = EntityId.from(cursor, COL_NID_READ)
val read = EntityId.entityId(cursor, COL_NID_READ)
if (read == null) {
dst.nid_read = null
} else {
@ -242,8 +242,8 @@ class NotificationTracking {
!cursor.moveToFirst() -> log.e("updateRead[$accountDbId,$notificationType]: can't find the data row.")
else -> {
val nid_show = EntityId.from(cursor, COL_NID_SHOW)
val nid_read = EntityId.from(cursor, COL_NID_READ)
val nid_show = EntityId.entityId(cursor, COL_NID_SHOW)
val nid_read = EntityId.entityId(cursor, COL_NID_READ)
when {
nid_show == null ->
log.e("updateRead[$accountDbId,$notificationType]: nid_show is null.")

View File

@ -64,6 +64,13 @@ data class PushMessage(
formatJson[JSON_ICON_LARGE] = value
}
var formatError: String?
get() = formatJson.string(JSON_ERROR)
set(value) {
formatJson[JSON_ERROR] = value
}
companion object : TableCompanion {
private val log = LogCategory("PushMessage")
const val TABLE = "push_message"
@ -85,6 +92,7 @@ data class PushMessage(
private const val JSON_TEXT_EXPAND = "text_expand"
private const val JSON_ICON_SMALL = "icon_small"
private const val JSON_ICON_LARGE = "icon_large"
private const val JSON_ERROR = "error"
val columnList = MetaColumns(TABLE, initialVersion = 65).apply {
deleteBeforeCreate = true

View File

@ -830,13 +830,12 @@ class SavedAccount(
}
// URLが相対指定だった場合にスキーマとホスト名を補う
fun supplyBaseUrl(url: String?): String? {
return when {
url == null || url.isEmpty() -> return null
fun supplyBaseUrl(url: String?): String? =
when {
url.isNullOrEmpty() -> null
url[0] == '/' -> "https://${apiHost.ascii}$url"
else -> url
}
}
fun isNicoru(account: TootAccount?) =
account?.apiHost == Host.FRIENDS_NICO

View File

@ -23,6 +23,7 @@ import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.ui.attrColor
// Subway Tooterの「アプリ設定/挙動/リンクを開く際にCustom Tabsを使わない」をONにして
@ -149,7 +150,7 @@ fun Activity.openCustomTab(url: String?) {
.setShowTitle(true)
.build()
.let {
log.w("startCustomTabIntent ComponentName=$cn")
log.i("startCustomTabIntent ComponentName=$cn")
openBrowserExcludeMe(
it.intent.also { intent ->
if (cn != null) intent.component = cn
@ -168,7 +169,7 @@ fun Activity.openCustomTab(url: String?) {
startCustomTabIntent(cn)
return
} catch (ex2: Throwable) {
log.e(ex2, "openCustomTab: missing chrome. retry to other application.")
log.e(ex2.withCaption("openCustomTab: missing chrome. retry to other application."))
}
}

View File

@ -13,6 +13,7 @@ import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
@ -393,7 +394,7 @@ class AttachmentUploader(
val jsonObject = result?.jsonObject
if (jsonObject != null) {
val a = parseItem(::TootAttachment, ServiceType.MISSKEY, jsonObject)
val a = parseItem(jsonObject) { tootAttachment(ServiceType.MISSKEY, it) }
if (a == null) {
result.error = "TootAttachment.parse failed"
} else {
@ -434,8 +435,9 @@ class AttachmentUploader(
// ポーリングして処理完了を待つ
pa.progress = context.getString(R.string.attachment_handling_waiting_async)
val id = parseItem(::TootAttachment, ServiceType.MASTODON, result?.jsonObject)
?.id
val id = parseItem(result?.jsonObject) {
tootAttachment(ServiceType.MASTODON, it)
}?.id
?: return TootApiResult("/api/v2/media did not return the media ID.")
var lastResponse = SystemClock.elapsedRealtime()
@ -467,8 +469,9 @@ class AttachmentUploader(
val jsonObject = result?.jsonObject
if (jsonObject != null) {
when (val a =
parseItem(::TootAttachment, ServiceType.MASTODON, jsonObject)) {
when (val a = parseItem(jsonObject) {
tootAttachment(ServiceType.MASTODON, it)
}) {
null -> result.error = "TootAttachment.parse failed"
else -> pa.attachment = a
}
@ -905,7 +908,7 @@ class AttachmentUploader(
val jsonObject = result?.jsonObject
if (jsonObject != null) {
val a = parseItem(::TootAttachment, ServiceType.MASTODON, jsonObject)
val a = parseItem(jsonObject) { tootAttachment(ServiceType.MASTODON, it) }
if (a == null) {
result.error = "TootAttachment.parse failed"
} else {
@ -935,8 +938,9 @@ class AttachmentUploader(
put("comment", description)
}.toPostRequestBuilder()
)?.also { result ->
resultAttachment =
parseItem(::TootAttachment, ServiceType.MISSKEY, result.jsonObject)
resultAttachment = parseItem(result.jsonObject) {
tootAttachment(ServiceType.MISSKEY, it)
}
}
} else {
client.request(
@ -945,8 +949,9 @@ class AttachmentUploader(
put("description", description)
}.toPutRequestBuilder()
)?.also { result ->
resultAttachment =
parseItem(::TootAttachment, ServiceType.MASTODON, result.jsonObject)
resultAttachment = parseItem(result.jsonObject) {
tootAttachment(ServiceType.MASTODON, it)
}
}
}
}

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.os.Handler
import android.os.SystemClock
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.entity.parseListP2
import jp.juggler.subwaytooter.api.entity.parseList
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.coroutine.launchMain
@ -163,7 +163,7 @@ class CustomEmojiLister(
log.w("getCachedEmoji: missing cache for $apiHostAscii")
return null
}
val emoji = cache.mapShortCode.get(shortcode)
val emoji = cache.mapShortCode[shortcode]
if (emoji == null) {
log.w("getCachedEmoji: missing emoji for $shortcode in $apiHostAscii")
return null
@ -225,12 +225,13 @@ class CustomEmojiLister(
}?.decodeJsonObject()
?.jsonArray("emojis")
?.let { emojis12 ->
parseListP2(
CustomEmoji.decodeMisskey,
accessInfo.apDomain,
accessInfo.apiHost,
emojis12,
)
parseList(emojis12) {
CustomEmoji.decodeMisskey(
accessInfo.apDomain,
accessInfo.apiHost,
it
)
}
}
// v13のemojisを読む
@ -245,12 +246,13 @@ class CustomEmojiLister(
?.decodeJsonObject()
?.jsonArray("emojis")
?.let { emojis13 ->
parseListP2(
CustomEmoji.decodeMisskey13,
accessInfo.apDomain,
accessInfo.apiHost,
emojis13,
)
parseList(emojis13) {
CustomEmoji.decodeMisskey13(
accessInfo.apDomain,
accessInfo.apiHost,
it
)
}
}
// マストドンのカスタム絵文字一覧を読む
@ -259,12 +261,13 @@ class CustomEmojiLister(
"https://$cacheKey/api/v1/custom_emojis",
accessInfo = accessInfo
)?.let { data ->
parseListP2(
CustomEmoji.decode,
accessInfo.apDomain,
accessInfo.apiHost,
data.decodeJsonArray()
)
parseList(data.decodeJsonArray()) {
CustomEmoji.decode(
accessInfo.apDomain,
accessInfo.apiHost,
it
)
}
}
val list = when {

View File

@ -89,9 +89,4 @@ class DecodeOptions(
fun decodeEmoji(s: String?): Spannable =
EmojiDecoder.decodeEmoji(this, s ?: "").workaroundForEmojiLineBreak()
// fun decodeEmojiNullable(s : String?) = when(s) {
// null -> null
// else -> EmojiDecoder.decodeEmoji(this, s)
// }
}

View File

@ -352,6 +352,113 @@ object EmojiDecoder {
private val reNicoru = """\Anicoru\d*\z""".asciiPattern(Pattern.CASE_INSENSITIVE)
private val reHohoemi = """\Ahohoemi\d*\z""".asciiPattern(Pattern.CASE_INSENSITIVE)
fun decodeEmojiCached(options: DecodeOptions, s: String): SpannableStringBuilder {
val builder = EmojiStringBuilder(options)
val emojiMapCustom = options.emojiMapCustom
val emojiMapProfile = options.emojiMapProfile
val useEmojioneShortcode = PrefB.bpEmojioneShortcode.value
val disableEmojiAnimation = PrefB.bpDisableEmojiAnimation.value
splitShortCode(s, callback = object : ShortCodeSplitterCallback {
override fun onString(part: String) {
builder.addUnicodeString(part)
}
override fun onShortCode(prevCodePoint: Int, part: String, name: String) {
// フレニコのプロフ絵文字
if (emojiMapProfile != null && name.length >= 2 && name[0] == '@') {
val emojiProfile = emojiMapProfile[name] ?: emojiMapProfile[name.substring(1)]
if (emojiProfile != null) {
val url = emojiProfile.url
if (url.isNotEmpty()) {
builder.addNetworkEmojiSpan(part, url)
return
}
}
}
// カスタム絵文字
fun CustomEmoji.customEmojiToUrl(): String = when {
disableEmojiAnimation && staticUrl?.isNotEmpty() == true ->
this.staticUrl
else ->
this.url
}
fun findCustomEmojiUrl(): String? {
val misskeyVersion = options.linkHelper?.misskeyVersion ?: 0
if (misskeyVersion >= 13) {
val cols = name.split("@", limit = 2)
val apiHostAscii = options.linkHelper?.apiHost?.ascii
// @以降にあるホスト名か、投稿者のホスト名か、閲覧先サーバのホスト名
val userHost = cols.elementAtOrNull(1)
?: options.authorDomain?.apiHost?.ascii
?: apiHostAscii
log.i(
"decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${
cols.elementAtOrNull(1)
} apiHostAscii=$apiHostAscii, userHost=$userHost"
)
when {
// 絵文字プロクシを利用できない
apiHostAscii == null -> {
log.w("decodeEmoji Misskey13 missing apiHostAscii")
}
userHost != null && userHost != "." && userHost != apiHostAscii -> {
// 投稿者のホスト名を使う
return "https://$apiHostAscii/emoji/${
cols.elementAtOrNull(0)
}@$userHost.webp"
}
else -> {
// 閲覧先サーバの絵文字を探す
App1.custom_emoji_lister.getCachedEmoji(apiHostAscii, name)
?.let { return it.customEmojiToUrl() }
}
}
}
return emojiMapCustom?.get(name)?.customEmojiToUrl()
}
val url = findCustomEmojiUrl()
if (url != null) {
builder.addNetworkEmojiSpan(part, url)
return
}
// 通常の絵文字
when {
reHohoemi.matcher(name).find() ->
builder.addImageSpan(part, R.drawable.emoji_hohoemi)
reNicoru.matcher(name).find() ->
builder.addImageSpan(part, R.drawable.emoji_nicoru)
else -> {
// EmojiOneのショートコード
val emoji = when {
useEmojioneShortcode ->
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
else -> null
}
when (emoji) {
null -> builder.addUnicodeString(part)
else -> builder.addImageSpan(part, emoji)
}
}
}
}
})
builder.closeNormalText()
return builder.sb
}
fun decodeEmoji(options: DecodeOptions, s: String): SpannableStringBuilder {
val builder = EmojiStringBuilder(options)

View File

@ -992,22 +992,21 @@ object HTMLDecoder {
status.account
)
val linkInfo = if (fullAcct != null) {
LinkInfo(
url = item.url,
caption = "@${(if (PrefB.bpMentionFullAcct.value) fullAcct else item.acct).pretty}",
ac = daoAcctColor.load(fullAcct),
mention = item,
tag = link_tag
)
} else {
LinkInfo(
val linkInfo = when (fullAcct) {
null -> LinkInfo(
url = item.url,
caption = "@${item.acct.pretty}",
ac = null,
mention = item,
tag = link_tag
)
else -> LinkInfo(
url = item.url,
caption = "@${(if (PrefB.bpMentionFullAcct.value) fullAcct else item.acct).pretty}",
ac = daoAcctColor.load(fullAcct),
mention = item,
tag = link_tag
)
}
val start = sb.length

View File

@ -69,6 +69,12 @@ fun LinkHelper.matchHost(src: TootAccount) =
apiHost == src.apiHost || apDomain == src.apDomain ||
apDomain == src.apiHost || apiHost == src.apDomain
fun LinkHelper.matchHost(srcApiHost:Host,srcApDomain:Host) =
apiHost == srcApiHost ||
apDomain == srcApDomain ||
apDomain == srcApiHost ||
apiHost == srcApDomain
// user や user@host から user@host を返す
fun getFullAcctOrNull(
rawAcct: Acct,

View File

@ -5,7 +5,6 @@ import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.getVisibilityString
@ -90,10 +89,6 @@ class PostImpl(
else -> 40 // TootPollsType.Mastodon
}
private val authRepo by lazy {
AuthRepo(activity)
}
private fun preCheckPollItemOne(list: List<String>, idx: Int, item: String) {
// 選択肢が長すぎる

View File

@ -1,6 +1,7 @@
package jp.juggler.util.data
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.withCaption
import java.io.*
import java.math.BigDecimal
import java.math.BigInteger
@ -642,8 +643,8 @@ class JsonTokenizer(reader: Reader) {
*/
fun nextValue(): Any? {
var c = nextClean()
val string: String
when (c) {
CHAR0 -> throw syntaxError("unexpected end.")
'"', '\'' -> return nextString(c)
'{' -> {
@ -672,13 +673,10 @@ class JsonTokenizer(reader: Reader) {
if (!eof) {
back()
}
string = sb.toString().trim { it <= ' ' }
if ("" == string) {
throw syntaxError("Missing value")
}
val string = sb.toString().trim { it <= ' ' }
return with(string) {
when {
isEmpty() -> ""
isEmpty() -> throw syntaxError("empty identifier.")
equals("true", ignoreCase = true) -> true
equals("false", ignoreCase = true) -> false
equals("null", ignoreCase = true) -> null
@ -1082,7 +1080,7 @@ private val log = LogCategory("Json")
fun String.decodeJsonValue() = try {
JsonTokenizer(this).nextValue()
} catch (ex: Throwable) {
log.e(ex, "decodeJsonValue failed. $this")
log.e(ex.withCaption("decodeJsonValue failed. $this"))
throw ex
}