From 92266869482da5e8f3c43508ac4028a56573a215 Mon Sep 17 00:00:00 2001 From: LavaDesu Date: Thu, 29 May 2025 03:12:05 +1000 Subject: [PATCH] feat(Scout): init --- .github/workflows/build.yml | 59 ++++ .gitignore | 12 + LICENCE | 19 + README.md | 8 + build.gradle.kts | 83 +++++ gradle.properties | 19 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 234 ++++++++++++ gradlew.bat | 89 +++++ plugins/Scout/build.gradle.kts | 17 + plugins/Scout/src/main/AndroidManifest.xml | 2 + .../restapi/RequiredHeadersInterceptor.kt | 5 + .../awoocord/scout/FilterTypeExtension.kt | 13 + .../kotlin/moe/lava/awoocord/scout/Scout.kt | 332 ++++++++++++++++++ .../awoocord/scout/api/SearchAPIInterface.kt | 44 +++ .../lava/awoocord/scout/parsing/DateNode.kt | 75 ++++ .../scout/parsing/SimpleParserRule.kt | 29 ++ .../lava/awoocord/scout/parsing/SortNode.kt | 46 +++ .../lava/awoocord/scout/parsing/UserIdNode.kt | 41 +++ .../awoocord/scout/ui/DatePickerFragment.kt | 34 ++ .../lava/awoocord/scout/ui/ScoutResource.kt | 8 + .../scout/ui/ScoutSearchStringProvider.kt | 35 ++ settings.gradle.kts | 14 + 24 files changed, 1224 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 README.md create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100755 gradlew.bat create mode 100644 plugins/Scout/build.gradle.kts create mode 100644 plugins/Scout/src/main/AndroidManifest.xml create mode 100644 plugins/Scout/src/main/kotlin/com/discord/restapi/RequiredHeadersInterceptor.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/DateNode.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SimpleParserRule.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/UserIdNode.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/DatePickerFragment.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt create mode 100644 plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt create mode 100644 settings.gradle.kts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..85317b5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: Build + +# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency +concurrency: + group: "build" + cancel-in-progress: true + +on: + push: + branches: + - main + paths-ignore: + - '*.md' + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@master + with: + path: "src" + + - name: Checkout builds + uses: actions/checkout@master + with: + ref: "builds" + path: "builds" + + - name: Checkout Aliucord + uses: actions/checkout@master + with: + repository: "Aliucord/Aliucord" + path: "repo" + + - name: Setup JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Build Plugins + run: | + cd $GITHUB_WORKSPACE/src + chmod +x gradlew + ./gradlew make generateUpdaterJson + cp **/build/*.zip $GITHUB_WORKSPACE/builds + cp build/updater.json $GITHUB_WORKSPACE/builds + + - name: Push builds + run: | + cd $GITHUB_WORKSPACE/builds + git config --local user.email "actions@github.com" + git config --local user.name "GitHub Actions" + git add . + git commit -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5a8eb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +**/build +/captures +.externalNativeBuild +.cxx +local.properties +/libs diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..4452855 --- /dev/null +++ b/LICENCE @@ -0,0 +1,19 @@ +Copyright (c) 2025 Cilly Leang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b979cb2 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# `Awoocord Plugins` + +## [Scout](https://github.com/LavaDesu/Awoocord/raw/builds/Scout.zip ) + +Reimplemented features from search of other clients: +- Sorting by oldest first +- Filter by date +- Search from user ID diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..93de107 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,83 @@ +import com.aliucord.gradle.AliucordExtension +import com.android.build.gradle.BaseExtension + +buildscript { + repositories { + google() + mavenCentral() + maven("https://maven.aliucord.com/snapshots") + maven("https://jitpack.io") + } + + dependencies { + classpath("com.android.tools.build:gradle:7.0.4") + classpath("com.aliucord:gradle:main-SNAPSHOT") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21") + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven("https://maven.aliucord.com/snapshots") + } +} + +fun Project.aliucord(configuration: AliucordExtension.() -> Unit) = extensions.getByName("aliucord").configuration() + +fun Project.android(configuration: BaseExtension.() -> Unit) = extensions.getByName("android").configuration() + +subprojects { + apply(plugin = "com.android.library") + apply(plugin = "com.aliucord.gradle") + apply(plugin = "kotlin-android") + + aliucord { + author("lavadesu", 368398754077868032L) + updateUrl.set("https://raw.githubusercontent.com/LavaDesu/Awoocord/builds/updater.json") + buildUrl.set("https://raw.githubusercontent.com/LavaDesu/Awoocord/builds/%s.zip") + } + + android { + compileSdkVersion(31) + + defaultConfig { + minSdk = 24 + targetSdk = 31 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + tasks.withType { + kotlinOptions { + jvmTarget = "11" + // Disables some unnecessary features + freeCompilerArgs = freeCompilerArgs + + "-Xno-call-assertions" + + "-Xno-param-assertions" + + "-Xno-receiver-assertions" + } + } + } + + dependencies { + val discord by configurations + val implementation by configurations + + // Stubs for all Discord classes + discord("com.discord:discord:aliucord-SNAPSHOT") + implementation("com.aliucord:Aliucord:main-SNAPSHOT") + + implementation("androidx.appcompat:appcompat:1.4.0") + implementation("com.google.android.material:material:1.4.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.2") + } +} + +task("clean") { + delete(rootProject.buildDir) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..01b80d7 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

