diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt index 858667f..6c2ba23 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt @@ -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 lateinit var values: Array } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt index ac3e2f6..8b130d8 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt @@ -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( + patcher.instead( "getHasSuggestions", CharSequence::class.java, FilterType::class.java, SearchStringProvider::class.java, ) { param -> val query = param.args[0] as CharSequence - val res = (param.result as List).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() + + val res = mutableListOf() + 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("getValidFilters") { + setOf(FilterTypeExtension.EXCLUDE, FilterType.HAS) + } + + // Patch updateQuery to either include or exclude our has option + patcher.instead( + "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( + "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 + + 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 { diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt index 8700d51..0582780 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt @@ -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 } diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt index ce85b6f..184ced8 100644 --- a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt @@ -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" }