diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2e691bb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,36 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 100 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_any_line_comment_at_first_column = false +ij_any_line_comment_add_space = true +ij_smart_tabs = false +ij_wrap_on_typing = false + +[plugin/Frecents/src/main/java/*.java] +ij_formatter_enabled = false + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ktlint_code_style = ktlint_official +ktlint_experimental = enabled +ktlint_standard_no-wildcard-imports = disabled +ktlint_standard_string-template-indent = disabled +ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_function-expression-body = disabled +ktlint_function_signature_body_expression_wrapping = default +ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 6 +ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 3 +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 6 + +[*.xml] +ij_xml_continuation_indent_size = 4 + +[*.yml] +indent_size = 2 \ No newline at end of file diff --git a/ExamplePlugins/java/MyFirstCommand/src/main/AndroidManifest.xml b/ExamplePlugins/java/MyFirstCommand/src/main/AndroidManifest.xml deleted file mode 100644 index a45f28d..0000000 --- a/ExamplePlugins/java/MyFirstCommand/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/ExamplePlugins/java/MyFirstCommand/src/main/java/com/github/yournamehere/MyFirstCommand.java b/ExamplePlugins/java/MyFirstCommand/src/main/java/com/github/yournamehere/MyFirstCommand.java deleted file mode 100644 index 0b6c233..0000000 --- a/ExamplePlugins/java/MyFirstCommand/src/main/java/com/github/yournamehere/MyFirstCommand.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.github.yournamehere; - -import android.content.Context; -import com.aliucord.Utils; -import com.aliucord.annotations.AliucordPlugin; -import com.aliucord.api.CommandsAPI; -import com.aliucord.entities.Plugin; -import com.discord.api.commands.ApplicationCommandType; - -import java.util.Arrays; - -// Aliucord Plugin annotation. Must be present on the main class of your plugin -@AliucordPlugin(requiresRestart = false /* Whether your plugin requires a restart after being installed/updated */) -// Plugin class. Must extend Plugin and override start and stop -// Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/1_introduction.md#basic-plugin-structure -public class MyFirstCommand extends Plugin { - @Override - public void start(Context context) { - // Register a command with the name hello and description "My first command!" and no arguments. - // Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/2_commands.md - commands.registerCommand("hello", "My first command!", ctx -> { - // Just return a command result with hello world as the content - return new CommandsAPI.CommandResult( - "Hello World!", - null, // List of embeds - false // Whether to send visible for everyone - ); - }); - - // A bit more advanced command with arguments - commands.registerCommand( - "hellowitharguments", - "Hello World but with arguments!", - Arrays.asList( - Utils.createCommandOption(ApplicationCommandType.STRING, "name", "Person to say hello to"), - Utils.createCommandOption(ApplicationCommandType.USER, "user", "User to say hello to") - ), - ctx -> { - // Check if a user argument was passed - if (ctx.containsArg("user")) { - var user = ctx.getRequiredUser("user"); - return new CommandsAPI.CommandResult("Hello " + user.getUsername() + "!"); - } else { - // Returns either the argument value if present, or the defaultValue ("World" in this case) - var name = ctx.getStringOrDefault("name", "World"); - return new CommandsAPI.CommandResult("Hello " + name + "!"); - } - } - ); - } - - @Override - public void stop(Context context) { - // Unregister all commands - commands.unregisterAll(); - } -} diff --git a/ExamplePlugins/java/MyFirstPatch/build.gradle.kts b/ExamplePlugins/java/MyFirstPatch/build.gradle.kts deleted file mode 100644 index 1ebb20c..0000000 --- a/ExamplePlugins/java/MyFirstPatch/build.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -version = "1.0.0" // Plugin version. Increment this to trigger the updater -description = "My first patch!" // Plugin description that will be shown to user - -aliucord { - // Changelog of your plugin - changelog.set(""" - Some changelog - """.trimIndent()) - // Image or Gif that will be shown at the top of your changelog page - // changelogMedia.set("https://cool.png") - - // 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/ExamplePlugins/java/MyFirstPatch/src/main/AndroidManifest.xml b/ExamplePlugins/java/MyFirstPatch/src/main/AndroidManifest.xml deleted file mode 100644 index a45f28d..0000000 --- a/ExamplePlugins/java/MyFirstPatch/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/ExamplePlugins/java/MyFirstPatch/src/main/java/com/github/yournamehere/MyFirstPatch.java b/ExamplePlugins/java/MyFirstPatch/src/main/java/com/github/yournamehere/MyFirstPatch.java deleted file mode 100644 index c7c614f..0000000 --- a/ExamplePlugins/java/MyFirstPatch/src/main/java/com/github/yournamehere/MyFirstPatch.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.github.yournamehere; - -import android.content.Context; -import com.aliucord.CollectionUtils; -import com.aliucord.annotations.AliucordPlugin; -import com.aliucord.entities.MessageEmbedBuilder; -import com.aliucord.entities.Plugin; -import com.aliucord.patcher.*; -import com.aliucord.wrappers.embeds.MessageEmbedWrapper; -import com.discord.models.user.CoreUser; -import com.discord.stores.StoreUserTyping; -import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage; -import com.discord.widgets.chat.list.entries.ChatListEntry; -import com.discord.widgets.chat.list.entries.MessageEntry; - -// Aliucord Plugin annotation. Must be present on the main class of your plugin -@AliucordPlugin(requiresRestart = false /* Whether your plugin requires a restart after being installed/updated */) -// Plugin class. Must extend Plugin and override start and stop -// Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/1_introduction.md#basic-plugin-structure -public class MyFirstPatch extends Plugin { - @Override - public void start(Context context) throws Throwable { - // Patch that adds an embed with message statistics to each message - // Patched method is WidgetChatListAdapterItemMessage.onConfigure(int type, ChatListEntry entry) - patcher.patch( - // see https://docs.oracle.com/javase/tutorial/reflect/class/classNew.html - WidgetChatListAdapterItemMessage.class.getDeclaredMethod("onConfigure", int.class, ChatListEntry.class), - new Hook(param -> { // see https://api.xposed.info/reference/de/robv/android/xposed/XC_MethodHook.MethodHookParam.html - // Obtain the second argument passed to the method, so the ChatListEntry - // Because this is a Message item, it will always be a MessageEntry, so cast it to that - var entry = (MessageEntry) param.args[1]; - - // You need to be careful when messing with messages, because they may be loading - // (user sent a message, and it is currently sending) - if (entry.getMessage().isLoading()) return; - - // Now add an embed with the statistics - - // This method may be called multiple times per message, e.g. if it is edited, - // so first remove existing embeds - CollectionUtils.removeIf(entry.getMessage().getEmbeds(), e -> { - // MessageEmbed is an obfuscated class. However, Aliucord provides wrappers for commonly used - // obfuscated classes, the MessageEmbedWrapper in this case. - return "Message Statistics".equals(MessageEmbedWrapper.getTitle(e)); - }); - - // Creating embeds is a pain, so Aliucord provides a convenient builder - var embed = new MessageEmbedBuilder(). - setTitle("Message Statistics") - .addField("Length", entry.getMessage().getContent() != null ? Integer.toString(entry.getMessage().getContent().length()) : "0", false) - .addField("ID", Long.toString(entry.getMessage().getId()), false).build(); - - entry.getMessage().getEmbeds().add(embed); - }) - ); - - // Patch that renames Juby to JoobJoob - patcher.patch( - CoreUser.class.getDeclaredMethod("getUsername"), - new PreHook(param -> { // see https://api.xposed.info/reference/de/robv/android/xposed/XC_MethodHook.MethodHookParam.html - if (((CoreUser) param.thisObject).getId() == 925141667688878090L) { - // setResult() in before patches skips original method invocation - param.setResult("JoobJoob"); - } - }) - ); - - // Patch that hides your typing status by replacing the method and simply doing nothing - patcher.patch( - StoreUserTyping.class.getDeclaredMethod("setUserTyping", long.class), - InsteadHook.DO_NOTHING - ); - } - - @Override - public void stop(Context context) { - // Remove all patches - patcher.unpatchAll(); - } -} diff --git a/ExamplePlugins/kotlin/MyFirstCommand/src/main/AndroidManifest.xml b/ExamplePlugins/kotlin/MyFirstCommand/src/main/AndroidManifest.xml deleted file mode 100644 index a45f28d..0000000 --- a/ExamplePlugins/kotlin/MyFirstCommand/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/ExamplePlugins/kotlin/MyFirstCommand/src/main/kotlin/com/github/yournamehere/MyFirstCommand.kt b/ExamplePlugins/kotlin/MyFirstCommand/src/main/kotlin/com/github/yournamehere/MyFirstCommand.kt deleted file mode 100644 index 59577a4..0000000 --- a/ExamplePlugins/kotlin/MyFirstCommand/src/main/kotlin/com/github/yournamehere/MyFirstCommand.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.yournamehere - -import android.content.Context -import com.aliucord.Utils -import com.aliucord.annotations.AliucordPlugin -import com.aliucord.api.CommandsAPI -import com.aliucord.entities.Plugin -import com.discord.api.commands.ApplicationCommandType - -// Aliucord Plugin annotation. Must be present on the main class of your plugin -@AliucordPlugin(requiresRestart = false /* Whether your plugin requires a restart after being installed/updated */) -// Plugin class. Must extend Plugin and override start and stop -// Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/1_introduction.md#basic-plugin-structure -class MyFirstCommand : Plugin() { - override fun start(context: Context) { - // Register a command with the name hello and description "My first command!" and no arguments. - // Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/2_commands.md - commands.registerCommand("hello", "My first command!") { - // Just return a command result with hello world as the content - CommandsAPI.CommandResult( - "Hello World!", - null, // List of embeds - false // Whether to send visible for everyone - ) - } - - // A bit more advanced command with arguments - commands.registerCommand("hellowitharguments", "Hello World but with arguments!", listOf( - Utils.createCommandOption(ApplicationCommandType.STRING, "name", "Person to say hello to"), - Utils.createCommandOption(ApplicationCommandType.USER, "user", "User to say hello to") - )) { ctx -> - // Check if a user argument was passed - if (ctx.containsArg("user")) { - val user = ctx.getRequiredUser("user") - CommandsAPI.CommandResult("Hello ${user.username}!") - } else { - // Returns either the argument value if present, or the defaultValue ("World" in this case) - val name = ctx.getStringOrDefault("name", "World") - CommandsAPI.CommandResult("Hello $name!") - } - } - } - - override fun stop(context: Context) { - // Unregister our commands - commands.unregisterAll() - } -} diff --git a/ExamplePlugins/kotlin/MyFirstPatch/build.gradle.kts b/ExamplePlugins/kotlin/MyFirstPatch/build.gradle.kts deleted file mode 100644 index 1ebb20c..0000000 --- a/ExamplePlugins/kotlin/MyFirstPatch/build.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -version = "1.0.0" // Plugin version. Increment this to trigger the updater -description = "My first patch!" // Plugin description that will be shown to user - -aliucord { - // Changelog of your plugin - changelog.set(""" - Some changelog - """.trimIndent()) - // Image or Gif that will be shown at the top of your changelog page - // changelogMedia.set("https://cool.png") - - // 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/ExamplePlugins/kotlin/MyFirstPatch/src/main/AndroidManifest.xml b/ExamplePlugins/kotlin/MyFirstPatch/src/main/AndroidManifest.xml deleted file mode 100644 index a45f28d..0000000 --- a/ExamplePlugins/kotlin/MyFirstPatch/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/README.md b/README.md index e00b299..5e46207 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,38 @@ -# `Aliucord Plugin Repo Template` +# Aliucord Plugin Repo Template + +--- Template for an [Aliucord](https://github.com/Aliucord) plugin repo -⚠️ Make sure you check "Include all branches" when using this template +⚠️ Make sure you check "Include all branches" when using this template \ +⚠️ Consider getting familiar with Java and/or Kotlin and Gradle before starting + +## Pre-requisites + +- Java JDK 11 or newer. OpenJDK recommended +- [Android Studio](https://developer.android.com/studio) - ## Getting started with writing your first plugin -This template includes 2 example plugins demonstrating commands and patches which you can find in the ExamplePlugins folder. +This template includes an example plugin written in Kotlin and Java, demonstrating how to implement +a command and patches. + +To set up your development environment: + +1. Clone this repository to your local machine. +2. Open the cloned repository in Android Studio. +3. Open the gradle build script at [plugin/build.gradle.kts](plugin/build.gradle.kts), read the + comments and replace all the placeholders +4. Familiarize yourself with the project structure. Most files are commented + +To build and deploy your plugin: -1. Open the root build.gradle.kts, read the comments and replace all the placeholders -2. Familiarize yourself with the project structure. Most files are commented -3. Build or deploy your first plugin using: - - Windows: `.\gradlew.bat MyFirstCommand:make` or `.\gradlew.bat MyFirstCommand:deployWithAdb` - - Linux & Mac: `./gradlew MyFirstCommand:make` or `./gradlew MyFirstCommand:deployWithAdb` +- On Linux & Mac, run `./gradlew MyFirstKotlinPlugin:make` to build the plugin. + Use `./gradlew MyFirstKotlinPlugin:deployWithAdb` to deploy directly to a connected device. +- On Windows, use `.\gradlew.bat MyFirstKotlinPlugin:make` + and `.\gradlew.bat MyFirstKotlinPlugin:deployWithAdb` for building and deploying, respectively. ## License -Everything in this repo is released into the public domain. You may use it however you want with no conditions whatsoever +Everything in this repo is released into the public domain. You may use it however you want with no +conditions whatsoever diff --git a/build.gradle.kts b/build.gradle.kts index 0a34669..ea3b6d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,89 +1,8 @@ -import com.aliucord.gradle.AliucordExtension -import com.android.build.gradle.BaseExtension - -buildscript { - repositories { - google() - mavenCentral() - // Aliucords Maven repo which contains our tools and dependencies - maven("https://maven.aliucord.com/snapshots") - // Shitpack which still contains some Aliucord dependencies for now. TODO: Remove - maven("https://jitpack.io") - } - - dependencies { - classpath("com.android.tools.build:gradle:7.0.4") - // Aliucord gradle plugin which makes everything work and builds plugins - classpath("com.aliucord:gradle:main-SNAPSHOT") - // Kotlin support. Remove if you want to use Java - 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") - // Remove if using Java - apply(plugin = "kotlin-android") - - // Fill out with your info - aliucord { - author("DISCORD USERNAME", 123456789L) - updateUrl.set("https://raw.githubusercontent.com/USERNAME/REPONAME/builds/updater.json") - buildUrl.set("https://raw.githubusercontent.com/USERNAME/REPONAME/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" // Required - // 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) -} +@file:Suppress("DSL_SCOPE_VIOLATION") + +plugins { + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.aliucord) apply false + alias(libs.plugins.ktlint) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 01b80d7..8001bb3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,10 +10,10 @@ 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 +org.gradle.parallel=true +org.gradle.configureondemand=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 +android.suppressUnsupportedCompileSdk=34 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..6dc81ea --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,19 @@ +[versions] +discord = "aliucord-SNAPSHOT" +aliucord = "main-SNAPSHOT" +#noinspection GradleDependency Aliucord uses Kotlin 1.5.21 +kotlin = "1.5.21" +android = "7.4.2" +ktlintPlugin = "12.1.1" +#noinspection GradleDependency Newer versions incompatible with other plugins +ktlint = "0.50.0" + +[libraries] +discord = { module = "com.discord:discord", version.ref = "discord" } +aliucord = { module = "com.aliucord:Aliucord", version.ref = "aliucord" } + +[plugins] +android-library = { id = "com.android.library", version.ref = "android" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +aliucord = { id = "com.aliucord.gradle", version.ref = "aliucord" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintPlugin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index da0e964..3c39e7f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jun 09 16:18:24 EST 2021 +#Fri Jul 28 20:21:56 EDT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/ExamplePlugins/java/MyFirstCommand/build.gradle.kts b/plugin/MyFirstJavaPlugin/build.gradle.kts similarity index 77% rename from ExamplePlugins/java/MyFirstCommand/build.gradle.kts rename to plugin/MyFirstJavaPlugin/build.gradle.kts index 355488d..27dd107 100644 --- a/ExamplePlugins/java/MyFirstCommand/build.gradle.kts +++ b/plugin/MyFirstJavaPlugin/build.gradle.kts @@ -1,11 +1,13 @@ version = "1.0.0" // Plugin version. Increment this to trigger the updater -description = "My first commands!" // Plugin description that will be shown to user +description = "My first Java plugin!" // Plugin description that will be shown to user aliucord { // Changelog of your plugin - changelog.set(""" + changelog.set( + """ Some changelog - """.trimIndent()) + """.trimIndent() + ) // Image or Gif that will be shown at the top of your changelog page // changelogMedia.set("https://cool.png") @@ -16,4 +18,4 @@ aliucord { // Excludes this plugin from the updater, meaning it won't show up for users. // Set this if the plugin is unfinished excludeFromUpdaterJson.set(true) -} +} \ No newline at end of file diff --git a/plugin/MyFirstJavaPlugin/src/main/java/com/github/yournamehere/MyFirstJavaPlugin.java b/plugin/MyFirstJavaPlugin/src/main/java/com/github/yournamehere/MyFirstJavaPlugin.java new file mode 100644 index 0000000..525d2f2 --- /dev/null +++ b/plugin/MyFirstJavaPlugin/src/main/java/com/github/yournamehere/MyFirstJavaPlugin.java @@ -0,0 +1,117 @@ +package com.github.yournamehere; + +import android.content.Context; + +import com.aliucord.Utils; +import com.aliucord.annotations.AliucordPlugin; +import com.aliucord.api.CommandsAPI; +import com.aliucord.entities.MessageEmbedBuilder; +import com.aliucord.entities.Plugin; +import com.aliucord.patcher.Hook; +import com.aliucord.patcher.InsteadHook; +import com.aliucord.patcher.PreHook; +import com.aliucord.wrappers.embeds.MessageEmbedWrapper; +import com.discord.api.commands.ApplicationCommandType; +import com.discord.models.user.CoreUser; +import com.discord.stores.StoreUserTyping; +import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage; +import com.discord.widgets.chat.list.entries.ChatListEntry; +import com.discord.widgets.chat.list.entries.MessageEntry; + +import java.util.Arrays; +import java.util.Objects; + +// Aliucord Plugin annotation. Must be present on the main class of your plugin +// Plugin class. Must extend Plugin and override start and stop +// Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/1_introduction.md#basic-plugin-structure +@AliucordPlugin( + requiresRestart = false // Whether your plugin requires a restart after being installed/updated +) +class MyFirstJavaPlugin extends Plugin { + @Override + public void start(Context context) throws Throwable { + // Register a command with the name hello and description "My first command!" and no arguments. + // Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/2_commands.md + commands.registerCommand("hello", "My first command!", ctx -> { + // Just return a command result with hello world as the content + return new CommandsAPI.CommandResult( + "Hello World!", + null, // List of embeds + false // Whether to send visible for everyone + ); + }); + + // A bit more advanced command with arguments + commands.registerCommand( + "hellowitharguments", + "Hello World but with arguments!", + Arrays.asList( + Utils.createCommandOption(ApplicationCommandType.STRING, "name", "Person to say hello to"), + Utils.createCommandOption(ApplicationCommandType.USER, "user", "User to say hello to") + ), + ctx -> { + String username; + + // Check if a user argument was passed + if (ctx.containsArg("user")) { + username = ctx.getRequiredUser("user").getUsername(); + } else { + // Returns either the argument value if present, or the defaultValue ("World" in this case) + username = ctx.getStringOrDefault("name", "World"); + } + + // Return the final result that will be displayed in chat as a response to the command + return new CommandsAPI.CommandResult("Hello " + username + "!"); + } + ); + + // Patch that adds an embed with message statistics to each message + // Patched method is WidgetChatListAdapterItemMessage.onConfigure(int type, ChatListEntry entry) + patcher.patch(WidgetChatListAdapterItemMessage.class.getDeclaredMethod("onConfigure", int.class, ChatListEntry.class), new Hook(param -> { + // see https://api.xposed.info/reference/de/robv/android/xposed/XC_MethodHook.MethodHookParam.html + // Obtain the second argument passed to the method, so the ChatListEntry + // Because this is a Message item, it will always be a MessageEntry, so cast it to that + var entry = (MessageEntry) param.args[1]; + var message = entry.getMessage(); + + // You need to be careful when messing with messages, because they may be loading + // (user sent a message, and it is currently sending) + if (message.isLoading()) return; + + // Now add an embed with the statistics + + // This method may be called multiple times per message, e.g. if it is edited, + // so first remove existing embeds + message.getEmbeds().removeIf(it -> Objects.equals(new MessageEmbedWrapper(it).getTitle(), "Message Statistics")); + + // Creating embeds is a pain, so Aliucord provides a convenient builder + var embed = new MessageEmbedBuilder() + .setTitle("Message Statistics") + .addField("Length", message.getContent() != null ? Integer.toString(message.getContent().length()) : "0", false) + .addField("ID", Long.toString(message.getId()), false).build(); + + message.getEmbeds().add(embed); + })); + + // Patch that renames Juby to JoobJoob + patcher.patch( + CoreUser.class.getDeclaredMethod("getUsername"), + new PreHook(param -> { // see https://api.xposed.info/reference/de/robv/android/xposed/XC_MethodHook.MethodHookParam.html + if (((CoreUser) param.thisObject).getId() == 925141667688878090L) { + // setResult() in before patches skips original method invocation + param.setResult("JoobJoob"); + } + }) + ); + + // Patch that hides your typing status by replacing the method and simply doing nothing + // This patches the method StoreUserTyping.setUserTyping(long channelId) + patcher.patch(StoreUserTyping.class.getDeclaredMethod("setUserTyping", long.class), InsteadHook.DO_NOTHING); + } + + @Override + public void stop(Context context) { + // Remove all patches + patcher.unpatchAll(); + } +} \ No newline at end of file diff --git a/ExamplePlugins/kotlin/MyFirstCommand/build.gradle.kts b/plugin/MyFirstKotlinPlugin/build.gradle.kts similarity index 77% rename from ExamplePlugins/kotlin/MyFirstCommand/build.gradle.kts rename to plugin/MyFirstKotlinPlugin/build.gradle.kts index 355488d..00cc5ef 100644 --- a/ExamplePlugins/kotlin/MyFirstCommand/build.gradle.kts +++ b/plugin/MyFirstKotlinPlugin/build.gradle.kts @@ -1,11 +1,13 @@ version = "1.0.0" // Plugin version. Increment this to trigger the updater -description = "My first commands!" // Plugin description that will be shown to user +description = "My first Kotlin plugin!" // Plugin description that will be shown to user aliucord { // Changelog of your plugin - changelog.set(""" + changelog.set( + """ Some changelog - """.trimIndent()) + """.trimIndent() + ) // Image or Gif that will be shown at the top of your changelog page // changelogMedia.set("https://cool.png") @@ -16,4 +18,4 @@ aliucord { // Excludes this plugin from the updater, meaning it won't show up for users. // Set this if the plugin is unfinished excludeFromUpdaterJson.set(true) -} +} \ No newline at end of file diff --git a/ExamplePlugins/kotlin/MyFirstPatch/src/main/kotlin/com/github/yournamehere/MyFirstPatch.kt b/plugin/MyFirstKotlinPlugin/src/main/kotlin/com/github/yournamehere/MyFirstKotlinPlugin.kt similarity index 53% rename from ExamplePlugins/kotlin/MyFirstPatch/src/main/kotlin/com/github/yournamehere/MyFirstPatch.kt rename to plugin/MyFirstKotlinPlugin/src/main/kotlin/com/github/yournamehere/MyFirstKotlinPlugin.kt index bab47b9..e2305d3 100644 --- a/ExamplePlugins/kotlin/MyFirstPatch/src/main/kotlin/com/github/yournamehere/MyFirstPatch.kt +++ b/plugin/MyFirstKotlinPlugin/src/main/kotlin/com/github/yournamehere/MyFirstKotlinPlugin.kt @@ -1,11 +1,14 @@ package com.github.yournamehere import android.content.Context +import com.aliucord.Utils import com.aliucord.annotations.AliucordPlugin +import com.aliucord.api.CommandsAPI import com.aliucord.entities.MessageEmbedBuilder import com.aliucord.entities.Plugin import com.aliucord.patcher.* import com.aliucord.wrappers.embeds.MessageEmbedWrapper.Companion.title +import com.discord.api.commands.ApplicationCommandType import com.discord.models.user.CoreUser import com.discord.stores.StoreUserTyping import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage @@ -13,33 +16,77 @@ import com.discord.widgets.chat.list.entries.ChatListEntry import com.discord.widgets.chat.list.entries.MessageEntry // Aliucord Plugin annotation. Must be present on the main class of your plugin -@AliucordPlugin(requiresRestart = false /* Whether your plugin requires a restart after being installed/updated */) // Plugin class. Must extend Plugin and override start and stop // Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/1_introduction.md#basic-plugin-structure -class MyFirstPatch : Plugin() { +@AliucordPlugin( + requiresRestart = false // Whether your plugin requires a restart after being installed/updated +) +class MyFirstKotlinPlugin : Plugin() { override fun start(context: Context) { + // Register a command with the name hello and description "My first command!" and no arguments. + // Learn more: https://github.com/Aliucord/documentation/blob/main/plugin-dev/2_commands.md + commands.registerCommand("hello", "My first command!") { + // Just return a command result with hello world as the content + CommandsAPI.CommandResult( + "Hello World!", + null, // List of embeds + false // Whether to send visible for everyone + ) + } + + // A bit more advanced command with arguments + commands.registerCommand( + "hellowitharguments", + "Hello World but with arguments!", + listOf( + Utils.createCommandOption( + ApplicationCommandType.STRING, + "name", + "Person to say hello to" + ), + Utils.createCommandOption( + ApplicationCommandType.USER, + "user", + "User to say hello to" + ) + ) + ) { ctx -> + // Check if a user argument was passed + val username = if (ctx.containsArg("user")) { + ctx.getRequiredUser("user").username + } else { + // Returns either the argument value if present, or the defaultValue ("World" in this case) + ctx.getStringOrDefault("name", "World") + } + + // Return the final result that will be displayed in chat as a response to the command + CommandsAPI.CommandResult("Hello $username!") + } + // Patch that adds an embed with message statistics to each message // Patched method is WidgetChatListAdapterItemMessage.onConfigure(int type, ChatListEntry entry) - patcher.after /* Class whose method to patch */( + patcher.after( "onConfigure", // Method name // Refer to https://kotlinlang.org/docs/reflection.html#class-references // and https://docs.oracle.com/javase/tutorial/reflect/class/classNew.html Int::class.java, // int type ChatListEntry::class.java // ChatListEntry entry - ) { param -> // see https://api.xposed.info/reference/de/robv/android/xposed/XC_MethodHook.MethodHookParam.html + ) { param -> + // see https://api.xposed.info/reference/de/robv/android/xposed/XC_MethodHook.MethodHookParam.html // Obtain the second argument passed to the method, so the ChatListEntry // Because this is a Message item, it will always be a MessageEntry, so cast it to that val entry = param.args[1] as MessageEntry + val message = entry.message // You need to be careful when messing with messages, because they may be loading // (user sent a message, and it is currently sending) - if (entry.message.isLoading) return@after + if (message.isLoading) return@after // Now add an embed with the statistics // This method may be called multiple times per message, e.g. if it is edited, // so first remove existing embeds - entry.message.embeds.removeIf { + message.embeds.removeIf { // MessageEmbed.getTitle() is actually obfuscated, but Aliucord provides extensions for commonly used // obfuscated Discord classes, so just import the MessageEmbed.title extension and boom goodbye obfuscation! it.title == "Message Statistics" @@ -48,15 +95,16 @@ class MyFirstPatch : Plugin() { // Creating embeds is a pain, so Aliucord provides a convenient builder MessageEmbedBuilder().run { setTitle("Message Statistics") - addField("Length", (entry.message.content?.length ?: 0).toString(), false) - addField("ID", entry.message.id.toString(), false) + addField("Length", "${message.content?.length ?: 0}", false) + addField("ID", message.id.toString(), false) - entry.message.embeds.add(build()) + message.embeds.add(build()) } } // Patch that renames Juby to JoobJoob - patcher.before("getUsername") { param -> // see https://api.xposed.info/reference/de/robv/android/xposed/XC_MethodHook.MethodHookParam.html + patcher.before("getUsername") { param -> + // see https://api.xposed.info/reference/de/robv/android/xposed/XC_MethodHook.MethodHookParam.html // in before, after and instead patches, `this` refers to the instance of the class // the patched method is on, so the CoreUser instance here if (id == 925141667688878090) { @@ -67,7 +115,8 @@ class MyFirstPatch : Plugin() { // Patch that hides your typing status by replacing the method and simply doing nothing patcher.instead( - "setUserTyping", Long::class.java // long channelId + "setUserTyping", + Long::class.java // java.lang.Long channelId ) { null } } @@ -75,4 +124,4 @@ class MyFirstPatch : Plugin() { // Remove all patches patcher.unpatchAll() } -} +} \ No newline at end of file diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts new file mode 100644 index 0000000..57a03a2 --- /dev/null +++ b/plugin/build.gradle.kts @@ -0,0 +1,77 @@ +@file:Suppress("UnstableApiUsage") + +import com.aliucord.gradle.AliucordExtension +import com.android.build.gradle.LibraryExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jlleitschuh.gradle.ktlint.KtlintExtension + +// TODO: Change to your repository +val repo = "Aliucord/plugins-template" + +subprojects { + val libs = rootProject.libs + + apply { + plugin(libs.plugins.android.library.get().pluginId) + plugin(libs.plugins.aliucord.get().pluginId) + plugin(libs.plugins.kotlin.android.get().pluginId) + plugin(libs.plugins.ktlint.get().pluginId) + } + + configure { + // TODO: Change to your package name + namespace = "com.github.yournamehere" + + compileSdk = 34 + + defaultConfig { + minSdk = 24 + } + + buildFeatures { + renderScript = false + shaders = false + buildConfig = true + resValues = false + aidl = false + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + } + + configure { + // TODO: Change to your name and user ID + author("yournamehere", 0L) + + updateUrl.set("https://raw.githubusercontent.com/$repo/builds/updater.json") + buildUrl.set("https://raw.githubusercontent.com/$repo/builds/%s.zip") + } + + configure { + version.set(libs.versions.ktlint) + + coloredOutput.set(true) + outputColorName.set("RED") + ignoreFailures.set(true) + } + + dependencies { + val discord by configurations + val compileOnly by configurations + val implementation by configurations + + discord(libs.discord) + compileOnly(libs.aliucord) + // compileOnly("com.github.Aliucord:Aliucord:unspecified") + } + + tasks.withType { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 54b32ab..5353a27 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,22 +1,29 @@ -rootProject.name = "AliucordPlugins" +@file:Suppress("UnstableApiUsage") -// This file sets what projects are included. Every time you add a new project, you must add it -// to the includes below. +pluginManagement { + repositories { + google() + gradlePluginPortal() + maven("https://maven.aliucord.com/snapshots") + maven("https://jitpack.io") + } +} -// Plugins are included like this -include( - "MyFirstCommand", - "MyFirstPatch" -) +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("https://maven.aliucord.com/snapshots") + } -// This is required because plugins are in the ExamplePlugins/kotlin subdirectory. -// -// Assuming you put all your plugins into the project root, so on the same -// level as this file, simply remove everything below. -// -// Otherwise, if you want a different structure, for example all plugins in a folder called "plugins", -// then simply change the path -rootProject.children.forEach { - // Change kotlin to java if you'd rather use java - it.projectDir = file("ExamplePlugins/kotlin/${it.name}") + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) } + +rootProject.name = "aliucord-plugins" + +rootDir + .resolve("plugin") + .listFiles { file -> + file.isDirectory && file.resolve("build.gradle.kts").exists() + }!! + .forEach { include(":plugin:${it.name}") } \ No newline at end of file