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