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

943 lines
39 KiB
Kotlin

package jp.juggler.subwaytooter.table
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.provider.BaseColumns
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.auth.Auth2Result
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.pref.lazyContext
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorEx
import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.media.MovieResizeMode
import jp.juggler.util.media.MovieResizeConfig
import jp.juggler.util.media.ResizeConfig
import jp.juggler.util.media.ResizeType
import kotlin.math.max
/**
* ユーザが操作するアカウント
* - Access.saveNew で insert。直後にロードし直す
* - 通知の更新状況などは AccountNotificationStatus に保存するようになった
*/
class SavedAccount(
val db_id: Long,
acctArg: String,
apiHostArg: String? = null,
apDomainArg: String? = null,
var accountJson: JsonObject? = null,
var confirmBoost: Boolean = false,
var confirmFavourite: Boolean = false,
var confirmFollow: Boolean = false,
var confirmFollowLocked: Boolean = false,
var confirmPost: Boolean = false,
var confirmReaction: Boolean = true,
var confirmUnbookmark: Boolean = true,
var confirmUnboost: Boolean = false,
var confirmUnfavourite: Boolean = false,
var confirmUnfollow: Boolean = false,
var defaultSensitive: Boolean = false,
var defaultText: String = "",
var dontHideNsfw: Boolean = false,
var dontShowTimeout: Boolean = false,
var expandCw: Boolean = false,
var extraJson: JsonObject = JsonObject(),
var imageMaxMegabytes: String? = null,
var imageResize: String? = null,
var loginAccount: TootAccount? = null, // 疑似アカウントではnull
var maxTootChars: Int = 0,
var movieMaxMegabytes: String? = null,
var notificationBoost: Boolean = false,
var notificationFavourite: Boolean = false,
var notificationFollow: Boolean = false,
var notificationFollowRequest: Boolean = false,
var notificationMention: Boolean = false,
var notificationPost: Boolean = false,
var notificationReaction: Boolean = false,
var notificationUpdate: Boolean = true,
var notificationVote: Boolean = false,
var pushPolicy: String? = null,
var tokenJson: JsonObject? = null,
var visibility: TootVisibility = TootVisibility.Public,
// var soundUri: String = "",
// private var lastNotificationError: String? = null,
// private var last_push_endpoint: String? = null,
// private var last_subscription_error: String? = null,
// private var register_key: String? = null
// private var register_time: Long = 0
// var notification_tag: String? = null
override var misskeyVersion: Int = 0,
) : LinkHelper {
// SavedAccountのロード時にhostを供給する必要があった
override val apiHost: Host
override val apDomain: Host
val username: String
val acct: Acct
private val jsonDelegates = JsonDelegates { extraJson }
@JsonPropInt("movieTranscodeMode", 0)
var movieTranscodeMode by jsonDelegates.int
@JsonPropString("movieTranscodeBitrate", "2000000")
var movieTranscodeBitrate by jsonDelegates.string
@JsonPropString("movieTranscodeFramerate", "30")
var movieTranscodeFramerate by jsonDelegates.string
@JsonPropString("movieTranscodeSquarePixels", "2304000")
var movieTranscodeSquarePixels by jsonDelegates.string
@JsonPropString("lang2", LANG_WEB)
var lang by jsonDelegates.string
@JsonPropBoolean("notification_status_reference", true)
var notificationStatusReference by jsonDelegates.boolean
@JsonPropBoolean("notificationPushEnable", true)
var notificationPushEnable by jsonDelegates.boolean
@JsonPropBoolean("notificationPullEnable", false)
var notificationPullEnable by jsonDelegates.boolean
@JsonPropInt("notificationAccentColor", 0)
var notificationAccentColor by jsonDelegates.int
init {
// log.i("ctor acctArg $acctArg")
// acctArg はMastodonの生のやつで、ドメイン部分がない場合がある
// Acct.parse はHost部分がnullのacctになるかもしれない
val tmpAcct = Acct.parse(acctArg)
this.username = tmpAcct.username
if (username.isEmpty()) error("missing username in acct")
val tmpApiHost = apiHostArg?.notEmpty()?.let { Host.parse(it) }
val tmpApDomain = apDomainArg?.notEmpty()?.let { Host.parse(it) }
this.apiHost = tmpApiHost ?: tmpApDomain ?: tmpAcct.host ?: error("missing apiHost")
this.apDomain = tmpApDomain ?: tmpApiHost ?: tmpAcct.host ?: error("missing apDomain")
// Full Acct
this.acct = tmpAcct.followHost(apDomain)
}
constructor(context: Context, cursor: Cursor) : this(
db_id = cursor.getLong(COL_ID), // db_id
acctArg = cursor.getString(COL_USER), // acct
apiHostArg = cursor.getStringOrNull(COL_HOST), // host
apDomainArg = cursor.getStringOrNull(COL_DOMAIN), // host
accountJson = cursor.getStringOrNull(COL_ACCOUNT)?.decodeJsonObject(),
confirmBoost = cursor.getBoolean(COL_CONFIRM_BOOST),
confirmFavourite = cursor.getBoolean(COL_CONFIRM_FAVOURITE),
confirmFollow = cursor.getBoolean(COL_CONFIRM_FOLLOW),
confirmFollowLocked = cursor.getBoolean(COL_CONFIRM_FOLLOW_LOCKED),
confirmPost = cursor.getBoolean(COL_CONFIRM_POST),
confirmReaction = cursor.getBoolean(COL_CONFIRM_REACTION),
confirmUnbookmark = cursor.getBoolean(COL_CONFIRM_UNBOOKMARK),
confirmUnboost = cursor.getBoolean(COL_CONFIRM_UNBOOST),
confirmUnfavourite = cursor.getBoolean(COL_CONFIRM_UNFAVOURITE),
confirmUnfollow = cursor.getBoolean(COL_CONFIRM_UNFOLLOW),
defaultSensitive = cursor.getBoolean(COL_DEFAULT_SENSITIVE),
defaultText = cursor.getStringOrNull(COL_DEFAULT_TEXT) ?: "",
dontHideNsfw = cursor.getBoolean(COL_DONT_HIDE_NSFW),
dontShowTimeout = cursor.getBoolean(COL_DONT_SHOW_TIMEOUT),
expandCw = cursor.getBoolean(COL_EXPAND_CW),
extraJson = cursor.getStringOrNull(COL_EXTRA_JSON)?.decodeJsonObject() ?: JsonObject(),
imageMaxMegabytes = cursor.getStringOrNull(COL_IMAGE_MAX_MEGABYTES),
imageResize = cursor.getStringOrNull(COL_IMAGE_RESIZE),
maxTootChars = cursor.getInt(COL_MAX_TOOT_CHARS),
movieMaxMegabytes = cursor.getStringOrNull(COL_MOVIE_MAX_MEGABYTES),
notificationBoost = cursor.getBoolean(COL_NOTIFICATION_BOOST),
notificationFavourite = cursor.getBoolean(COL_NOTIFICATION_FAVOURITE),
notificationFollow = cursor.getBoolean(COL_NOTIFICATION_FOLLOW),
notificationFollowRequest = cursor.getBoolean(COL_NOTIFICATION_FOLLOW_REQUEST),
notificationMention = cursor.getBoolean(COL_NOTIFICATION_MENTION),
notificationPost = cursor.getBoolean(COL_NOTIFICATION_POST),
notificationReaction = cursor.getBoolean(COL_NOTIFICATION_REACTION),
notificationUpdate = cursor.getBoolean(COL_NOTIFICATION_UPDATE),
notificationVote = cursor.getBoolean(COL_NOTIFICATION_VOTE),
pushPolicy = cursor.getStringOrNull(COL_PUSH_POLICY),
// soundUri = cursor.getString(COL_SOUND_URI),
tokenJson = cursor.getString(COL_TOKEN).decodeJsonObject(),
visibility = TootVisibility.parseSavedVisibility(cursor.getStringOrNull(COL_VISIBILITY))
?: TootVisibility.Public,
misskeyVersion = cursor.getInt(COL_MISSKEY_VERSION),
// lastNotificationError = cursor.getStringOrNull(COL_LAST_NOTIFICATION_ERROR)
// last_push_endpoint = cursor.getStringOrNull(COL_LAST_PUSH_ENDPOINT)
// last_subscription_error = cursor.getStringOrNull(COL_LAST_SUBSCRIPTION_ERROR)
// notification_tag = cursor.getStringOrNull(COL_NOTIFICATION_TAG)
// register_key = cursor.getStringOrNull(COL_REGISTER_KEY)
// register_time = cursor.getLong(COL_REGISTER_TIME)
) {
loginAccount = if (accountJson?.get("id") == null) {
null // 疑似アカウント
} else {
TootParser(
context,
LinkHelper.create(
apiHostArg = this@SavedAccount.apiHost,
apDomainArg = this@SavedAccount.apDomain,
misskeyVersion = misskeyVersion
)
).account(accountJson)
?: error("missing loginAccount for $accountJson")
}
}
companion object : TableCompanion {
private val log = LogCategory("SavedAccount")
override val table = "access_info"
private const val COL_ID = BaseColumns._ID
private const val COL_HOST = "h"
private const val COL_DOMAIN = "d"
private const val COL_USER = "u"
private const val COL_ACCOUNT = "a"
private const val COL_TOKEN = "t"
private const val COL_CONFIRM_BOOST = "confirm_boost"
private const val COL_CONFIRM_FAVOURITE = "confirm_favourite"
private const val COL_CONFIRM_FOLLOW = "confirm_follow"
private const val COL_CONFIRM_FOLLOW_LOCKED = "confirm_follow_locked"
private const val COL_CONFIRM_POST = "confirm_post"
private const val COL_CONFIRM_REACTION = "confirm_reaction"
private const val COL_CONFIRM_UNBOOKMARK = "confirm_unbookmark"
private const val COL_CONFIRM_UNBOOST = "confirm_unboost"
private const val COL_CONFIRM_UNFAVOURITE = "confirm_unfavourite"
private const val COL_CONFIRM_UNFOLLOW = "confirm_unfollow"
private const val COL_DEFAULT_SENSITIVE = "default_sensitive"
private const val COL_DEFAULT_TEXT = "default_text"
private const val COL_DONT_HIDE_NSFW = "dont_hide_nsfw"
private const val COL_DONT_SHOW_TIMEOUT = "dont_show_timeout"
private const val COL_EXPAND_CW = "expand_cw"
private const val COL_EXTRA_JSON = "extra_json"
private const val COL_IMAGE_MAX_MEGABYTES = "image_max_megabytes"
private const val COL_IMAGE_RESIZE = "image_resize"
private const val COL_MAX_TOOT_CHARS = "max_toot_chars"
private const val COL_MISSKEY_VERSION = "is_misskey"
private const val COL_MOVIE_MAX_MEGABYTES = "movie_max_megabytes"
private const val COL_NOTIFICATION_BOOST = "notification_boost"
private const val COL_NOTIFICATION_FAVOURITE = "notification_favourite"
private const val COL_NOTIFICATION_FOLLOW = "notification_follow"
private const val COL_NOTIFICATION_FOLLOW_REQUEST = "notification_follow_request"
private const val COL_NOTIFICATION_MENTION = "notification_mention"
private const val COL_NOTIFICATION_POST = "notification_post"
private const val COL_NOTIFICATION_REACTION = "notification_reaction"
private const val COL_NOTIFICATION_TAG = "notification_server"
private const val COL_NOTIFICATION_UPDATE = "notification_update"
private const val COL_NOTIFICATION_VOTE = "notification_vote"
private const val COL_PUSH_POLICY = "push_policy"
private const val COL_VISIBILITY = "visibility"
// private const val COL_SOUND_URI = "sound_uri"
// private const val COL_LAST_NOTIFICATION_ERROR = "last_notification_error"
// private const val COL_LAST_PUSH_ENDPOINT = "last_push_endpoint"
// private const val COL_LAST_SUBSCRIPTION_ERROR = "last_subscription_error"
// private const val COL_REGISTER_KEY = "register_key"
// private const val COL_REGISTER_TIME = "register_time"
// COL_MISSKEY_VERSIONのカラム名がおかしいのは、昔はboolean扱いだったから
// 0: not misskey
// 1: old(v10) misskey
// 11: misskey v11
val columnList = MetaColumns(table, 0).apply {
column(0, COL_ID, "INTEGER PRIMARY KEY")
column(0, COL_ACCOUNT, "text not null")
column(0, COL_CONFIRM_BOOST, MetaColumns.TS_TRUE)
column(0, COL_DONT_HIDE_NSFW, MetaColumns.TS_ZERO)
column(0, COL_HOST, "text not null")
column(0, COL_TOKEN, "text not null")
column(0, COL_USER, "text not null")
column(0, COL_VISIBILITY, "text")
column(2, COL_NOTIFICATION_BOOST, MetaColumns.TS_TRUE)
column(2, COL_NOTIFICATION_FAVOURITE, MetaColumns.TS_TRUE)
column(2, COL_NOTIFICATION_FOLLOW, MetaColumns.TS_TRUE)
column(2, COL_NOTIFICATION_MENTION, MetaColumns.TS_TRUE)
column(10, COL_CONFIRM_FOLLOW, MetaColumns.TS_TRUE)
column(10, COL_CONFIRM_FOLLOW_LOCKED, MetaColumns.TS_TRUE)
column(10, COL_CONFIRM_POST, MetaColumns.TS_TRUE)
column(10, COL_CONFIRM_UNFOLLOW, MetaColumns.TS_TRUE)
column(13, COL_NOTIFICATION_TAG, MetaColumns.TS_EMPTY)
// column(14, COL_REGISTER_KEY, MetaColumns.TS_EMPTY)
// column(14, COL_REGISTER_TIME, MetaColumns.TS_ZERO)
// column(16, COL_SOUND_URI, MetaColumns.TS_EMPTY)
column(18, COL_DONT_SHOW_TIMEOUT, MetaColumns.TS_ZERO)
column(23, COL_CONFIRM_FAVOURITE, MetaColumns.TS_TRUE)
column(24, COL_CONFIRM_UNBOOST, MetaColumns.TS_TRUE)
column(24, COL_CONFIRM_UNFAVOURITE, MetaColumns.TS_TRUE)
column(27, COL_DEFAULT_TEXT, MetaColumns.TS_EMPTY)
column(28, COL_MISSKEY_VERSION, MetaColumns.TS_ZERO)
column(33, COL_NOTIFICATION_REACTION, MetaColumns.TS_TRUE)
column(33, COL_NOTIFICATION_VOTE, MetaColumns.TS_TRUE)
column(38, COL_DEFAULT_SENSITIVE, MetaColumns.TS_ZERO)
column(38, COL_EXPAND_CW, MetaColumns.TS_ZERO)
column(39, COL_MAX_TOOT_CHARS, MetaColumns.TS_ZERO)
// column(42, COL_LAST_NOTIFICATION_ERROR, "text")
column(44, COL_NOTIFICATION_FOLLOW_REQUEST, MetaColumns.TS_TRUE)
// column(45, COL_LAST_SUBSCRIPTION_ERROR, "text")
// column(46, COL_LAST_PUSH_ENDPOINT, "text")
column(56, COL_DOMAIN, "text")
column(57, COL_NOTIFICATION_POST, MetaColumns.TS_TRUE)
column(59, COL_IMAGE_MAX_MEGABYTES, "text default null")
column(59, COL_IMAGE_RESIZE, "text default null")
column(59, COL_MOVIE_MAX_MEGABYTES, "text default null")
column(60, COL_PUSH_POLICY, "text default null")
column(61, COL_CONFIRM_REACTION, MetaColumns.TS_TRUE)
column(62, COL_CONFIRM_UNBOOKMARK, MetaColumns.TS_TRUE)
column(63, COL_EXTRA_JSON, "text default null")
column(64, COL_NOTIFICATION_UPDATE, MetaColumns.TS_TRUE)
createExtra = {
arrayOf(
"create index if not exists ${table}_user on $table(u)",
"create index if not exists ${table}_host on $table(h,u)"
)
}
}
// アプリデータのインポート時に呼ばれる
fun onDBDelete(db: SQLiteDatabase) {
try {
db.execSQL("drop table if exists $table")
} catch (ex: Throwable) {
log.e(ex, "can't delete table $table.")
}
}
override fun onDBCreate(db: SQLiteDatabase) =
columnList.onDBCreate(db)
override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) =
columnList.onDBUpgrade(db, oldVersion, newVersion)
/////////////////////////////////
val defaultResizeConfig = ResizeConfig(ResizeType.LongSide, 1280)
internal val resizeConfigList = arrayOf(
ResizeConfig(ResizeType.None, 0),
ResizeConfig(ResizeType.LongSide, 640),
ResizeConfig(ResizeType.LongSide, 800),
ResizeConfig(ResizeType.LongSide, 1024),
ResizeConfig(ResizeType.LongSide, 1280),
ResizeConfig(ResizeType.LongSide, 1600),
ResizeConfig(ResizeType.LongSide, 2048),
ResizeConfig(ResizeType.SquarePixel, 640),
ResizeConfig(ResizeType.SquarePixel, 800),
ResizeConfig(ResizeType.SquarePixel, 1024),
ResizeConfig(ResizeType.SquarePixel, 1280),
ResizeConfig(ResizeType.SquarePixel, 1440, R.string.size_1920_1080),
ResizeConfig(ResizeType.SquarePixel, 1600),
ResizeConfig(ResizeType.SquarePixel, 2048)
)
const val LANG_WEB = "(web)"
const val LANG_DEVICE = "(device)"
// 横断検索用の、何とも紐ついていないアカウント
// 保存しない。
val na by lazy {
SavedAccount(-1L, "?@?").apply {
notificationFollow = false
notificationFollowRequest = false
notificationFavourite = false
notificationBoost = false
notificationMention = false
notificationReaction = false
notificationVote = false
notificationPost = false
notificationUpdate = false
}
}
private fun parse(context: Context, cursor: Cursor) =
try {
SavedAccount(context, cursor)
} catch (ex: Throwable) {
log.e(ex, "parse failed.")
null
}
private fun Boolean.booleanToInt(trueValue: Int, falseValue: Int = 0) =
if (this) trueValue else falseValue
}
class Access(
val db: SQLiteDatabase,
val context: Context,
) {
fun saveNew(
acct: String,
host: String,
domain: String,
account: JsonObject,
token: JsonObject,
misskeyVersion: Int = 0,
): Long {
try {
return ContentValues().apply {
put(COL_USER, acct)
put(COL_HOST, host)
put(COL_DOMAIN, domain)
put(COL_ACCOUNT, account.toString())
put(COL_TOKEN, token.toString())
put(COL_MISSKEY_VERSION, misskeyVersion)
}.let { db.insert(table, null, it) }
} catch (ex: Throwable) {
log.e(ex, "SavedAccount.insert failed.")
errorEx(ex, "SavedAccount.insert failed.")
}
}
fun updateTokenInfo(item: SavedAccount, auth2Result: Auth2Result) {
item.run {
if (isInvalidId) error("updateTokenInfo: missing db_id")
this.tokenJson = auth2Result.tokenJson
this.loginAccount = auth2Result.tootAccount
ContentValues().apply {
put(COL_TOKEN, auth2Result.tokenJson.toString())
put(COL_ACCOUNT, auth2Result.accountJson.toString())
put(COL_MISSKEY_VERSION, auth2Result.tootInstance.misskeyVersionMajor)
}.let { db.update(table, it, "$COL_ID=?", arrayOf(db_id.toString())) }
}
}
/**
* ユーザ登録の確認手順が完了しているかどうか
*
* - マストドン以外だと何もしないはず
*/
suspend fun checkConfirmed(item: SavedAccount, client: TootApiClient) {
item.run {
// 承認待ち状態ではないならチェックしない
if (loginAccount?.id != EntityId.CONFIRMING) return
// DBに保存されていないならチェックしない
if (isInvalidId) return
// アクセストークンがないならチェックしない
val accessToken = bearerAccessToken ?: return
// ユーザ情報を取得してみる。承認済みなら読めるはず
// 読めなければ例外が出る
val userJson = client.verifyAccount(
accessToken = accessToken,
outTokenInfo = null,
misskeyVersion = 0, // Mastodon only
)
// 読めたらアプリ内の記録を更新する
TootParser(context, this).account(userJson)?.let { ta ->
this.loginAccount = ta
db.update(
table,
ContentValues().apply {
put(COL_ACCOUNT, userJson.toString())
},
"$COL_ID=?",
arrayOf(db_id.toString())
)
checkNotificationImmediateAll(context, onlyEnqueue = true)
checkNotificationImmediate(context, db_id)
}
}
}
fun save(item: SavedAccount) {
item.run {
if (isInvalidId) error("saveSetting: missing db_id")
ContentValues().apply {
put(COL_ACCOUNT, accountJson?.toString())
put(COL_CONFIRM_BOOST, confirmBoost)
put(COL_CONFIRM_FAVOURITE, confirmFavourite)
put(COL_CONFIRM_FOLLOW, confirmFollow)
put(COL_CONFIRM_FOLLOW_LOCKED, confirmFollowLocked)
put(COL_CONFIRM_POST, confirmPost)
put(COL_CONFIRM_REACTION, confirmReaction)
put(COL_CONFIRM_UNBOOKMARK, confirmUnbookmark)
put(COL_CONFIRM_UNBOOST, confirmUnboost)
put(COL_CONFIRM_UNFAVOURITE, confirmUnfavourite)
put(COL_CONFIRM_UNFOLLOW, confirmUnfollow)
put(COL_DEFAULT_SENSITIVE, defaultSensitive)
put(COL_DEFAULT_TEXT, defaultText)
put(COL_DONT_HIDE_NSFW, dontHideNsfw)
put(COL_DONT_SHOW_TIMEOUT, dontShowTimeout)
put(COL_EXPAND_CW, expandCw)
put(COL_EXTRA_JSON, extraJson.toString())
put(COL_IMAGE_MAX_MEGABYTES, imageMaxMegabytes)
put(COL_IMAGE_RESIZE, imageResize)
put(COL_MAX_TOOT_CHARS, maxTootChars)
put(COL_MOVIE_MAX_MEGABYTES, movieMaxMegabytes)
put(COL_NOTIFICATION_BOOST, notificationBoost)
put(COL_NOTIFICATION_FAVOURITE, notificationFavourite)
put(COL_NOTIFICATION_FOLLOW, notificationFollow)
put(COL_NOTIFICATION_FOLLOW_REQUEST, notificationFollowRequest)
put(COL_NOTIFICATION_MENTION, notificationMention)
put(COL_NOTIFICATION_POST, notificationPost)
put(COL_NOTIFICATION_REACTION, notificationReaction)
put(COL_NOTIFICATION_UPDATE, notificationUpdate)
put(COL_NOTIFICATION_VOTE, notificationVote)
put(COL_PUSH_POLICY, pushPolicy)
// put(COL_SOUND_URI, soundUri)
put(COL_TOKEN, tokenJson?.toString())
put(COL_VISIBILITY, visibility.id.toString())
put(COL_MISSKEY_VERSION, misskeyVersion)
}.let { db.update(table, it, "$COL_ID=?", arrayOf(db_id.toString())) }
}
}
// onResumeの時に設定を読み直す
fun reloadSetting(item: SavedAccount, newData: SavedAccount? = null) {
item.run {
if (isInvalidId) error("SavedAccount.reloadSetting missing db_id")
// DBから削除されてるかもしれない
val b = newData ?: loadAccount(db_id) ?: return
this.accountJson = b.accountJson
this.confirmBoost = b.confirmBoost
this.confirmFavourite = b.confirmFavourite
this.confirmPost = b.confirmPost
this.confirmReaction = b.confirmReaction
this.confirmUnbookmark = b.confirmUnbookmark
this.confirmUnboost = b.confirmUnboost
this.confirmUnfavourite = b.confirmUnfavourite
this.defaultSensitive = b.defaultSensitive
this.defaultText = b.defaultText
this.dontHideNsfw = b.dontHideNsfw
this.dontShowTimeout = b.dontShowTimeout
this.expandCw = b.expandCw
this.extraJson = b.extraJson
this.imageMaxMegabytes = b.imageMaxMegabytes
this.imageResize = b.imageResize
this.movieMaxMegabytes = b.movieMaxMegabytes
this.notificationBoost = b.notificationBoost
this.notificationFavourite = b.notificationFavourite
this.notificationFollow = b.notificationFollow
this.notificationFollowRequest = b.notificationFollowRequest
this.notificationMention = b.notificationMention
this.notificationPost = b.notificationPost
this.notificationReaction = b.notificationReaction
this.notificationUpdate = b.notificationUpdate
this.notificationVote = b.notificationVote
this.pushPolicy = b.pushPolicy
// this.soundUri = b.soundUri
this.tokenJson = b.tokenJson
this.visibility = b.visibility
this.misskeyVersion = b.misskeyVersion
}
}
fun delete(dbId: Long) {
try {
db.deleteById(table, dbId.toString(), COL_ID)
} catch (ex: Throwable) {
log.e(ex, "SavedAccount.delete failed.")
errorEx(ex, "SavedAccount.delete failed.")
}
}
// fun clearRegistrationCache() {
// ContentValues().apply {
// put(COL_REGISTER_KEY, REGISTER_KEY_UNREGISTERED)
// put(COL_REGISTER_TIME, 0L)
// }.let { db.update(table, it, null, null) }
// }
fun loadAccount(dbId: Long): SavedAccount? =
try {
db.query(
table,
null,
"$COL_ID=?",
arrayOf(dbId.toString()),
null,
null,
null
)?.use { cursor ->
when {
cursor.moveToFirst() -> parse(lazyContext, cursor)
else -> {
log.e("moveToFirst failed. db_id=$dbId")
null
}
}
}
} catch (ex: Throwable) {
log.e(ex, "loadAccount failed.")
null
}
fun loadAccountList() =
ArrayList<SavedAccount>().also { result ->
try {
db.rawQuery("select * from $table", emptyArray()).use { cursor ->
while (cursor.moveToNext()) {
parse(lazyContext, cursor)?.let { result.add(it) }
}
}
} catch (ex: Throwable) {
log.e(ex, "loadAccountList failed.")
lazyContext.showToast(
true,
ex.withCaption("(SubwayTooter) broken in-app database?")
)
}
}
fun loadRealAccounts() =
ArrayList<SavedAccount>().also { result ->
try {
db.rawQuery("select * from $table where $COL_USER not like '?%'", emptyArray())
.use { cursor ->
while (cursor.moveToNext()) {
parse(lazyContext, cursor)?.let { result.add(it) }
}
}
} catch (ex: Throwable) {
log.e(ex, "loadAccountList failed.")
lazyContext.showToast(
true,
ex.withCaption("(SubwayTooter) broken in-app database?")
)
}
}
// fun loadByTag(tag: String): ArrayList<SavedAccount> {
// val result = ArrayList<SavedAccount>()
// try {
// db.query(
// table,
// null,
// "$COL_NOTIFICATION_TAG=?",
// arrayOf(tag),
// null,
// null,
// null
// )
// .use { cursor ->
// while (cursor.moveToNext()) {
// val a = parse(context, cursor)
// if (a != null) result.add(a)
// }
// }
// } catch (ex: Throwable) {
// log.e(ex, "loadByTag failed.")
// errorEx(ex, "SavedAccount.loadByTag failed.")
// }
//
// return result
// }
/**
* acctを指定してアカウントを取得する
*/
fun loadAccountByAcct(fullAcct: Acct) =
try {
db.query(
table,
null,
"$COL_USER=?",
arrayOf(fullAcct.ascii),
null,
null,
null
).use { cursor ->
if (cursor.moveToNext()) parse(context, cursor) else null
}
} catch (ex: Throwable) {
log.e(ex, "loadAccountByAcct failed.")
null
}
fun hasRealAccount(): Boolean {
try {
db.query(
table,
null,
"$COL_USER NOT LIKE '?@%'",
null,
null,
null,
null,
"1"
)
.use { cursor ->
if (cursor.moveToNext()) {
return true
}
}
} catch (ex: Throwable) {
log.e(ex, "hasNonPseudoAccount failed.")
}
return false
}
fun isSingleAccount(): Boolean =
try {
db.rawQuery(
"select count(*) from $table where $COL_USER NOT LIKE '?@%' limit 1",
emptyArray()
)?.use {
it.moveToNext() && it.getInt(0) == 1
} ?: false
} catch (ex: Throwable) {
log.e(ex, "getCount failed.")
errorEx(ex, "SavedAccount.getCount failed.")
}
// private fun charAtLower(src : CharSequence, pos : Int) : Char {
// val c = src[pos]
// return if(c >= 'a' && c <= 'z') c - ('a' - 'A') else c
// }
//
// @Suppress("SameParameterValue")
// private fun host_match(
// a : CharSequence,
// a_startArg : Int,
// b : CharSequence,
// b_startArg : Int
// ) : Boolean {
// var a_start = a_startArg
// var b_start = b_startArg
//
// val a_end = a.length
// val b_end = b.length
//
// var a_remain = a_end - a_start
// val b_remain = b_end - b_start
//
// // 文字数が違う
// if(a_remain != b_remain) return false
//
// // 文字数がゼロ
// if(a_remain <= 0) return true
//
// // 末尾の文字が違う
// if(charAtLower(a, a_end - 1) != charAtLower(b, b_end - 1)) return false
//
// // 先頭からチェック
// while(a_remain -- > 0) {
// if(charAtLower(a, a_start ++) != charAtLower(b, b_start ++)) return false
// }
//
// return true
// }
fun sweepBuggieData() {
// https://github.com/tateisu/SubwayTooter/issues/107
// COL_ACCOUNTの内容がおかしければ削除する
val list = ArrayList<Long>()
try {
db.query(
table,
null,
"$COL_ACCOUNT like ?",
arrayOf("jp.juggler.subwaytooter.api.entity.TootAccount@%"),
null,
null,
null
).use { cursor ->
val idxId = cursor.getColumnIndexOrThrow(COL_ID)
while (cursor.moveToNext()) {
list.add(cursor.getLong(idxId))
}
}
} catch (ex: Throwable) {
log.e(ex, "sweepBuggieData failed.")
}
list.forEach {
try {
db.delete(table, "$COL_ID=?", arrayOf(it.toString()))
} catch (ex: Throwable) {
log.e(ex, "sweepBuggieData failed.")
}
}
}
}
// Notestock検索カラムなど、特定のSNSサーバと紐ついていないアカウントなら真
val isNA: Boolean
get() = acct == Acct.UNKNOWN
// 実在しない疑似アカウントなら真。NAの場合も真を返す
val isPseudo: Boolean
get() = username == "?"
// DB用のIDが無効なら真
val isInvalidId: Boolean
get() = db_id <= 0L
// Mastodonのユーザ作成の承認まち状態ではないなら真
val isConfirmed: Boolean
get() = EntityId.CONFIRMING != loginAccount?.id
// Mastodon用のアクセストークン
val bearerAccessToken: String?
get() = tokenJson?.string("access_token")
// Misskey用のAPIトークン
val misskeyApiToken: String?
get() = tokenJson?.string(AuthBase.KEY_API_KEY_MISSKEY)
override fun hashCode(): Int = acct.hashCode()
override fun equals(other: Any?): Boolean =
when (other) {
is SavedAccount -> acct == other.acct
else -> false
}
// APIリクエスト用のJsonObjectに misskeyApiToken を格納する
fun putMisskeyApiToken(params: JsonObject = JsonObject()): JsonObject {
val apiKey = misskeyApiToken
if (apiKey?.isNotEmpty() == true) params["i"] = apiKey
return params
}
fun getFullAcct(who: TootAccount?) = getFullAcct(who?.acct)
fun isRemoteUser(who: TootAccount): Boolean = !isLocalUser(who.acct)
fun isLocalUser(who: TootAccount?): Boolean = isLocalUser(who?.acct)
private fun isLocalUser(acct: Acct?): Boolean {
acct ?: return false
return acct.host == null || acct.host == this.apDomain
}
// fun isRemoteUser(acct : String) : Boolean {
// return ! isLocalUser(acct)
// }
fun isMe(who: TootAccount?): Boolean = isMe(who?.acct)
// fun isMe(who_acct : String) : Boolean = isMe(Acct.parse(who_acct))
fun isMe(acct: Acct?): Boolean {
acct ?: return false
if (acct.username != this.acct.username) return false
return acct.host == null || acct.host == this.acct.host
}
// URLが相対指定だった場合にスキーマとホスト名を補う
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
fun notificationFlags() = 0 +
notificationBoost.booleanToInt(1) +
notificationFavourite.booleanToInt(2) +
notificationFollow.booleanToInt(4) +
notificationMention.booleanToInt(8) +
notificationReaction.booleanToInt(16) +
notificationVote.booleanToInt(32) +
notificationFollowRequest.booleanToInt(64) +
notificationPost.booleanToInt(128) +
notificationUpdate.booleanToInt(256) +
notificationStatusReference.booleanToInt(512)
fun canNotificationShowing(type: String?) = when (type) {
TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY,
-> notificationMention
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_QUOTE,
-> notificationBoost
TootNotification.TYPE_FAVOURITE -> notificationFavourite
TootNotification.TYPE_FOLLOW,
TootNotification.TYPE_UNFOLLOW,
TootNotification.TYPE_ADMIN_SIGNUP,
TootNotification.TYPE_ADMIN_REPORT,
-> notificationFollow
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY,
-> notificationFollowRequest
TootNotification.TYPE_EMOJI_REACTION_PLEROMA,
TootNotification.TYPE_EMOJI_REACTION,
TootNotification.TYPE_REACTION,
-> notificationReaction
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL,
TootNotification.TYPE_POLL_VOTE_MISSKEY,
-> notificationVote
TootNotification.TYPE_STATUS -> notificationPost
TootNotification.TYPE_UPDATE -> notificationUpdate
TootNotification.TYPE_STATUS_REFERENCE -> notificationStatusReference
// 未知の通知はオフらない
else -> true
}
fun getResizeConfig() =
resizeConfigList.find { it.spec == this.imageResize } ?: defaultResizeConfig
fun getMovieMaxBytes(ti: TootInstance) = 1000000 * max(
1,
this.movieMaxMegabytes?.toIntOrNull()
?: if (ti.instanceType == InstanceType.Pixelfed) 15 else 40
)
fun getImageMaxBytes(ti: TootInstance) = 1000000 * max(
1,
this.imageMaxMegabytes?.toIntOrNull()
?: if (ti.instanceType == InstanceType.Pixelfed) 15 else 8
)
fun getMovieResizeConfig() =
MovieResizeConfig(
mode = MovieResizeMode.fromInt(movieTranscodeMode),
limitBitrate = movieTranscodeBitrate.toLongOrNull()
?.takeIf { it >= 100_000L } ?: 2_000_000L,
limitFrameRate = movieTranscodeFramerate.toIntOrNull()
?.takeIf { it >= 1 } ?: 30,
limitSquarePixels = movieTranscodeSquarePixels.toIntOrNull()
?.takeIf { it > 0 } ?: 2304000,
)
fun isRequiredPushSubscription() = (notificationFlags() != 0) && notificationPushEnable
fun isRequiredPullCheck() = (notificationFlags() != 0) && notificationPullEnable
}