feat(Scout): add authorType filter

whew, the code is getting quite big... A refactor will be coming soon

Some light linting was also done here
This commit is contained in:
Cilly Leang 2026-02-18 03:14:40 +11:00
parent 302ea0094a
commit e4ab9f936d
Signed by: cilly
GPG key ID: 6500251E087653C9
12 changed files with 347 additions and 37 deletions

View file

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

View file

@ -1,8 +1,17 @@
@file:Suppress("EnumValuesSoftDeprecate", "CanConvertToMultiDollarString")
/**
* Hi to anyone who might be reading this; I am sorry for the atrocious code in this plugin
* but I promise I'll be fixing it up soon :3
*/
package moe.lava.awoocord.scout package moe.lava.awoocord.scout
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import com.aliucord.Utils import com.aliucord.Utils
@ -27,6 +36,7 @@ import com.discord.api.channel.Channel
import com.discord.api.channel.ChannelUtils import com.discord.api.channel.ChannelUtils
import com.discord.api.channel.`ChannelUtils$getSortByNameAndType$1` import com.discord.api.channel.`ChannelUtils$getSortByNameAndType$1`
import com.discord.api.permission.Permission import com.discord.api.permission.Permission
import com.discord.databinding.WidgetSearchSuggestionItemHeaderBinding
import com.discord.databinding.WidgetSearchSuggestionsItemHasBinding import com.discord.databinding.WidgetSearchSuggestionsItemHasBinding
import com.discord.databinding.WidgetSearchSuggestionsItemSuggestionBinding import com.discord.databinding.WidgetSearchSuggestionsItemSuggestionBinding
import com.discord.models.member.GuildMember import com.discord.models.member.GuildMember
@ -71,6 +81,10 @@ import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import com.lytefast.flexinput.R import com.lytefast.flexinput.R
import moe.lava.awoocord.scout.api.SearchAPIInterface import moe.lava.awoocord.scout.api.SearchAPIInterface
import moe.lava.awoocord.scout.entries.AuthorTypeSuggestion
import moe.lava.awoocord.scout.entries.AuthorTypeViewHolder
import moe.lava.awoocord.scout.parsing.AuthorType
import moe.lava.awoocord.scout.parsing.AuthorTypeNode
import moe.lava.awoocord.scout.parsing.DateNode import moe.lava.awoocord.scout.parsing.DateNode
import moe.lava.awoocord.scout.parsing.SimpleParserRule import moe.lava.awoocord.scout.parsing.SimpleParserRule
import moe.lava.awoocord.scout.parsing.SortNode import moe.lava.awoocord.scout.parsing.SortNode
@ -84,7 +98,10 @@ import b.a.k.b as FormatUtils
private val WidgetSearchSuggestionsAdapter.FilterViewHolder.binding private val WidgetSearchSuggestionsAdapter.FilterViewHolder.binding
by accessField<WidgetSearchSuggestionsItemSuggestionBinding>() by accessField<WidgetSearchSuggestionsItemSuggestionBinding>()
@AliucordPlugin() private val WidgetSearchSuggestionsAdapter.HeaderViewHolder.binding
by accessField<WidgetSearchSuggestionItemHeaderBinding>()
@AliucordPlugin
@Suppress("unused", "unchecked_cast") @Suppress("unused", "unchecked_cast")
class Scout : Plugin() { class Scout : Plugin() {
lateinit var scoutRes: ScoutResource lateinit var scoutRes: ScoutResource
@ -107,6 +124,7 @@ class Scout : Plugin() {
override fun start(context: Context) { override fun start(context: Context) {
extendFilterType() extendFilterType()
extendHasAnswerOption() extendHasAnswerOption()
extendSuggestionCategory()
fixFiltersKeying() fixFiltersKeying()
fixHasFilterSuggestion() fixHasFilterSuggestion()
fixSearchPadding() fixSearchPadding()
@ -120,9 +138,10 @@ class Scout : Plugin() {
} }
override fun stop(context: Context) { override fun stop(context: Context) {
patcher.unpatchAll()
resetFilterType() resetFilterType()
resetHasAnswerOption() resetHasAnswerOption()
patcher.unpatchAll() resetSuggestionCategory()
} }
// Creates a new custom search API implementation, for the extra `min_id` param in search queries // Creates a new custom search API implementation, for the extra `min_id` param in search queries
@ -148,7 +167,7 @@ class Scout : Plugin() {
private var origFilterTypes: Array<FilterType>? = null private var origFilterTypes: Array<FilterType>? = null
// Creates new pseudo-values of the `FilterType` enum for date filters // Creates new pseudo-values of the `FilterType` enum for date filters
@Suppress("LocalVariableName") @Suppress("LocalVariableName", "AssignedValueIsNeverRead")
private fun extendFilterType() { private fun extendFilterType() {
val cls = FilterType::class.java val cls = FilterType::class.java
val constructor = cls.declaredConstructors[0] val constructor = cls.declaredConstructors[0]
@ -163,18 +182,20 @@ class Scout : Plugin() {
val EXPAND = constructor.newInstance("EXPAND", nextIdx++) as FilterType val EXPAND = constructor.newInstance("EXPAND", nextIdx++) as FilterType
val SORT = constructor.newInstance("SORT", nextIdx++) as FilterType val SORT = constructor.newInstance("SORT", nextIdx++) as FilterType
val EXCLUDE = constructor.newInstance("EXCLUDE", nextIdx++) as FilterType val EXCLUDE = constructor.newInstance("EXCLUDE", nextIdx++) as FilterType
val AUTHOR_TYPE = constructor.newInstance("AUTHOR_TYPE", nextIdx++) as FilterType
val BEFORE = constructor.newInstance("BEFORE", nextIdx++) as FilterType val BEFORE = constructor.newInstance("BEFORE", nextIdx++) as FilterType
val DURING = constructor.newInstance("DURING", nextIdx++) as FilterType val DURING = constructor.newInstance("DURING", nextIdx++) as FilterType
val AFTER = constructor.newInstance("AFTER", nextIdx++) as FilterType val AFTER = constructor.newInstance("AFTER", nextIdx++) as FilterType
FilterTypeExtension.EXPAND = EXPAND FilterTypeExtension.EXPAND = EXPAND
FilterTypeExtension.SORT = SORT FilterTypeExtension.SORT = SORT
FilterTypeExtension.EXCLUDE = EXCLUDE FilterTypeExtension.EXCLUDE = EXCLUDE
FilterTypeExtension.AUTHOR_TYPE = AUTHOR_TYPE
FilterTypeExtension.BEFORE = BEFORE FilterTypeExtension.BEFORE = BEFORE
FilterTypeExtension.DURING = DURING FilterTypeExtension.DURING = DURING
FilterTypeExtension.AFTER = AFTER FilterTypeExtension.AFTER = AFTER
FilterTypeExtension.dates = arrayOf(BEFORE, DURING, AFTER) FilterTypeExtension.dates = arrayOf(BEFORE, DURING, AFTER)
FilterTypeExtension.values = arrayOf(EXPAND, SORT, EXCLUDE, BEFORE, DURING, AFTER) FilterTypeExtension.filters = arrayOf(SORT, AUTHOR_TYPE, EXCLUDE) + FilterTypeExtension.dates
FilterTypeExtension.filters = arrayOf(SORT, EXCLUDE, BEFORE, DURING, AFTER) FilterTypeExtension.values = arrayOf(EXPAND) + FilterTypeExtension.filters
val newValues = values.toMutableList() val newValues = values.toMutableList()
newValues.addAll(FilterTypeExtension.values) newValues.addAll(FilterTypeExtension.values)
@ -194,7 +215,7 @@ class Scout : Plugin() {
private var origHasAnswerOptions: Array<HasAnswerOption>? = null private var origHasAnswerOptions: Array<HasAnswerOption>? = null
// Creates new pseudo-values of the `HasAnswerOption` enum for poll and forwarded filters // Creates new pseudo-values of the `HasAnswerOption` enum for poll and forwarded filters
@Suppress("LocalVariableName") @Suppress("LocalVariableName", "AssignedValueIsNeverRead")
private fun extendHasAnswerOption() { private fun extendHasAnswerOption() {
val cls = HasAnswerOption::class.java val cls = HasAnswerOption::class.java
val constructor = cls.declaredConstructors[0] val constructor = cls.declaredConstructors[0]
@ -207,7 +228,7 @@ class Scout : Plugin() {
var nextIdx = values.size var nextIdx = values.size
val POLL = constructor.newInstance("POLL", nextIdx++, "poll") as HasAnswerOption val POLL = constructor.newInstance("POLL", nextIdx++, "poll") as HasAnswerOption
val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx, "snapshot") as HasAnswerOption val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx++, "snapshot") as HasAnswerOption
HasAnswerOptionExtension.POLL = POLL HasAnswerOptionExtension.POLL = POLL
HasAnswerOptionExtension.SNAPSHOT = SNAPSHOT HasAnswerOptionExtension.SNAPSHOT = SNAPSHOT
HasAnswerOptionExtension.values = arrayOf(POLL, SNAPSHOT) HasAnswerOptionExtension.values = arrayOf(POLL, SNAPSHOT)
@ -228,6 +249,40 @@ class Scout : Plugin() {
origHasAnswerOptions = null origHasAnswerOptions = null
} }
private var origSuggestionCategories: Array<SearchSuggestion.Category>? = null
// Creates new pseudo-values of the suggestion categories to add correct headers
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
private fun extendSuggestionCategory() {
val cls = SearchSuggestion.Category::class.java
val constructor = cls.declaredConstructors[0]
constructor.isAccessible = true
val field = cls.getDeclaredField("\$VALUES")
field.isAccessible = true
val values = field.get(null) as Array<SearchSuggestion.Category>
origSuggestionCategories = origSuggestionCategories ?: values
var nextIdx = values.size
val AUTHOR_TYPE = constructor.newInstance("AUTHOR_TYPE", nextIdx++) as SearchSuggestion.Category
SuggestionCategoryExtension.AUTHOR_TYPE = AUTHOR_TYPE
SuggestionCategoryExtension.values = arrayOf(AUTHOR_TYPE)
val newValues = values.toMutableList()
newValues.addAll(SuggestionCategoryExtension.values)
field.set(null, newValues.toTypedArray())
}
private fun resetSuggestionCategory() {
if (origSuggestionCategories == null)
return logger.error("No unpatched suggestion categories?", null)
val cls = SearchSuggestion.Category::class.java
val field = cls.getDeclaredField("\$VALUES")
field.isAccessible = true
field.set(null, origSuggestionCategories)
origSuggestionCategories = null
}
// Patch to key filters properly for smoother recycling // Patch to key filters properly for smoother recycling
// Thank u discord for keying every filter type the same thing!! /s // Thank u discord for keying every filter type the same thing!! /s
private fun fixFiltersKeying() { private fun fixFiltersKeying() {
@ -341,24 +396,22 @@ class Scout : Plugin() {
CharSequence::class.java, CharSequence::class.java,
FilterType::class.java, FilterType::class.java,
SearchStringProvider::class.java, SearchStringProvider::class.java,
) { param -> ) { (_, query: CharSequence, type: FilterType, provider: SearchStringProvider) ->
val query = param.args[0] as CharSequence // Generate entries for author type
val filterType = param.args[1] as FilterType if (type == FilterTypeExtension.AUTHOR_TYPE) {
val ossProvider = param.args[2] as SearchStringProvider return@instead AuthorType.values()
.filter { it.value.contains(query) }
if (filterType != FilterType.HAS && filterType != FilterTypeExtension.EXCLUDE) .map { AuthorTypeSuggestion(it) }
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))
} }
res.toList()
}
// Generate entries for has options, including new ones
if (type == FilterType.HAS || type == FilterTypeExtension.EXCLUDE)
return@instead HasAnswerOption.values()
.filter { it.getLocalizedInputText(provider).contains(query) }
.map { HasSuggestion(it) }
listOf<Any>()
}
} }
// Patching HasNode related methods for our exclude: filter type // Patching HasNode related methods for our exclude: filter type
@ -384,9 +437,9 @@ class Scout : Plugin() {
val opt = field.get(this) as HasAnswerOption val opt = field.get(this) as HasAnswerOption
if (filterType == FilterType.HAS) if (filterType == FilterType.HAS)
builder.appendParam("has", opt.restParamValue); builder.appendParam("has", opt.restParamValue)
else if (filterType == FilterTypeExtension.EXCLUDE) else if (filterType == FilterTypeExtension.EXCLUDE)
builder.appendParam("has", "-" + opt.restParamValue); builder.appendParam("has", "-" + opt.restParamValue)
} }
// Patching the behaviour when the has suggestion is clicked // Patching the behaviour when the has suggestion is clicked
@ -416,8 +469,6 @@ class Scout : Plugin() {
) )
getAnswerReplacementStart.isAccessible = true getAnswerReplacementStart.isAccessible = true
logger.info(query.joinToString("|") { it.text })
val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int
val previousFilterText = query[replacementIdx] val previousFilterText = query[replacementIdx]
val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString) val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString)
@ -441,6 +492,7 @@ class Scout : Plugin() {
var minID = params["min_id"] var minID = params["min_id"]
var maxID = params["max_id"] var maxID = params["max_id"]
val sortOrder = params["sort_order"] val sortOrder = params["sort_order"]
val authorType = params["author_type"]
self.`$oldestMessageId`?.let { self.`$oldestMessageId`?.let {
if (sortOrder?.getOrNull(0) == "asc") if (sortOrder?.getOrNull(0) == "asc")
minID = listOf(it.toString()) minID = listOf(it.toString())
@ -461,7 +513,8 @@ class Scout : Plugin() {
retryAttempts, retryAttempts,
self.`$searchQuery`.includeNsfw, self.`$searchQuery`.includeNsfw,
listOf("timestamp"), listOf("timestamp"),
sortOrder sortOrder,
authorType,
) )
else else
searchApi.searchChannelMessages( searchApi.searchChannelMessages(
@ -475,7 +528,8 @@ class Scout : Plugin() {
retryAttempts, retryAttempts,
self.`$searchQuery`.includeNsfw, self.`$searchQuery`.includeNsfw,
listOf("timestamp"), listOf("timestamp"),
sortOrder sortOrder,
authorType,
) )
} }
) )
@ -495,6 +549,8 @@ class Scout : Plugin() {
DateNode.getDateRule(), DateNode.getDateRule(),
SortNode.getFilterRule(ssProvider.sortFilterString), SortNode.getFilterRule(ssProvider.sortFilterString),
SortNode.getSortRule(ssProvider), SortNode.getSortRule(ssProvider),
AuthorTypeNode.getFilterRule(ssProvider.authorTypeFilter),
AuthorTypeNode.getAuthorTypesRule(),
SimpleParserRule(Pattern.compile("^\\s*?${ssProvider.excludeFilterString}:", 64)) { _, _, obj -> SimpleParserRule(Pattern.compile("^\\s*?${ssProvider.excludeFilterString}:", 64)) { _, _, obj ->
ParseSpec(FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString), obj) ParseSpec(FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString), obj)
} }
@ -503,6 +559,7 @@ class Scout : Plugin() {
} }
// This is probably the worst bit of this plugin // This is probably the worst bit of this plugin
@SuppressLint("SetTextI18n")
private fun patchSearchUI(context: Context) { private fun patchSearchUI(context: Context) {
// Run when a filter suggestion is clicked // Run when a filter suggestion is clicked
// Most of the code is copied from its implementation // Most of the code is copied from its implementation
@ -515,7 +572,7 @@ class Scout : Plugin() {
) { param -> ) { param ->
val filter = param.args[0] as FilterType val filter = param.args[0] as FilterType
if (filter !in FilterTypeExtension.values) if (filter !in FilterTypeExtension.values)
return@before; // Exit if not an extended filter type return@before // Exit if not an extended filter type
val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod( val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
"replaceAndPublish", "replaceAndPublish",
@ -549,7 +606,7 @@ class Scout : Plugin() {
getAnswerReplacementStart.invoke(this, list), getAnswerReplacementStart.invoke(this, list),
listOf(filterNode, DateNode(it)), listOf(filterNode, DateNode(it)),
list list
); )
} }
} }
@ -558,14 +615,21 @@ class Scout : Plugin() {
lastIndex, lastIndex,
listOf(filterNode, SortNode(ssProvider.sortOldString)), listOf(filterNode, SortNode(ssProvider.sortOldString)),
list list
); )
if (filter == FilterTypeExtension.EXCLUDE) if (filter == FilterTypeExtension.EXCLUDE)
replaceAndPublish.invoke(this, replaceAndPublish.invoke(this,
lastIndex, lastIndex,
listOf(filterNode), listOf(filterNode),
list list
); )
if (filter == FilterTypeExtension.AUTHOR_TYPE)
replaceAndPublish.invoke(this,
lastIndex,
listOf(filterNode),
list
)
param.result = null param.result = null
} }
@ -584,6 +648,7 @@ class Scout : Plugin() {
FilterTypeExtension.AFTER -> false to scoutRes.getDrawableId("baseline_update_24") FilterTypeExtension.AFTER -> false to scoutRes.getDrawableId("baseline_update_24")
FilterTypeExtension.SORT -> true to R.e.ic_sort_white_24dp FilterTypeExtension.SORT -> true to R.e.ic_sort_white_24dp
FilterTypeExtension.EXCLUDE -> false to scoutRes.getDrawableId("baseline_do_disturb_on_24") FilterTypeExtension.EXCLUDE -> false to scoutRes.getDrawableId("baseline_do_disturb_on_24")
FilterTypeExtension.AUTHOR_TYPE -> true to R.e.ic_members_24dp
else -> false to null else -> false to null
} }
@ -605,6 +670,8 @@ class Scout : Plugin() {
param.result = ScoutResource.SORT_ANSWER param.result = ScoutResource.SORT_ANSWER
if (type == FilterTypeExtension.EXCLUDE) if (type == FilterTypeExtension.EXCLUDE)
param.result = ssProvider.getIdentifier("search_answer_has") param.result = ssProvider.getIdentifier("search_answer_has")
if (type == FilterTypeExtension.AUTHOR_TYPE)
param.result = ScoutResource.AUTHOR_TYPE_ANSWER
} }
// Patch for retrieving filter name // Patch for retrieving filter name
@ -619,6 +686,7 @@ class Scout : Plugin() {
FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during") FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during")
FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after") FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after")
FilterTypeExtension.SORT -> ScoutResource.SORT_FILTER FilterTypeExtension.SORT -> ScoutResource.SORT_FILTER
FilterTypeExtension.AUTHOR_TYPE -> ScoutResource.AUTHOR_TYPE_FILTER
else -> null else -> null
} }
res?.let { param.result = it } res?.let { param.result = it }
@ -641,6 +709,8 @@ class Scout : Plugin() {
ScoutResource.SORT_FILTER -> ssProvider.sortFilterString ScoutResource.SORT_FILTER -> ssProvider.sortFilterString
ScoutResource.SORT_ANSWER -> ssProvider.sortOldString ScoutResource.SORT_ANSWER -> ssProvider.sortOldString
ScoutResource.EXCLUDE_FILTER -> ssProvider.excludeFilterString ScoutResource.EXCLUDE_FILTER -> ssProvider.excludeFilterString
ScoutResource.AUTHOR_TYPE_FILTER -> ssProvider.authorTypeFilter
ScoutResource.AUTHOR_TYPE_ANSWER -> ssProvider.authorTypeAnswer
else -> null else -> null
} }
override?.let { override?.let {
@ -700,6 +770,56 @@ class Scout : Plugin() {
} }
param.result = res.toList() param.result = res.toList()
} }
// Patch to add header for new categories
patcher.before<WidgetSearchSuggestionsAdapter.HeaderViewHolder>(
"onConfigure",
Int::class.javaPrimitiveType!!,
MGRecyclerDataPayload::class.java,
) { (param, _: Int, payload: SingleTypePayload<SearchSuggestion.Category>) ->
val category = payload.data
if (category == SuggestionCategoryExtension.AUTHOR_TYPE) {
binding.b.text = "Author Type"
param.result = null
}
}
// Patch to add entries depending on category
patcher.after<WidgetSearchSuggestions.Model>(
List::class.java,
List::class.java,
) { (_, _: List<QueryNode>, suggestions: List<SearchSuggestion>) ->
var lastCategory: SearchSuggestion.Category? = null
val newItems = mutableListOf<MGRecyclerDataPayload>()
suggestions.forEach {
if (it is AuthorTypeSuggestion) {
if (lastCategory != it.category) {
newItems.add(
SingleTypePayload(it.category, it.category.name, 0)
)
lastCategory = it.category
}
newItems.add(
SingleTypePayload(it, it.type.value, SuggestionCategoryExtension.AdapterType.AUTHOR_TYPE)
)
}
}
suggestionItems.removeAll { it in newItems }
suggestionItems.addAll(0, newItems)
}
// Patch to add new types of suggestion entries
patcher.before<WidgetSearchSuggestionsAdapter>(
"onCreateViewHolder",
ViewGroup::class.java,
Int::class.javaPrimitiveType!!,
) { (param, _: ViewGroup, id: Int) ->
when (id) {
SuggestionCategoryExtension.AdapterType.AUTHOR_TYPE -> {
param.result = AuthorTypeViewHolder(this, scoutRes)
}
}
}
} }
// Adds support for searching in threads // Adds support for searching in threads
@ -707,7 +827,7 @@ class Scout : Plugin() {
// Patch query parser for in: to support names with spaces, by wrapping them in quotes // Patch query parser for in: to support names with spaces, by wrapping them in quotes
// This enables searching for threads which can have spaces in their names // This enables searching for threads which can have spaces in their names
patcher.instead<QueryParser.Companion>("getInAnswerRule") { patcher.instead<QueryParser.Companion>("getInAnswerRule") {
val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64); val compile = Pattern.compile("^\\s*#(\".*?\"|[^ ]+)", 64)
`QueryParser$Companion$getInAnswerRule$1`(compile, compile) `QueryParser$Companion$getInAnswerRule$1`(compile, compile)
} }
@ -802,7 +922,7 @@ class Scout : Plugin() {
// Now it matches something like @<username>[#<discrim>] (bots still have discriminators) // Now it matches something like @<username>[#<discrim>] (bots still have discriminators)
// The @ is required unfortunately, to distinguish it from literally any other word // The @ is required unfortunately, to distinguish it from literally any other word
patcher.instead<QueryParser.Companion>("getUserRule") { patcher.instead<QueryParser.Companion>("getUserRule") {
val regex = Pattern.compile("^\\s*@(?:([^@#:]+)#([0-9]{4})|([a-z0-9._]{2,32}))", 64); val regex = Pattern.compile("^\\s*@(?:([^@#:]+)#([0-9]{4})|([a-z0-9._]{2,32}))", 64)
// Returns a new rule to support our optional second group (discriminator) // Returns a new rule to support our optional second group (discriminator)
return@instead SimpleParserRule(regex) { matcher, _, obj -> return@instead SimpleParserRule(regex) { matcher, _, obj ->

View file

@ -0,0 +1,12 @@
package moe.lava.awoocord.scout
import com.discord.utilities.search.suggestion.entries.SearchSuggestion
object SuggestionCategoryExtension {
lateinit var AUTHOR_TYPE: SearchSuggestion.Category
lateinit var values: Array<SearchSuggestion.Category>
object AdapterType {
const val AUTHOR_TYPE = 7
}
}

View file

@ -24,6 +24,7 @@ interface SearchAPIInterface {
@t("include_nsfw") includeNsfw: Boolean?, @t("include_nsfw") includeNsfw: Boolean?,
@t("sort_by") sortBy: List<String>?, // "timestamp" is one, not sure about any other sort types @t("sort_by") sortBy: List<String>?, // "timestamp" is one, not sure about any other sort types
@t("sort_order") sortOrder: List<String>?, // "asc" or "desc" @t("sort_order") sortOrder: List<String>?, // "asc" or "desc"
@t("author_type") authorType: List<String>?,
): Observable<ModelSearchResponse?> ): Observable<ModelSearchResponse?>
@f("guilds/{guildId}/messages/search") @f("guilds/{guildId}/messages/search")
@ -40,5 +41,6 @@ interface SearchAPIInterface {
@t("include_nsfw") includeNsfw: Boolean?, @t("include_nsfw") includeNsfw: Boolean?,
@t("sort_by") sortBy: List<String>?, @t("sort_by") sortBy: List<String>?,
@t("sort_order") sortOrder: List<String>?, @t("sort_order") sortOrder: List<String>?,
@t("author_type") authorType: List<String>?,
): Observable<ModelSearchResponse?> ): Observable<ModelSearchResponse?>
} }

View file

@ -0,0 +1,9 @@
package moe.lava.awoocord.scout.entries
import com.discord.utilities.search.suggestion.entries.SearchSuggestion
import moe.lava.awoocord.scout.SuggestionCategoryExtension
import moe.lava.awoocord.scout.parsing.AuthorType
data class AuthorTypeSuggestion(val type: AuthorType) : SearchSuggestion {
override fun getCategory() = SuggestionCategoryExtension.AUTHOR_TYPE
}

View file

@ -0,0 +1,77 @@
package moe.lava.awoocord.scout.entries
import android.widget.ImageView
import android.widget.TextView
import com.aliucord.Utils
import com.aliucord.utils.ViewUtils.findViewById
import com.discord.stores.StoreSearchInput
import com.discord.stores.StoreStream
import com.discord.utilities.mg_recycler.MGRecyclerDataPayload
import com.discord.utilities.mg_recycler.MGRecyclerViewHolder
import com.discord.utilities.mg_recycler.SingleTypePayload
import com.discord.utilities.search.query.node.filter.FilterNode
import com.discord.widgets.search.suggestions.`WidgetSearchSuggestions$configureUI$4`
import com.discord.widgets.search.suggestions.WidgetSearchSuggestionsAdapter
import com.lytefast.flexinput.R
import moe.lava.awoocord.scout.FilterTypeExtension
import moe.lava.awoocord.scout.parsing.AuthorType
import moe.lava.awoocord.scout.parsing.AuthorTypeNode
import moe.lava.awoocord.scout.ui.ScoutResource
private val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
"replaceAndPublish",
Int::class.javaPrimitiveType!!,
List::class.java,
List::class.java
).apply { isAccessible = true }
private val getAnswerReplacementStart = StoreSearchInput::class.java.getDeclaredMethod(
"getAnswerReplacementStart",
List::class.java,
).apply { isAccessible = true }
class AuthorTypeViewHolder(
adapter: WidgetSearchSuggestionsAdapter,
// This should be fine (?)
private val scoutRes: ScoutResource,
) : MGRecyclerViewHolder<WidgetSearchSuggestionsAdapter, MGRecyclerDataPayload>(
Utils.getResId("widget_search_suggestions_item_has", "layout"),
adapter,
) {
private val imageView = itemView.findViewById<ImageView>("search_suggestions_item_has_icon")
private val textView = itemView.findViewById<TextView>("search_suggestions_item_has_text")
override fun onConfigure(i: Int, oPayload: MGRecyclerDataPayload) {
super.onConfigure(i, oPayload)
@Suppress("UNCHECKED_CAST")
val payload = oPayload as SingleTypePayload<AuthorTypeSuggestion>
val type = payload.data.type
textView.text = when (type) {
AuthorType.Bot -> "bot"
AuthorType.User -> "user"
AuthorType.Webhook -> "webhook"
}
when (type) {
AuthorType.Bot -> imageView.setImageDrawable(scoutRes.getDrawable("smart_toy_24px"))
AuthorType.User -> imageView.setImageResource(R.e.ic_members_24dp)
AuthorType.Webhook -> imageView.setImageDrawable(scoutRes.getDrawable("webhook_24px"))
}
itemView.setOnClickListener {
val hasHandler = adapter.onHasClicked as `WidgetSearchSuggestions$configureUI$4`
val query = hasHandler.`$model`.query
val storeInput = StoreStream.getSearch().storeSearchInput
replaceAndPublish.invoke(
storeInput,
getAnswerReplacementStart.invoke(storeInput, query) as Int,
listOf(
FilterNode(FilterTypeExtension.AUTHOR_TYPE, "authorType"),
AuthorTypeNode(type)
),
query,
)
}
}
}