Y{ 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Ď>lQ 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..6a68175 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugins/Scout/build.gradle.kts b/plugins/Scout/build.gradle.kts new file mode 100644 index 0000000..ec50e58 --- /dev/null +++ b/plugins/Scout/build.gradle.kts @@ -0,0 +1,17 @@ +version = "1.0.0" +description = "Backported and improved search functionality" + +aliucord { + // Changelog of your plugin + changelog.set(""" + 1.0.0 - Initial release >w< + """.trimIndent()) + + // Add additional authors to this plugin + // author("Name", 0) + // author("Name", 0) + + // Excludes this plugin from the updater, meaning it won't show up for users. + // Set this if the plugin is unfinished + excludeFromUpdaterJson.set(true) +} diff --git a/plugins/Scout/src/main/AndroidManifest.xml b/plugins/Scout/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a45f28d --- /dev/null +++ b/plugins/Scout/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/plugins/Scout/src/main/kotlin/com/discord/restapi/RequiredHeadersInterceptor.kt b/plugins/Scout/src/main/kotlin/com/discord/restapi/RequiredHeadersInterceptor.kt new file mode 100644 index 0000000..2992c63 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/com/discord/restapi/RequiredHeadersInterceptor.kt @@ -0,0 +1,5 @@ +package com.discord.restapi + +// Stub +@Suppress("ClassName") +abstract class `RequiredHeadersInterceptor$HeadersProvider` {} 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 new file mode 100644 index 0000000..858667f --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/FilterTypeExtension.kt @@ -0,0 +1,13 @@ +package moe.lava.awoocord.scout + +import com.discord.utilities.search.query.FilterType + +object FilterTypeExtension { + lateinit var BEFORE: FilterType + lateinit var DURING: FilterType + lateinit var AFTER: FilterType + lateinit var SORT: 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 new file mode 100644 index 0000000..d02f20f --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/Scout.kt @@ -0,0 +1,332 @@ +package moe.lava.awoocord.scout + +import android.content.Context +import android.content.res.Resources +import androidx.core.content.ContextCompat +import com.aliucord.Utils +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.patcher.PreHook +import com.aliucord.patcher.after +import com.aliucord.patcher.before +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.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.query.FilterType +import com.discord.utilities.search.query.node.QueryNode +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 +import com.discord.utilities.search.strings.SearchStringProvider +import com.discord.utilities.search.suggestion.SearchSuggestionEngine +import com.discord.utilities.search.suggestion.entries.FilterSuggestion +import com.discord.utilities.search.suggestion.entries.SearchSuggestion +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.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 + +@AliucordPlugin(requiresRestart = false) +@Suppress("unused", "unchecked_cast") +class Scout : Plugin() { + lateinit var ssProvider: ScoutSearchStringProvider + lateinit var searchApi: SearchAPIInterface + + override fun start(context: Context) { + ssProvider = ScoutSearchStringProvider(context) + searchApi = buildSearchApi(context) + extendFilterType() + patchQueryParser() + patchQuery() + patchSearchUI(context) + } + + override fun stop(context: Context) { + resetFilterType() + patcher.unpatchAll() + } + + // Creates a new custom search API implementation, for the extra `min_id` param in search queries + private fun buildSearchApi(context: Context): SearchAPIInterface { + @Suppress("cast_never_succeeds") + val appHeadersProvider = AppHeadersProvider.INSTANCE as HeadersProvider + val requiredHeadersInterceptor = RequiredHeadersInterceptor(appHeadersProvider) + val persistentCookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context)) + val restAPIBuilder = RestAPIBuilder(BuildConfig.HOST_API, persistentCookieJar) + + return RestAPIBuilder.`build$default`( + restAPIBuilder, + SearchAPIInterface::class.java, + false, + 0L, + listOf(requiredHeadersInterceptor), + "client_base", + false, + null, + 102, + null + ) as SearchAPIInterface + } + + private val origFilterTypes: Array? = null + // Creates new pseudo-values of the `FilterType` enum for date filters + @Suppress("LocalVariableName") + private fun extendFilterType() { + val cls = FilterType::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 + var nextIdx = values.size + + 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.BEFORE = BEFORE + FilterTypeExtension.DURING = DURING + FilterTypeExtension.AFTER = AFTER + FilterTypeExtension.SORT = SORT + FilterTypeExtension.dates = arrayOf(BEFORE, DURING, AFTER) + FilterTypeExtension.values = arrayOf(BEFORE, DURING, AFTER, SORT) + + val newValues = values.toMutableList() + newValues.addAll(FilterTypeExtension.values) + field.set(null, newValues.toTypedArray()) + } + + private fun resetFilterType() { + if (origFilterTypes == null) + return logger.error("No unpatched filter types?", null) + + val cls = FilterType::class.java + val field = cls.getDeclaredField("\$VALUES") + field.isAccessible = true + field.set(null, origFilterTypes) + } + + // Patches the search query to also insert `min_id`, required for searching "after:" and "during:" + private fun patchQuery() { + patcher.patch( + `SearchFetcher$getRestObservable$3`::class.java.getDeclaredMethod("call", Integer::class.java), + PreHook { param -> + val self = param.thisObject as `SearchFetcher$getRestObservable$3`<*, *> + val retryAttempts = param.args[0] as Int? + val params = self.`$searchQuery`.params + val maxID = self.`$oldestMessageId`?.let { listOf(it.toString()) } ?: params["max_id"] + param.result = if (self.`$searchTarget`.type == StoreSearch.SearchTarget.Type.GUILD) + searchApi.searchGuildMessages( + self.`$searchTarget`.id, + params["min_id"], + maxID, + params["author_id"], + params["mentions"], + params["channel_id"], + params["has"], + params["content"], + retryAttempts, + self.`$searchQuery`.includeNsfw, + listOf("timestamp"), + params["sort_order"] + ) + else + searchApi.searchChannelMessages( + self.`$searchTarget`.id, + params["min_id"], + maxID, + params["author_id"], + params["mentions"], + params["has"], + params["content"], + retryAttempts, + self.`$searchQuery`.includeNsfw, + listOf("timestamp"), + params["sort_order"] + ) + } + ) + } + + // Patch parser for date parsing + private fun patchQueryParser() { + patcher.after(SearchStringProvider::class.java) { + // We need to access and insert into the rules before the rest + val field = Parser::class.java.getDeclaredField("rules").apply { isAccessible = true } + val rules = field.get(this) as ArrayList> + rules.addAll(0, listOf( + UserIdNode.getUserIdRule(), + DateNode.getBeforeRule(ssProvider.beforeFilterString), + DateNode.getDuringRule(ssProvider.duringFilterString), + DateNode.getAfterRule(ssProvider.afterFilterString), + DateNode.getDateRule(), + SortNode.getFilterRule(ssProvider.sortFilterString), + SortNode.getSortRule(ssProvider), + )) + } + } + + // This is probably the worst bit of this plugin + private fun patchSearchUI(context: Context) { + // Run when a filter suggestion is clicked + // Most of the code is copied from its implementation + // Patch needed to support the new filter types + patcher.before( + "onFilterClicked", + FilterType::class.java, + SearchStringProvider::class.java, + List::class.java, + ) { param -> + val filter = param.args[0] as FilterType + if (filter !in FilterTypeExtension.values) + return@before; // Exit if not an extended filter type + + 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 + + // Original implementation + val filterNode = FilterNode(filter, ssProvider.stringFor(filter)) + val list = (param.args[2] as List).toMutableList() + val lastIndex = if (list.isEmpty()) { + 0 + } else if (list.last() is ContentNode) + list.lastIndex + else + list.size + + // Open a Date Picker + if (filter in FilterTypeExtension.dates) { + replaceAndPublish.invoke(this, lastIndex, listOf(filterNode), list) + DatePickerFragment.open(Utils.appActivity.supportFragmentManager) { + replaceAndPublish.invoke(this, + getAnswerReplacementStart.invoke(this, list), + listOf(filterNode, DateNode(it)), + list + ); + } + } + + if (filter == FilterTypeExtension.SORT) + replaceAndPublish.invoke(this, + lastIndex, + listOf(filterNode, SortNode(ssProvider.sortOldString)), + list + ); + param.result = null + } + + // Patch to set icons + @Suppress("ResourceType") + patcher.before( + "getIconDrawable", + Context::class.java, + FilterType::class.java + ) { param -> + val type = param.args[1] as FilterType + if (type in FilterTypeExtension.dates) + param.result = ContextCompat.getDrawable(context, ScoutResource.DRAWABLE_IC_CLOCK) + if (type == FilterTypeExtension.SORT) + param.result = ContextCompat.getDrawable(context, ScoutResource.DRAWABLE_IC_SORT_WHITE) + } + + // Patch for retrieving sample filter answer/placeholder + patcher.before( + "getAnswerText", + FilterType::class.java + ) { param -> + val type = param.args[0] as FilterType + if (type in FilterTypeExtension.dates) + param.result = ssProvider.getIdentifier("search_answer_date") + if (type == FilterTypeExtension.SORT) + param.result = ScoutResource.SORT_ANSWER + } + + // Patch for retrieving filter name + patcher.before( + "getFilterText", + FilterType::class.java + ) { param -> + val type = param.args[0] as FilterType + val res = when (type) { + FilterTypeExtension.BEFORE -> ssProvider.getIdentifier("search_filter_before") + FilterTypeExtension.DURING -> ssProvider.getIdentifier("search_filter_during") + FilterTypeExtension.AFTER -> ssProvider.getIdentifier("search_filter_after") + FilterTypeExtension.SORT -> ScoutResource.SORT_FILTER + else -> null + } + res?.let { param.result = it } + } + + // Patch formatting utils to use our custom lowercase strings + // This is called by FilterViewHolder.onConfigure, using the results from getAnswerText and getFilterText + patcher.patch( + b.a.k.b::class.java.getDeclaredMethod("c", + Resources::class.java, + Int::class.javaPrimitiveType!!, + Array::class.java, + Function1::class.java + ), + PreHook { param -> + val resID = param.args[1] as Int + val objArr = param.args[2] as Array<*> + val override = when (resID) { + ScoutResource.SORT_FILTER -> ssProvider.sortFilterString + ScoutResource.SORT_ANSWER -> ssProvider.sortOldString + else -> null + } + override?.let { + // Why invoke? Becuase I can't for the life of me get Function1 to cast properly + param.result = b.a.k.b::class.java.getDeclaredMethod("g", + CharSequence::class.java, + Array::class.java, + Function1::class.java + ).invoke(null, it, objArr.copyOf(), param.args[3]) + } + } + ) + + // Patch to add our new filters into the initial suggestions + patcher.after( + "getFilterSuggestions", + CharSequence::class.java, + SearchStringProvider::class.java, + Boolean::class.javaPrimitiveType!!, + ) { param -> + val query = param.args[0] as CharSequence + val res = (param.result as List).toMutableList() + for (type in FilterTypeExtension.values) { + val st = ssProvider.stringFor(type) + ":" + + if (st.contains(query)) + res.add(FilterSuggestion(type)) + } + param.result = res.toList() + } + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt new file mode 100644 index 0000000..6bbe273 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/api/SearchAPIInterface.kt @@ -0,0 +1,44 @@ +package moe.lava.awoocord.scout.api + +import com.discord.models.domain.ModelSearchResponse +import i0.f0.f +import i0.f0.s +import i0.f0.t +import rx.Observable + +// io.f0.f = retrofit @GET +// io.f0.s = retrofit @Path +// io.f0.t = retrofit @Query + +interface SearchAPIInterface { + @f("channels/{channelId}/messages/search") + fun searchChannelMessages( + @s("channelId") channelId: Long, + @t("min_id") minId: List?, + @t("max_id") maxId: List?, + @t("author_id") authorId: List?, + @t("mentions") mentions: List?, + @t("has") has: List?, + @t("content") content: List?, + @t("attempts") attempts: Int?, + @t("include_nsfw") includeNsfw: Boolean?, + @t("sort_by") sortBy: List?, // "timestamp" is one, not sure about any other sort types + @t("sort_order") sortOrder: List?, // "asc" or "desc" + ): Observable + + @f("guilds/{guildId}/messages/search") + fun searchGuildMessages( + @s("guildId") guildId: Long, + @t("min_id") minId: List?, + @t("max_id") maxId: List?, + @t("author_id") authorId: List?, + @t("mentions") mentions: List?, + @t("channel_id") channelId: List?, + @t("has") has: List?, + @t("content") content: List?, + @t("attempts") attempts: Int?, + @t("include_nsfw") includeNsfw: Boolean?, + @t("sort_by") sortBy: List?, + @t("sort_order") sortOrder: List?, + ): Observable +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/DateNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/DateNode.kt new file mode 100644 index 0000000..d0ffa02 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/DateNode.kt @@ -0,0 +1,75 @@ +package moe.lava.awoocord.scout.parsing + +import com.discord.simpleast.core.parser.ParseSpec +import com.discord.utilities.SnowflakeUtils +import com.discord.utilities.search.network.SearchQuery +import com.discord.utilities.search.query.FilterType +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.text.SimpleDateFormat +import java.util.Locale +import java.util.regex.Pattern + +class DateNode(private val date: Long?, private val unparsed: String) : AnswerNode() { + + constructor(unparsed: String) : this(fmt.parse(unparsed)?.time, unparsed) + + companion object { + val fmt = SimpleDateFormat("yyyy-MM-dd", Locale.US) + val regex: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}", Pattern.UNICODE_CASE) + fun getDateRule(): ParserRule { + return SimpleParserRule(regex) { matcher, parser, obj -> + checkNotNull(matcher) { "matcher" } + checkNotNull(parser) { "parser" } + val match = matcher.group() + val date = fmt.parse(match) + val node = DateNode(date?.time, match) + ParseSpec(node, obj) + } + } + + private fun getFilterRule(str: String, type: FilterType): ParserRule { + val regex = Pattern.compile("^\\s*?(${str}):", 64); + return SimpleParserRule(regex) { _, _, obj -> + ParseSpec(FilterNode(type, str), obj) + } + } + + fun getBeforeRule(str: String): ParserRule = getFilterRule(str, FilterTypeExtension.BEFORE) + fun getDuringRule(str: String): ParserRule = getFilterRule(str, FilterTypeExtension.DURING) + fun getAfterRule(str: String): ParserRule = getFilterRule(str, FilterTypeExtension.AFTER) + } + + override fun getValidFilters(): Set = FilterTypeExtension.dates.toSet() + override fun isValid(searchData: SearchData?): Boolean = date != null + override fun getText(): CharSequence? = unparsed + + private val snowflake: String? + get() = date?.let { SnowflakeUtils.fromTimestamp(date).toString() } + private val nextDaySnowflake: String? + get() = date?.let { SnowflakeUtils.fromTimestamp(date + 86_400_000).toString() } + + override fun updateQuery( + builder: SearchQuery.Builder?, + searchData: SearchData?, + filterType: FilterType? + ) { + checkNotNull(builder) { "queryBuilder" } + checkNotNull(date) { "date" } + when (filterType) { + FilterTypeExtension.BEFORE -> { + builder.appendParam("max_id", snowflake) + } + FilterTypeExtension.AFTER -> { + builder.appendParam("min_id", nextDaySnowflake) + } + FilterTypeExtension.DURING -> { + builder.appendParam("min_id", snowflake) + builder.appendParam("max_id", nextDaySnowflake) + } + else -> return + } + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SimpleParserRule.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SimpleParserRule.kt new file mode 100644 index 0000000..cc5408f --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SimpleParserRule.kt @@ -0,0 +1,29 @@ +package moe.lava.awoocord.scout.parsing + +import android.content.Context +import com.discord.simpleast.core.parser.ParseSpec +import com.discord.simpleast.core.parser.Parser +import com.discord.simpleast.core.parser.Rule +import com.discord.utilities.search.query.node.QueryNode +import java.util.regex.Matcher +import java.util.regex.Pattern + +internal typealias ParserRule = Rule +internal class SimpleParserRule( + regex: Pattern, + private val parseMethod: ( + matcher: Matcher, + parser: Parser, + obj: Any + ) -> ParseSpec +) : ParserRule(regex) { + override fun parse( + matcher: Matcher?, + parser: Parser, + obj: Any + ): ParseSpec { + checkNotNull(matcher) { "matcher" } + checkNotNull(parser) { "parser" } + return parseMethod(matcher, parser, obj) + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt new file mode 100644 index 0000000..e74f2a9 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/SortNode.kt @@ -0,0 +1,46 @@ +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 moe.lava.awoocord.scout.ui.ScoutSearchStringProvider +import java.util.regex.Pattern + +class SortNode(private val text: String): AnswerNode() { + companion object { + fun getSortRule(ssProvider: ScoutSearchStringProvider): Rule { + val regexStr = "^\\s*(${ssProvider.sortOldString})" + val regex = Pattern.compile(regexStr, Pattern.UNICODE_CASE) + return SimpleParserRule(regex) { _, _, obj -> + ParseSpec(SortNode(ssProvider.sortOldString), obj) + } + } + + fun getFilterRule(str: String): ParserRule { + val regex = Pattern.compile("^\\s*?(${str}):", 64); + return SimpleParserRule(regex) { _, _, obj -> + ParseSpec(FilterNode(FilterTypeExtension.SORT, str), obj) + } + } + } + + override fun getValidFilters() = setOf(FilterTypeExtension.SORT) + override fun isValid(searchData: SearchData?) = true + override fun getText() = this.text + + override fun updateQuery( + builder: SearchQuery.Builder?, + searchData: SearchData?, + filterType: FilterType? + ) { + checkNotNull(builder) { "queryBuilder" } + builder.appendParam("sort_order", "asc") + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/UserIdNode.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/UserIdNode.kt new file mode 100644 index 0000000..a3c88b7 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/parsing/UserIdNode.kt @@ -0,0 +1,41 @@ +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.validation.SearchData +import java.util.regex.Pattern + +class UserIdNode(private val userID: String) : AnswerNode() { + companion object { + fun getUserIdRule(): Rule { + val regex = Pattern.compile("^\\d{17,19}", Pattern.UNICODE_CASE) + return SimpleParserRule(regex) { matcher, _, obj -> + ParseSpec(UserIdNode(matcher.group()), obj) + } + } + } + + override fun getValidFilters() = setOf(FilterType.FROM, FilterType.MENTIONS) + override fun isValid(searchData: SearchData?) = true + override fun getText() = userID.toString() + + override fun updateQuery( + builder: SearchQuery.Builder?, + searchData: SearchData?, + filterType: FilterType? + ) { + checkNotNull(builder) { "queryBuilder" } + checkNotNull(searchData) { "searchData" } + val str = when (filterType) { + FilterType.FROM -> "author_id" + FilterType.MENTIONS -> "mentions" + else -> return + } + builder.appendParam(str, userID) + } +} diff --git a/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/DatePickerFragment.kt b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/DatePickerFragment.kt new file mode 100644 index 0000000..5230c7b --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/DatePickerFragment.kt @@ -0,0 +1,34 @@ +package moe.lava.awoocord.scout.ui + +import android.app.DatePickerDialog +import android.app.Dialog +import android.os.Bundle +import android.widget.DatePicker +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import java.util.Calendar + +class DatePickerFragment( + private val callback: (String) -> Unit +) : DialogFragment(), DatePickerDialog.OnDateSetListener { + companion object { + fun open(fragmentManager: FragmentManager, callback: (date: String) -> Unit) { + DatePickerFragment(callback).show(fragmentManager, "datePicker") + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val c = Calendar.getInstance() + + val year = c.get(Calendar.YEAR) + val month = c.get(Calendar.MONTH) + val day = c.get(Calendar.DAY_OF_MONTH) + + android.app.AlertDialog.THEME_DEVICE_DEFAULT_DARK + return DatePickerDialog(requireContext(), this, year, month, day) + } + + override fun onDateSet(picker: DatePicker, year: Int, month: Int, day: Int) { + callback("%04d-%02d-%02d".format(year, month, day)) + } +} 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 new file mode 100644 index 0000000..8700d51 --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutResource.kt @@ -0,0 +1,8 @@ +package moe.lava.awoocord.scout.ui + +object ScoutResource { + const val SORT_FILTER = 0xfffffff0.toInt() + const val SORT_ANSWER = 0xfffffff1.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 new file mode 100644 index 0000000..ee7ac1b --- /dev/null +++ b/plugins/Scout/src/main/kotlin/moe/lava/awoocord/scout/ui/ScoutSearchStringProvider.kt @@ -0,0 +1,35 @@ +package moe.lava.awoocord.scout.ui + +import android.content.Context +import com.discord.utilities.search.query.FilterType +import moe.lava.awoocord.scout.FilterTypeExtension + +private fun String.decapitalise(context: Context) = + this.replaceFirstChar { it.lowercase(context.resources.configuration.locales[0]) } + +class ScoutSearchStringProvider(private val context: Context) { + fun getIdentifier(name: String) = + context.resources.getIdentifier(name, "string", "com.discord") + fun getString(name: String) = + context.getString(getIdentifier(name)) + + fun stringFor(type: FilterType) = when (type) { + FilterTypeExtension.BEFORE -> beforeFilterString + FilterTypeExtension.DURING -> duringFilterString + FilterTypeExtension.AFTER -> afterFilterString + FilterTypeExtension.SORT -> sortFilterString + else -> throw IllegalArgumentException("invalid extended filter type") + } + + // Surprising!! Discord has localised strings of these + val beforeFilterString: String + get() = getString("search_filter_before") + val duringFilterString: String + get() = getString("search_filter_during") + val afterFilterString: String + get() = getString("search_filter_after") + val sortFilterString: String + get() = getString("sort").decapitalise(context) + val sortOldString: String + get() = getString("search_oldest_short").decapitalise(context) +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..90c91a9 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,14 @@ +rootProject.name = "Awoocord" + +// This file sets what projects are included. Every time you add a new project, you must add it +// to the includes below. + +// Plugins are included like this +include( + "Scout" +) + +rootProject.children.forEach { + // Change kotlin to java if you'd rather use java + it.projectDir = file("plugins/${it.name}") +}