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

380 lines
16 KiB
Kotlin

package jp.juggler.subwaytooter.table
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.provider.BaseColumns
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootRelationShip
import jp.juggler.util.*
class UserRelation {
var following = false // 認証ユーザからのフォロー状態にある
var followed_by = false // 認証ユーザは被フォロー状態にある
var blocking = false // 認証ユーザからブロックした
var blocked_by = false // 認証ユーザからブロックされた(Misskeyのみ。Mastodonでは常にfalse)
var muting = false
var requested = false // 認証ユーザからのフォローは申請中である
var requested_by = false // 相手から認証ユーザへのフォローリクエスト申請中(Misskeyのみ。Mastodonでは常にfalse)
var following_reblogs = 0 // このユーザからのブーストをTLに表示する
var endorsed = false // ユーザをプロフィールで紹介する
var notifying = false // ユーザの投稿を通知する
var note: String? = null
// 認証ユーザからのフォロー状態
fun getFollowing(who: TootAccount?): Boolean {
return if (requested && !following && who != null && !who.locked) true else following
}
// 認証ユーザからのフォローリクエスト申請中状態
fun getRequested(who: TootAccount?): Boolean {
return if (requested && !following && who != null && !who.locked) false else requested
}
companion object : TableCompanion {
const val REBLOG_HIDE =
0 // don't show the boosts from target account will be shown on authorized user's home TL.
const val REBLOG_SHOW =
1 // show the boosts from target account will be shown on authorized user's home TL.
const val REBLOG_UNKNOWN = 2 // not following, or instance don't support hide reblog.
private val mMemoryCache = androidx.collection.LruCache<String, UserRelation>(2048)
private val log = LogCategory("UserRelationMisskey")
override val table = "user_relation_misskey"
val columnList: ColumnMeta.List = ColumnMeta.List(table, 30).apply {
createExtra = {
arrayOf(
"create unique index if not exists ${table}_id on $table($COL_DB_ID,$COL_WHO_ID)",
"create index if not exists ${table}_time on $table($COL_TIME_SAVE)",
)
}
deleteBeforeCreate = true
}
val COL_ID =
ColumnMeta(columnList, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true)
private val COL_TIME_SAVE = ColumnMeta(columnList, 0, "time_save", "integer not null")
// SavedAccount のDB_ID。 疑似アカウント用のエントリは -2L
private val COL_DB_ID = ColumnMeta(columnList, 0, "db_id", "integer not null")
// ターゲットアカウントのID
val COL_WHO_ID = ColumnMeta(columnList, 0, "who_id", "text not null")
private val COL_FOLLOWING = ColumnMeta(columnList, 0, "following", "integer not null")
private val COL_FOLLOWED_BY = ColumnMeta(columnList, 0, "followed_by", "integer not null")
private val COL_BLOCKING = ColumnMeta(columnList, 0, "blocking", "integer not null")
private val COL_MUTING = ColumnMeta(columnList, 0, "muting", "integer not null")
private val COL_REQUESTED = ColumnMeta(columnList, 0, "requested", "integer not null")
private val COL_FOLLOWING_REBLOGS =
ColumnMeta(columnList, 0, "following_reblogs", "integer not null")
private val COL_ENDORSED = ColumnMeta(columnList, 32, "endorsed", "integer default 0")
private val COL_BLOCKED_BY = ColumnMeta(columnList, 34, "blocked_by", "integer default 0")
private val COL_REQUESTED_BY =
ColumnMeta(columnList, 35, "requested_by", "integer default 0")
private val COL_NOTE = ColumnMeta(columnList, 55, "note", "text default null")
private val COL_NOTIFYING = ColumnMeta(columnList, 58, "notifying", "integer default 0")
private const val DB_ID_PSEUDO = -2L
override fun onDBCreate(db: SQLiteDatabase) =
columnList.onDBCreate(db)
override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) =
columnList.onDBUpgrade(db, oldVersion, newVersion)
fun deleteOld(now: Long) {
try {
val expire = now - 86400000L * 365
App1.database.delete(table, "$COL_TIME_SAVE<?", arrayOf(expire.toString()))
} catch (ex: Throwable) {
log.e(ex, "deleteOld failed.")
}
try {
// 旧型式のテーブルの古いデータの削除だけは時々回す
val table = "user_relation"
val COL_TIME_SAVE = "time_save"
val expire = now - 86400000L * 365
App1.database.delete(table, "$COL_TIME_SAVE<?", arrayOf(expire.toString()))
} catch (_: Throwable) {
}
}
private fun key(dbId: Long, whoId: String) = "$dbId:$whoId"
private fun key(dbId: Long, whoId: EntityId) = key(dbId, whoId.toString())
private fun ContentValues.fromUserRelation(src: UserRelation) {
put(COL_FOLLOWING, src.following)
put(COL_FOLLOWED_BY, src.followed_by)
put(COL_BLOCKING, src.blocking)
put(COL_MUTING, src.muting)
put(COL_REQUESTED, src.requested)
put(COL_FOLLOWING_REBLOGS, src.following_reblogs)
put(COL_ENDORSED, src.endorsed)
put(COL_BLOCKED_BY, src.blocked_by)
put(COL_REQUESTED_BY, src.requested_by)
put(COL_NOTIFYING, src.notifying)
put(COL_NOTE, src.note) // may null
}
private fun ContentValues.fromTootRelationShip(src: TootRelationShip) {
put(COL_FOLLOWING, src.following)
put(COL_FOLLOWED_BY, src.followed_by)
put(COL_BLOCKING, src.blocking)
put(COL_MUTING, src.muting)
put(COL_REQUESTED, src.requested)
put(COL_FOLLOWING_REBLOGS, src.showing_reblogs)
put(COL_ENDORSED, src.endorsed)
put(COL_BLOCKED_BY, src.blocked_by)
put(COL_REQUESTED_BY, src.requested_by)
put(COL_NOTIFYING, src.notifying)
put(COL_NOTE, src.note) // may null
}
// マストドン用
fun save1Mastodon(now: Long, dbId: Long, src: TootRelationShip): UserRelation {
val id: String = src.id.toString()
try {
ContentValues().apply {
put(COL_TIME_SAVE, now)
put(COL_DB_ID, dbId)
put(COL_WHO_ID, id)
fromTootRelationShip(src)
}.let { App1.database.replaceOrThrow(table, null, it) }
mMemoryCache.remove(key(dbId, id))
} catch (ex: Throwable) {
log.e(ex, "save failed.")
}
return load(dbId, id)
}
// マストドン用
fun saveListMastodon(now: Long, dbId: Long, srcList: Iterable<TootRelationShip>) {
val db = App1.database
db.execSQL("BEGIN TRANSACTION")
val bOK = try {
val cv = ContentValues()
cv.put(COL_TIME_SAVE, now)
cv.put(COL_DB_ID, dbId)
for (src in srcList) {
val id = src.id.toString()
cv.put(COL_WHO_ID, id)
cv.fromTootRelationShip(src)
db.replaceOrThrow(table, null, cv)
}
true
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "saveList failed.")
false
}
when {
!bOK -> db.execSQL("ROLLBACK TRANSACTION")
else -> {
db.execSQL("COMMIT TRANSACTION")
for (src in srcList) {
mMemoryCache.remove(key(dbId, src.id))
}
}
}
}
fun save1Misskey(now: Long, dbId: Long, whoId: String, src: UserRelation?) {
src ?: return
try {
ContentValues().apply {
put(COL_TIME_SAVE, now)
put(COL_DB_ID, dbId)
put(COL_WHO_ID, whoId)
fromUserRelation(src)
}.let { App1.database.replaceOrThrow(table, null, it) }
mMemoryCache.remove(key(dbId, whoId))
} catch (ex: Throwable) {
log.e(ex, "save failed.")
}
}
fun saveListMisskey(
now: Long,
dbId: Long,
srcList: List<Map.Entry<EntityId, UserRelation>>,
start: Int,
end: Int,
) {
val db = App1.database
db.execSQL("BEGIN TRANSACTION")
val bOK = try {
val cv = ContentValues()
cv.put(COL_TIME_SAVE, now)
cv.put(COL_DB_ID, dbId)
for (i in start until end) {
val entry = srcList[i]
val id = entry.key
val src = entry.value
cv.put(COL_WHO_ID, id.toString())
cv.fromUserRelation(src)
db.replaceOrThrow(table, null, cv)
}
true
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "saveList failed.")
false
}
when {
!bOK -> db.execSQL("ROLLBACK TRANSACTION")
else -> {
db.execSQL("COMMIT TRANSACTION")
for (i in start until end) {
val entry = srcList[i]
val key = key(dbId, entry.key)
mMemoryCache.remove(key)
}
}
}
}
// Misskeyのリレーション取得APIから
fun saveListMisskeyRelationApi(now: Long, dbId: Long, list: ArrayList<TootRelationShip>) {
val db = App1.database
db.execSQL("BEGIN TRANSACTION")
val bOK = try {
val cv = ContentValues()
cv.put(COL_TIME_SAVE, now)
cv.put(COL_DB_ID, dbId)
for (src in list) {
val id = src.id.toString()
cv.put(COL_WHO_ID, id)
cv.fromTootRelationShip(src)
db.replace(table, null, cv)
}
true
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "saveListMisskeyRelationApi failed.")
false
}
when {
!bOK -> db.execSQL("ROLLBACK TRANSACTION")
else -> {
db.execSQL("COMMIT TRANSACTION")
for (src in list) {
mMemoryCache.remove(key(dbId, src.id))
}
}
}
}
private val loadWhere = "$COL_DB_ID=? and $COL_WHO_ID=?"
private val loadWhereArg = object : ThreadLocal<Array<String?>>() {
override fun initialValue(): Array<String?> = Array(2) { null }
}
fun load(dbId: Long, whoId: EntityId): UserRelation {
//
val key = key(dbId, whoId)
val cached: UserRelation? = mMemoryCache.get(key)
if (cached != null) return cached
val dst = load(dbId, whoId.toString())
mMemoryCache.put(key, dst)
return dst
}
fun load(dbId: Long, whoId: String): UserRelation {
try {
val where_arg = loadWhereArg.get() ?: arrayOfNulls<String?>(2)
where_arg[0] = dbId.toString()
where_arg[1] = whoId
App1.database.query(table, null, loadWhere, where_arg, null, null, null)
.use { cursor ->
if (cursor.moveToNext()) {
val dst = UserRelation()
dst.following = cursor.getBoolean(COL_FOLLOWING)
dst.followed_by = cursor.getBoolean(COL_FOLLOWED_BY)
dst.blocking = cursor.getBoolean(COL_BLOCKING)
dst.muting = cursor.getBoolean(COL_MUTING)
dst.requested = cursor.getBoolean(COL_REQUESTED)
dst.following_reblogs = cursor.getInt(COL_FOLLOWING_REBLOGS)
dst.endorsed = cursor.getBoolean(COL_ENDORSED)
dst.blocked_by = cursor.getBoolean(COL_BLOCKED_BY)
dst.requested_by = cursor.getBoolean(COL_REQUESTED_BY)
dst.notifying = cursor.getBoolean(COL_NOTIFYING)
dst.note = cursor.getStringOrNull(COL_NOTE)
return dst
}
}
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "load failed.")
}
return UserRelation()
}
// MisskeyはUserエンティティにユーザリレーションが含まれたり含まれなかったりする
fun fromAccount(parser: TootParser, src: JsonObject, id: EntityId) {
// アカウントのjsonがユーザリレーションを含まないなら何もしない
src["isFollowing"] ?: return
// プロフカラムで ユーザのプロフ(A)とアカウントTL(B)を順に取得すると
// (A)ではisBlockingに情報が入っているが、(B)では情報が入っていない
// 対策として(A)でリレーションを取得済みのユーザは(B)のタイミングではリレーションを読み捨てる
val map = parser.misskeyUserRelationMap
if (map.containsKey(id)) return
map[id] = UserRelation().apply {
following = src.optBoolean("isFollowing")
followed_by = src.optBoolean("isFollowed")
muting = src.optBoolean("isMuted")
blocking = src.optBoolean("isBlocking")
blocked_by = src.optBoolean("isBlocked")
endorsed = false
requested = src.optBoolean("hasPendingFollowRequestFromYou")
requested_by = src.optBoolean("hasPendingFollowRequestToYou")
}
}
fun loadPseudo(acct: Acct) = load(DB_ID_PSEUDO, acct.ascii)
fun createCursorPseudo(): Cursor =
App1.database.query(
table,
arrayOf(COL_ID.name, COL_WHO_ID.name),
"$COL_DB_ID=$DB_ID_PSEUDO and ( $COL_MUTING=1 or $COL_BLOCKING=1 )",
null,
null,
null,
"$COL_WHO_ID asc"
)
fun deletePseudo(rowId: Long) {
try {
App1.database.delete(table, "$COL_ID=$rowId", null)
} catch (ex: Throwable) {
log.trace(ex)
}
}
}
fun savePseudo(acct: String) =
save1Misskey(System.currentTimeMillis(), DB_ID_PSEUDO, acct, this)
}