View file

@ -0,0 +1,62 @@
package moe.lava.awoocord.scout.parsing
import android.content.Context
import com.discord.simpleast.core.parser.ParseSpec
import com.discord.simpleast.core.parser.Rule
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.AnswerNode
import com.discord.utilities.search.query.node.filter.FilterNode
import com.discord.utilities.search.validation.SearchData
import moe.lava.awoocord.scout.FilterTypeExtension
import java.util.regex.Pattern
// TODO: not localised, maybe one day
enum class AuthorType(val value: String) {
User("user"),
Bot("bot"),
Webhook("webhook"),
;
companion object {
fun from(value: String) = when (value) {
"user" -> User
"bot" -> Bot
"webhook" -> Webhook
else -> throw IllegalArgumentException("Unknown author type $value")
}
}
}
class AuthorTypeNode(val type: AuthorType): AnswerNode() {
companion object {
fun getAuthorTypesRule(): Rule<Context, QueryNode, Any> {
val joined = AuthorType.values().joinToString("|") { it.value }
val regexStr = "^\\s*(${joined})"
val regex = Pattern.compile(regexStr, Pattern.UNICODE_CASE)
return SimpleParserRule(regex) { matcher, _, obj ->
ParseSpec(AuthorTypeNode(AuthorType.from(matcher.group())), obj)
}
}
fun getFilterRule(str: String): ParserRule {
val regex = Pattern.compile("^\\s*?(${str}):", 64);
return SimpleParserRule(regex) { _, _, obj ->
ParseSpec(FilterNode(FilterTypeExtension.AUTHOR_TYPE, str), obj)
}
}
}
override fun getValidFilters() = setOf(FilterTypeExtension.AUTHOR_TYPE)
override fun isValid(searchData: SearchData?) = true
override fun getText() = type.value
override fun updateQuery(
builder: SearchQuery.Builder,
searchData: SearchData?,
filterType: FilterType?
) {
builder.appendParam("author_type", type.value)
}
}

