SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt

531 lines
19 KiB
Kotlin
Raw Normal View History

2021-06-28 09:09:00 +02:00
package jp.juggler.subwaytooter.column
2021-05-17 16:13:04 +02:00
import android.content.Context
2021-06-28 09:09:00 +02:00
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.TootApiClient
2021-05-17 16:13:04 +02:00
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.table.*
2021-06-28 09:35:09 +02:00
import jp.juggler.util.*
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.WordTrieTree
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.AdapterChange
import jp.juggler.util.ui.AdapterChangeType
2021-05-17 16:13:04 +02:00
import java.util.regex.Pattern
2021-06-28 09:35:09 +02:00
private val log = LogCategory("ColumnFilters")
2021-05-17 21:33:28 +02:00
val Column.isFilterEnabled: Boolean
get() = withAttachment ||
withHighlight ||
regexText.isNotEmpty() ||
dontShowNormalToot ||
dontShowNonPublicToot ||
quickFilter != Column.QUICK_FILTER_ALL ||
dontShowBoost ||
dontShowFavourite ||
dontShowFollow ||
dontShowReply ||
dontShowReaction ||
dontShowVote ||
(languageFilter?.isNotEmpty() == true)
2021-05-17 21:33:28 +02:00
2021-05-17 16:13:04 +02:00
// マストドン2.4.3rcのキーワードフィルタのコンテキスト
fun Column.getFilterContext() = when (type) {
ColumnType.STATUS_HISTORY -> null
ColumnType.HOME,
ColumnType.LIST_TL,
ColumnType.MISSKEY_HYBRID,
-> TootFilterContext.Home
2021-05-17 16:13:04 +02:00
ColumnType.NOTIFICATIONS,
ColumnType.NOTIFICATION_FROM_ACCT,
-> TootFilterContext.Notifications
2021-05-17 16:13:04 +02:00
ColumnType.CONVERSATION,
ColumnType.CONVERSATION_WITH_REFERENCE,
ColumnType.DIRECT_MESSAGES,
-> TootFilterContext.Thread
2021-05-17 16:13:04 +02:00
ColumnType.PROFILE -> TootFilterContext.Account
else -> TootFilterContext.Public
2021-05-17 16:13:04 +02:00
// ColumnType.MISSKEY_HYBRID や ColumnType.MISSKEY_ANTENNA_TL はHOMEでもPUBLICでもある…
// Misskeyだし関係ないが、NONEにするとアプリ内で完結するフィルタも働かなくなる
}
// カラム設定に正規表現フィルタを含めるなら真
fun Column.canStatusFilter() =
when (type) {
ColumnType.SEARCH_MSP,
ColumnType.SEARCH_TS,
ColumnType.SEARCH_NOTESTOCK,
ColumnType.STATUS_HISTORY,
-> true
else -> getFilterContext() != null
2021-05-17 16:13:04 +02:00
}
// カラム設定に「すべての画像を隠す」ボタンを含めるなら真
fun Column.canNSFWDefault(): Boolean = canStatusFilter()
// カラム設定に「ブーストを表示しない」ボタンを含めるなら真
fun Column.canFilterBoost(): Boolean = when (type) {
ColumnType.HOME, ColumnType.MISSKEY_HYBRID, ColumnType.PROFILE,
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT,
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL,
2021-06-28 09:09:00 +02:00
-> true
2021-05-17 16:13:04 +02:00
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
ColumnType.HASHTAG_FROM_ACCT -> false
ColumnType.CONVERSATION,
ColumnType.CONVERSATION_WITH_REFERENCE,
ColumnType.DIRECT_MESSAGES,
-> isMisskey
2021-05-17 16:13:04 +02:00
else -> false
}
// カラム設定に「返信を表示しない」ボタンを含めるなら真
fun Column.canFilterReply(): Boolean = when (type) {
ColumnType.HOME, ColumnType.MISSKEY_HYBRID, ColumnType.PROFILE,
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT,
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL, ColumnType.DIRECT_MESSAGES,
2021-06-28 09:09:00 +02:00
-> true
2021-05-17 16:13:04 +02:00
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
ColumnType.HASHTAG_FROM_ACCT -> true
else -> false
}
fun Column.canFilterNormalToot(): Boolean = when (type) {
ColumnType.NOTIFICATIONS -> true
ColumnType.HOME, ColumnType.MISSKEY_HYBRID,
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL,
2021-06-28 09:09:00 +02:00
-> true
2021-05-17 16:13:04 +02:00
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
ColumnType.HASHTAG_FROM_ACCT -> true
else -> false
}
fun Column.canFilterNonPublicToot(): Boolean = when (type) {
ColumnType.HOME, ColumnType.MISSKEY_HYBRID,
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL,
2021-06-28 09:09:00 +02:00
-> true
2021-05-17 16:13:04 +02:00
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
ColumnType.HASHTAG_FROM_ACCT -> true
else -> false
}
fun Column.onFiltersChanged2(filterList: List<TootFilter>) {
2021-05-17 16:13:04 +02:00
val newFilter = encodeFilterTree(filterList) ?: return
this.keywordFilterTrees = newFilter
checkFiltersForListData(newFilter)
}
fun Column.onFilterDeleted(filter: TootFilter, filterList: List<TootFilter>) {
2021-05-17 16:13:04 +02:00
if (type == ColumnType.KEYWORD_FILTER) {
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
2021-05-17 16:13:04 +02:00
if (o is TootFilter) {
if (o.id == filter.id) continue
}
tmpList.add(o)
2021-05-17 16:13:04 +02:00
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
2021-05-17 16:13:04 +02:00
fireShowContent(reason = "onFilterDeleted")
}
} else {
if (getFilterContext() != null) {
2021-05-17 16:13:04 +02:00
onFiltersChanged2(filterList)
}
}
}
2021-05-17 21:33:28 +02:00
@Suppress("unused")
2021-05-17 16:13:04 +02:00
fun Column.onLanguageFilterChanged() {
// TODO
}
fun Column.initFilter() {
columnRegexFilter = Column.COLUMN_REGEX_FILTER_DEFAULT
val regexText = this.regexText
if (regexText.isNotEmpty()) {
2021-05-17 16:13:04 +02:00
try {
val re = Pattern.compile(regexText)
columnRegexFilter =
2021-05-17 16:13:04 +02:00
{ text: CharSequence? ->
when {
text?.isEmpty() != false -> false
else -> re.matcher(text).find()
}
2021-05-17 16:13:04 +02:00
}
} catch (ex: Throwable) {
log.e(ex, "initFilter failed.")
2021-05-17 16:13:04 +02:00
}
}
favMuteSet = daoFavMute.acctSet()
highlightTrie = daoHighlightWord.nameSet()
2021-05-17 16:13:04 +02:00
}
private fun Column.isFilteredByAttachment(status: TootStatus): Boolean {
// オプションがどれも設定されていないならフィルタしない(false)
if (!(withAttachment || withHighlight)) return false
2021-05-17 16:13:04 +02:00
val matchMedia = withAttachment && status.reblog?.hasMedia() ?: status.hasMedia()
2021-05-17 16:13:04 +02:00
val matchHighlight =
withHighlight && null != (status.reblog?.highlightAny ?: status.highlightAny)
2021-05-17 16:13:04 +02:00
// どれかの条件を満たすならフィルタしない(false)、どれも満たさないならフィルタする(true)
return !(matchMedia || matchHighlight)
}
fun Column.isFiltered(status: TootStatus): Boolean {
val isMe = accessInfo.isMe(status.account) ||
accessInfo.isMe(status.reblog?.account)
2021-05-17 16:13:04 +02:00
val filterTrees = keywordFilterTrees
if (filterTrees != null && !isMe) {
val ti = TootInstance.getCached(accessInfo)
if (ti?.versionGE(TootInstance.VERSION_4_0_0) == true) {
// v4 はサーバ側でフィルタしてる
// XXX: フィルタが後から更新されたら再チェックが必要か?
val filteredV4 = status.filteredV4 ?: status.reblog?.filteredV4
if (filteredV4.isNullOrEmpty()) {
// フィルタされていない
} else if (filteredV4.any { it.isHide }) {
// 隠すフィルタ
log.d("isFiltered: status muted by filteredV4 hide.")
return true
} else {
// 警告フィルタ
status.updateKeywordFilteredFlag(
accessInfo,
filterTrees,
matchedFiltersV4 = filteredV4
)
}
} else {
if (status.matchKeywordFilterWithReblog(accessInfo, filterTrees.treeHide) != null) {
log.d("status filtered by treeIrreversible")
return true
} else {
// 警告フィルタ
status.updateKeywordFilteredFlag(accessInfo, filterTrees)
}
2021-05-17 16:13:04 +02:00
}
}
if (isFilteredByAttachment(status)) return true
val reblog = status.reblog
if (dontShowBoost) {
2021-05-17 16:13:04 +02:00
if (reblog != null) return true
}
if (dontShowReply) {
2021-05-17 16:13:04 +02:00
if (status.in_reply_to_id != null) return true
if (reblog?.in_reply_to_id != null) return true
}
if (dontShowNormalToot) {
2021-05-17 16:13:04 +02:00
if (status.in_reply_to_id == null && reblog == null) return true
}
if (dontShowNonPublicToot) {
2021-05-17 16:13:04 +02:00
if (!status.visibility.isPublic) return true
}
if (columnRegexFilter(status.decoded_content)) return true
if (columnRegexFilter(reblog?.decoded_content)) return true
if (columnRegexFilter(status.decoded_spoiler_text)) return true
if (columnRegexFilter(reblog?.decoded_spoiler_text)) return true
2021-05-17 16:13:04 +02:00
if (checkLanguageFilter(status)) return true
if (accessInfo.isPseudo) {
var r = daoUserRelation.loadPseudo(accessInfo.getFullAcct(status.account))
2021-05-17 16:13:04 +02:00
if (r.muting || r.blocking) return true
if (reblog != null) {
r = daoUserRelation.loadPseudo(accessInfo.getFullAcct(reblog.account))
2021-05-17 16:13:04 +02:00
if (r.muting || r.blocking) return true
}
}
return status.checkMuted()
}
// true if the status will be hidden
private fun Column.checkLanguageFilter(status: TootStatus?): Boolean {
status ?: return false
val languageFilter = languageFilter ?: return false
2021-05-17 16:13:04 +02:00
val allow = languageFilter.boolean(
status.language ?: status.reblog?.language ?: TootStatus.LANGUAGE_CODE_UNKNOWN
)
?: languageFilter.boolean(TootStatus.LANGUAGE_CODE_DEFAULT)
?: true
return !allow
}
fun Column.isFiltered(item: TootNotification): Boolean {
if (when (quickFilter) {
2021-05-17 16:13:04 +02:00
Column.QUICK_FILTER_ALL -> when (item.type) {
TootNotification.TYPE_FAVOURITE -> dontShowFavourite
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_QUOTE,
-> dontShowBoost
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_FOLLOW,
TootNotification.TYPE_UNFOLLOW,
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY,
2022-04-17 07:56:57 +02:00
TootNotification.TYPE_ADMIN_SIGNUP,
-> dontShowFollow
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY,
-> dontShowReply
2021-05-17 16:13:04 +02:00
2021-05-27 07:54:04 +02:00
TootNotification.TYPE_EMOJI_REACTION_PLEROMA,
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_EMOJI_REACTION,
TootNotification.TYPE_REACTION,
-> dontShowReaction
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL,
TootNotification.TYPE_POLL_VOTE_MISSKEY,
-> dontShowVote
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_STATUS,
TootNotification.TYPE_UPDATE,
TootNotification.TYPE_STATUS_REFERENCE,
-> dontShowNormalToot
2021-05-17 16:13:04 +02:00
else -> false
}
else -> when (item.type) {
TootNotification.TYPE_FAVOURITE -> quickFilter != Column.QUICK_FILTER_FAVOURITE
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_QUOTE,
-> quickFilter != Column.QUICK_FILTER_BOOST
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_FOLLOW,
TootNotification.TYPE_UNFOLLOW,
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY,
2022-04-17 07:56:57 +02:00
TootNotification.TYPE_ADMIN_SIGNUP,
-> quickFilter != Column.QUICK_FILTER_FOLLOW
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY,
-> quickFilter != Column.QUICK_FILTER_MENTION
2021-05-17 16:13:04 +02:00
2021-05-27 07:54:04 +02:00
TootNotification.TYPE_EMOJI_REACTION_PLEROMA,
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_EMOJI_REACTION,
TootNotification.TYPE_REACTION,
-> quickFilter != Column.QUICK_FILTER_REACTION
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL,
TootNotification.TYPE_POLL_VOTE_MISSKEY,
-> quickFilter != Column.QUICK_FILTER_VOTE
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_STATUS,
TootNotification.TYPE_UPDATE,
TootNotification.TYPE_STATUS_REFERENCE,
-> quickFilter != Column.QUICK_FILTER_POST
2021-05-17 16:13:04 +02:00
else -> true
}
}
) {
2021-06-28 09:35:09 +02:00
log.d("isFiltered: ${item.type} notification filtered.")
2021-05-17 16:13:04 +02:00
return true
}
val status = item.status
val filterTrees = keywordFilterTrees
if (status != null && filterTrees != null) {
val ti = TootInstance.getCached(accessInfo)
if (ti?.versionGE(TootInstance.VERSION_4_0_0) == true) {
// v4 はサーバ側でフィルタしてる
// XXX: フィルタが後から更新されたら再チェックが必要か?
val filterResults = status.filteredV4 ?: status.reblog?.filteredV4
if (filterResults.isNullOrEmpty()) {
// フィルタされていない
} else if (filterResults.any { it.isHide }) {
// 隠すフィルタ
log.d("isFiltered: status muted by filteredV4 hide.")
return true
} else {
// 警告フィルタ
status.updateKeywordFilteredFlag(
accessInfo,
filterTrees,
matchedFiltersV4 = filterResults
)
}
} else {
// v4未満は端末側でのチェック
if (status.matchKeywordFilterWithReblog(accessInfo, filterTrees.treeHide) != null) {
// 隠すフィルタ
log.d("isFiltered: status muted by treeIrreversible.")
return true
} else {
// 警告フィルタ
// just update _filtered flag for reversible filter
status.updateKeywordFilteredFlag(accessInfo, filterTrees)
}
2021-05-17 16:13:04 +02:00
}
}
2021-05-17 16:13:04 +02:00
if (checkLanguageFilter(status)) return true
if (status?.checkMuted() == true) {
2021-06-28 09:35:09 +02:00
log.d("isFiltered: status muted by in-app muted words.")
2021-05-17 16:13:04 +02:00
return true
}
// ふぁぼ魔ミュート
when (item.type) {
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_QUOTE,
TootNotification.TYPE_FAVOURITE,
2021-05-27 07:54:04 +02:00
TootNotification.TYPE_EMOJI_REACTION_PLEROMA,
2021-05-17 16:13:04 +02:00
TootNotification.TYPE_EMOJI_REACTION,
TootNotification.TYPE_REACTION,
TootNotification.TYPE_FOLLOW,
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY,
2022-04-17 07:56:57 +02:00
TootNotification.TYPE_ADMIN_SIGNUP,
-> {
2021-05-17 16:13:04 +02:00
val who = item.account
if (who != null && favMuteSet?.contains(accessInfo.getFullAcct(who)) == true) {
2021-06-28 09:35:09 +02:00
log.d("${accessInfo.getFullAcct(who)} is in favMuteSet.")
2021-05-17 16:13:04 +02:00
return true
}
}
}
return false
}
// フィルタを読み直してリストを返す。またはnull
suspend fun Column.loadFilter2(client: TootApiClient): List<TootFilter>? {
if (accessInfo.isPseudo || accessInfo.isMisskey) return null
if (getFilterContext() == null) return null
var result = client.request(ApiPath.PATH_FILTERS_V2)
if (result?.response?.code == 404) {
result = client.request(ApiPath.PATH_FILTERS)
}
2021-05-17 16:13:04 +02:00
val jsonArray = result?.jsonArray ?: return null
return TootFilter.parseList(jsonArray)
}
fun Column.encodeFilterTree(filterList: List<TootFilter>?): FilterTrees? {
val columnContext = getFilterContext()
if (columnContext == null || filterList == null) return null
2021-05-17 16:13:04 +02:00
val result = FilterTrees()
val now = System.currentTimeMillis()
for (filter in filterList) {
if (filter.time_expires_at > 0L && now >= filter.time_expires_at) continue
if (!filter.hasContext(columnContext)) continue
2021-05-17 16:13:04 +02:00
for (kw in filter.keywords) {
val validator = when (kw.whole_word) {
true -> WordTrieTree.WORD_VALIDATOR
else -> WordTrieTree.EMPTY_VALIDATOR
}
when (filter.hide) {
true -> result.treeHide
else -> result.treeWarn
}.add(
s = kw.keyword,
tag = filter,
validator = validator
)
result.treeAll.add(
s = kw.keyword,
tag = filter,
validator = validator
)
}
2021-05-17 16:13:04 +02:00
}
return result
}
// フィルタ更新時に全部チェックし直す
2021-05-17 16:13:04 +02:00
fun Column.checkFiltersForListData(trees: FilterTrees?) {
trees ?: return
val changeList = ArrayList<AdapterChange>()
listData.forEachIndexed { idx, item ->
2021-05-17 16:13:04 +02:00
when (item) {
is TootStatus -> {
val oldFiltered = item.filtered
item.updateKeywordFilteredFlag(accessInfo, trees, checkAll = true)
if (oldFiltered != item.filtered) {
2021-05-17 16:13:04 +02:00
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx))
}
}
is TootNotification -> {
val s = item.status
if (s != null) {
val oldFiltered = s.filtered
s.updateKeywordFilteredFlag(accessInfo, trees, checkAll = true)
if (oldFiltered != s.filtered) {
2021-05-17 16:13:04 +02:00
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx))
}
}
}
}
}
fireShowContent(reason = "filter updated", changeList = changeList)
}
fun reloadFilter(context: Context, accessInfo: SavedAccount) {
2021-06-13 13:48:48 +02:00
launchMain {
var resultList: List<TootFilter>? = null
context.runApiTask(
accessInfo,
progressStyle = ApiTask.PROGRESS_NONE
2021-06-13 13:48:48 +02:00
) { client ->
var result = client.request(ApiPath.PATH_FILTERS_V2)
if (result?.response?.code == 404) {
result = client.request(ApiPath.PATH_FILTERS)
}
result?.jsonArray?.let {
resultList = TootFilter.parseList(it)
2021-05-17 16:13:04 +02:00
}
result
}
2021-05-17 16:13:04 +02:00
2021-06-13 13:48:48 +02:00
resultList?.let {
2021-06-28 09:35:09 +02:00
log.d("update filters for ${accessInfo.acct.pretty}")
for (column in App1.getAppState(context).columnList) {
if (column.accessInfo == accessInfo) {
column.onFiltersChanged2(it)
2021-05-17 16:13:04 +02:00
}
}
}
}
2021-05-17 16:13:04 +02:00
}