feat(Scout): add exclude: filter

it is basically the opposite of has:
This commit is contained in:
LavaDesu 2025-05-29 23:34:19 +10:00
parent 47ae69de2d
commit 34768502b2
Signed by: cilly
GPG key ID: 6500251E087653C9
4 changed files with 120 additions and 11 deletions

View file

@ -7,6 +7,7 @@ object FilterTypeExtension {
lateinit var DURING: FilterType
lateinit var AFTER: FilterType
lateinit var SORT: FilterType
lateinit var EXCLUDE: FilterType
lateinit var dates: Array<FilterType>
lateinit var values: Array<FilterType>
}

View file

@ -14,15 +14,18 @@ import com.discord.BuildConfig
import com.discord.restapi.RequiredHeadersInterceptor
import com.discord.restapi.RequiredHeadersInterceptor.HeadersProvider
import com.discord.restapi.RestAPIBuilder
import com.discord.simpleast.core.parser.ParseSpec
import com.discord.simpleast.core.parser.Parser
import com.discord.simpleast.core.parser.Rule
import com.discord.stores.StoreSearch
import com.discord.stores.StoreSearchInput
import com.discord.utilities.rest.RestAPI.AppHeadersProvider
import com.discord.utilities.search.network.`SearchFetcher$getRestObservable$3`
import com.discord.utilities.search.network.SearchQuery
import com.discord.utilities.search.query.FilterType
import com.discord.utilities.search.query.node.QueryNode
import com.discord.utilities.search.query.node.answer.HasAnswerOption
import com.discord.utilities.search.query.node.answer.HasNode
import com.discord.utilities.search.query.node.content.ContentNode
import com.discord.utilities.search.query.node.filter.FilterNode
import com.discord.utilities.search.query.parsing.QueryParser
@ -31,17 +34,20 @@ import com.discord.utilities.search.suggestion.SearchSuggestionEngine
import com.discord.utilities.search.suggestion.entries.FilterSuggestion
import com.discord.utilities.search.suggestion.entries.HasSuggestion
import com.discord.utilities.search.suggestion.entries.SearchSuggestion
import com.discord.utilities.search.validation.SearchData
import com.discord.widgets.search.suggestions.WidgetSearchSuggestionsAdapter
import com.franmontiel.persistentcookiejar.PersistentCookieJar
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import moe.lava.awoocord.scout.api.SearchAPIInterface
import moe.lava.awoocord.scout.parsing.DateNode
import moe.lava.awoocord.scout.parsing.SimpleParserRule
import moe.lava.awoocord.scout.parsing.SortNode
import moe.lava.awoocord.scout.parsing.UserIdNode
import moe.lava.awoocord.scout.ui.DatePickerFragment
import moe.lava.awoocord.scout.ui.ScoutResource
import moe.lava.awoocord.scout.ui.ScoutSearchStringProvider
import java.util.regex.Pattern
@AliucordPlugin(requiresRestart = false)
@Suppress("unused", "unchecked_cast")
@ -49,14 +55,18 @@ class Scout : Plugin() {
lateinit var ssProvider: ScoutSearchStringProvider
lateinit var searchApi: SearchAPIInterface
override fun start(context: Context) {
override fun load(context: Context) {
ssProvider = ScoutSearchStringProvider(context)
searchApi = buildSearchApi(context)
}
override fun start(context: Context) {
extendFilterType()
extendHasAnswerOption()
patchQueryParser()
patchQuery()
patchHasAnswerOption()
patchHasNode()
patchQuery()
patchQueryParser()
patchSearchUI(context)
}
@ -102,16 +112,18 @@ class Scout : Plugin() {
origFilterTypes = origFilterTypes ?: values
var nextIdx = values.size
val EXCLUDE = constructor.newInstance("EXCLUDE", nextIdx++) as FilterType
val BEFORE = constructor.newInstance("BEFORE", nextIdx++) as FilterType
val DURING = constructor.newInstance("DURING", nextIdx++) as FilterType
val AFTER = constructor.newInstance("AFTER", nextIdx++) as FilterType
val SORT = constructor.newInstance("SORT", nextIdx) as FilterType
FilterTypeExtension.EXCLUDE = EXCLUDE
FilterTypeExtension.BEFORE = BEFORE
FilterTypeExtension.DURING = DURING
FilterTypeExtension.AFTER = AFTER
FilterTypeExtension.SORT = SORT
FilterTypeExtension.dates = arrayOf(BEFORE, DURING, AFTER)
FilterTypeExtension.values = arrayOf(BEFORE, DURING, AFTER, SORT)
FilterTypeExtension.values = arrayOf(EXCLUDE, BEFORE, DURING, AFTER, SORT)
val newValues = values.toMutableList()
newValues.addAll(FilterTypeExtension.values)
@ -212,21 +224,96 @@ class Scout : Plugin() {
param.result = 0x7f08032e
}
patcher.after<SearchSuggestionEngine>(
patcher.instead<SearchSuggestionEngine>(
"getHasSuggestions",
CharSequence::class.java,
FilterType::class.java,
SearchStringProvider::class.java,
) { param ->
val query = param.args[0] as CharSequence
val res = (param.result as List<SearchSuggestion>).toMutableList()
for (type in HasAnswerOptionExtension.values) {
val st = ssProvider.stringFor(type) + ":"
val filterType = param.args[1] as FilterType
val ossProvider = param.args[2] as SearchStringProvider
if (st.contains(query))
res.add(HasSuggestion(type))
if (filterType != FilterType.HAS && filterType != FilterTypeExtension.EXCLUDE)
return@instead listOf<Any>()
val res = mutableListOf<HasSuggestion>()
for (opt in HasAnswerOption.values()) {
val filterText = opt.getLocalizedInputText(ossProvider)
if (filterText.contains(query))
res.add(HasSuggestion(opt))
}
param.result = res.toList()
res.toList()
}
}
// Patching HasNode related methods for our exclude: filter type
private fun patchHasNode() {
patcher.instead<HasNode>("getValidFilters") {
setOf(FilterTypeExtension.EXCLUDE, FilterType.HAS)
}
// Patch updateQuery to either include or exclude our has option
patcher.instead<HasNode>(
"updateQuery",
SearchQuery.Builder::class.java,
SearchData::class.java,
FilterType::class.java,
) { param ->
val builder = param.args[0] as SearchQuery.Builder?
val filterType = param.args[2] as FilterType
checkNotNull(builder) { "queryBuilder" }
val field = HasNode::class.java.getDeclaredField("hasAnswerOption")
field.isAccessible = true
val opt = field.get(this) as HasAnswerOption
if (filterType == FilterType.HAS)
builder.appendParam("has", opt.restParamValue);
else if (filterType == FilterTypeExtension.EXCLUDE)
builder.appendParam("has", "-" + opt.restParamValue);
}
// Patching the behaviour when the has suggestion is clicked
patcher.before<StoreSearchInput>(
"onHasClicked",
HasAnswerOption::class.java,
CharSequence::class.java,
CharSequence::class.java,
List::class.java,
) { param ->
val opt = param.args[0] as HasAnswerOption
val hasFilterText = param.args[1] as CharSequence
val filterAnswer = param.args[2] as CharSequence
val query = param.args[3] as List<QueryNode>
val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
"replaceAndPublish",
Int::class.javaPrimitiveType!!,
List::class.java,
List::class.java
)
replaceAndPublish.isAccessible = true
val getAnswerReplacementStart = StoreSearchInput::class.java.getDeclaredMethod(
"getAnswerReplacementStart",
List::class.java,
)
getAnswerReplacementStart.isAccessible = true
logger.info(query.joinToString("|") { it.text })
val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int
val previousFilterText = query[replacementIdx]
val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString)
FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString)
else
FilterNode(FilterType.HAS, hasFilterText)
replaceAndPublish.invoke(this, replacementIdx, listOf(filterNode, HasNode(opt, filterAnswer)), query)
}
}
@ -296,6 +383,9 @@ class Scout : Plugin() {
DateNode.getDateRule(),
SortNode.getFilterRule(ssProvider.sortFilterString),
SortNode.getSortRule(ssProvider),
SimpleParserRule(Pattern.compile("^\\s*?${ssProvider.excludeFilterString}:", 64)) { _, _, obj ->
ParseSpec(FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString), obj)
}
))
}
}
@ -357,6 +447,14 @@ class Scout : Plugin() {
listOf(filterNode, SortNode(ssProvider.sortOldString)),
list
);
if (filter == FilterTypeExtension.EXCLUDE)
replaceAndPublish.invoke(this,
lastIndex,
listOf(filterNode),
list
);
param.result = null
}
@ -372,6 +470,8 @@ class Scout : Plugin() {
param.result = ContextCompat.getDrawable(context, ScoutResource.DRAWABLE_IC_CLOCK)
if (type == FilterTypeExtension.SORT)
param.result = ContextCompat.getDrawable(context, ScoutResource.DRAWABLE_IC_SORT_WHITE)
if (type == FilterTypeExtension.EXCLUDE)
param.result = ContextCompat.getDrawable(context, ScoutResource.DRAWABLE_IC_SORT_WHITE)
}
// Patch for retrieving sample filter answer/placeholder
@ -384,6 +484,8 @@ class Scout : Plugin() {
param.result = ssProvider.getIdentifier("search_answer_date")
if (type == FilterTypeExtension.SORT)
param.result = ScoutResource.SORT_ANSWER
if (type == FilterTypeExtension.EXCLUDE)
param.result = ssProvider.getIdentifier("search_answer_has")
}
// Patch for retrieving filter name
@ -393,6 +495,7 @@ class Scout : Plugin() {
) { param ->
val type = param.args[0] as FilterType
val res = when (type) {
FilterTypeExtension.EXCLUDE -> ScoutResource.EXCLUDE_FILTER
FilterTypeExtension.BEFORE -> ssProvider.getIdentifier("search_filter_before")
FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during")
FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after")
@ -417,6 +520,7 @@ class Scout : Plugin() {
val override = when (resID) {
ScoutResource.SORT_FILTER -> ssProvider.sortFilterString
ScoutResource.SORT_ANSWER -> ssProvider.sortOldString
ScoutResource.EXCLUDE_FILTER -> ssProvider.excludeFilterString
else -> null
}
override?.let {

View file

@ -3,6 +3,7 @@ package moe.lava.awoocord.scout.ui
object ScoutResource {
const val SORT_FILTER = 0xfffffff0.toInt()
const val SORT_ANSWER = 0xfffffff1.toInt()
const val EXCLUDE_FILTER = 0xfffffff2.toInt()
const val DRAWABLE_IC_CLOCK = 0x7f0803bb
const val DRAWABLE_IC_SORT_WHITE =0x7f080586
}

View file

@ -16,6 +16,7 @@ class ScoutSearchStringProvider(private val context: Context) {
context.getString(getIdentifier(name))
fun stringFor(type: FilterType) = when (type) {
FilterTypeExtension.EXCLUDE -> excludeFilterString
FilterTypeExtension.BEFORE -> beforeFilterString
FilterTypeExtension.DURING -> duringFilterString
FilterTypeExtension.AFTER -> afterFilterString
@ -46,4 +47,6 @@ class ScoutSearchStringProvider(private val context: Context) {
get() = "poll"
val hasForwardString: String
get() = "forward"
val excludeFilterString: String
get() = "exclude"
}