View file

@ -36,11 +36,10 @@ class SortNode(private val text: String): AnswerNode() {
override fun getText() = this.text override fun getText() = this.text
override fun updateQuery( override fun updateQuery(
builder: SearchQuery.Builder?, builder: SearchQuery.Builder,
searchData: SearchData?, searchData: SearchData?,
filterType: FilterType? filterType: FilterType?
) { ) {
checkNotNull(builder) { "queryBuilder" }
builder.appendParam("sort_order", "asc") builder.appendParam("sort_order", "asc")
} }
} }

View file

@ -10,6 +10,8 @@ class ScoutResource(private val resources: Resources) {
val SORT_FILTER = View.generateViewId() val SORT_FILTER = View.generateViewId()
val SORT_ANSWER = View.generateViewId() val SORT_ANSWER = View.generateViewId()
val EXCLUDE_FILTER = View.generateViewId() val EXCLUDE_FILTER = View.generateViewId()
val AUTHOR_TYPE_FILTER = View.generateViewId()
val AUTHOR_TYPE_ANSWER = View.generateViewId()
} }
fun getId(name: String, type: String) = fun getId(name: String, type: String) =

View file

@ -21,6 +21,7 @@ class ScoutSearchStringProvider(private val context: Context) {
FilterTypeExtension.DURING -> duringFilterString FilterTypeExtension.DURING -> duringFilterString
FilterTypeExtension.AFTER -> afterFilterString FilterTypeExtension.AFTER -> afterFilterString
FilterTypeExtension.SORT -> sortFilterString FilterTypeExtension.SORT -> sortFilterString
FilterTypeExtension.AUTHOR_TYPE -> authorTypeFilter
else -> throw IllegalArgumentException("invalid extended filter type") else -> throw IllegalArgumentException("invalid extended filter type")
} }
@ -51,4 +52,9 @@ class ScoutSearchStringProvider(private val context: Context) {
get() = "forward" get() = "forward"
val excludeFilterString: String val excludeFilterString: String
get() = "exclude" get() = "exclude"
val authorTypeFilter: String
get() = "authorType"
val authorTypeAnswer: String
// TODO, could probably be localisable by joining each part together
get() = "user, bot or webhook"
} }

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M160,600Q110,600 75,565Q40,530 40,480Q40,430 75,395Q110,360 160,360L160,280Q160,247 183.5,223.5Q207,200 240,200L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L720,200Q753,200 776.5,223.5Q800,247 800,280L800,360Q850,360 885,395Q920,430 920,480Q920,530 885,565Q850,600 800,600L800,760Q800,793 776.5,816.5Q753,840 720,840L240,840Q207,840 183.5,816.5Q160,793 160,760L160,600ZM402.5,502.5Q420,485 420,460Q420,435 402.5,417.5Q385,400 360,400Q335,400 317.5,417.5Q300,435 300,460Q300,485 317.5,502.5Q335,520 360,520Q385,520 402.5,502.5ZM642.5,502.5Q660,485 660,460Q660,435 642.5,417.5Q625,400 600,400Q575,400 557.5,417.5Q540,435 540,460Q540,485 557.5,502.5Q575,520 600,520Q625,520 642.5,502.5ZM320,680L640,680L640,600L320,600L320,680ZM240,760L720,760Q720,760 720,760Q720,760 720,760L720,280Q720,280 720,280Q720,280 720,280L240,280Q240,280 240,280Q240,280 240,280L240,760Q240,760 240,760Q240,760 240,760ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M280,840Q197,840 138.5,781.5Q80,723 80,640Q80,567 125.5,512.5Q171,458 240,444L240,527Q205,539 182.5,570Q160,601 160,640Q160,690 195,725Q230,760 280,760Q330,760 365,725Q400,690 400,640L400,600L635,600Q643,591 654.5,585.5Q666,580 680,580Q705,580 722.5,597.5Q740,615 740,640Q740,665 722.5,682.5Q705,700 680,700Q666,700 654.5,694.5Q643,689 635,680L476,680Q462,749 407.5,794.5Q353,840 280,840ZM680,840Q624,840 578.5,812.5Q533,785 507,740L614,740Q628,750 645,755Q662,760 680,760Q730,760 765,725Q800,690 800,640Q800,590 765,555Q730,520 680,520Q660,520 643,525.5Q626,531 611,542L489,339Q468,335 454,319Q440,303 440,280Q440,255 457.5,237.5Q475,220 500,220Q525,220 542.5,237.5Q560,255 560,280Q560,285 560,288.5Q560,292 558,297L645,443Q653,441 662,440.5Q671,440 680,440Q763,440 821.5,498.5Q880,557 880,640Q880,723 821.5,781.5Q763,840 680,840ZM280,700Q255,700 237.5,682.5Q220,665 220,640Q220,618 234,602Q248,586 268,581L362,425Q333,398 316.5,360.5Q300,323 300,280Q300,197 358.5,138.5Q417,80 500,80Q583,80 641.5,138.5Q700,197 700,280L620,280Q620,230 585,195Q550,160 500,160Q450,160 415,195Q380,230 380,280Q380,323 406,355.5Q432,388 472,397L337,622Q339,627 339.5,631Q340,635 340,640Q340,665 322.5,682.5Q305,700 280,700Z"/>
</vector>