APIのEntityがクラスイニシャライザ内部で複雑な処理をするのを減らす。それはsuspendにできない…
This commit is contained in:
parent
60bacaac64
commit
ecbed39f5b
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
// 読めなくてもエラー処理は行わない
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."))
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
// 応答ボディのテキストがあれば追加
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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.")
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 から 添付ファイルの画像のピクセルサイズが取得できるようになった
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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.")
|
||||
)
|
||||
|
|
|
@ -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 -> "?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// リアクションカラムの絵文字絞り込み用
|
||||
|
|
|
@ -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"))
|
||||
)
|
||||
|
|
|
@ -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"))
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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) }
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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("") // カラムヘッダの再表示
|
||||
|
|
|
@ -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!!")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -254,6 +254,7 @@ class LoginForm(
|
|||
short = true,
|
||||
).decodeHTML(it)
|
||||
}.replace("""\n[\s\n]+""".toRegex(), "\n")
|
||||
.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
||||
// 選択肢が長すぎる
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue