diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index feae5a21..ba1cd0fd 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -21,8 +21,6 @@ jobs: with: java-version: '17' distribution: 'adopt' - cache: maven - - name: Build with Maven - run: mvn -B package --file pom.xml + cache: gradle - name: Test with Maven - run: mvn test + run: ./gradlew test diff --git a/README.md b/README.md index 34556d16..c1f0e0c8 100644 --- a/README.md +++ b/README.md @@ -1,231 +1,76 @@ -# 🍰 Nexus Framework -The formal successor of [Velen](https://github.com/ShindouMihou/Velen) which takes a more OOP (Object Oriented Programming) approach to integrating slash commands onto your Discord bot without the need of builders. Nexus is slightly inspired by Laravel and is aimed to be more efficient than Velen at performing certain tasks. +![Splashscreen](https://github.com/ShindouMihou/Nexus/assets/69381903/e2e2118b-07c4-4c49-9322-0507dc1ebf5c) -## 🌉 Dependencies -Nexus doesn't enforce other dependencies other than the latest stable version of Javacord. Every development version of Javacord will have a branch of Nexus that is dedicated to compatiability changes (if the development version includes a breaking change), we recommend including any SLF4J-compatiable logging framework although Nexus supports adapters for custom logging. -- [💻 Logging](https://github.com/ShindouMihou/Nexus/#-Logging) +# -## 📦 Installation -The framework doesn't have any plans of moving to Maven Central at this moment, as such, it is recommended to use [Jitpack.io](https://jitpack.io/#pw.mihou/Nexus) to install the framework onto your project. Please follow the instructions written there. -- [pw.mihou.Nexus](https://jitpack.io/#pw.mihou/Nexus) +
Discord Bot Framework for Javacord, simplified.
+
-## 🧑‍🎨 Artisan +Nexus is a Javacord framework, written in Kotlin-Java, designed to enable developers to add application commands to their Discord bots with simplicity. It is the successor of the [Velen](https://github.com/ShindouMihou/velen) framework and takes an "object-based" approach to designing application commands (slash commands, context menus, etc). -### 🌸 Your Nexus instance -To start with using Nexus, one must create a global Nexus instance. It is recommended to place this instance as a **`public static`** field on your Main class or similar that can be accessed at any time without needing to recreate the instance. -```java -Nexus nexus = Nexus.builder().build(); -``` - -You can configure the message configuration of Nexus and related configuration (e.g. maximum lifespan of an cross-shard Nexus request) from the [`NexusBuilder`](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java). - -Before proceeding forward, you need to add a few lines to how you create your DiscordApi instance, for example: -```java -new DiscordApiBuilder() - .setToken(...) - .addListener(nexus) - .setTotalShards(1) - .loginAll() - .forEach(future -> future.thenAccept(shard -> { - nexus.getShardManager().put(shard); - //... other stuff like onShardLogin() - }).exceptionally(ExceptionLogger.get())); -``` - -In particular, you need the add these two lines: -```java -.addListener(nexus) -``` -```java -nexus.getShardManager().put(shard); -``` - -The former allows Nexus to listen into specific events like slash command events and handle them, the latter enables Nexus to use its own shard manager to get the DiscordApi instance of specific shards (e.g. during command synchronization and many other parts of the framework). Both of those lines are considered necessary for the framework to function. - -Another line that you may need to add if you are using the `DiscordApi#disconnect()` method is: -```java -DiscordApi shard = ... -nexus.getShardManager().remove(shard.getCurrentShard()); -``` - -You should add that line before calling `disconnect()` to tell Nexus that the given shard is no longer useable and removes it from its shard manager (important especially since Nexus will hold a reference to the shard until you call that function). - -These methods allows Nexus to perform command synchronization and similar (**REQUIRED for command synchronization**). You can view an example of those methods in use from below. -- [Synchronization Example](https://github.com/ShindouMihou/Nexus/blob/master/examples/synchronization/Main.java) - -### 🫓 Fundamentals of creating commands -You can design commands in Nexus simply by creating a class that implements the [`NexusHandler`](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/facade/NexusHandler.java) interface before creating two required `String` fields named: `name` and `description` which are required to create slash commands. -```java -public class PingCommand implements NexusHandler { - - private final String name = "ping" - private final String description = "Ping pong!" - - @Override - public void onEvent(NexusCommandEvent event) {} - -} -``` -When creating class fields, one must ensure that it doesn't conflict with the names of the variables found in [NexusCommandCore](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandCore.java) that has the `@Required` or `@WithDefault` annotations as these are options that must be set or can be overriden by defining these fields with the proper types on your class. - -For instance, we want to include the option of making our command ephemeral then all we need to do is define a `options` field with `List` before defining the value of the field with the ephemeral option. - -```java -public class PingCommand implements NexusHandler { - - private final String name = "ping" - private final String description = "Ping pong!" - private final List options = List.of( - SlashCommandOption.create( - SlashCommandOptionType.BOOLEAN, "ephemeral", "Whether to make the command ephemeral or not.", false - ) - ); - - @Override - public void onEvent(NexusCommandEvent event) {} - -} -``` - -All fields with the exception of those defined in the [NexusCommandCore](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandCore.java) are not visible to interceptors but can be made visible by including the `@Share` annotation before the command which places the field into an immutable Map (whatever the field's value is upon generation will be the final value known to Nexus). You can see an example of this on the Authentication middlewares example: -- [Authentication Examples](https://github.com/ShindouMihou/Nexus/tree/master/examples/authentication) -- [Permissions Authentication Middleware](https://github.com/ShindouMihou/Nexus/blob/f8942c4eca80ea5da92a71c14ae3b6d12cbf0e79/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java#L21) -- [Role Authentication Middleware](https://github.com/ShindouMihou/Nexus/blob/f8942c4eca80ea5da92a71c14ae3b6d12cbf0e79/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java#L59) -- [User Authentication Middleware](https://github.com/ShindouMihou/Nexus/blob/f8942c4eca80ea5da92a71c14ae3b6d12cbf0e79/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java#L102) +## Example -You can also view a demonstration of "Shared Fields" from the shared_fields example: -- [Shared Fields Example](https://github.com/ShindouMihou/Nexus/tree/master/examples/shared_fields) +```kotlin +object PingCommand: NexusHandler { + val name: String = "ping" + val description: String = "Ping, Pong!" -> We do not recommend using `event.getInteraction().respondLater()` or similar methods but instead use the `event.respondLater()` methods which **respects middlewares**. -> To explain this further, middlewares are allowed to request to Discord for an extension time to process the middleware's functions which is done through Nexus and this creates an `InteractionOriginalResponseUpdater` that Nexus stores for other middlewares to use or for the command to use. -> This is explained more on **Intercepting Commands** - -After creating the command class, you can then tell Nexus to include it by using: -```java -Nexus nexus = ...; -nexus.listenOne(new SomeCommand()); -``` - -You can also create a `NexusCommand` without enabling the event dispatcher (for cases when you want to see the result of the command generation) by using: -```java -Nexus nexus = ...; -NexusCommand command = nexus.defineOne(new SomeCommand()); -``` - -### ♒ Intercepting Commands -Nexus includes the ability to intercept specific commands by including either the `middlewares` or `afterwares` field that takes a `List` with the values inside the `List` being the key names of the interceptors. The framework provides several middlewares by default that you can add to your command or globally (via the `Nexus.addGlobalMiddleware(...)` method). -- [Common Interceptors](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/NexusCommonInterceptors.java) -- [Common Interceptors Implementation](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/core/NexusCommonInterceptorsCore.java) -- [Nexus Auth Middlewares Implementation](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java) - -You can create your own middleware by using the method: `NexusCommandInterceptor.addMiddleware("middleware.name", event -> ...)` - -You can create your own afterware by using the method: `NexusCommandInterceptor.addAfterware("afterware.name", event -> ...)` - -These two methods utilizes Java's Lambdas to allow creating of the interceptors without the need of creating a new class but if you want to move the handling to their own classes then you can simply implement the [`NexusMiddleware`](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddleware.java) or [`NexusAfterware`](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusAfterware.java) interface. -```java -public class SomeMiddleware implements NexusMiddleware { - - @Override - public void onBeforeCommand(NexusMiddlewareEvent event) { } + override fun onEvent(event: NexusCommandEvent) { + // Auto-deferred response (automatically handles deferring of responses when the framework + // detects potentially late response). + event.autoDefer(ephemeral = true) { + return@autodefer NexusMessage.from("Hello ${event.user.name}") + } + // Manual response (you are in control of deferring, etc.) + // event.respondNowWith("Hello ${event.user.name}!") + } } ``` +```kotlin +object ReportUserContextMenu: NexusUserContextMenu() { + val name = "test" -Middlewares can control the execution of a command by utilizing the methods that [`NexusMiddlewareEvent`](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.java) provides which includes: -- `next()` : Tells Nexus to move forward to the next middleware to process if any, otherwise executes the command. -- `stop()` : Stops the command from executing, this causes an `Interaction has failed` result in the Discord client of the user. -- `stop(NexusMessage)` : Stops the command from executing with a response sent which can be either an Embed or a String. -- `stopIf(boolean, NexusMessage)` : Same as above but only stops if the boolean provided is equals to true. -- `stopIf(Predicate, NexusMessage)`: Same as above but evaluates the result from the function provided with Predicate. -- `stopIf(Predicate)` : Same as above but doesn't send a message response. - -You do not need to tell Nexus to do `next()` since the default response of a middleware will always be `next()`. -```java -public class ServerOnlyMiddleware implements NexusMiddleware { - - @Override - public void onBeforeCommand(NexusMiddlewareEvent event) { - event.stopIf(event.getServer().isEmpty()); + override fun onEvent(event: NexusContextMenuEvent) { + val target = event.interaction.target + event.respondNowEphemerallyWith("${target.discriminatedName} has been reported to our servers!") } - } ``` -The above tells a simple example of a middleware the prevents the execution if the server is not present. -```java -Nexus nexus = ... -NexusCommandInterceptor.addMiddleware("middlewares.gate.server", new ServerOnlyMiddleware()); -nexus.addGlobalMiddleware("middlewares.gate.server"); -``` - -Command Interceptors doesn't have any set of rules but we recommend following a similar scheme as package-classes of Java (e.g. `nexus.auth.permissions`) to make it easier to read but once again it is not required. - -Interceptors can access shared fields of a command as long as the field is defined with `@Share` which then will allow the interceptor to freely access it via the `event.getCommand().get("fieldName", Type.class)` which returns an `Optional`. You can see examples of these being used from the following references: -- [Permissions Authentication Middleware](https://github.com/ShindouMihou/Nexus/blob/f8942c4eca80ea5da92a71c14ae3b6d12cbf0e79/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java#L21) -- [Role Authentication Middleware](https://github.com/ShindouMihou/Nexus/blob/f8942c4eca80ea5da92a71c14ae3b6d12cbf0e79/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java#L59) -- [User Authentication Middleware](https://github.com/ShindouMihou/Nexus/blob/f8942c4eca80ea5da92a71c14ae3b6d12cbf0e79/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java#L102) -- [Shared Fields Example](https://github.com/ShindouMihou/Nexus/tree/master/examples/shared_fields) -Middlewares that can take more than 3 seconds to complete should always use a delayed response which tells Nexus to generate a `InteractionOriginalResponseUpdater` for the command to use. A delayed response doesn't mean that the interaction is already answered, it just tells Discord that we are taking a bit longer to respond. You can use the following methods to perform a delayed response: -- `askDelayedResponse()`: Asks for a delayed response, this won't allow you to respond to the command directly. -- `askDelayedResponseAsEphemeral()`: Same as above but as ephemeral. -- `askDelayedResponseAsEphemeralIf(boolean)`: Same as above but responds as an ephemeral if the boolean is true. -- `askDelayedResponseAsEphemeralIf(Predicate)`: Same as above but evaluates the boolean from the Predicate. +## Getting Started -You are free to answer the interaction yourself but it will not be communicated cross-middleware and to the command. The methods above will allow middlewares and the command to use a single `InteractionOriginalResponseUpdater`. +To get started with Nexus, we recommend reading the wiki in its chronological order: +- [`GitHub Wiki`](https://github.com/ShindouMihou/Nexus/wiki) -### 🏜️ Synchronizing Commands -Nexus includes built-in synchronization methods for slash commands that are modifiable to one's liking. To understand how to synchronize commands to Discord, please visit our examples instead: -- [Synchronization Example](https://github.com/ShindouMihou/Nexus/tree/master/examples/synchronization) +You can also read the examples that we have: +- [`Examples`](examples) -You can modify the synchronization methods by implementing the following interface: -- [NexusSynchronizeMethods](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/NexusSynchronizeMethods.java) - -For reference, you can view the default methods that Nexus uses: -- [NexusDefaultSynchronizeMethods](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/defaults/NexusDefaultSynchronizeMethods.java) - -After defining your own synchronize methods, you can then add this line at startup: -```java -NexusSynchronizer.SYNCHRONIZE_METHODS.set(); -``` +If you want to install Nexus as a dependency, you can head to Jitpack, select a version and follow the instructions there: +- [`Jitpack`](https://jitpack.io/#pw.mihou/Nexus) -What is the use-case for modifying the synchronize methods? -- [x] Customizable synchronize methods were implemented due to the requirements of one of the bots under development that uses custom `bulkOverwrite...` methods that aren't included in the official Javacord fork. Nexus needs to adapt to those requirements as well and therefore synchronize methods are completely customizable. - -> **Note** -> -> The following text below only applies to versions below v1.0.0-beta. The newer version ([**#8**](https://github.com/ShindouMihou/Nexus/pull/8)) has better synchronize methods that are more efficient and improved overall. - -The default synchronize methods should be more than enough for a bot that runs on a single cluster but for easier multi-cluster usage, it is recommended to use only one cluster to handle the synchronization of commands with a custom Javacord fork that allows for bulk-overwriting and updating of slash commands in servers using only the server id. -- You can refer to [BeemoBot/Javacord#1](https://github.com/BeemoBot/Javacord/pull/1) for more information. - -## 💻 Logging -The framework logs through SLF4J by default but one can also use our console logging adapter to log to console in a similar format to Javacord's fallback logger. You can read more about how to use the console logging adapter on [v1.0.0-alpha3.0.6 Release Notes](https://github.com/ShindouMihou/Nexus/releases/tag/v1.0.0-alpha3.06). - -One can also create their own logging adapter by creating a class that implements [`NexusLoggingAdapter`](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/core/logger/adapters/NexusLoggingAdapter.java) and routing Nexus to use those methods. -```java -Nexus.setLogger(); -``` +## Bots using Nexus +Nexus is used in production by Discord bots, such as: +- [Beemo](https://beemo.gg): An anti-raid Discord bot that prevents raids on many large servers. +- [Amelia-chan](https://github.com/Amelia-chan/Amelia): A simple RSS Discord bot for the novel site, ScribbleHub. +- [Threadscore](https://threadscore.mihou.pw): Gamifying Q&A for Discord. -### ⏰ Rate-limiting -You can rate-limit or add cooldowns to commands by including a simple `Duration cooldown = ...` field onto your command and including the `NexusCommonInterceptors.NEXUS_RATELIMITER` onto the list of middlewares of a command. The rate-limiter is made into a middleware to allow for custom implementations with the cooldown accessible from the [`NexusCommand`](https://github.com/ShindouMihou/Nexus/blob/master/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommand.java) instance directly. +If you want to add your bot to the list, feel free to add it by creating a pull request! -## 📰 Pagination -Nexus includes a simple pagination implementation which one can use easily, you can view an example implementation of this from the examples below: -- [Poem Command](https://github.com/ShindouMihou/Nexus/blob/master/examples/PoemCommand.java) +## Features +Nexus was created from the ground up to power Discord bots with a simplistic yet flexible developer experience without compromising +on performance, allowing developers to build their Discord bots fast and clean. +- [x] **Object-based commands** +- [x] **Object-based context menus** +- [x] **Middlewares, Afterwares** +- [x] **Supports auto-deferring of responses** +- [x] **Flexible command synchronization system** +- [x] **Supports optional validation and subcommand routers** +- [x] **Clean, developer-oriented API** +- [x] **Supports persistent slash command indexes, and many more** +- [x] **Supports different pagination systems** -# 🌇 Nexus is used by -- [Mana](https://manabot.fun): The original reason for Nexus' creation, Mana is an anime Discord bot that brings anime into communities. -- [Amelia-chan](https://github.com/Amelia-chan/Amelia): A specialized RSS feed bot created for the ScribbleHub novel platform. -- More to be added, feel free to create an issue if you want to add yours here! +We recommend reading the [`GitHub Wiki`](https://github.com/ShindouMihou/Nexus/wiki) to learn more about the different features of Nexus. -# 📚 License -Nexus follows Apache 2.0 license which allows the following permissions: -- ✔ Commercial Use -- ✔ Modification -- ✔ Distribution -- ✔ Patent use -- ✔ Private use +## License -The contributors and maintainers of Nexus are not to be held liability over any creations that uses Nexus. We also forbid trademark use of -the library and there is no warranty as stated by Apache 2.0 license. You can read more about the Apache 2.0 license on [GitHub](https://github.com/ShindouMihou/Nexus/blob/master/LICENSE). +Nexus is distributed under the Apache 2.0 license, the same one used by [Javacord](https://github.com/Javacord/Javacord). See [**LICENSE**](LICENSE) for more information. diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..8bede088 --- /dev/null +++ b/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.9.10' + id 'java' +} + +group = 'pw.mihou' +version = '1.0.0-beta' +description = 'Nexus is the next-generation Javacord framework that aims to create Discord bots with less code, dynamic, more simplicity and beauty.' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.javacord:javacord:3.8.0' + implementation 'org.slf4j:slf4j-api:2.0.3' + testImplementation 'org.jetbrains.kotlin:kotlin-test' + compileOnly 'com.google.code.findbugs:jsr305:3.0.2' +} + +test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + compilerOptions { + freeCompilerArgs.add('-Xjvm-default=all') + } +} \ No newline at end of file diff --git a/examples/authentication/permissions/PingCommand.java b/examples/authentication/permissions/PingCommand.java deleted file mode 100644 index d7d950a2..00000000 --- a/examples/authentication/permissions/PingCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package pw.mihou.nexus.commands; - -import org.javacord.api.entity.permission.PermissionType; -import pw.mihou.nexus.core.reflective.annotations.Share; -import pw.mihou.nexus.features.command.facade.NexusCommandEvent; -import pw.mihou.nexus.features.command.facade.NexusHandler; -import pw.mihou.nexus.features.command.interceptors.commons.NexusCommonInterceptors; - -import java.util.List; - -public class PingCommand implements NexusHandler { - - private final String name = "ping"; - private final String description = "An example of how a command can share custom fields to a middleware, etc."; - - private final List middlewares = List.of(NexusCommonInterceptors.NEXUS_AUTH_PERMISSIONS_MIDDLEWARE); - @Share private final List requiredPermissions = List.of(PermissionType.ATTACH_FILE); - - @Override - public void onEvent(NexusCommandEvent event) { - event.respondNow().setContent("Pong!").respond(); - } -} \ No newline at end of file diff --git a/examples/authentication/roles/PingCommand.java b/examples/authentication/roles/PingCommand.java deleted file mode 100644 index 44c4b4fa..00000000 --- a/examples/authentication/roles/PingCommand.java +++ /dev/null @@ -1,22 +0,0 @@ -package pw.mihou.nexus.commands; - -import pw.mihou.nexus.core.reflective.annotations.Share; -import pw.mihou.nexus.features.command.facade.NexusCommandEvent; -import pw.mihou.nexus.features.command.facade.NexusHandler; -import pw.mihou.nexus.features.command.interceptors.commons.NexusCommonInterceptors; - -import java.util.List; - -public class PingCommand implements NexusHandler { - - private final String name = "ping"; - private final String description = "An example of how a command can share custom fields to a middleware, etc."; - - private final List middlewares = List.of(NexusCommonInterceptors.NEXUS_AUTH_ROLES_MIDDLEWARE); - @Share private final List requiredRoles = List.of(949964174505689149L); - - @Override - public void onEvent(NexusCommandEvent event) { - event.respondNow().setContent("Pong!").respond(); - } -} \ No newline at end of file diff --git a/examples/authentication/user/PingCommand.java b/examples/authentication/user/PingCommand.java deleted file mode 100644 index cf695132..00000000 --- a/examples/authentication/user/PingCommand.java +++ /dev/null @@ -1,22 +0,0 @@ -package pw.mihou.nexus.commands; - -import pw.mihou.nexus.core.reflective.annotations.Share; -import pw.mihou.nexus.features.command.facade.NexusCommandEvent; -import pw.mihou.nexus.features.command.facade.NexusHandler; -import pw.mihou.nexus.features.command.interceptors.commons.NexusCommonInterceptors; - -import java.util.List; - -public class PingCommand implements NexusHandler { - - private final String name = "ping"; - private final String description = "An example of how a command can share custom fields to a middleware, etc."; - - private final List middlewares = List.of(NexusCommonInterceptors.NEXUS_AUTH_USER_MIDDLEWARE); - @Share private final List requiredUsers = List.of(584322030934032393L); - - @Override - public void onEvent(NexusCommandEvent event) { - event.respondNow().setContent("Pong!").respond(); - } -} \ No newline at end of file diff --git a/examples/interceptors/AnonymousInterceptors.java b/examples/interceptors/AnonymousInterceptors.java index 0d3feb1e..10c31094 100644 --- a/examples/interceptors/AnonymousInterceptors.java +++ b/examples/interceptors/AnonymousInterceptors.java @@ -13,7 +13,7 @@ public class AnonymousInterceptors implements NexusHandler { private final String description = "An example of how a command can share custom fields to a middleware, etc."; private final List middlewares = List.of( - NexusCommandInterceptor.middleware((event) -> event.stopIf(event.getServer().isEmpty())) + Nexus.getInterceptors().middleware((event) -> event.stopIf(event.getServer().isEmpty())) ); @Override diff --git a/examples/interceptors/InlineNamedMiddlewares.java b/examples/interceptors/InlineNamedMiddlewares.java index c755a745..e9a49643 100644 --- a/examples/interceptors/InlineNamedMiddlewares.java +++ b/examples/interceptors/InlineNamedMiddlewares.java @@ -12,7 +12,7 @@ public class InlineNamedMiddlewares implements NexusHandler { private final String description = "An example of how a command can share custom fields to a middleware, etc."; private final List middlewares = List.of( - NexusCommandInterceptor.addMiddleware("nexus.auth.server", (event) -> event.stopIf(event.getServer().isEmpty())) + Nexus.getInterceptors().middleware("nexus.auth.server", (event) -> event.stopIf(event.getServer().isEmpty())) ); @Override diff --git a/examples/option_validators/OptionValidators.kt b/examples/option_validators/OptionValidators.kt new file mode 100644 index 00000000..a217a9eb --- /dev/null +++ b/examples/option_validators/OptionValidators.kt @@ -0,0 +1,13 @@ +package pw.mihou.nexus.features.command.validation.examples + +import pw.mihou.nexus.features.command.validation.OptionValidation +import pw.mihou.nexus.features.command.validation.errors.ValidationError + +object OptionValidators { + val PING_PONG_VALIDATOR = OptionValidation.create( + validator = { option -> option.equals("ping", ignoreCase = true) || option.equals("pong", ignoreCase = true) }, + error = { ValidationError.create("❌ You must either use `ping` or `pong` as an answer!") }, + // If you want to support optional values then you must add the line below. + // requirements = OptionValidation.createRequirements { nonNull = null } + ) +} \ No newline at end of file diff --git a/examples/option_validators/PingCommand.kt b/examples/option_validators/PingCommand.kt new file mode 100644 index 00000000..449d8cf3 --- /dev/null +++ b/examples/option_validators/PingCommand.kt @@ -0,0 +1,23 @@ +package pw.mihou.nexus.features.command.validation.examples + +import org.javacord.api.interaction.SlashCommandOption +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler + +object PingCommand: NexusHandler { + val name = "ping" + val description = "Does a ping-pong based on your answer." + + val options = NexusCommand.createOptions( + SlashCommandOption.createStringOption("answer", "It must be either ping or pong!", true) + ) + + val validators = NexusCommand.createValidators( + OptionValidators.PING_PONG_VALIDATOR.withCollector { event -> event.interaction.getArgumentStringValueByName("answer") } + ) + + override fun onEvent(event: NexusCommandEvent) { + event.respondNowWith("Just kidding! Your answer didn't matter in the first place!") + } +} \ No newline at end of file diff --git a/examples/router/PingCommand.kt b/examples/router/PingCommand.kt new file mode 100644 index 00000000..ccd3bc1f --- /dev/null +++ b/examples/router/PingCommand.kt @@ -0,0 +1,27 @@ +package pw.mihou.nexus.features.command.router.example + +import org.javacord.api.interaction.SlashCommandOption +import org.javacord.api.interaction.SlashCommandOptionType +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.command.router.SubcommandRouter + +object PingCommand: NexusHandler { + private val name = "ping" + private val description = "Pings and pongs!" + + private val options = listOf( + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "pong", "Performs a pong!"), + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "ping", "Performs a ping!") + ) + + private val router = SubcommandRouter.create { + route("ping", PingSubcommand) + route("pong", PongSubcommand) + } + + override fun onEvent(event: NexusCommandEvent) = router.accept(event) { + // Optionally you can add additional shared data such as this: + event.store("message", "Ping pong! You can add stuff like this here!") + } +} \ No newline at end of file diff --git a/examples/router/PingSubcommand.kt b/examples/router/PingSubcommand.kt new file mode 100644 index 00000000..838bbad7 --- /dev/null +++ b/examples/router/PingSubcommand.kt @@ -0,0 +1,12 @@ +package pw.mihou.nexus.features.command.router.example + +import org.javacord.api.interaction.SlashCommandInteractionOption +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.router.types.Routeable + +object PingSubcommand: Routeable { + override fun accept(event: NexusCommandEvent, option: SlashCommandInteractionOption) { + val message = event["message", String::class.java] + event.respondNowWith("Pong! $message") + } +} \ No newline at end of file diff --git a/examples/router/PongSubcommand.kt b/examples/router/PongSubcommand.kt new file mode 100644 index 00000000..eef57432 --- /dev/null +++ b/examples/router/PongSubcommand.kt @@ -0,0 +1,12 @@ +package pw.mihou.nexus.features.command.router.example + +import org.javacord.api.interaction.SlashCommandInteractionOption +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.router.types.Routeable + +object PongSubcommand: Routeable { + override fun accept(event: NexusCommandEvent, option: SlashCommandInteractionOption) { + val message = event["message", String::class.java] + event.respondNowWith("Ping! $message") + } +} \ No newline at end of file diff --git a/examples/shared_fields/PingCommand.java b/examples/shared_fields/PingCommand.java index 07d49fa1..c7881a8f 100644 --- a/examples/shared_fields/PingCommand.java +++ b/examples/shared_fields/PingCommand.java @@ -15,6 +15,6 @@ public class PingCommand implements NexusHandler { @Override public void onEvent(NexusCommandEvent event) { - event.respondNow().setContent("Pong!").respond(); + event.respondNowWith("Pong!"); } } \ No newline at end of file diff --git a/examples/synchronization/Main.java b/examples/synchronization/Main.java index f457e0d0..b909944a 100644 --- a/examples/synchronization/Main.java +++ b/examples/synchronization/Main.java @@ -1,7 +1,6 @@ -package pw.mihou.nexus; - import org.javacord.api.DiscordApiBuilder; import org.javacord.api.util.logging.ExceptionLogger; +import pw.mihou.nexus.Nexus; import pw.mihou.nexus.core.threadpool.NexusThreadPool; import pw.mihou.nexus.features.command.facade.NexusCommand; @@ -10,17 +9,14 @@ public class Test { - private static final Nexus nexus = Nexus.builder().build(); - public static void main(String[] args) { - nexus.listenMany(new AGlobalCommand(), new ASpecificServerCommand()); - NexusCommand dynamic = nexus.createCommandFrom(new ADynamicCommand()); - + Nexus.commands(new AGlobalCommand(), new ASpecificServerCommand()); + NexusCommand dynamic = Nexus.getCommandManager().get("dynamic", null); new DiscordApiBuilder() .setToken(System.getenv("token")) .setAllIntents() .setTotalShards(4) - .addListener(nexus) + .addListener(Nexus.INSTANCE) .loginAllShards() .forEach(future -> future.thenAccept(discordApi -> { System.out.println("Shard " + discordApi.getCurrentShard() + " is now online."); @@ -32,31 +28,33 @@ public static void main(String[] args) { // and also allows Nexus to function more completely. // IMPORTANT IMPORTANT IMPORTANT IMPORTANT // ------------------ - nexus.getShardManager().put(discordApi); - + Nexus.getShardingManager().set(discordApi); }).exceptionally(ExceptionLogger.get())); //--------------------- // Global synchronization of all commands, recommended at startup. // This updates, creates or removes any commands that are missing, outdated or removed. //---------------------- - nexus.getSynchronizer() - .synchronize(4) - .thenAccept(unused -> System.out.println("Synchronization with Discord's and Nexus' command repository is now complete.")) - .exceptionally(ExceptionLogger.get()); + Nexus.getSynchronizer() + .synchronize() + .addFinalCompletionListener(unused -> System.out.println("Successsfully migrated all commands to Discord.")) + .addTaskErrorListener(exception -> { + System.out.println("An error occurred while trying to migrate commands to Discord: "); + exception.printStackTrace(); + }); //------------------ // Demonstration of dynamic server command updating. //----------------- NexusThreadPool.schedule(() -> { - dynamic.addSupportFor(853911163355922434L, 858685857511112736L); + dynamic.associate(853911163355922434L, 858685857511112736L); System.out.println("Attempting to perform dynamic updates..."); // We recommend using batch update if you performed more than 1 `addSupportFor` methods. // As batch update will update all of those command using only one request. - // batchUpdate(853911163355922434L, 4); - // batchUpdate(858685857511112736L, 4); + // batchUpdate(853911163355922434L); + // batchUpdate(858685857511112736L); // Single update, on the otherwise, allows multiple server ids but sends a single create or update // request for a command and doesn't scale well when done with many commands. @@ -64,13 +62,13 @@ public static void main(String[] args) { }, 1, TimeUnit.MINUTES); NexusThreadPool.schedule(() -> { - dynamic.removeSupportFor(853911163355922434L); + dynamic.disassociate(853911163355922434L); System.out.println("Attempting to perform dynamic updates..."); // The same information as earlier, batch update will update the entire server slash command list // which means it will remove any slash commands that are no longer supporting that server // and will update or create any slash commands that still support that server. - // batchUpdate(853911163355922434L, 4); + // batchUpdate(853911163355922434L); // Single delete is fine when you are only deleting one command on a pile of servers. singleDelete(dynamic, 4, 853911163355922434L); @@ -80,22 +78,21 @@ public static void main(String[] args) { /** * Updates, removes or creates any commands that are outdated, removed or missing. This is recommended * especially when you recently added support to a lot of servers. Not recommended on startup since - * {@link pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer#synchronize(int)} is more recommended for + * {@link pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer#synchronize()} is more recommended for * startup-related synchronization. * * @param serverId The server id to synchronize commands to. - * @param totalShards The total shards of the server. */ - private static void batchUpdate(long serverId, int totalShards) { - nexus.getSynchronizer() - .batchUpdate(serverId, totalShards) + private static void batchUpdate(long serverId) { + Nexus.getSynchronizer() + .batchUpdate(serverId) .thenAccept(unused -> System.out.println("A batch update was complete. [server="+serverId+"]")) .exceptionally(ExceptionLogger.get()); } /** * Updates a single command on one or many servers. This is practically the same as batch update but utilizes a more - * update or create approach whilst {@link Test#batchUpdate(long, int)} overrides the entire server slash command list + * update or create approach whilst {@link Test#batchUpdate(long)} overrides the entire server slash command list * with what Nexus knows. * * @param command The command to update on the specified servers. @@ -103,15 +100,18 @@ private static void batchUpdate(long serverId, int totalShards) { * @param serverIds The server ids to update the bot on. */ private static void singleUpdate(NexusCommand command, int totalShards, long... serverIds) { - nexus.getSynchronizer() + Nexus.getSynchronizer() .upsert(command, totalShards, serverIds) - .thenAccept(unused -> System.out.println("A batch upsert was complete. [servers="+ Arrays.toString(serverIds) +"]")) - .exceptionally(ExceptionLogger.get()); + .addFinalCompletionListener(unused -> System.out.println("A batch upsert was complete. [servers="+ Arrays.toString(serverIds) +"]")) + .addTaskErrorListener(exception -> { + System.out.println("An error occurred while trying to update commands: "); + exception.printStackTrace(); + }); } /** * Deletes a single command on one or many servers. This is practically the same as batch update but utilizes a more - * delete approach whilst {@link Test#batchUpdate(long, int)} overrides the entire server slash command list + * delete approach whilst {@link Test#batchUpdate(long)} overrides the entire server slash command list * with what Nexus knows. * * @param command The command to update on the specified servers. @@ -119,10 +119,13 @@ private static void singleUpdate(NexusCommand command, int totalShards, long... * @param serverIds The server ids to update the bot on. */ private static void singleDelete(NexusCommand command, int totalShards, long... serverIds) { - nexus.getSynchronizer() + Nexus.getSynchronizer() .delete(command, totalShards, serverIds) - .thenAccept(unused -> System.out.println("A batch delete was complete. [servers="+ Arrays.toString(serverIds) +"]")) - .exceptionally(ExceptionLogger.get()); + .addFinalCompletionListener(unused -> System.out.println("A batch delete was complete. [servers="+ Arrays.toString(serverIds) +"]")) + .addTaskErrorListener(exception -> { + System.out.println("An error occurred while trying to delete commands: "); + exception.printStackTrace(); + }); } } diff --git a/examples/synchronization/README.md b/examples/synchronization/README.md index 9fc07170..91c7796c 100644 --- a/examples/synchronization/README.md +++ b/examples/synchronization/README.md @@ -9,6 +9,13 @@ the batch override and performs single updates for cases where you aren't sure a You can view a full example of how this synchronization looks from the `Main.java` file on this folder. +> **Warning** +> Due to the nature of commands being able to have multiple servers, Nexus uses custom handling for Futures and this +> includes handling errors. To handle errors, please add `.addTaskErrorListener(...)` which is similar to `.exceptionally(...) +> while `.addTaskCompletionListener(...)` is similar to `.thenAccept(...)` although is scoped towards a single task. +> +> If you want to listen to the actual completion of all tasks, you have to use the `.addFinalTaskCompletionListener(...)` instead. + # 🥞 EngineX & Synchronization EngineX is a critical factor into the new synchronization system as it allows Nexus to queue any synchronization requests for a specific shard or for any shard available to take without the end-user finding a way where to place the synchronization method. It's workings are a bit complicated but diff --git a/examples/synchronization/commands/ADynamicCommand.java b/examples/synchronization/commands/ADynamicCommand.java index b365895e..b0952132 100644 --- a/examples/synchronization/commands/ADynamicCommand.java +++ b/examples/synchronization/commands/ADynamicCommand.java @@ -12,14 +12,12 @@ public class ADynamicCommand implements NexusHandler { // 0L is recognized by Nexus as a switch to recognize this command as // a server slash command. It is ignored in any sort of updates. - private final List serverIds = List.of( - 0L - ); + // + // For more verbosity, Nexus has this as a public static field. + private final List serverIds = NexusCommand.with(NexusCommand.PLACEHOLDER_SERVER_ID); @Override public void onEvent(NexusCommandEvent event) { - event.respondNow() - .setContent("Dyna-dynam-iteee!") - .respond(); + event.respondNowWith("Dyna-dynam-iteee!"); } } diff --git a/examples/synchronization/commands/AGlobalCommand.java b/examples/synchronization/commands/AGlobalCommand.java index 21004e25..6df20625 100644 --- a/examples/synchronization/commands/AGlobalCommand.java +++ b/examples/synchronization/commands/AGlobalCommand.java @@ -10,8 +10,6 @@ public class AGlobalCommand implements NexusHandler { @Override public void onEvent(NexusCommandEvent event) { - event.respondNow() - .setContent("Pong! Ping! Hello Global!") - .respond(); + event.respondNowWith("Pong! Ping! Hello Global!"); } } diff --git a/examples/synchronization/commands/ASpecificServerCommand.java b/examples/synchronization/commands/ASpecificServerCommand.java index 9d34b337..0e7d53e4 100644 --- a/examples/synchronization/commands/ASpecificServerCommand.java +++ b/examples/synchronization/commands/ASpecificServerCommand.java @@ -9,14 +9,10 @@ public class ASpecificServerCommand implements NexusHandler { private final String name = "specificServer"; private final String description = "This is a command dedicated to a specific server!"; - private final List serverIds = List.of( - 807084089013174272L - ); + private final List serverIds = NexusCommand.with(807084089013174272L); @Override public void onEvent(NexusCommandEvent event) { - event.respondNow() - .setContent("This command is dedicated to this server!") - .respond(); + event.respondNow("This command is dedicated to this server!"); } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..41d9927a Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..41dfb879 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + 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='"-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 100644 index 00000000..107acd32 --- /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="-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/jitpack.yml b/jitpack.yml index 75b8fdb9..3a4344f2 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,5 +1,3 @@ -jdk: - openjdk17 before_install: - - sdk install java 17.0.1-open - - sdk use java 17.0.1-open + - sdk install java 17.0.5-tem + - sdk use java 17.0.5-tem \ No newline at end of file diff --git a/pom.xml b/pom.xml deleted file mode 100755 index 2f6bfe41..00000000 --- a/pom.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - 4.0.0 - - pw.mihou - Nexus - 1.0.0-ALPHA4 - Nexus - Nexus is the next-generation Javacord framework that aims to create Discord bots with less code, dynamic, more simplicity and beauty. - https://github.com/ShindouMihou/Nexus - - - snapshots-repo - https://oss.sonatype.org/content/repositories/snapshots/ - - - - - Apache 2.0 License - https://github.com/ShindouMihou/Nexus/blob/master/LICENSE - repo - - - - - mihoushindou - Shindou Mihou - mihou@manabot.fun - Mana Network - https://manabot.fun - - developer - - +8 - - https://avatars.githubusercontent.com/u/69381903 - - - - - scm:git:git://github.com/ShindouMihou/Nexus.git - scm:git:git://github.com/ShindouMihou/Nexus.git - https://github.com/ShindouMihou/Nexus.git - HEAD - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - ossrh - https://s01.oss.sonatype.org/ - true - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - 17 - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.22.2 - - - - - - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots - - - - 17 - 17 - - - - - org.javacord - javacord - 3.5.0 - pom - - - org.slf4j - slf4j-api - 1.7.35 - - - com.google.code.findbugs - jsr305 - 3.0.2 - - - org.junit.jupiter - junit-jupiter - 5.8.2 - test - - - - diff --git a/src/main/java/pw/mihou/nexus/Nexus.java b/src/main/java/pw/mihou/nexus/Nexus.java deleted file mode 100755 index 8c9f3714..00000000 --- a/src/main/java/pw/mihou/nexus/Nexus.java +++ /dev/null @@ -1,169 +0,0 @@ -package pw.mihou.nexus; - -import org.javacord.api.listener.interaction.ButtonClickListener; -import org.javacord.api.listener.interaction.SlashCommandCreateListener; -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.core.builder.NexusBuilder; -import pw.mihou.nexus.core.configuration.core.NexusConfiguration; -import pw.mihou.nexus.core.enginex.facade.NexusEngineX; -import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter; -import pw.mihou.nexus.core.managers.NexusShardManager; -import pw.mihou.nexus.core.managers.facade.NexusCommandManager; -import pw.mihou.nexus.features.command.facade.NexusCommand; -import pw.mihou.nexus.features.command.responders.NexusResponderRepository; -import pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer; - -import java.util.List; - -public interface Nexus extends SlashCommandCreateListener, ButtonClickListener { - - /** - * This creates a new {@link NexusBuilder} which can be used to create - * a new {@link Nexus} instance. - * - * @return A new {@link NexusBuilder} instance. - */ - static NexusBuilder builder() { - return new NexusBuilder(); - } - - /** - * Sets the logging adapter that Nexus should use. - * - * @param adapter The logging adapter that Nexus should use. - */ - static void setLogger(NexusLoggingAdapter adapter) { - NexusCore.logger = adapter; - } - - /** - * Retrieves the command manager that is being utilized by - * this {@link Nexus} instance. - * - * @return the {@link NexusCommandManager} instance that is being utilized - * by this {@link Nexus} instance. - */ - NexusCommandManager getCommandManager(); - - /** - * Retrieves the command synchronizer that is available for - * this {@link Nexus} instance. - * - * @return The command synchronizer that is usable by this - * {@link Nexus} instance. - */ - NexusSynchronizer getSynchronizer(); - - /** - * Retrieves the command responder repository that is responsible for - * handling cross-middleware and command responders. - * - * @return The command responder repository reasonable for this shard's - * cross-middleware and command responders. - */ - NexusResponderRepository getResponderRepository(); - - /** - * Retrieves the shard manager that is being utilized by - * this {@link Nexus} instance. - * - * @return The {@link NexusShardManager} instance that is being utilized by - * this {@link Nexus} instance. - */ - NexusShardManager getShardManager(); - - /** - * Retrieves the {@link NexusConfiguration} that is being utilized by - * this {@link Nexus} instance. - * - * @return The {@link NexusConfiguration} that is being utilized by this - * {@link Nexus} instance. - */ - NexusConfiguration getConfiguration(); - - /** - * Gets the queueing engine for this {@link Nexus} instance. - * - * @return The queueing engine of this instance. - */ - NexusEngineX getEngineX(); - - /** - * This creates a new command and attaches them if the annotation {@link pw.mihou.nexus.features.command.annotation.NexusAttach} is - * present on the model. - * - * @see Nexus#listenMany(Object...) - * @see Nexus#defineMany(Object...) - * @see Nexus#listenOne(Object) - * @see Nexus#defineOne(Object) - * @param model The model that will be used as a reference. - * @return The Nexus Command instance that is generated from the reference. - */ - @Deprecated - NexusCommand createCommandFrom(Object model); - - /** - * Molds one command from the reference provided, not to be confused with {@link Nexus#listenOne(Object)}, this does not - * enable the event dispatcher for this command and won't be listed in {@link NexusCommandManager}. This is intended for when you want - * to simply test the result of the command generation engine of Nexus. - * - * @param command The models used as a reference for the command definition. - * @return All the commands that were generated from the references provided. - */ - NexusCommand defineOne(Object command); - - /** - * Molds one command from the reference provided and allows events to be dispatched onto the command when an event - * is intended to be dispatched to the given command. - * - * @param command The model used as a reference for the command definition. - * @return All the commands that were generated from the references provided. - */ - NexusCommand listenOne(Object command); - - /** - * Molds many commands from the references provided, not to be confused with {@link Nexus#listenMany(Object...)}, this does not - * enable the event dispatcher for this command and won't be listed in {@link NexusCommandManager}. This is intended for when you want - * to simply test the result of the command generation engine of Nexus. - * - * @param commands The models used as a reference for the command definition. - * @return All the commands that were generated from the references provided. - */ - List defineMany(Object... commands); - - /** - * Molds many commands from the references provided and allows events to be dispatched onto the commands when an event - * is intended to be dispatched to the given command. - * - * @param commands The models used as a reference for the command definition. - * @return All the commands that were generated from the references provided. - */ - List listenMany(Object... commands); - - /** - * Adds a set of middlewares into the global middleware list which are pre-appended into - * the commands that are created after. - * - * @param middlewares The middlewares to add. - * @return {@link Nexus} for chain-calling methods. - */ - Nexus addGlobalMiddlewares(String... middlewares); - - /** - * Adds a set of afterwares into the global afterware list which are pre-appended into - * the commands that are created after. - * - * @param afterwares The afterwares to add. - * @return {@link Nexus} for chain-calling methods. - */ - Nexus addGlobalAfterwares(String... afterwares); - - /** - * This starts the Nexus instance and allow it to perform its - * necessary executions such as indexing. - * - * @return The {@link Nexus} instance. - */ - Nexus start(); - -} diff --git a/src/main/java/pw/mihou/nexus/Nexus.kt b/src/main/java/pw/mihou/nexus/Nexus.kt new file mode 100644 index 00000000..caef3e36 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/Nexus.kt @@ -0,0 +1,289 @@ +package pw.mihou.nexus + +import org.javacord.api.event.interaction.ButtonClickEvent +import org.javacord.api.event.interaction.MessageContextMenuCommandEvent +import org.javacord.api.event.interaction.SlashCommandCreateEvent +import org.javacord.api.event.interaction.UserContextMenuCommandEvent +import org.javacord.api.listener.interaction.ButtonClickListener +import org.javacord.api.listener.interaction.MessageContextMenuCommandListener +import org.javacord.api.listener.interaction.SlashCommandCreateListener +import org.javacord.api.listener.interaction.UserContextMenuCommandListener +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import pw.mihou.nexus.configuration.NexusConfiguration +import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter +import pw.mihou.nexus.core.logger.adapters.defaults.NexusConsoleLoggingAdapter +import pw.mihou.nexus.core.managers.core.NexusCommandManagerCore +import pw.mihou.nexus.core.managers.facade.NexusCommandManager +import pw.mihou.nexus.core.reflective.NexusReflection +import pw.mihou.nexus.core.threadpool.NexusThreadPool +import pw.mihou.nexus.express.NexusExpress +import pw.mihou.nexus.express.core.NexusExpressCore +import pw.mihou.nexus.features.command.core.NexusCommandCore +import pw.mihou.nexus.features.command.core.NexusCommandDispatcher +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.command.interceptors.NexusCommandInterceptors +import pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer +import pw.mihou.nexus.features.contexts.NexusContextMenu +import pw.mihou.nexus.features.contexts.core.NexusContextMenuDispatcher +import pw.mihou.nexus.features.contexts.facade.NexusContextMenuHandler +import pw.mihou.nexus.features.paginator.feather.NexusFeatherPaging +import pw.mihou.nexus.features.paginator.feather.core.NexusFeatherViewEventCore +import pw.mihou.nexus.features.paginator.feather.core.NexusFeatherViewPagerCore +import pw.mihou.nexus.sharding.NexusShardingManager + +object Nexus: SlashCommandCreateListener, ButtonClickListener, UserContextMenuCommandListener, MessageContextMenuCommandListener { + + /** + * The [NexusConfiguration] that is being used by this one and only instance of [Nexus]. It contains all the + * globally configurable parameters of Nexus and is recommended to be configured. + */ + @JvmStatic + val configuration = NexusConfiguration() + + /** + * A short-hand intended to be used by internal methods to receive direct access to the logger without + * navigating to the configuration. You shouldn't use this at all unless you want to send log messages that seem to + * be from Nexus. (it can get messy, don't recommend). + */ + @JvmStatic + @set:JvmName("setLogger") + var logger: NexusLoggingAdapter + get() = configuration.global.logger + set(value) { configuration.global.logger = value } + + /** + * [NexusExpress] is a local shard router that any developer including internal methods uses as a simple, straightforward + * router to route their different events, actions to specific or any available shards. + * + * You can learn more about this at [Understanding Nexus Express](https://github.com/ShindouMihou/Nexus/wiki/Nexus-Express-Way). + */ + @JvmStatic + val express: NexusExpress = NexusExpressCore() + + /** + * [NexusShardingManager] is the simple sharding manager of Nexus that keeps a record of all the shards that the bot has active. + * It is recommended and even required to set this up properly to have the framework running at its optimal and proper. + * + * You can learn more at [Understanding Nexus Sharding Manager](https://github.com/ShindouMihou/Nexus/wiki/Sharding-Manager). + */ + @JvmStatic + @get:JvmName("getShardingManager") + val sharding = NexusShardingManager() + + /** + * [NexusCommandManager] is the command manager of Nexus that holds knowledge of all the commands, identifiers and indexes of the framework. + * You can use this to index commands, extract indexes and many more related to commands that may be handy. + */ + @JvmStatic + val commandManager: NexusCommandManager = NexusCommandManagerCore() + + /** + * Global middlewares are middlewares that are prepended into the commands, these are first-order middlewares which means these are executed + * first before any second-order middlewares (e.g. ones specified by the command). + * + * You can learn more about middlewares at [Understanding Command Interceptors](https://github.com/ShindouMihou/Nexus/wiki/Understanding-Command-Interceptors). + */ + @JvmStatic + val globalMiddlewares: Set get() = configuration.global.middlewares + + /** + * Global afterwares are afterwares that are prepended into the commands, these are first-order afterwares which means these are executed + * first before any second-order afterwares (e.g. ones specified by the command). + * + * You can learn more about afterwares at [Understanding Command Interceptors](https://github.com/ShindouMihou/Nexus/wiki/Command-Interceptors). + */ + @JvmStatic + val globalAfterwares: Set get() = configuration.global.afterwares + + /** + * [NexusSynchronizer] is a tool that is used to synchronize commands between Discord and the bot. + * + * You can learn more about it at [Understanding Synchronization](https://github.com/ShindouMihou/Nexus/wiki/Command-Synchronization). + */ + @JvmStatic + val synchronizer = NexusSynchronizer() + + @get:JvmSynthetic + internal val launch get() = configuration.launch + + @JvmStatic + internal val launcher get() = configuration.launch.launcher + + /** + * [NexusCommandInterceptors] is the interface between the interceptor registry (middlewares and afterwares) and + * the [Nexus] interface, allowing simpler and more straightforward interceptor additions. + */ + @JvmStatic + val interceptors = NexusCommandInterceptors + + /** + * Configures the [NexusConfiguration] in a more Kotlin way. + * @param modifier the modifier to modify the state of the framework. + */ + @JvmStatic + @JvmSynthetic + fun configure(modifier: NexusConfiguration.() -> Unit): Nexus { + modifier(configuration) + return this + } + + /** + * Adds one or more commands onto the command manager. + * @param commands the commands to add to the command manager. + */ + @JvmStatic + fun commands(vararg commands: Any): List { + val list = mutableListOf() + + for (command in commands) { + list.add(manifest(command).apply { commandManager.add(this) }) + } + + return list + } + + /** + * Adds one or more context menus onto the command manager. + * @param contextMenus the context menus to add to the command manager. + */ + @JvmStatic + fun > contextMenus(vararg contextMenus: Any): List { + val list = mutableListOf() + + for (contextMenu in contextMenus) { + list.add(manifest(contextMenu).apply { commandManager.add(this) }) + } + + return list + } + + /** + * Adds one command onto the command manager. + * @param command the command to add to the command manager. + */ + @JvmStatic + fun command(command: Any): NexusCommand { + return manifest(command).apply { commandManager.add(this) } + } + + /** + * Adds one context menu onto the command manager. + * @param contextMenu the context menu to add to the command manager. + */ + @JvmStatic + fun > contextMenu(contextMenu: Any): NexusContextMenu { + return manifest(contextMenu).apply { commandManager.add(this) } + } + + /** + * Manifests a model into a [NexusCommand] instance by mapping all the fields that are understood by the + * engine into the instance. This doesn't auto-add the command into the manager. + * + * @param model the model to manifest into a [NexusCommand] instance. + * @return the [NexusCommand] instance that was manifested. + */ + @JvmStatic + fun manifest(model: Any): NexusCommand { + return (NexusReflection.copy(model, NexusCommandCore::class.java) as NexusCommand) + } + + /** + * Manifests a model into a [NexusContextMenu] instance by mapping all the fields that are understood by the + * engine into the instance. This doesn't auto-add the context menu into the manager. + * + * @param model the model to manifest into a [NexusContextMenu] instance. + * @return the [NexusContextMenu] instance that was manifested. + */ + @JvmStatic + fun > manifest(model: Any): NexusContextMenu { + return (NexusReflection.copy(model, NexusContextMenu::class.java) as NexusContextMenu) + } + + /** + * Adds one or more global middlewares. + * + * You can learn more about middlewares at [Understanding Command Interceptors](https://github.com/ShindouMihou/Nexus/wiki/Command-Interceptors). + * @see [globalMiddlewares] + */ + @JvmStatic + fun addGlobalMiddlewares(vararg names: String): Nexus { + configuration.global.middlewares.addAll(names) + return this + } + + /** + * Adds one or more global afterwares. + * + * You can learn more about afterwares at [Understanding Command Interceptors](https://github.com/ShindouMihou/Nexus/wiki/Command-Interceptors). + * @see [globalAfterwares] + */ + @JvmStatic + fun addGlobalAfterwares(vararg names: String): Nexus { + configuration.global.afterwares.addAll(names) + return this + } + + /** + * An internal method that is used to receive events from Javacord to dispatch to the right command. You should not + * use this method at all unless you want to send your own [SlashCommandCreateEvent] that you somehow managed to + * create. + * + * @param event The [SlashCommandCreateEvent] received from Javacord. + */ + override fun onSlashCommandCreate(event: SlashCommandCreateEvent) { + val command = (commandManager as NexusCommandManagerCore).acceptEvent(event) as NexusCommandCore? ?: return + launcher.launch { NexusCommandDispatcher.dispatch(command, event) } + } + + private val FEATHER_KEY_DELIMITER_REGEX = "\\[\\$;".toRegex() + + /** + * An internal method that is used to receive events from Javacord to dispatch to the right [NexusFeatherPaging]. You + * should not use this method at all unless you want to send your own [ButtonClickEvent] that you somehow managed + * to create. + * + * @param event The [ButtonClickEvent] received from Javacord. + */ + override fun onButtonClick(event: ButtonClickEvent) { + if (!event.buttonInteraction.customId.contains("[$;")) return + + val keys = event.buttonInteraction.customId.split(FEATHER_KEY_DELIMITER_REGEX, limit = 3) + if (keys.size < 3 || !NexusFeatherPaging.views.containsKey(keys[0])) return + + launcher.launch { + try { + NexusFeatherPaging.views[keys[0]]!! + .onEvent(NexusFeatherViewEventCore(event, NexusFeatherViewPagerCore(keys[1], keys[0]), keys[2])) + } catch (exception: Throwable) { + logger.error("An uncaught exception was received by Nexus Feather with the following stacktrace.", exception) + } + } + } + + /** + * An internal method that is used to receive events from Javacord to dispatch to the right context menu. You should not + * use this method at all unless you want to send your own [UserContextMenuCommandEvent] that you somehow managed to + * create. + * + * @param event The [UserContextMenuCommandEvent] received from Javacord. + */ + override fun onUserContextMenuCommand(event: UserContextMenuCommandEvent) { + val contextMenu = (commandManager as NexusCommandManagerCore).acceptEvent(event) ?: return + NexusContextMenuDispatcher.dispatch(event, contextMenu) + } + + /** + * An internal method that is used to receive events from Javacord to dispatch to the right context menu. You should not + * use this method at all unless you want to send your own [MessageContextMenuCommandEvent] that you somehow managed to + * create. + * + * @param event The [MessageContextMenuCommandEvent] received from Javacord. + */ + override fun onMessageContextMenuCommand(event: MessageContextMenuCommandEvent) { + val contextMenu = (commandManager as NexusCommandManagerCore).acceptEvent(event) ?: return + NexusContextMenuDispatcher.dispatch(event, contextMenu) + } + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/commons/Pair.java b/src/main/java/pw/mihou/nexus/commons/Pair.java deleted file mode 100755 index 5ca075fd..00000000 --- a/src/main/java/pw/mihou/nexus/commons/Pair.java +++ /dev/null @@ -1,72 +0,0 @@ -package pw.mihou.nexus.commons; - -import java.util.Objects; - -public class Pair { - - private final L left; - private final R right; - - /** - * Creates a new Pair of Key-Value or otherwise called, Left-Right object. - * This is used to hold two objects at the same time. - * - * @param left The left value. - * @param right The right value. - */ - public Pair(L left, R right) { - this.left = left; - this.right = right; - } - - /** - * Creates a new Pair of Key-Value or otherwise called, Left-Right object. - * This is used to hold two objects at the same time. - * - * @param left The value of the key (or the left item). - * @param right The value of the value (or the right item). - * @param The type of the key value. - * @param The type of the left value. - * @return A key-value or left-right object containing all the values. - */ - public static Pair of(L left, R right) { - return new Pair<>(left, right); - } - - /** - * Returns the key or the left value. - * - * @return The key or the left value. - */ - public L getLeft() { - return left; - } - - /** - * Returns the value or the right value. - * - * @return The value or the right value. - */ - public R getRight() { - return right; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Pair pair = (Pair) o; - return getLeft().equals(pair.getLeft()) && - getRight().equals(pair.getRight()); - } - - @Override - public int hashCode() { - return Objects.hash(getLeft(), getRight()); - } - - @Override - public String toString() { - return "Pair (" + left + ", " + right + ")"; - } -} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/configuration/NexusConfiguration.kt b/src/main/java/pw/mihou/nexus/configuration/NexusConfiguration.kt new file mode 100644 index 00000000..c64569f1 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/NexusConfiguration.kt @@ -0,0 +1,14 @@ +package pw.mihou.nexus.configuration + +import pw.mihou.nexus.configuration.modules.* + +class NexusConfiguration internal constructor() { + + @JvmField val express = NexusExpressConfiguration() + @JvmField val global = NexusGlobalConfiguration() + @JvmField val commonsInterceptors = NexusCommonsInterceptorsConfiguration() + @JvmField val loggingTemplates = NexusLoggingTemplatesConfiguration() + @JvmField val launch = NexusLaunchConfiguration() + @JvmField val interceptors = NexusInterceptorsConfiguration() + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsConfiguration.kt b/src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsConfiguration.kt new file mode 100644 index 00000000..6e451705 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsConfiguration.kt @@ -0,0 +1,5 @@ +package pw.mihou.nexus.configuration.modules + +class NexusCommonsInterceptorsConfiguration internal constructor() { + @JvmField val messages = NexusCommonsInterceptorsMessageConfiguration() +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsMessageConfiguration.kt b/src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsMessageConfiguration.kt new file mode 100644 index 00000000..4413fe9f --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsMessageConfiguration.kt @@ -0,0 +1,17 @@ +package pw.mihou.nexus.configuration.modules + +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.messages.NexusMessage + +class NexusCommonsInterceptorsMessageConfiguration internal constructor() { + + @set:JvmName("setRatelimitedMessage") + @get:JvmName("getRatelimitedMessage") + @Volatile + var ratelimited: (event: NexusCommandEvent, remainingSeconds: Long) -> NexusMessage = { _, remainingSeconds -> + NexusMessage.with(true) { + this.setContent("***SLOW DOWN***!\nYou are executing commands too fast, please try again in $remainingSeconds seconds.") + } + } + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/configuration/modules/NexusExpressConfiguration.kt b/src/main/java/pw/mihou/nexus/configuration/modules/NexusExpressConfiguration.kt new file mode 100644 index 00000000..863a1cd9 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusExpressConfiguration.kt @@ -0,0 +1,21 @@ +package pw.mihou.nexus.configuration.modules + +import java.time.Duration + +class NexusExpressConfiguration internal constructor() { + + /** + * Maximum timeout refers to the maximum amount of time that an express request should + * be kept waiting for a shard to be active, once the timeout is reached, the requests + * will be expired and cancelled. + */ + @Volatile + var maximumTimeout = Duration.ofMinutes(10) + + /** + * Whether to show warnings regarding requests in Express that have expired. + */ + @Volatile + var showExpiredWarnings = false + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/configuration/modules/NexusGlobalConfiguration.kt b/src/main/java/pw/mihou/nexus/configuration/modules/NexusGlobalConfiguration.kt new file mode 100644 index 00000000..e24f3a07 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusGlobalConfiguration.kt @@ -0,0 +1,57 @@ +package pw.mihou.nexus.configuration.modules + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.helpers.NOPLogger +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter +import pw.mihou.nexus.core.logger.adapters.defaults.NexusConsoleLoggingAdapter +import pw.mihou.nexus.core.logger.adapters.defaults.NexusDefaultLoggingAdapter + +class NexusGlobalConfiguration internal constructor() { + + /** + * A set that includes the names of the middlewares that will be included in the commands and processed + * prior to execution, these middlewares will take a higher precedence than the middlewares defined in + * the commands themselves. + * + * Note: This does not create any middlewares, rather it tells the dispatcher what middlewares to reference + * before processing the local middlewares. + */ + val middlewares: MutableSet = mutableSetOf() + + /** + * A set that includes the names of the afterwares that will be included in the commands and processed + * at the end of execution, these afterwares will take a higher precedence than the afterwares defined in + * the commands themselves. + * + * Note: This does not create any afterwares, rather it tells the dispatcher what afterwares to reference + * before processing the local afterwares. + */ + val afterwares: MutableSet = mutableSetOf() + + /** + * An adapter to help Nexus adopt the same way of logging that your application does. + * + * You can leave this as default if you are using an SLF4J-based logger such as Logback or + * if you are using a Log4j logger but with a SLF4J bridge as the default logging adapter uses SLF4J. + */ + @Volatile var logger: NexusLoggingAdapter = + if (LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) == null || LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) is NOPLogger) NexusConsoleLoggingAdapter() + else NexusDefaultLoggingAdapter() + + /** + * To enable global inheritance, wherein all the commands inherits properties from the class provided below, you can + * specify which class you want to be inherited and Nexus will take care of inheriting them. Including a global inheritance + * class would mean the properties of the global inheritance class will be inherited by children commands regardless of + * whether they have a local inheritance class. + */ + @JvmField @Volatile var inheritance: Any? = null + + /** + * When in an automatic defer situation, the framework will automatically defer the response when the time has + * surpassed the specified amount. You can specify this to any value less than 3,000 but the default value should + * be more than enough even when considering network latencies. + */ + @JvmField @Volatile var autoDeferAfterMilliseconds: Long = 2350 +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/configuration/modules/NexusInterceptorsConfiguration.kt b/src/main/java/pw/mihou/nexus/configuration/modules/NexusInterceptorsConfiguration.kt new file mode 100644 index 00000000..297480e0 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusInterceptorsConfiguration.kt @@ -0,0 +1,22 @@ +package pw.mihou.nexus.configuration.modules + +class NexusInterceptorsConfiguration internal constructor() { + /** + * Sets whether to defer middleware responses when the middlewares have reached processing time beyond + * [NexusGlobalConfiguration.autoDeferAfterMilliseconds]. + * This is only possible in middlewares because middlewares uses a custom method of responding. + * + * WARNING: You have to write your command to also use deferred responses. It is solely your responsibility to + * ensure that whichever commands uses middlewares that would take longer than the specified [NexusGlobalConfiguration.autoDeferAfterMilliseconds] + * has to use deferred responses. + */ + @Volatile + @set:JvmName("setAutoDeferMiddlewareResponses") + @get:JvmName("autoDeferMiddlewareResponses") + var autoDeferMiddlewareResponses = false + + @Volatile + @set:JvmName("setAutoDeferAsEphemeral") + @get:JvmName("autoDeferAsEphemeral") + var autoDeferAsEphemeral = true +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/configuration/modules/NexusLaunchConfiguration.kt b/src/main/java/pw/mihou/nexus/configuration/modules/NexusLaunchConfiguration.kt new file mode 100644 index 00000000..f043eed0 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusLaunchConfiguration.kt @@ -0,0 +1,34 @@ +package pw.mihou.nexus.configuration.modules + +import pw.mihou.nexus.core.threadpool.NexusThreadPool +import java.util.concurrent.TimeUnit + +class NexusLaunchConfiguration internal constructor() { + @JvmField var launcher: NexusLaunchWrapper = NexusLaunchWrapper { task -> + NexusThreadPool.executorService.submit { task.run() } + } + @JvmField var scheduler: NexusScheduledLaunchWrapper = NexusScheduledLaunchWrapper { timeInMillis, task -> + return@NexusScheduledLaunchWrapper object: Cancellable { + val scheduledTask = NexusThreadPool.scheduledExecutorService.schedule(task::run, timeInMillis, TimeUnit.MILLISECONDS) + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + return scheduledTask.cancel(mayInterruptIfRunning) + } + } + } +} + +fun interface NexusLaunchWrapper { + fun launch(task: NexusLaunchTask) +} + +fun interface NexusScheduledLaunchWrapper { + fun launch(timeInMillis: Long, task: NexusLaunchTask): Cancellable +} + +interface Cancellable { + fun cancel(mayInterruptIfRunning: Boolean): Boolean +} + +fun interface NexusLaunchTask { + fun run() +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/configuration/modules/NexusLoggingTemplatesConfiguration.kt b/src/main/java/pw/mihou/nexus/configuration/modules/NexusLoggingTemplatesConfiguration.kt new file mode 100644 index 00000000..033d9ead --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusLoggingTemplatesConfiguration.kt @@ -0,0 +1,49 @@ +package pw.mihou.nexus.configuration.modules + +import org.javacord.api.interaction.ApplicationCommand +import org.javacord.api.interaction.SlashCommand +import pw.mihou.nexus.Nexus + +class NexusLoggingTemplatesConfiguration internal constructor() { + + @get:JvmSynthetic + @set:JvmName("setGlobalCommandsSynchronizedMessage") + var GLOBAL_COMMANDS_SYNCHRONIZED: (commands: Set) -> String = + { commands -> "All global commands have been pushed to Discord successfully. {size=" + commands.size + "}" } + + @get:JvmSynthetic + @set:JvmName("setServerCommandsSynchronizedMessage") + var SERVER_COMMANDS_SYNCHRONIZED: (server: Long, commands: Set) -> String = + { server, commands -> "All server commands for $server has been pushed to Discord successfully. {size=" + commands.size + "}"} + + @get:JvmSynthetic + @set:JvmName("setServerCommandDeletedMessage") + var SERVER_COMMAND_DELETED: (server: Long, command: SlashCommand) -> String = + { server, command -> command.name + " server command has been removed. {server=$server}" } + + @get:JvmSynthetic + @set:JvmName("setServerCommandUpdatedMessage") + var SERVER_COMMAND_UPDATED: (server: Long, command: SlashCommand) -> String = + { server, command -> command.name + " server command has been updated. {server=$server}" } + + @get:JvmSynthetic + @set:JvmName("setServerCommandCreatedMessage") + var SERVER_COMMAND_CREATED: (server: Long, command: SlashCommand) -> String = + { server, command -> command.name + " server command has been created. {server=$server}" } + + @get:JvmSynthetic + @set:JvmName("setCommandsIndexedMessage") + var COMMANDS_INDXED: (timeTakenInMilliseconds: Long) -> String = + { millis -> "All commands have been indexed and stored in the index store. {timeTaken=$millis}" } + + @get:JvmSynthetic + @set:JvmName("setIndexingCommandsMessage") + var INDEXING_COMMANDS = "All commands are now being queued for indexing, this will take some time especially with large bots, but will allow for " + + "server-specific slash command mentions, faster and more precise command matching..." + +} + +@JvmSynthetic +internal fun String.debug() = Nexus.logger.debug(this) +@JvmSynthetic +internal fun String.info() = Nexus.logger.info(this) \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/NexusCore.java b/src/main/java/pw/mihou/nexus/core/NexusCore.java deleted file mode 100755 index b1cfbf12..00000000 --- a/src/main/java/pw/mihou/nexus/core/NexusCore.java +++ /dev/null @@ -1,230 +0,0 @@ -package pw.mihou.nexus.core; - -import org.javacord.api.DiscordApi; -import org.javacord.api.DiscordApiBuilder; -import org.javacord.api.event.interaction.ButtonClickEvent; -import org.javacord.api.event.interaction.SlashCommandCreateEvent; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.core.configuration.core.NexusConfiguration; -import pw.mihou.nexus.core.enginex.core.NexusEngineXCore; -import pw.mihou.nexus.core.enginex.facade.NexusEngineX; -import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter; -import pw.mihou.nexus.core.logger.adapters.defaults.NexusDefaultLoggingAdapter; -import pw.mihou.nexus.core.managers.core.NexusCommandManagerCore; -import pw.mihou.nexus.core.managers.NexusShardManager; -import pw.mihou.nexus.core.managers.facade.NexusCommandManager; -import pw.mihou.nexus.core.reflective.NexusReflectiveCore; -import pw.mihou.nexus.core.threadpool.NexusThreadPool; -import pw.mihou.nexus.features.command.core.NexusCommandDispatcher; -import pw.mihou.nexus.features.command.core.NexusCommandCore; -import pw.mihou.nexus.features.command.facade.NexusCommand; -import pw.mihou.nexus.features.command.interceptors.commons.core.NexusCommonInterceptorsCore; -import pw.mihou.nexus.features.command.interceptors.facades.NexusCommandInterceptor; -import pw.mihou.nexus.features.command.responders.NexusResponderRepository; -import pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer; -import pw.mihou.nexus.features.messages.defaults.NexusDefaultMessageConfiguration; -import pw.mihou.nexus.features.messages.facade.NexusMessageConfiguration; -import pw.mihou.nexus.features.paginator.feather.NexusFeatherPaging; -import pw.mihou.nexus.features.paginator.feather.core.NexusFeatherViewEventCore; -import pw.mihou.nexus.features.paginator.feather.core.NexusFeatherViewPagerCore; - - -import java.util.*; -import java.util.function.Consumer; - -public class NexusCore implements Nexus { - - private final NexusCommandManagerCore commandManager = new NexusCommandManagerCore(this); - private NexusShardManager shardManager; - public static NexusLoggingAdapter logger = new NexusDefaultLoggingAdapter(); - private final NexusMessageConfiguration messageConfiguration; - private final List globalMiddlewares = new ArrayList<>(); - private final List globalAfterwares = new ArrayList<>(); - private final DiscordApiBuilder builder; - private final Consumer onShardLogin; - private final NexusConfiguration nexusConfiguration; - private final NexusEngineX engineX = new NexusEngineXCore(this); - private final NexusSynchronizer synchronizer = new NexusSynchronizer(this); - private final NexusResponderRepository responderRepository = new NexusResponderRepository(); - - /** - * Creates a new Nexus Core with a customized {@link NexusMessageConfiguration} and - * default specifications. - * - * @param messageConfiguration The message configuration to use. - * @param builder The builder when creating a new {@link DiscordApi} instance. - * @param onShardLogin This is executed everytime a shard logins. - */ - public NexusCore( - NexusMessageConfiguration messageConfiguration, - DiscordApiBuilder builder, - Consumer onShardLogin, - NexusConfiguration nexusConfiguration - ) { - this.builder = builder; - this.onShardLogin = onShardLogin; - this.shardManager = new NexusShardManager(this); - this.nexusConfiguration = nexusConfiguration; - this.messageConfiguration = Objects.requireNonNullElseGet(messageConfiguration, NexusDefaultMessageConfiguration::new); - NexusCommandInterceptor.addRepository(new NexusCommonInterceptorsCore()); - } - - @Override - public NexusCommandManager getCommandManager() { - return commandManager; - } - - @Override - public NexusSynchronizer getSynchronizer() { - return synchronizer; - } - - @Override - public NexusResponderRepository getResponderRepository() { - return responderRepository; - } - - @Override - public NexusShardManager getShardManager() { - return shardManager; - } - - @Override - public NexusConfiguration getConfiguration() { - return nexusConfiguration; - } - - @Override - public NexusEngineX getEngineX() { - return engineX; - } - - @Override - @Deprecated(forRemoval = true) - public NexusCommand createCommandFrom(Object model) { - return listenOne(model); - } - - @Override - public NexusCommand defineOne(Object command) { - return NexusReflectiveCore.command(command, this); - } - - @Override - public NexusCommand listenOne(Object command) { - NexusCommand definition = defineOne(command); - getCommandManager().addCommand(definition); - - return definition; - } - - @Override - public List defineMany(Object... commands) { - return Arrays.stream(commands) - .map(reference -> ((NexusCommand) NexusReflectiveCore.command(reference, this))) - .toList(); - } - - @Override - public List listenMany(Object... commands) { - List definitions = defineMany(commands); - definitions.forEach(definition -> getCommandManager().addCommand(definition)); - - return definitions; - } - - /** - * Gets the list of global middlewares that are pre-appended into - * commands that are registered. - * - * @return The list of global middlewares. - */ - public List getGlobalMiddlewares() { - return globalMiddlewares; - } - - /** - * Gets the list of global afterwares that are pre-appended into - * commands that are registered. - * - * @return The list of global afterwares. - */ - public List getGlobalAfterwares() { - return globalAfterwares; - } - - @Override - public Nexus addGlobalMiddlewares(String... middlewares) { - globalMiddlewares.addAll(Arrays.asList(middlewares)); - return this; - } - - @Override - public Nexus addGlobalAfterwares(String... afterwares) { - globalAfterwares.addAll(Arrays.asList(afterwares)); - return this; - } - - @Override - public Nexus start() { - if (builder != null && onShardLogin != null) { - List shards = new ArrayList<>(); - builder.addListener(this) - .loginAllShards() - .forEach(future -> future.thenAccept(shards::add).join()); - - this.shardManager = new NexusShardManager( - this, - shards.stream() - .sorted(Comparator.comparingInt(DiscordApi::getCurrentShard)) - .toArray(DiscordApi[]::new) - ); - - // The shard startup should only happen once all the shards are connected. - getShardManager().asStream().forEachOrdered(onShardLogin); - } - - return this; - } - - - @Override - public void onSlashCommandCreate(SlashCommandCreateEvent event) { - commandManager - .acceptEvent(event) - .map(nexusCommand -> (NexusCommandCore) nexusCommand) - .ifPresent(nexusCommand -> - NexusThreadPool.executorService.submit(() -> - NexusCommandDispatcher.dispatch(nexusCommand, event) - ) - ); - } - - /** - * An internal method which is used by the command handler to retrieve the message - * configuration that is being utilized by this instance. - * - * @return The {@link NexusMessageConfiguration} that is being utilized by Nexus. - */ - public NexusMessageConfiguration getMessageConfiguration() { - return messageConfiguration; - } - - @Override - public void onButtonClick(ButtonClickEvent event) { - if (!event.getButtonInteraction().getCustomId().contains("[$;")) return; - - String[] keys = event.getButtonInteraction().getCustomId().split("\\[\\$;", 3); - if (keys.length < 3 || !NexusFeatherPaging.views.containsKey(keys[0])) return; - - NexusThreadPool.executorService.submit(() -> { - try { - NexusFeatherPaging.views.get(keys[0]) - .onEvent(new NexusFeatherViewEventCore(event, new NexusFeatherViewPagerCore(keys[1], keys[0]), keys[2])); - } catch (Throwable exception) { - logger.error("An uncaught exception was received by Nexus Feather with the following stacktrace."); - exception.printStackTrace(); - } - }); - } -} diff --git a/src/main/java/pw/mihou/nexus/core/async/NexusLaunchable.kt b/src/main/java/pw/mihou/nexus/core/async/NexusLaunchable.kt new file mode 100644 index 00000000..c121f902 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/async/NexusLaunchable.kt @@ -0,0 +1,195 @@ +package pw.mihou.nexus.core.async + +import pw.mihou.nexus.Nexus +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Launchables is an asynchronous task that can have multiple completion stages and exceptions handle-able contrary to the + * one that [CompletableFuture] has. It's a more complex variant that is designed to support multiple tasks inside a single asynchronous + * task. + * + * A launchable has two stages of completion: + * - non-final completion is when the task calls a `complete()` that can indicate that a task inside the task has completed with a result. + * - final completion is when the task itself completes and is actually called by the launchable itself and returns the result of the task. + * + * Due to this, a task exception listener can also indicate that one of the tasks had an exception or the main task itself had an exception. + * + * A key part to remember when using the launchable is that secondary exceptions will not cause `.join()` to throw an exception, but primary + * exceptions (one that is caught by the final task completion or uncaught exceptions) will throw. + */ +class NexusLaunchable + (private val task: NexusLaunchableStack.NexusLaunchableStackSignal.() -> PrimaryResult) { + + private val stack: NexusLaunchableStack = NexusLaunchableStack() + private val finalCompletionStack: MutableList> = CopyOnWriteArrayList() + + private val finalFuture = CompletableFuture() + + init { + finalCompletionStack.add { finalFuture.complete(null) } + Nexus.configuration.launch.launcher.launch { + try { + val result = task(stack.stackSingal) + + Nexus.configuration.launch.launcher.launch { + for (stack in finalCompletionStack) { + try { + stack.on(result) + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught in a launchable.", exception) + } + } + } + } catch (exception: Exception) { + finalFuture.completeExceptionally(exception) + if (stack.taskErrorStack.isEmpty()) { + Nexus.logger.error("An uncaught exception was caught in a launchable.", exception) + return@launch + } + + stack.stackSingal.error(exception) + } + } + } + + /** + * Adds one or more non-final task completion listeners to the launchable. + * Not to be confused with [addFinalCompletionListeners] which adds one or more **final** task completion listeners + * that are triggered at the very end of the task. + * + * **Task completion and final task completion are different** in that **task completion can be triggered multiple times + * when the task itself calls the complete method** while **final task completion occurs at the end of the function itself**. + * + * @param launchables the tasks to execute. + */ + fun addTaskCompletionListeners(launchables: List>): NexusLaunchable { + stack.taskCompletionStack.addAll(launchables) + return this + } + + /** + * Adds one or more final task completion listeners to the launchable. + * Not to be confused with [addTaskCompletionListener] which adds one or more **non-final** task completion listeners + * that are triggered anytime the task itself calls a complete. + * + * **Task completion and final task completion are different** in that **task completion can be triggered multiple times + * when the task itself calls the complete method** while **final task completion occurs at the end of the function itself**. + * + * @param launchables the tasks to execute. + */ + fun addFinalCompletionListeners(launchables: List>): NexusLaunchable { + finalCompletionStack.addAll(launchables) + return this + } + + /** + * Adds one or more task error listeners to the launchable, there can be more than one times when the launchables are + * called. If an exception is uncaught and reaches the final task completion, it will call this otherwise console logs + * the exception. + * + * @param launchables the tasks to execute. + */ + fun addTaskErrorListeners(launchables: List>): NexusLaunchable { + stack.taskErrorStack.addAll(launchables) + return this + } + + /** + * Adds one non-final task completion listeners to the launchable. + * Not to be confused with [addFinalCompletionListener] which adds one **final** task completion listeners + * that is triggered at the very end of the task. + * + * **Task completion and final task completion are different** in that **task completion can be triggered multiple times + * when the task itself calls the complete method** while **final task completion occurs at the end of the function itself**. + * + * @param launchable the task to execute. + */ + fun addTaskCompletionListener(launchable: NexusLaunchableCallback): NexusLaunchable { + stack.taskCompletionStack.add(launchable) + return this + } + + /** + * Adds one or more final task completion listeners to the launchable. + * Not to be confused with [addTaskCompletionListener] which adds one **non-final** task completion listeners + * that is triggered anytime the task itself calls a complete. + * + * **Task completion and final task completion are different** in that **task completion can be triggered multiple times + * when the task itself calls the complete method** while **final task completion occurs at the end of the function itself**. + * + * @param launchable the tasks to execute. + */ + fun addFinalCompletionListener(launchable: NexusLaunchableCallback): NexusLaunchable { + finalCompletionStack.add(launchable) + return this + } + + /** + * Adds one task error listeners to the launchable, there can be more than one times when the launchables are + * called. If an exception is uncaught and reaches the final task completion, it will call this otherwise console logs + * the exception. + * + * @param launchables the tasks to execute. + */ + fun addTaskErrorListener(launchable: NexusLaunchableCallback): NexusLaunchable { + stack.taskErrorStack.add(launchable) + return this + } + + fun join() = finalFuture.join() +} + +fun interface NexusLaunchableCallback { + fun on(result: Result) +} + +class NexusLaunchableStack internal constructor() { + val taskCompletionStack: MutableList> = CopyOnWriteArrayList() + val taskErrorStack: MutableList> = CopyOnWriteArrayList() + + val stackSingal: NexusLaunchableStackSignal = NexusLaunchableStackSignal(this) + + class NexusLaunchableStackSignal internal constructor(private val stack: NexusLaunchableStack) { + + /** + * Signals that a secondary task has been completed, this will not signal a final completion task. + * Using this method will call all the secondary task completion listeners in a sequential manner inside another + * asynchronous task. + * + * @param result the result to send to the secondary task completion. + */ + fun complete(result: Result) { + Nexus.configuration.launch.launcher.launch { + for (taskCompletionStack in stack.taskCompletionStack) { + try { + taskCompletionStack.on(result) + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught in a launchable.") + exception.printStackTrace() + } + } + } + } + + /** + * Signals that an exception occurred, this will not signal an exception on any that is listening onto `.join()`. + * Using this method will call all the task error completion listeners in a sequential manner inside another + * asynchronous task. + * + * @param exception the exception to be sent to the listeners. + */ + fun error(exception: Exception) { + Nexus.configuration.launch.launcher.launch { + for (taskErrorStack in stack.taskErrorStack) { + try { + taskErrorStack.on(exception) + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught in a launchable.") + exception.printStackTrace() + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java b/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java deleted file mode 100755 index 2eb56e2b..00000000 --- a/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java +++ /dev/null @@ -1,91 +0,0 @@ -package pw.mihou.nexus.core.builder; - -import org.javacord.api.DiscordApi; -import org.javacord.api.DiscordApiBuilder; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.core.configuration.core.NexusConfiguration; -import pw.mihou.nexus.features.messages.facade.NexusMessageConfiguration; - -import java.time.Duration; -import java.util.function.Consumer; -import java.util.function.Function; - -public class NexusBuilder { - - private NexusMessageConfiguration messageConfiguration; - private DiscordApiBuilder builder; - private Consumer onShardLogin; - private NexusConfiguration nexusConfiguration = new NexusConfiguration( - Duration.ofMinutes(10) - ); - - /** - * Sets the {@link NexusMessageConfiguration} that {@link Nexus} uses whenever - * it needs to handle a situation where the user needs to be notified, for instance, when reaching - * a rate-limit. - * - * @param configuration The {@link NexusMessageConfiguration} to use. This is optional and can be null. - * @return {@link NexusBuilder} for chain-calling methods. - */ - public NexusBuilder setMessageConfiguration(NexusMessageConfiguration configuration) { - this.messageConfiguration = configuration; - return this; - } - - /** - * Sets the {@link DiscordApiBuilder} instance that {@link Nexus} will use for - * creating a new {@link DiscordApi} to store in its {@link pw.mihou.nexus.core.managers.NexusShardManager}. - * - * @param builder The builder to use whenever Nexus boots. - * @return {@link NexusBuilder} for chain-calling methods. - */ - public NexusBuilder setDiscordApiBuilder(DiscordApiBuilder builder) { - this.builder = builder; - return this; - } - - /** - * Sets the {@link DiscordApiBuilder} instance that {@link Nexus} will use for - * creating a new {@link DiscordApi} to store in its {@link pw.mihou.nexus.core.managers.NexusShardManager}. - * - * @param builder The builder to use whenever Nexus boots. - * @return {@link NexusBuilder} for chain-calling methods. - */ - public NexusBuilder setDiscordApiBuilder(Function builder) { - return this.setDiscordApiBuilder(builder.apply(new DiscordApiBuilder())); - } - - /** - * Sets the handler for whenever a new {@link DiscordApi} shard is booted - * successfully. - * - * @param onShardLogin The handler for whenever a new {@link DiscordApi} shard is booted. - * @return {@link NexusBuilder} for chain-calling methods. - */ - public NexusBuilder setOnShardLogin(Consumer onShardLogin) { - this.onShardLogin = onShardLogin; - return this; - } - - /** - * Sets the {@link NexusConfiguration} for the {@link Nexus} instance to build. - * - * @param nexusConfiguration The {@link NexusConfiguration} to use. - * @return {@link NexusBuilder} for chain-calling methods. - */ - public NexusBuilder setNexusConfiguration(NexusConfiguration nexusConfiguration) { - this.nexusConfiguration = nexusConfiguration; - return this; - } - - /** - * This builds a new {@link Nexus} instance that uses the configuration - * that was specified in this builder settings. - * - * @return The new {@link Nexus} instance. - */ - public Nexus build() { - return new NexusCore(messageConfiguration, builder, onShardLogin, nexusConfiguration); - } -} diff --git a/src/main/java/pw/mihou/nexus/core/configuration/core/NexusConfiguration.java b/src/main/java/pw/mihou/nexus/core/configuration/core/NexusConfiguration.java deleted file mode 100644 index e537e68f..00000000 --- a/src/main/java/pw/mihou/nexus/core/configuration/core/NexusConfiguration.java +++ /dev/null @@ -1,11 +0,0 @@ -package pw.mihou.nexus.core.configuration.core; - -import java.time.Duration; - -/** - * {@link NexusConfiguration} is a record that contains all the configuration - * settings of a {@link pw.mihou.nexus.Nexus} instance and is recommended to have. - */ -public record NexusConfiguration( - Duration timeBeforeExpiringEngineRequests -) {} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/core/NexusEngineXCore.java b/src/main/java/pw/mihou/nexus/core/enginex/core/NexusEngineXCore.java deleted file mode 100644 index be7c3f31..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/core/NexusEngineXCore.java +++ /dev/null @@ -1,167 +0,0 @@ -package pw.mihou.nexus.core.enginex.core; - -import org.javacord.api.DiscordApi; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.core.enginex.event.NexusEngineEvent; -import pw.mihou.nexus.core.enginex.event.NexusEngineQueuedEvent; -import pw.mihou.nexus.core.enginex.event.core.NexusEngineEventCore; -import pw.mihou.nexus.core.enginex.event.status.NexusEngineEventStatus; -import pw.mihou.nexus.core.enginex.facade.NexusEngineX; -import pw.mihou.nexus.core.threadpool.NexusThreadPool; - -import java.time.Duration; -import java.util.Map; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - -public class NexusEngineXCore implements NexusEngineX { - - private final BlockingQueue globalQueue = new LinkedBlockingQueue<>(); - private final Map> localQueue = new ConcurrentHashMap<>(); - private final AtomicBoolean hasGlobalProcessed = new AtomicBoolean(false); - private final Nexus nexus; - - /** - * Creates a new {@link NexusEngineX} instance that can be used to broadcast, queue - * specific events into specific shards or global shards. - * - * @param nexus The {@link Nexus} event to handle this event for. - */ - public NexusEngineXCore(Nexus nexus) { - this.nexus = nexus; - } - - /** - * Gets the global blocking queue that is dedicated for all shards to - * accept at any point of time when needed. - * - * @return The blocking queue that any shard can accept. - */ - public BlockingQueue getGlobalQueue() { - return globalQueue; - } - - /** - * An open executable method that is used by {@link pw.mihou.nexus.core.managers.NexusShardManager} to tell the EngineX - * to proceed with sending requests to the specific shard. - * - * @param shard The shard to process the events. - */ - public void onShardReady(DiscordApi shard) { - CompletableFuture.runAsync(() -> { - while (!getLocalQueue(shard.getCurrentShard()).isEmpty()) { - try { - NexusEngineEvent event = getLocalQueue(shard.getCurrentShard()).poll(); - - if (event != null) { - ((NexusEngineEventCore) event).process(shard); - } - } catch (Throwable exception) { - NexusCore.logger.error("An uncaught exception was received by Nexus' EngineX with the following stacktrace."); - exception.printStackTrace(); - } - } - }); - - if (!hasGlobalProcessed.get() && !getGlobalQueue().isEmpty()) { - hasGlobalProcessed.set(true); - CompletableFuture.runAsync(() -> { - while (!getGlobalQueue().isEmpty()) { - try { - NexusEngineEvent event = getGlobalQueue().poll(); - - if (event != null) { - ((NexusEngineEventCore) event).process(shard); - } - } catch (Throwable exception) { - NexusCore.logger.error("An uncaught exception was received by Nexus' EngineX with the following stacktrace."); - exception.printStackTrace(); - } - } - hasGlobalProcessed.set(false); - }); - } - } - - /** - * Gets the local queue for this shard. If it doesn't exist then it will add - * a queue instead and return the newly created queue. - * - * @param shard The shard to get the queue of. - * @return The blocking queue for this shard. - */ - public BlockingQueue getLocalQueue(int shard) { - if (!localQueue.containsKey(shard)) { - localQueue.put(shard, new LinkedBlockingQueue<>()); - } - - return localQueue.get(shard); - } - - @Override - public NexusEngineEvent queue(int shard, NexusEngineQueuedEvent event) { - NexusEngineEventCore engineEvent = new NexusEngineEventCore(event); - - if (nexus.getShardManager().getShard(shard) == null) { - getLocalQueue(shard).add(engineEvent); - - Duration expiration = nexus.getConfiguration().timeBeforeExpiringEngineRequests(); - if (!(expiration.isZero() || expiration.isNegative())) { - NexusThreadPool.schedule(() -> { - if (engineEvent.status() == NexusEngineEventStatus.WAITING) { - boolean removeFromQueue = localQueue.get(shard).remove(engineEvent); - NexusCore.logger.warn( - "An engine request that was specified for a shard was expired because the shard failed to take hold of the request before expiration. " + - "[shard={};acknowledged={}]", - shard, removeFromQueue - ); - engineEvent.expire(); - } - }, expiration.toMillis(), TimeUnit.MILLISECONDS); - } - } else { - engineEvent.process(nexus.getShardManager().getShard(shard)); - } - - return engineEvent; - } - - @Override - public NexusEngineEvent queue(NexusEngineQueuedEvent event) { - NexusEngineEventCore engineEvent = new NexusEngineEventCore(event); - - if (nexus.getShardManager().size() == 0) { - globalQueue.add(engineEvent); - - Duration expiration = nexus.getConfiguration().timeBeforeExpiringEngineRequests(); - if (!(expiration.isZero() || expiration.isNegative())) { - NexusThreadPool.schedule(() -> { - if (engineEvent.status() == NexusEngineEventStatus.WAITING) { - boolean removeFromQueue = globalQueue.remove(engineEvent); - NexusCore.logger.warn( - "An engine request that was specified for a shard was expired because the shard failed to take hold of the request before expiration. " + - "[acknowledged={}]", - removeFromQueue - ); - engineEvent.expire(); - } - }, expiration.toMillis(), TimeUnit.MILLISECONDS); - } - } else { - DiscordApi shard = nexus.getShardManager().asStream().findFirst().orElseThrow(); - engineEvent.process(shard); - } - - return engineEvent; - } - - @Override - public void broadcast(Consumer event) { - nexus.getShardManager() - .asStream() - .forEach(api -> CompletableFuture - .runAsync(() -> event.accept(api), NexusThreadPool.executorService)); - } -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineEvent.java b/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineEvent.java deleted file mode 100644 index dee6a170..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineEvent.java +++ /dev/null @@ -1,30 +0,0 @@ -package pw.mihou.nexus.core.enginex.event; - -import pw.mihou.nexus.core.enginex.event.listeners.NexusEngineEventStatusChange; -import pw.mihou.nexus.core.enginex.event.status.NexusEngineEventStatus; - -public interface NexusEngineEvent { - - /** - * Cancels this event from executing. This changes the status from {@link NexusEngineEventStatus#WAITING} to - * {@link NexusEngineEventStatus#STOPPED}, this is ignored if the cancel was executed during any other status - * other than waiting. - */ - void cancel(); - - /** - * Gets the current status of this event. - * - * @return The currents status of this event. - */ - NexusEngineEventStatus status(); - - /** - * Adds a status change listener for this event. - * - * @param event The procedures to execute whenever a status change occurs. - * @return The {@link NexusEngineEvent} for chain-calling methods. - */ - NexusEngineEvent addStatusChangeListener(NexusEngineEventStatusChange event); - -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineQueuedEvent.java b/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineQueuedEvent.java deleted file mode 100644 index 8c504da1..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineQueuedEvent.java +++ /dev/null @@ -1,18 +0,0 @@ -package pw.mihou.nexus.core.enginex.event; - -import org.javacord.api.DiscordApi; -import pw.mihou.nexus.core.enginex.event.media.NexusEngineEventWriteableStore; - -public interface NexusEngineQueuedEvent { - - /** - * Executed whenever it needs to be processed by the specific shard that - * is responsible for handling this event. - * - * @param api The {@link DiscordApi} that is handling this event. - * @param store The {@link NexusEngineEventWriteableStore} that can be used to store - * and access data shared between the event and the queuer. - */ - void onEvent(DiscordApi api, NexusEngineEventWriteableStore store); - -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/event/core/NexusEngineEventCore.java b/src/main/java/pw/mihou/nexus/core/enginex/event/core/NexusEngineEventCore.java deleted file mode 100644 index f7aefbf8..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/core/NexusEngineEventCore.java +++ /dev/null @@ -1,82 +0,0 @@ -package pw.mihou.nexus.core.enginex.event.core; - -import org.javacord.api.DiscordApi; -import pw.mihou.nexus.core.enginex.event.NexusEngineEvent; -import pw.mihou.nexus.core.enginex.event.NexusEngineQueuedEvent; -import pw.mihou.nexus.core.enginex.event.listeners.NexusEngineEventStatusChange; -import pw.mihou.nexus.core.enginex.event.media.NexusEngineEventWriteableStore; -import pw.mihou.nexus.core.enginex.event.status.NexusEngineEventStatus; -import pw.mihou.nexus.core.threadpool.NexusThreadPool; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; - -public class NexusEngineEventCore implements NexusEngineEvent { - - private final AtomicReference status = new AtomicReference<>(NexusEngineEventStatus.WAITING); - private final NexusEngineQueuedEvent event; - private final List listeners = new ArrayList<>(); - private final NexusEngineEventWriteableStore store = new NexusEngineEventWriteableStore(new ConcurrentHashMap<>()); - - public NexusEngineEventCore(NexusEngineQueuedEvent event) { - this.event = event; - } - - @Override - public void cancel() { - if (status() == NexusEngineEventStatus.WAITING) { - changeStatus(NexusEngineEventStatus.STOPPED); - } - } - - /** - * Expires this event and stops it from proceeding in any - * form of way. - */ - public void expire() { - if (status() == NexusEngineEventStatus.WAITING) { - changeStatus(NexusEngineEventStatus.EXPIRED); - } - } - - /** - * Applies a new status to this event - * @param newStatus The new status to use for this event. - */ - private void changeStatus(NexusEngineEventStatus newStatus) { - NexusEngineEventStatus oldStatus = status.get(); - status.set(newStatus); - - listeners.forEach(listener -> CompletableFuture - .runAsync(() -> listener.onStatusChange(this, oldStatus, newStatus), NexusThreadPool.executorService)); - } - - /** - * Proceeds to process the event if it is still available to process. - * - * @param api The shard that will be processing the event. - */ - public void process(DiscordApi api) { - if (status.get() == NexusEngineEventStatus.STOPPED || status.get() == NexusEngineEventStatus.EXPIRED) { - return; - } - - changeStatus(NexusEngineEventStatus.PROCESSING); - CompletableFuture.runAsync(() -> event.onEvent(api, store), NexusThreadPool.executorService) - .thenAccept(unused -> changeStatus(NexusEngineEventStatus.FINISHED)); - } - - @Override - public NexusEngineEventStatus status() { - return status.get(); - } - - @Override - public NexusEngineEvent addStatusChangeListener(NexusEngineEventStatusChange event) { - listeners.add(event); - return this; - } -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/event/listeners/NexusEngineEventStatusChange.java b/src/main/java/pw/mihou/nexus/core/enginex/event/listeners/NexusEngineEventStatusChange.java deleted file mode 100644 index 5d088f19..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/listeners/NexusEngineEventStatusChange.java +++ /dev/null @@ -1,23 +0,0 @@ -package pw.mihou.nexus.core.enginex.event.listeners; - -import pw.mihou.nexus.core.enginex.event.NexusEngineEvent; -import pw.mihou.nexus.core.enginex.event.status.NexusEngineEventStatus; - -public interface NexusEngineEventStatusChange { - - /** - * Executed whenever a {@link NexusEngineEvent} changes its status quo from - * one status such as {@link NexusEngineEventStatus#WAITING} to another status such as - * {@link NexusEngineEventStatus#PROCESSING}. - * - * @param event The event that broadcasted this event. - * @param oldStatus The old status of the event. - * @param newStatus The new status of the event. - */ - void onStatusChange( - NexusEngineEvent event, - NexusEngineEventStatus oldStatus, - NexusEngineEventStatus newStatus - ); - -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/event/media/NexusEngineEventReadableStore.java b/src/main/java/pw/mihou/nexus/core/enginex/event/media/NexusEngineEventReadableStore.java deleted file mode 100644 index f2bbbb37..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/media/NexusEngineEventReadableStore.java +++ /dev/null @@ -1,55 +0,0 @@ -package pw.mihou.nexus.core.enginex.event.media; - -import javax.annotation.Nullable; - -public interface NexusEngineEventReadableStore { - - @Nullable - Object get(String key); - - @Nullable - default String getString(String key) { - return as(key, String.class); - } - - @Nullable - default Boolean getBoolean(String key) { - return as(key, Boolean.class); - } - - @Nullable - default Integer getInteger(String key) { - return as(key, Integer.class); - } - - @Nullable - default Long getLong(String key) { - return as(key, Long.class); - } - - @Nullable - default Double getDouble(String key) { - return as(key, Double.class); - } - - /** - * Checks if the type for this class is of the specified class and - * returns the value if it is otherwise returns null. - * - * @param key The key of the data to acquire. - * @param typeClass The type of class that this object is required to be. - * @param The type of class that this object is required to be. - * @return The object value if it matches the requirements. - */ - @Nullable - default NextType as(String key, Class typeClass) { - Object value = get(key); - - if (typeClass.isInstance(value)) { - return typeClass.cast(value); - } - - return null; - } - -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/event/media/NexusEngineEventWriteableStore.java b/src/main/java/pw/mihou/nexus/core/enginex/event/media/NexusEngineEventWriteableStore.java deleted file mode 100644 index 65274d13..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/media/NexusEngineEventWriteableStore.java +++ /dev/null @@ -1,31 +0,0 @@ -package pw.mihou.nexus.core.enginex.event.media; - -import javax.annotation.Nullable; -import java.util.Map; -import java.util.Optional; - -/** - * A temporary session storage space for any {@link pw.mihou.nexus.core.enginex.event.NexusEngineEvent} to store - * any form of data received from the event. - */ -public record NexusEngineEventWriteableStore( - Map data -) implements NexusEngineEventReadableStore { - - /** - * Writes data into the temporary store that can be accessed through the - * {@link NexusEngineEventReadableStore}. - * - * @param key The key name of the data. - * @param value The value name of the data. - */ - public void write(String key, Object value) { - data.put(key, value); - } - - @Nullable - @Override - public Object get(String key) { - return data.get(key); - } -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/event/status/NexusEngineEventStatus.java b/src/main/java/pw/mihou/nexus/core/enginex/event/status/NexusEngineEventStatus.java deleted file mode 100644 index 743c2ed4..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/status/NexusEngineEventStatus.java +++ /dev/null @@ -1,11 +0,0 @@ -package pw.mihou.nexus.core.enginex.event.status; - -public enum NexusEngineEventStatus { - - EXPIRED, - STOPPED, - WAITING, - PROCESSING, - FINISHED - -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusDiscordShard.java b/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusDiscordShard.java deleted file mode 100644 index 830c79b5..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusDiscordShard.java +++ /dev/null @@ -1,15 +0,0 @@ -package pw.mihou.nexus.core.enginex.facade; - -import org.javacord.api.DiscordApi; - -public interface NexusDiscordShard { - - /** - * Unwraps the {@link NexusDiscordShard} to gain access to the actual - * {@link DiscordApi} instance that is being wrapped inside. - * - * @return The {@link DiscordApi} that is being handled by this instance. - */ - DiscordApi asDiscordApi(); - -} diff --git a/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java b/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java deleted file mode 100644 index b2bf6978..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java +++ /dev/null @@ -1,38 +0,0 @@ -package pw.mihou.nexus.core.enginex.facade; - -import org.javacord.api.DiscordApi; -import pw.mihou.nexus.core.enginex.event.NexusEngineEvent; -import pw.mihou.nexus.core.enginex.event.NexusEngineQueuedEvent; - -import java.util.function.Consumer; - -public interface NexusEngineX { - - /** - * Queues an event to be executed by the specific shard that - * it is specified for. - * - * @param shard The shard to handle this event. - * @param event The event to execute for this shard. - * @return The controller and status viewer for the event. - */ - NexusEngineEvent queue(int shard, NexusEngineQueuedEvent event); - - /** - * Queues an event to be executed by any specific shard that is available - * to take the event. - * - * @param event The event to execute by a shard. - * @return The controller and status viewer for the event. - */ - NexusEngineEvent queue(NexusEngineQueuedEvent event); - - /** - * Broadcasts the event for all shards to execute, this doesn't wait for any shards - * that aren't available during the time that it was executed. - * - * @param event The event to broadcast to all shards. - */ - void broadcast(Consumer event); - -} diff --git a/src/main/java/pw/mihou/nexus/core/exceptions/NexusFailedActionException.java b/src/main/java/pw/mihou/nexus/core/exceptions/NexusFailedActionException.java new file mode 100644 index 00000000..1eff186f --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/exceptions/NexusFailedActionException.java @@ -0,0 +1,9 @@ +package pw.mihou.nexus.core.exceptions; + +public class NexusFailedActionException extends RuntimeException { + + public NexusFailedActionException(String message) { + super(message); + } + +} diff --git a/src/main/java/pw/mihou/nexus/core/exceptions/NoSuchInterceptorException.kt b/src/main/java/pw/mihou/nexus/core/exceptions/NoSuchInterceptorException.kt new file mode 100644 index 00000000..73888ce5 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/exceptions/NoSuchInterceptorException.kt @@ -0,0 +1,9 @@ +package pw.mihou.nexus.core.exceptions + +class NoSuchMiddlewareException(middleware: String): + RuntimeException("No middleware with the identifier of $middleware can be found, please validate that the name is correct and " + + "the middleware does exist. Initializing the command before the middlewares are added can also cause this exception.") + +class NoSuchAfterwareException(afterware: String): + RuntimeException("No afterware with the identifier of $afterware can be found, please validate that the name is correct and " + + "the afterware does exist. Initializing the command before the afterwares are added can also cause this exception.") \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/exceptions/NotInheritableException.kt b/src/main/java/pw/mihou/nexus/core/exceptions/NotInheritableException.kt new file mode 100644 index 00000000..71cf98b3 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/exceptions/NotInheritableException.kt @@ -0,0 +1,6 @@ +package pw.mihou.nexus.core.exceptions + +import java.lang.RuntimeException + +class NotInheritableException(clazz: Class<*>): + RuntimeException("${clazz.name} is not an inheritable class, ensure that there is at least one empty constructor, or no constructors at all.") \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/NexusConsoleLoggingAdapter.java b/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/NexusConsoleLoggingAdapter.java index 2a083c58..7962b661 100644 --- a/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/NexusConsoleLoggingAdapter.java +++ b/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/NexusConsoleLoggingAdapter.java @@ -1,10 +1,13 @@ package pw.mihou.nexus.core.logger.adapters.defaults; +import org.jetbrains.annotations.Nullable; import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter; import pw.mihou.nexus.core.logger.adapters.defaults.configuration.NexusConsoleLoggingConfiguration; import pw.mihou.nexus.core.logger.adapters.defaults.configuration.enums.NexusConsoleLoggingLevel; +import java.io.PrintStream; import java.time.Instant; +import java.util.Arrays; import java.util.Objects; public class NexusConsoleLoggingAdapter implements NexusLoggingAdapter { @@ -90,7 +93,21 @@ private String format(String message, NexusConsoleLoggingLevel level, Object... */ private void log(String message, NexusConsoleLoggingLevel level, Object... values) { if (configuration.allowed().contains(level)) { - System.out.println(format(message, level, values)); + PrintStream printStream = System.out; + if (level == NexusConsoleLoggingLevel.ERROR) { + printStream = System.err; + } + printStream.println(format(message, level, values)); + + if (!message.contains("{}")) { + Arrays.stream(values).forEach(object -> { + if (object instanceof Exception) { + ((Exception) object).printStackTrace(); + } else if (object instanceof Throwable) { + ((Throwable) object).printStackTrace(); + } + }); + } } } diff --git a/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/NexusDefaultLoggingAdapter.java b/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/NexusDefaultLoggingAdapter.java index 103ea6ca..05d79ab9 100644 --- a/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/NexusDefaultLoggingAdapter.java +++ b/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/NexusDefaultLoggingAdapter.java @@ -2,11 +2,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pw.mihou.nexus.Nexus; import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter; public class NexusDefaultLoggingAdapter implements NexusLoggingAdapter { - private final Logger logger = LoggerFactory.getLogger("Nexus.Core"); + private final Logger logger = LoggerFactory.getLogger(Nexus.class); @Override public void info(String message, Object... values) { diff --git a/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/configuration/NexusConsoleLoggingConfiguration.java b/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/configuration/NexusConsoleLoggingConfiguration.java index 529f9c19..4a0e363d 100644 --- a/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/configuration/NexusConsoleLoggingConfiguration.java +++ b/src/main/java/pw/mihou/nexus/core/logger/adapters/defaults/configuration/NexusConsoleLoggingConfiguration.java @@ -1,6 +1,5 @@ package pw.mihou.nexus.core.logger.adapters.defaults.configuration; -import pw.mihou.nexus.core.logger.adapters.defaults.NexusConsoleLoggingAdapter; import pw.mihou.nexus.core.logger.adapters.defaults.configuration.enums.NexusConsoleLoggingLevel; import javax.annotation.Nonnull; diff --git a/src/main/java/pw/mihou/nexus/core/managers/NexusShardManager.java b/src/main/java/pw/mihou/nexus/core/managers/NexusShardManager.java deleted file mode 100755 index 7615d819..00000000 --- a/src/main/java/pw/mihou/nexus/core/managers/NexusShardManager.java +++ /dev/null @@ -1,121 +0,0 @@ -package pw.mihou.nexus.core.managers; - -import org.javacord.api.DiscordApi; -import org.javacord.api.entity.server.Server; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.core.enginex.core.NexusEngineXCore; - -import javax.annotation.Nullable; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; - -public class NexusShardManager { - - private final ConcurrentHashMap shards; - private final Nexus nexus; - - /** - * This creates a new Shard Manager that is then utilized by - * {@link pw.mihou.nexus.Nexus}. - * - * @param shards The shards to utilize. - */ - public NexusShardManager(Nexus nexus, DiscordApi... shards) { - this(nexus); - Arrays.stream(shards).forEach(this::put); - } - - /** - * Creates a new {@link NexusShardManager} without any shards. This allows more - * flexibility over how the shards are added. - */ - public NexusShardManager(Nexus nexus) { - this.nexus = nexus; - this.shards = new ConcurrentHashMap<>(); - } - - /** - * Gets the shard at the specific shard number. - * - * @param number The number of the shard to fetch. - * @return The shard with the shard number specified. - */ - @Nullable - public DiscordApi getShard(int number) { - return shards.get(number); - } - - /** - * Gets the shard that is responsible for the specific server. - * - * @param server The ID of the server to lookup. - * @return The shard responsible for the server, if present. - */ - public Optional getShardOf(long server) { - return asStream() - .filter(discordApi -> discordApi.getServerById(server).isPresent()) - .findFirst(); - } - - /** - * Gets the given server from any of the shards if there is any shard - * responsible for that given server. - * - * @param id The id of the server to get. - * @return The server instance, if present. - */ - public Optional getServerBy(long id) { - return getShardOf(id).flatMap(shard -> shard.getServerById(id)); - } - - /** - * Adds or replaces the shard registered on the shard manager. - * This is recommended to do during restarts of a shard. - * - * @param api The Discord API to store. - */ - public void put(DiscordApi api) { - this.shards.put(api.getCurrentShard(), api); - - ((NexusEngineXCore) nexus.getEngineX()).onShardReady(api); - } - - /** - * Removes the shard with the specific shard key. - * - * @param shard The number of the shard to remove. - */ - public void remove(int shard) { - this.shards.remove(shard); - } - - /** - * Retrieves all the {@link DiscordApi} shards and transforms - * them into an Array Stream. - * - * @return A stream of all the shards registered in the shard manager. - */ - public Stream asStream() { - return shards.values().stream(); - } - - /** - * Retrieves all the {@link DiscordApi} shards and transforms them - * into a {@link Collection}. - * - * @return A {@link Collection} of all the shards registered in the shard manager. - */ - public Collection asCollection() { - return shards.values(); - } - - /** - * Gets the current size of the shard manager. - * - * @return The current size of the shard manager. - */ - public int size() { - return shards.size(); - } -} diff --git a/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.java b/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.java deleted file mode 100755 index 49f06320..00000000 --- a/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.java +++ /dev/null @@ -1,167 +0,0 @@ -package pw.mihou.nexus.core.managers.core; - -import org.javacord.api.event.interaction.SlashCommandCreateEvent; -import org.javacord.api.interaction.ApplicationCommand; -import org.javacord.api.interaction.SlashCommand; -import org.javacord.api.interaction.SlashCommandInteraction; -import org.javacord.api.util.logging.ExceptionLogger; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter; -import pw.mihou.nexus.core.managers.facade.NexusCommandManager; -import pw.mihou.nexus.features.command.core.NexusCommandCore; -import pw.mihou.nexus.features.command.facade.NexusCommand; - -import java.util.*; -import java.util.stream.Collectors; - -public class NexusCommandManagerCore implements NexusCommandManager { - - private final Map commands = new HashMap<>(); - - private final Map indexes = new HashMap<>(); - - private final NexusCore nexusCore; - private static final NexusLoggingAdapter logger = NexusCore.logger; - - /** - * Creates a new Nexus Command Manager that is utilized to manage commands, - * index commands, etc. - * - * @param nexusCore The nexus core that is in charge of this command manager. - */ - public NexusCommandManagerCore(NexusCore nexusCore) { - this.nexusCore = nexusCore; - } - - @Override - public Collection getCommands() { - return commands.values(); - } - - @Override - public Nexus addCommand(NexusCommand command) { - commands.put(((NexusCommandCore) command).uuid, command); - - return nexusCore; - } - - @Override - public Optional getCommandById(long id) { - return Optional.ofNullable(commands.get(indexes.getOrDefault(id, null))); - } - - @Override - public Optional getCommandByUUID(String uuid) { - return Optional.ofNullable(commands.get(uuid)); - } - - @Override - public Optional getCommandByName(String name) { - return getCommands().stream() - .filter(nexusCommand -> nexusCommand.getName().equalsIgnoreCase(name)) - .findFirst(); - } - - @Override - public Optional getCommandByName(String name, long server) { - return getCommands().stream() - .filter(nexusCommand -> - nexusCommand.getName().equalsIgnoreCase(name) && nexusCommand.getServerIds().contains(server) - ) - .findFirst(); - } - - /** - * This performs indexing based on the data analyzed from the - * {@link SlashCommandCreateEvent} and returns the results for post-processing - * from the {@link NexusCore}. This is what we call dynamic indexing. - * - * @param event The event to handle. - */ - public Optional acceptEvent(SlashCommandCreateEvent event) { - SlashCommandInteraction interaction = event.getSlashCommandInteraction(); - - if (getCommandById(interaction.getCommandId()).isPresent()) { - return getCommandById(interaction.getCommandId()); - } - - if (interaction.getServer().isPresent()) { - return getCommands().stream() - .filter(nexusCommand -> nexusCommand.getName().equalsIgnoreCase(interaction.getCommandName()) - && nexusCommand.getServerIds().contains(interaction.getServer().get().getId())) - .findFirst() - .or(() -> getCommandByName(interaction.getCommandName())) - .map(command -> { - indexes.put(interaction.getCommandId(), ((NexusCommandCore) command).uuid); - return command; - }); - } - - return getCommandByName(interaction.getCommandName()).map(command -> { - indexes.put(interaction.getCommandId(), ((NexusCommandCore) command).uuid); - - return command; - }); - } - - @Override - public void index() { - logger.info("Nexus is now performing command indexing, this will delay your boot time by a few seconds but improve performance and precision in look-ups..."); - long start = System.currentTimeMillis(); - nexusCore.getShardManager() - .asStream() - .findFirst() - .orElseThrow(() -> - new IllegalStateException( - "Nexus was unable to perform command indexing because there are no shards registered in Nexus's Shard Manager." - ) - ).getGlobalSlashCommands().thenAcceptAsync(slashCommands -> { - Map newIndexes = slashCommands.stream() - .collect(Collectors.toMap(slashCommand -> slashCommand.getName().toLowerCase(), ApplicationCommand::getId)); - - // Ensure the indexes are clear otherwise we might end up with some wrongly placed commands. - indexes.clear(); - - if (commands.isEmpty()) { - return; - } - - // Perform indexing which is basically mapping the ID of the slash command - // to the Nexus Command that will be called everytime the command executes. - getCommands().stream() - .filter(nexusCommand -> nexusCommand.getServerIds().isEmpty()) - .forEach(nexusCommand -> indexes.put( - newIndexes.get(nexusCommand.getName().toLowerCase()), - ((NexusCommandCore) nexusCommand).uuid) - ); - - Map> serverIndexes = new HashMap<>(); - - for (NexusCommand command : getCommands().stream().filter(nexusCommand -> !nexusCommand.getServerIds().isEmpty()).toList()) { - command.getServerIds().forEach(id -> { - if (!serverIndexes.containsKey(id)) { - nexusCore.getShardManager().getShardOf(id) - .flatMap(discordApi -> discordApi.getServerById(id)) - .ifPresent(server -> { - serverIndexes.put(server.getId(), new HashMap<>()); - - for (SlashCommand slashCommand : server.getSlashCommands().join()) { - serverIndexes.get(server.getId()).put(slashCommand.getName().toLowerCase(), slashCommand.getId()); - } - }); - } - - indexes.put( - serverIndexes.get(id).get(command.getName().toLowerCase()), - ((NexusCommandCore) command).uuid - ); - }); - } - - serverIndexes.clear(); - logger.info("All global and server slash commands are now indexed. It took {} milliseconds to complete indexing.", System.currentTimeMillis() - start); - }).exceptionally(ExceptionLogger.get()).join(); - } - -} diff --git a/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.kt b/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.kt new file mode 100755 index 00000000..774a61eb --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.kt @@ -0,0 +1,238 @@ +package pw.mihou.nexus.core.managers.core + +import org.javacord.api.event.interaction.MessageContextMenuCommandEvent +import org.javacord.api.event.interaction.SlashCommandCreateEvent +import org.javacord.api.event.interaction.UserContextMenuCommandEvent +import org.javacord.api.interaction.ApplicationCommand +import org.javacord.api.interaction.ApplicationCommandInteraction +import org.javacord.api.util.logging.ExceptionLogger +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.configuration.modules.info +import pw.mihou.nexus.features.commons.NexusApplicationCommand +import pw.mihou.nexus.core.managers.facade.NexusCommandManager +import pw.mihou.nexus.core.managers.indexes.IndexStore +import pw.mihou.nexus.core.managers.indexes.defaults.InMemoryIndexStore +import pw.mihou.nexus.core.managers.indexes.exceptions.IndexIdentifierConflictException +import pw.mihou.nexus.core.managers.records.NexusMetaIndex +import pw.mihou.nexus.features.command.core.NexusCommandCore +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.contexts.enums.ContextMenuKinds +import pw.mihou.nexus.features.contexts.NexusContextMenu +import kotlin.collections.HashMap +import kotlin.collections.HashSet + +class NexusCommandManagerCore internal constructor() : NexusCommandManager { + private val commandsDelegate: MutableMap = HashMap() + private val contextMenusDelegate: MutableMap = HashMap() + + override val contextMenus: Collection + get() = contextMenusDelegate.values + + override val commands: Collection + get() = commandsDelegate.values + + override var indexStore: IndexStore = InMemoryIndexStore() + + override fun add(command: NexusCommand): NexusCommandManager { + if (commandsDelegate[command.uuid] != null) + throw IndexIdentifierConflictException(command.name) + + commandsDelegate[(command as NexusCommandCore).uuid] = command + return this + } + + override fun add(contextMenu: NexusContextMenu): NexusCommandManager { + if (contextMenusDelegate[contextMenu.uuid] != null) + throw IndexIdentifierConflictException(contextMenu.name) + + contextMenusDelegate[contextMenu.uuid] = contextMenu + return this + } + + override operator fun get(applicationId: Long): NexusCommand? = indexStore[applicationId]?.takeCommand() + override operator fun get(uuid: String): NexusCommand? = commandsDelegate[uuid] + override operator fun get(name: String, server: Long?): NexusCommand? { + return commands.firstOrNull { command -> + when (server) { + null -> { + command.name.equals(name, ignoreCase = true) + } + else -> { + command.name.equals(name, ignoreCase = true) && command.serverIds.contains(server) + } + } + } + } + + override fun getContextMenu(applicationId: Long): NexusContextMenu? { + return indexStore[applicationId]?.takeContextMenu() + } + + override fun getContextMenu(uuid: String): NexusContextMenu? { + return contextMenusDelegate[uuid] + } + + override fun getContextMenu(name: String, kind: ContextMenuKinds, server: Long?): NexusContextMenu? { + return contextMenus.firstOrNull { contextMenu -> + when(server) { + null -> { + contextMenu.name.equals(name, ignoreCase = true) && contextMenu.kind == kind + } + else -> { + contextMenu.name.equals(name, ignoreCase = true) && contextMenu.serverIds.contains(server) && contextMenu.kind == kind + } + } + } + } + + override fun export(): List { + return indexStore.all() + } + + private fun toIndex(applicationCommandId: Long, command: String, server: Long?): NexusMetaIndex { + return NexusMetaIndex(command = command, applicationCommandId = applicationCommandId, server = server) + } + + fun acceptEvent(event: SlashCommandCreateEvent): NexusCommand? { + val interaction = event.slashCommandInteraction + + val indexedCommand = get(interaction.commandId) + if (indexedCommand != null) { + return indexedCommand + } + + return if (interaction.server.isPresent) { + val server = interaction.server.get().id + + return when (val command = get(interaction.commandName, server)) { + null -> index(interaction, get(interaction.commandName, null), null) + else -> index(interaction, command, server) + } + + } else index(interaction, get(interaction.commandName, null), null) + } + + fun acceptEvent(event: UserContextMenuCommandEvent): NexusContextMenu? { + return acceptContextMenuEvent(ContextMenuKinds.USER, event.userContextMenuInteraction) + } + + fun acceptEvent(event: MessageContextMenuCommandEvent): NexusContextMenu? { + return acceptContextMenuEvent(ContextMenuKinds.MESSAGE, event.messageContextMenuInteraction) + } + + private fun acceptContextMenuEvent(kind: ContextMenuKinds, interaction: ApplicationCommandInteraction): NexusContextMenu? { + val indexedContextMenu = getContextMenu(interaction.commandId) + if (indexedContextMenu != null) { + return indexedContextMenu + } + + return if (interaction.server.isPresent) { + val server = interaction.server.get().id + + return when (val contextMenu = getContextMenu(interaction.commandName, kind, server)) { + null -> index(interaction, getContextMenu(interaction.commandName, kind, null), null) + else -> index(interaction, contextMenu, server) + } + } else index(interaction, getContextMenu(interaction.commandName, kind, null), null) + } + + private fun index(interaction: ApplicationCommandInteraction, uuid: Command?, server: Long?): Command? { + return uuid?.apply { indexStore.add(toIndex(interaction.commandId, this.uuid, server)) } + } + + override fun index() { + Nexus.configuration.loggingTemplates.INDEXING_COMMANDS.info() + + val start = System.currentTimeMillis() + Nexus.express + .awaitAvailable() + .thenAcceptAsync { shard -> + val applicationCommands = shard.globalApplicationCommands.join() + indexStore.clear() + for (applicationCommand in applicationCommands) { + index(applicationCommand) + } + + val servers: MutableSet = HashSet() + for (serverCommand in serverCommands) { + servers.addAll(serverCommand.serverIds) + } + for (serverContextMenu in serverContextMenus) { + servers.addAll(serverContextMenu.serverIds) + } + + for (server in servers) { + if (server == 0L) continue + val applicationCommandSet: Set = Nexus.express + .await(server) + .thenComposeAsync { it.api.getServerApplicationCommands(it) } + .join() + + for (applicationCommand in applicationCommandSet) { + index(applicationCommand) + } + } + + Nexus.configuration.loggingTemplates.COMMANDS_INDXED(System.currentTimeMillis() - start).info() + } + .exceptionally(ExceptionLogger.get()) + .join() + } + + private fun manifest(command: Command, snowflake: Long, server: Long?) = + toIndex(applicationCommandId = snowflake, command = command.uuid, server = server) + + override fun index(command: Command, snowflake: Long, server: Long?) { + indexStore.add(toIndex(applicationCommandId = snowflake, command = command.uuid, server = server)) + } + + override fun index(applicationCommandList: Set) { + val indexes = mutableListOf() + for (applicationCommand in applicationCommandList) { + val serverId: Long? = applicationCommand.serverId.orElse(null) + + if (serverId == null) { + for (command in commands) { + if (command.serverIds.isNotEmpty()) continue + if (!command.name.equals(applicationCommand.name, ignoreCase = true)) continue + + indexes.add(manifest(command, applicationCommand.id, null)) + break + } + } else { + for (command in commands) { + if (!command.name.equals(applicationCommand.name, ignoreCase = true)) continue + if (!command.serverIds.contains(serverId)) continue + + indexes.add(manifest(command, applicationCommand.id, serverId)) + break + } + } + } + + indexStore.addAll(indexes) + } + + override fun index(applicationCommand: ApplicationCommand) { + val serverId: Long? = applicationCommand.serverId.orElse(null) + + if (serverId == null) { + for (command in commands) { + if (command.serverIds.isNotEmpty()) continue + if (!command.name.equals(applicationCommand.name, ignoreCase = true)) continue + + index(command, applicationCommand.id, null) + break + } + return + } else { + for (command in commands) { + if (!command.name.equals(applicationCommand.name, ignoreCase = true)) continue + if (!command.serverIds.contains(serverId)) continue + + index(command, applicationCommand.id, serverId) + break + } + } + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java b/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java deleted file mode 100755 index d31502b1..00000000 --- a/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java +++ /dev/null @@ -1,73 +0,0 @@ -package pw.mihou.nexus.core.managers.facade; - -import org.javacord.api.interaction.SlashCommand; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.features.command.facade.NexusCommand; - -import javax.swing.text.html.Option; -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -public interface NexusCommandManager { - - /** - * Gets all the commands that are stored inside the Nexus registry - * of commands. - * - * @return All the commands stored in Nexus's command registry. - */ - Collection getCommands(); - - /** - * Adds a command to the registry. - * - * @param command The command to add. - * @return The {@link Nexus} for chain-calling methods. - */ - Nexus addCommand(NexusCommand command); - - /** - * Gets the command that matches the {@link SlashCommand#getId()}. This can return empty - * if the indexing is still in progress and no commands have a slash command index. - * - * @param id The ID of the slash command to look for. - * @return The first command that matches the ID specified. - */ - Optional getCommandById(long id); - - /** - * Gets the command that matches the special UUID assigned to all {@link NexusCommand}. This is useful - * for when you want to retrieve a command by only have a UUID which is what most Nexus methods will return. - * - * @param uuid The UUID of the command to look for. - * @return The first command that matches the UUID specified. - */ - Optional getCommandByUUID(String uuid); - - /** - * Gets the command that matches the {@link NexusCommand#getName()}. This will only fetch the first one that - * matches and therefore can ignore several other commands that have the same name. - * - * @param name The name of the command to look for. - * @return The first command that matches the name specified. - */ - Optional getCommandByName(String name); - - /** - * Gets the command that matches the {@link NexusCommand#getName()} and {@link NexusCommand#getServerIds()}. This is a precise - * method that fetches a server-only command, this will always return empty if there are no server slash commands. - * - * @param name The name of the command to look for. - * @param server The ID of the command to fetch. - * @return The first command that matches both the name and the server id. - */ - Optional getCommandByName(String name, long server); - - /** - * This indexes all the commands whether it'd be global or server commands to increase - * performance and precision of slash commands. - */ - void index(); - -} diff --git a/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.kt b/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.kt new file mode 100755 index 00000000..9bd17d88 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.kt @@ -0,0 +1,200 @@ +package pw.mihou.nexus.core.managers.facade + +import org.javacord.api.interaction.ApplicationCommand +import pw.mihou.nexus.core.managers.indexes.IndexStore +import pw.mihou.nexus.core.managers.records.NexusMetaIndex +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.commons.NexusApplicationCommand +import pw.mihou.nexus.features.contexts.enums.ContextMenuKinds +import pw.mihou.nexus.features.contexts.NexusContextMenu +import kotlin.collections.HashSet + +interface NexusCommandManager { + /** + * Gets all the commands that are stored inside the Nexus registry + * of commands. + * + * @return All the commands stored in Nexus's command registry. + */ + val commands: Collection + + /** + * Gets all the context menus that are stored inside the Nexus registry. + */ + val contextMenus: Collection + + /** + * An index store is a store that is being utilized to store [NexusMetaIndex] that allows Nexus to match commands faster + * than with regular O(N) methods. Nexus will auto-index all commands that do not have an index already, therefore, there is not + * much needed to change. + * + * You can, however, change this to a persistent in-memory store that Nexus will use (be sure to use some in-memory caching + * for performance reasons). + */ + var indexStore: IndexStore + + /** + * Gets all the global commands that are stored inside the Nexus registry of commands. + *



+ * In this scenario, the definition of a global command is a command that does not have an association + * with a server. + * + * @return All the global commands that were created inside the registry. + */ + val globalCommands: Set + get() { + val commands: MutableSet = HashSet() + + for (command in this.commands) { + if (command.isServerCommand) continue + commands.add(command) + } + + return commands + } + + /** + * Gets all the global context menus that are stored inside the Nexus registry. + */ + val globalContextMenus: Set + get() { + val contextMenus: MutableSet = HashSet() + for (contextMenu in this.contextMenus) { + if (contextMenu.isServerOnly) continue + contextMenus.add(contextMenu) + } + return contextMenus + } + + /** + * Gets all the server commands that are stored inside the Nexus registry of commands. + *



+ * In this scenario, the definition of a server command is a command that does have an association + * with a server. + * + * @return All the server commands that were created inside the registry. + */ + val serverCommands: Set + get() { + val commands: MutableSet = HashSet() + + for (command in this.commands) { + if (!command.isServerCommand) continue + commands.add(command) + } + + return commands + } + + /** + * Gets all the server-locked context menus that are stored inside the Nexus registry. + */ + val serverContextMenus: Set + get() { + val contextMenus: MutableSet = HashSet() + for (contextMenu in this.contextMenus) { + if (!contextMenu.isServerOnly) continue + contextMenus.add(contextMenu) + } + return contextMenus + } + + /** + * Gets all the commands that have an association with the given server. + *



+ * This method does a complete O(n) loop over the commands to identify any commands that matches the + * [List.contains] predicate over its server ids. + * + * @param server The server to find all associated commands of. + * @return All associated commands of the given server. + */ + fun commandsAssociatedWith(server: Long): Set { + val commands: MutableSet = HashSet() + + for (command in this.commands) { + if (!command.serverIds.contains(server)) continue + commands.add(command) + } + + return commands + } + + /** + * Gets all the context menus that have an association with the given server. + *



+ * This method does a complete O(n) loop over the context menus to identify any context menus that matches the + * [List.contains] predicate over its server ids. + * + * @param server The server to find all associated context menus of. + * @return All associated context menus of the given server. + */ + fun contextMenusAssociatedWith(server: Long): Set { + val contextMenus: MutableSet = HashSet() + for (contextMenu in this.contextMenus) { + if (!contextMenu.serverIds.contains(server)) continue + contextMenus.add(contextMenu) + } + return contextMenus + } + + fun add(command: NexusCommand): NexusCommandManager + fun add(contextMenu: NexusContextMenu): NexusCommandManager + + operator fun get(applicationId: Long): NexusCommand? + operator fun get(uuid: String): NexusCommand? + operator fun get(name: String, server: Long? = null): NexusCommand? + + fun getContextMenu(applicationId: Long): NexusContextMenu? + fun getContextMenu(uuid: String): NexusContextMenu? + fun getContextMenu(name: String, kind: ContextMenuKinds, server: Long? = null): NexusContextMenu? + + /** + * Exports the indexes that was created which can then be used to create a database copy of the given indexes. + *



+ * It is not recommended to use this for any other purposes other than creating a database copy because this creates + * more garbage for the garbage collector. + * + * @return A snapshot of the indexes that the command manager has. + */ + fun export(): List + + /** + * This indexes all the commands whether it'd be global or server commands to increase + * performance and precision of slash commands. + */ + fun index() + + /** + * Creates an index mapping of the given command and the given slash command snowflake. + *



+ * You can use this method to index commands from your database. + * + * @param command The command that will be associated with the given snowflake. + * @param snowflake The snowflake that will be associated with the given command. + * @param server The server where this index should be associated with, can be null to mean global command. + */ + fun index(command: Command, snowflake: Long, server: Long?) + + /** + * Creates an index of all the slash commands provided. This will map all the commands based on properties + * that matches e.g. the name (since a command can only have one global and one server command that have the same name) + * and the server property if available. + * + * @param applicationCommandList the command list to use for indexing. + */ + fun index(applicationCommandList: Set) + + /** + * Creates an index of the given slash command provided. This will map all the command based on the property + * that matches e.g. the name (since a command can only have one global and one server command that have the same name) + * and the server property if available. + * + * @param applicationCommand The command to index. + */ + fun index(applicationCommand: ApplicationCommand) + + fun mentionMany(server: Long?, vararg commands: String) = + indexStore.mentionMany(server, *commands) + fun mentionOne(server: Long?, command: String, override: String? = null, default: String) = + indexStore.mentionOne(server, command, override, default) +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/managers/indexes/IndexStore.kt b/src/main/java/pw/mihou/nexus/core/managers/indexes/IndexStore.kt new file mode 100644 index 00000000..84290aae --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/indexes/IndexStore.kt @@ -0,0 +1,94 @@ +package pw.mihou.nexus.core.managers.indexes + +import pw.mihou.nexus.core.managers.records.NexusMetaIndex + +interface IndexStore { + + /** + * Adds the [NexusMetaIndex] into the store which can be retrieved later on by methods such as + * [get] when needed. + * @param metaIndex the index to add into the index store. + */ + fun add(metaIndex: NexusMetaIndex) + + /** + * Gets the [NexusMetaIndex] from the store or an in-memory cache by the application command identifier. + * @param applicationCommandId the application command identifier from Discord's side. + * @return the [NexusMetaIndex] that was caught otherwise none. + */ + operator fun get(applicationCommandId: Long): NexusMetaIndex? + + /** + * Gets the [NexusMetaIndex] that matches the given specifications. + * @param command the command name. + * @param server the server that this command belongs. + * @return the [NexusMetaIndex] that matches. + */ + operator fun get(command: String, server: Long?): NexusMetaIndex? + + /** + * Gets one command mention tag from the [IndexStore]. + * @param server the server to fetch the commands from, if any. + * @param command the names of the commands to fetch. + * @param override overrides the name of the command, used to add subcommands and related. + * @param default the default value to use when there is no command like that. + * @return the mention tags of the command. + */ + fun mentionOne(server: Long?, command: String, override: String? = null, default: String): String { + val index = get(command, server) ?: return default + return "" + } + + /** + * Gets many command mention tags from the [IndexStore]. + * @param server the server to fetch the commands from, if any. + * @param names the names of the commands to fetch. + * @return the mention tags of each commands. + */ + fun mentionMany(server: Long?, vararg names: String): Map { + val indexes = many(server, *names) + val map = mutableMapOf() + for (index in indexes) { + map[index.command] = "" + } + return map + } + + /** + * Gets one or more [NexusMetaIndex] from the store. + * @param applicationCommandIds the application command identifiers from Discord's side. + * @return the [NexusMetaIndex]es that matches. + */ + fun many(vararg applicationCommandIds: Long): List + + /** + * Gets one or more [NexusMetaIndex] from the store. + * @param server the server that these commands belongs to. + * @param names the names of the commands to fetch. + * @return the [NexusMetaIndex]es that matches. + */ + fun many(server: Long?, vararg names: String): List + + /** + * Adds one or more [NexusMetaIndex] into the store, this is used in scenarios such as mass-synchronization which + * offers more than one indexes at the same time. + * + * @param metaIndexes the indexes to add into the store. + */ + fun addAll(metaIndexes: List) + + /** + * Gets all the [NexusMetaIndex] available in the store, this is used more when the command manager's indexes are + * exported somewhere. + * + * @return all the [NexusMetaIndex] known in the store. + */ + fun all(): List + + /** + * Clears all the known indexes in the database. This happens when the command manager performs a re-indexing which + * happens when the developer themselves has called for it. + */ + fun clear() + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/managers/indexes/defaults/InMemoryIndexStore.kt b/src/main/java/pw/mihou/nexus/core/managers/indexes/defaults/InMemoryIndexStore.kt new file mode 100644 index 00000000..4e3786ec --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/indexes/defaults/InMemoryIndexStore.kt @@ -0,0 +1,38 @@ +package pw.mihou.nexus.core.managers.indexes.defaults + +import pw.mihou.nexus.core.managers.indexes.IndexStore +import pw.mihou.nexus.core.managers.records.NexusMetaIndex + +class InMemoryIndexStore: IndexStore { + + private val indexes: MutableMap = mutableMapOf() + + override fun add(metaIndex: NexusMetaIndex) { + indexes[metaIndex.applicationCommandId] = metaIndex + } + + override operator fun get(applicationCommandId: Long): NexusMetaIndex? = indexes[applicationCommandId] + override fun get(command: String, server: Long?): NexusMetaIndex? { + return indexes.values.firstOrNull { it.command == command && it.server == server } + } + + override fun many(vararg applicationCommandIds: Long): List { + return applicationCommandIds.map(indexes::get).filterNotNull().toList() + } + + override fun many(server: Long?, vararg names: String): List { + return indexes.values.filter { it.server == server && names.contains(it.command) }.toList() + } + + override fun addAll(metaIndexes: List) { + for (metaIndex in metaIndexes) { + indexes[metaIndex.applicationCommandId] = metaIndex + } + } + + override fun all(): List = indexes.values.toList() + + override fun clear() { + indexes.clear() + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/managers/indexes/exceptions/IndexIdentifierConflictException.kt b/src/main/java/pw/mihou/nexus/core/managers/indexes/exceptions/IndexIdentifierConflictException.kt new file mode 100644 index 00000000..5bca0c33 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/indexes/exceptions/IndexIdentifierConflictException.kt @@ -0,0 +1,6 @@ +package pw.mihou.nexus.core.managers.indexes.exceptions + +class IndexIdentifierConflictException(name: String): + RuntimeException("An index-identifier conflict was identified between commands (or context menus) with the name $name. We do not " + + "recommend having commands (or context menus) with the same name that have the same unique identifier, please change one of the commands' (or context menus') identifier " + + "by using the @IdentifiableAs annotation. (https://github.com/ShindouMihou/Nexus/wiki/Slash-Command-Indexing)") \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/managers/records/NexusMetaIndex.kt b/src/main/java/pw/mihou/nexus/core/managers/records/NexusMetaIndex.kt new file mode 100644 index 00000000..b0ec96e4 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/records/NexusMetaIndex.kt @@ -0,0 +1,10 @@ +package pw.mihou.nexus.core.managers.records + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.contexts.NexusContextMenu + +data class NexusMetaIndex(val command: String, val applicationCommandId: Long, val server: Long?) { + fun takeCommand(): NexusCommand? = Nexus.commandManager[command] + fun takeContextMenu(): NexusContextMenu? = Nexus.commandManager.getContextMenu(command) +} diff --git a/src/main/java/pw/mihou/nexus/core/reflective/NexusReflection.kt b/src/main/java/pw/mihou/nexus/core/reflective/NexusReflection.kt new file mode 100644 index 00000000..06707b70 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/reflective/NexusReflection.kt @@ -0,0 +1,102 @@ +package pw.mihou.nexus.core.reflective + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.core.assignment.NexusUuidAssigner +import pw.mihou.nexus.core.reflective.annotations.* +import pw.mihou.nexus.features.command.annotation.IdentifiableAs +import java.lang.reflect.Field + +object NexusReflection { + + /** + * Accumulates all the declared fields of the `from` parameter, this is useful for cases such as + * interceptor repositories. + * + * @param from the origin object where all the fields will be accumulated from. + * @param accumulator the accumulator to use. + */ + fun accumulate(from: Any, accumulator: (field: Field) -> Unit) { + val instance = from::class.java + for (field in instance.declaredFields) { + field.isAccessible = true + accumulator(field) + } + } + + /** + * Mirrors the fields from origin (`from`) to a new instance of the `to` class. + * This requires the `to` class to have an empty constructor (no parameters), otherwise this will not work. + * + * @param from the origin object where all the fields will be copied. + * @param to the new instance class where all the new fields will be pushed. + * @return a new instance with all the fields copied. + */ + fun copy(from: Any, to: Class<*>): Any { + val instance = to.getDeclaredConstructor().newInstance() + val fields = NexusReflectionFields(from, instance) + + if (to.isAnnotationPresent(MustImplement::class.java)) { + val extension = to.getAnnotation(MustImplement::class.java).clazz.java + if (!extension.isAssignableFrom(from::class.java)) { + throw IllegalStateException("${from::class.java.name} must implement the following class: ${extension.name}") + } + } + + var uuid: String? = null + val uuidFields = fields.referencesWithAnnotation(Uuid::class.java) + if (uuidFields.isNotEmpty()) { + if (uuidFields.size == 1) { + uuid = fields.stringify(uuidFields[0].name) + } else { + for (field in uuidFields) { + if (uuid == null) { + uuid = fields.stringify(field.name) + } else { + uuid += ":" + fields.stringify(field.name) + } + } + } + } + + if (from::class.java.isAnnotationPresent(IdentifiableAs::class.java)) { + uuid = from::class.java.getAnnotation(IdentifiableAs::class.java).key + } + + for (field in instance::class.java.declaredFields) { + field.isAccessible = true + + if (field.isAnnotationPresent(InjectReferenceClass::class.java)) { + field.set(instance, from) + } else if(field.isAnnotationPresent(InjectUUID::class.java)) { + if (uuid == null) { + uuid = NexusUuidAssigner.request() + } + + field.set(instance, uuid) + } else if(field.isAnnotationPresent(Stronghold::class.java)) { + field.set(instance, fields.shared) + } else { + val value: Any = fields[field.name] ?: continue + val clazz = fromPrimitiveToNonPrimitive(field.type) + if (clazz.isAssignableFrom(value::class.java) || value::class.java == clazz) { + field.set(instance, value) + } + } + } + + return instance + } + + private fun fromPrimitiveToNonPrimitive(clazz: Class<*>): Class<*> { + return when (clazz) { + Boolean::class.java, Boolean::class.javaPrimitiveType -> Boolean::class.java + Int::class.java, Int::class.javaPrimitiveType -> Int::class.java + Long::class.java, Long::class.javaPrimitiveType -> Long::class.java + Char::class.java, Char::class.javaPrimitiveType -> Char::class.java + String::class.java -> String::class.java + Double::class.java, Double::class.javaPrimitiveType -> Double::class.java + else -> clazz + } + } + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectionFields.kt b/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectionFields.kt new file mode 100644 index 00000000..d593ada6 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectionFields.kt @@ -0,0 +1,184 @@ +package pw.mihou.nexus.core.reflective + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.core.exceptions.NotInheritableException +import pw.mihou.nexus.core.reflective.annotations.Required +import pw.mihou.nexus.core.reflective.annotations.Share +import pw.mihou.nexus.core.reflective.annotations.WithDefault +import pw.mihou.nexus.features.command.core.NexusCommandCore +import pw.mihou.nexus.features.inheritance.Inherits +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.InvocationTargetException +import java.util.* + +class NexusReflectionFields(private val from: Any, private val reference: Any) { + + private val _fields = mutableMapOf() + private val _shared = mutableMapOf() + + val shared: Map get() = Collections.unmodifiableMap(_shared) + val fields: Map get() = Collections.unmodifiableMap(_fields) + + @Suppress("UNCHECKED_CAST") + operator fun get(key: String): R? { + return _fields[key.lowercase()]?.let { it as? R } + } + + fun stringify(key: String): String? { + return _fields[key.lowercase()]?.let { + if (it is String) { + return it + } + + return it.toString() + } + } + + private val to = reference::class.java + + init { + initDefaults() + if (reference is NexusCommandCore) { + Nexus.configuration.global.inheritance?.let { parent -> + if (parent::class.java.isAnnotationPresent(Inherits::class.java)) { + Nexus.logger.warn("Nexus doesn't support @Inherits on parent-level, instead, use superclasses " + + "such as abstract classes instead. Causing class: ${parent::class.java.name}.") + } + if (parent::class.java.superclass != null) { + load(parent::class.java.superclass, parent) + } + load(parent::class.java, parent) + } + } + + if (from::class.java.isAnnotationPresent(Inherits::class.java)) { + val parent = from::class.java.getAnnotation(Inherits::class.java).value.java + if (parent.isAnnotationPresent(Inherits::class.java)) { + Nexus.logger.warn("Nexus doesn't support @Inherits on parent-level, instead, use superclasses " + + "such as abstract classes instead. Causing class: ${parent.name}.") + } + val instantiatedParent = instantiate(parent) + if (parent.superclass != null) { + load(parent.superclass, instantiatedParent) + } + load(instantiatedParent::class.java, instantiatedParent) + } + + if (from::class.java.superclass != null) { + load(from::class.java.superclass) + } + + load(from::class.java) + ensureHasRequired() + } + + fun referenceWithAnnotation(annotation: Class): Field? { + return to.declaredFields.find { it.isAnnotationPresent(annotation) } + } + + fun referencesWithAnnotation(annotation: Class): List { + return to.declaredFields.filter { it.isAnnotationPresent(annotation) } + } + + /** + * Loads all the declared fields of the `reference` class that has the [WithDefault] annotation, this + * should be done first in order to have the fields be overridden when there is a value. + */ + private fun initDefaults() { + for (field in to.declaredFields) { + if (!field.isAnnotationPresent(WithDefault::class.java)) continue + field.isAccessible = true + try { + _fields[field.name.lowercase()] = field.get(reference) + } catch (e: IllegalAccessException) { + throw IllegalStateException("Unable to complete reflection due to IllegalAccessException. [class=${to.name},field=${field.name}]") + } + } + } + + /** + * Instantiates the `clazz`, if the class has a singleton instance, then it will use that instead. This requires + * the class to have a constructor that has no parameters, otherwise it will fail. + * + * @param clazz the class to instantiate. + * @return the instantiated class. + */ + private fun instantiate(clazz: Class<*>): Any { + try { + val reference: Any + var singleton: Field? = null + + // Detecting singleton instances, whether by Kotlin, or self-declared by the authors. + // This is important because we don't want to doubly-instantiate the instance. + try { + singleton = clazz.getField("INSTANCE") + } catch (_: NoSuchFieldException) { + for (field in clazz.declaredFields) { + if (field.name.equals("INSTANCE") + || field::class.java.name.equals(clazz.name) + || field::class.java == clazz) { + singleton = field + } + } + } + + if (singleton != null) { + reference = singleton.get(null) + } else { + val constructor: Constructor<*> = if (clazz.constructors.isNotEmpty()) { + clazz.constructors.firstOrNull { it.parameterCount == 0 } ?: throw NotInheritableException(clazz) + } else { + clazz.getDeclaredConstructor() + } + + constructor.isAccessible = true + reference = constructor.newInstance() + } + + return reference + } catch (exception: Exception) { + when(exception) { + is InvocationTargetException, is InstantiationException, is IllegalAccessException, is NoSuchMethodException -> + throw IllegalStateException("Unable to instantiate class, likely no compatible constructor. [class=${clazz.name},error=${exception}]") + else -> throw exception + } + } + } + + /** + * Pulls all the fields from the `clazz` to their respective fields depending on the annotation, for example, if there + * is a [Share] annotation present, it will be recorded under [sharedFields] otherwise it will be under [_fields]. + * + * @param clazz the class to reference. + * @param ref the reference object. + */ + private fun load(clazz : Class<*>, ref: Any = from) { + clazz.declaredFields.forEach { + it.isAccessible = true + try { + val value = it.get(ref) ?: return + if (it.isAnnotationPresent(Share::class.java)) { + _shared[it.name.lowercase()] = value + return@forEach + } + + _fields[it.name.lowercase()] = value + } catch (e: IllegalAccessException) { + throw IllegalStateException("Unable to complete reflection due to IllegalAccessException. [class=${clazz.name},field=${it.name}]") + } + } + } + + /** + * Ensures that all required fields (ones with [Required] annotation) has a value, otherwise throws an exception. + */ + private fun ensureHasRequired() { + for (field in to.declaredFields) { + if (!field.isAnnotationPresent(Required::class.java)) continue + if (_fields[field.name.lowercase()] == null) { + throw IllegalStateException("${field.name} is a required field, therefore, needs to have a value in class ${from::class.java.name}.") + } + } + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectiveCore.java b/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectiveCore.java deleted file mode 100755 index c178aac6..00000000 --- a/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectiveCore.java +++ /dev/null @@ -1,86 +0,0 @@ -package pw.mihou.nexus.core.reflective; - -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.core.assignment.NexusUuidAssigner; -import pw.mihou.nexus.core.reflective.annotations.*; -import pw.mihou.nexus.core.reflective.core.NexusReflectiveVariableCore; -import pw.mihou.nexus.core.reflective.facade.NexusReflectiveVariableFacade; -import pw.mihou.nexus.features.command.core.NexusCommandCore; - -import java.util.*; - -public class NexusReflectiveCore { - - private static final Class REFERENCE_CLASS = NexusCommandCore.class; - - public static NexusCommandCore command(Object object, NexusCore core) { - NexusCommandCore reference = new NexusCommandCore(); - - NexusReflectiveVariableFacade facade = new NexusReflectiveVariableCore(object, reference); - - if (REFERENCE_CLASS.isAnnotationPresent(MustImplement.class)) { - Class extension = REFERENCE_CLASS.getAnnotation(MustImplement.class).clazz(); - - if (!extension.isAssignableFrom(object.getClass())) { - throw new IllegalStateException("Nexus was unable to complete reflection stage because class: " + - object.getClass().getName() - + " must implement the following class: " + extension.getName()); - } - } - - - Arrays.stream(reference.getClass().getDeclaredFields()) - .forEach(field -> { - field.setAccessible(true); - - try { - if (field.isAnnotationPresent(InjectReferenceClass.class)) { - field.set(reference, object); - } else if (field.isAnnotationPresent(InjectUUID.class)) { - field.set(reference, NexusUuidAssigner.request()); - } else if (field.isAnnotationPresent(InjectNexusCore.class)) { - field.set(reference, core); - } else if (field.isAnnotationPresent(Stronghold.class)){ - field.set(reference, facade.getSharedFields()); - } else { - facade.getWithType(field.getName(), fromPrimitiveToNonPrimitive(field.getType())).ifPresent(o -> { - try { - field.set(reference, o); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - }); - } - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - }); - - return reference; - } - - /** - * This identifies primitive classes and returns their non-primitive - * class values since for some reason, type-checking requires it. - * - * @param clazz The class to identify. - * @return The non-primitive class variant. - */ - private static Class fromPrimitiveToNonPrimitive(Class clazz) { - if (clazz.equals(Boolean.class) || clazz.equals(boolean.class)) - return Boolean.class; - else if (clazz.equals(Integer.class) || clazz.equals(int.class)) - return Integer.class; - else if (clazz.equals(Long.class) || clazz.equals(long.class)) - return Long.class; - else if (clazz.equals(Character.class) || clazz.equals(char.class)) - return Character.class; - else if (clazz.equals(String.class)) - return String.class; - else if (clazz.equals(Double.class) || clazz.equals(double.class)) - return Double.class; - else - return clazz; - } - -} diff --git a/src/main/java/pw/mihou/nexus/core/reflective/annotations/Uuid.kt b/src/main/java/pw/mihou/nexus/core/reflective/annotations/Uuid.kt new file mode 100644 index 00000000..7e1daa1f --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/reflective/annotations/Uuid.kt @@ -0,0 +1,5 @@ +package pw.mihou.nexus.core.reflective.annotations + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class Uuid() diff --git a/src/main/java/pw/mihou/nexus/core/reflective/core/NexusReflectiveVariableCore.java b/src/main/java/pw/mihou/nexus/core/reflective/core/NexusReflectiveVariableCore.java deleted file mode 100755 index 0de5ed9b..00000000 --- a/src/main/java/pw/mihou/nexus/core/reflective/core/NexusReflectiveVariableCore.java +++ /dev/null @@ -1,88 +0,0 @@ -package pw.mihou.nexus.core.reflective.core; - -import pw.mihou.nexus.core.reflective.annotations.Required; -import pw.mihou.nexus.core.reflective.annotations.Share; -import pw.mihou.nexus.core.reflective.annotations.WithDefault; -import pw.mihou.nexus.core.reflective.facade.NexusReflectiveVariableFacade; -import pw.mihou.nexus.features.command.core.NexusCommandCore; - -import javax.annotation.Nullable; -import java.util.*; - -public class NexusReflectiveVariableCore implements NexusReflectiveVariableFacade { - - private final HashMap fields = new HashMap<>(); - private final HashMap sharedFields = new HashMap<>(); - - public NexusReflectiveVariableCore(Object object, NexusCommandCore core) { - // We'll collect all the fields with the WithDefault annotation from the reference class first. - // then utilize those fields when we need a default value. Please ensure that the field always - // has a value beforehand. - - Arrays.stream(NexusCommandCore.class.getDeclaredFields()) - .filter(field -> field.isAnnotationPresent(WithDefault.class)) - .peek(field -> field.setAccessible(true)) - .forEach(field -> { - try { - fields.put(field.getName().toLowerCase(), field.get(core)); - } catch (IllegalAccessException e) { - e.printStackTrace(); - throw new IllegalStateException( - "Nexus was unable to complete variable reflection stage for class: " + NexusCommandCore.class.getName() - ); - } - }); - - // After collecting all the defaults, we can start bootstrapping the fields HashMap. - Arrays.stream(object.getClass().getDeclaredFields()).forEach(field -> { - field.setAccessible(true); - - try { - Object obj = field.get(object); - - if (obj == null) { - return; - } - - if (field.isAnnotationPresent(Share.class)) { - sharedFields.put(field.getName().toLowerCase(), obj); - return; - } - - fields.put(field.getName().toLowerCase(), obj); - } catch (IllegalAccessException e) { - e.printStackTrace(); - throw new IllegalStateException( - "Nexus was unable to complete variable reflection stage for class: " + object.getClass().getName() - ); - } - }); - - // Handling required fields, the difference between `clazz` and `object.getClass()` - // is that `clazz` refers to the NexusCommandImplementation while `object` refers - // to the developer-defined object. - Arrays.stream(NexusCommandCore.class.getDeclaredFields()) - .filter(field -> field.isAnnotationPresent(Required.class)) - .forEach(field -> { - @Nullable Object obj = fields.get(field.getName().toLowerCase()); - if (obj == null) { - throw new IllegalStateException( - "Nexus was unable to complete variable reflection stage for class: " + object.getClass().getName() + - " because the field: " + field.getName() + " is required to have a value." - ); - } - }); - } - - @Override - @SuppressWarnings("unchecked") - public Optional get(String field) { - return fields.containsKey(field.toLowerCase()) ? Optional.of((R) fields.get(field.toLowerCase())) : Optional.empty(); - - } - - @Override - public Map getSharedFields() { - return Collections.unmodifiableMap(sharedFields); - } -} diff --git a/src/main/java/pw/mihou/nexus/core/reflective/facade/NexusReflectiveVariableFacade.java b/src/main/java/pw/mihou/nexus/core/reflective/facade/NexusReflectiveVariableFacade.java deleted file mode 100755 index 5ecd7597..00000000 --- a/src/main/java/pw/mihou/nexus/core/reflective/facade/NexusReflectiveVariableFacade.java +++ /dev/null @@ -1,47 +0,0 @@ -package pw.mihou.nexus.core.reflective.facade; - -import java.util.Map; -import java.util.Optional; - -/** - * This facade is dedicated to {@link pw.mihou.nexus.core.reflective.core.NexusReflectiveVariableCore} which - * is utilized by {@link pw.mihou.nexus.core.reflective.NexusReflectiveCore} to grab variables with reflection - * and so forth while abiding by the rules of default values. - */ -public interface NexusReflectiveVariableFacade { - - /** - * Gets the value of the field with the specified name. - * - * @param field The field name to fetch. - * @param The type to expect returned. - * @return The field value if present. - */ - Optional get(String field); - - /** - * Gets the map containing all the shared fields. - * - * @return An unmodifiable map that contains all the shared - * fields that were defined in the class. - */ - Map getSharedFields(); - - /** - * Gets the value of the field with the specified name that - * matches the specific type. - * - * @param field The field name to fetch. - * @param rClass The type in class to expect. - * @param The type to expect returned. - * @return The field value if present and also matches the type. - */ - @SuppressWarnings("unchecked") - default Optional getWithType(String field, Class rClass) { - return get(field).filter(o -> - rClass.isAssignableFrom(o.getClass()) - || o.getClass().equals(rClass) - ).map(o -> (R) o); - } - -} diff --git a/src/main/java/pw/mihou/nexus/express/NexusExpress.kt b/src/main/java/pw/mihou/nexus/express/NexusExpress.kt new file mode 100644 index 00000000..7015cbd0 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/NexusExpress.kt @@ -0,0 +1,87 @@ +package pw.mihou.nexus.express + +import org.javacord.api.DiscordApi +import org.javacord.api.entity.server.Server +import pw.mihou.nexus.express.event.NexusExpressEvent +import pw.mihou.nexus.express.request.NexusExpressRequest +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer +import java.util.function.Predicate + +interface NexusExpress { + + /** + * Queues an event to be executed by the specific shard that + * it is specified for. + * + * @param shard The shard to handle this event. + * @param event The event to execute for this shard. + * @return The controller and status viewer for the event. + */ + fun queue(shard: Int, event: NexusExpressRequest): NexusExpressEvent + + /** + * Queues an event to be executed by the shard that matches the given predicate. + * + * @param predicate The predicate that the shard should match. + * @param event The event to execute for this shard. + * @return The controller and status viewer for the event. + */ + fun queue(predicate: Predicate, event: NexusExpressRequest): NexusExpressEvent + + /** + * Queues an event to be executed by any specific shard that is available + * to take the event. + * + * @param event The event to execute by a shard. + * @return The controller and status viewer for the event. + */ + fun queue(event: NexusExpressRequest): NexusExpressEvent + + /** + * Creates an awaiting listener to wait for a given shard to be ready and returns + * the shard itself if it is ready. + * + * @param shard The shard number to wait to complete. + * @return The [DiscordApi] instance of the given shard. + */ + fun await(shard: Int): CompletableFuture + + /** + * Creates an awaiting listener to wait for a given shard that has the given server. + * + * @param server The server to wait for a shard to contain. + * @return The [DiscordApi] instance of the given shard. + */ + fun await(server: Long): CompletableFuture + + /** + * Creates an awaiting listener to wait for any available shards to be ready and returns + * the shard that is available. + * + * @return The [DiscordApi] available to take any action. + */ + fun awaitAvailable(): CompletableFuture + + /** + * A short-hand method to cause a failed future whenever the event has expired or has failed to be + * processed which can happen at times. + *



+ * It is recommended to use ExpressWay with CompletableFutures as this provides an extra kill-switch + * whenever a shard somehow isn't available after the expiration time. + * + * @param event The event that was queued. + * @param future The future to fail when the status of the event has failed. + * @param A random type. + */ + fun failFutureOnExpire(event: NexusExpressEvent, future: CompletableFuture) + + /** + * Broadcasts the event for all shards to execute, this doesn't wait for any shards + * that aren't available during the time that it was executed. + * + * @param event The event to broadcast to all shards. + */ + fun broadcast(event: Consumer) + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/express/core/NexusExpressCore.kt b/src/main/java/pw/mihou/nexus/express/core/NexusExpressCore.kt new file mode 100644 index 00000000..2d9b53a1 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/core/NexusExpressCore.kt @@ -0,0 +1,251 @@ +package pw.mihou.nexus.express.core + +import org.javacord.api.DiscordApi +import org.javacord.api.entity.server.Server +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.core.exceptions.NexusFailedActionException +import pw.mihou.nexus.express.NexusExpress +import pw.mihou.nexus.express.event.NexusExpressEvent +import pw.mihou.nexus.express.event.core.NexusExpressEventCore +import pw.mihou.nexus.express.event.status.NexusExpressEventStatus +import pw.mihou.nexus.express.request.NexusExpressRequest +import java.util.concurrent.* +import java.util.concurrent.locks.ReentrantLock +import java.util.function.Consumer +import java.util.function.Predicate +import kotlin.concurrent.withLock + +internal class NexusExpressCore: NexusExpress { + + private val globalQueue: BlockingQueue = LinkedBlockingQueue() + private val predicateQueue: BlockingQueue, NexusExpressEvent>> = LinkedBlockingQueue() + + private val predicateQueueProcessingLock = ReentrantLock() + private val globalQueueProcessingLock = ReentrantLock() + + private val localQueue: MutableMap> = ConcurrentHashMap() + + fun ready(shard: DiscordApi) { + Nexus.launcher.launch { + val local = localQueue(shard.currentShard) + while (!local.isEmpty()) { + try { + val event = local.poll() + + if (event != null) { + Nexus.launcher.launch { + (event as NexusExpressEventCore).process(shard) + } + } + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught from Nexus Express Way.", exception) + } + } + + predicateQueueProcessingLock.withLock { + while (!predicateQueue.isEmpty()) { + try { + val (predicate, _) = predicateQueue.peek() + + if (!predicate.test(shard)) continue + val (_, event) = predicateQueue.poll() + + Nexus.launcher.launch { + (event as NexusExpressEventCore).process(shard) + } + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught from Nexus Express Way.", exception) + } + } + } + } + + Nexus.launcher.launch { + globalQueueProcessingLock.withLock { + while(globalQueue.isNotEmpty()) { + try { + val event = globalQueue.poll() + + if (event != null) { + Nexus.launcher.launch { + (event as NexusExpressEventCore).process(shard) + } + } + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught from Nexus Express Way.", exception) + } + } + } + } + } + + private fun localQueue(shard: Int): BlockingQueue { + return localQueue.computeIfAbsent(shard) { LinkedBlockingQueue() } + } + + override fun queue(shard: Int, event: NexusExpressRequest): NexusExpressEvent { + val expressEvent = NexusExpressEventCore(event) + + if (Nexus.sharding[shard] == null){ + localQueue(shard).add(expressEvent) + + val maximumTimeout = Nexus.configuration.express.maximumTimeout + if (!maximumTimeout.isZero && !maximumTimeout.isNegative) { + Nexus.launch.scheduler.launch(maximumTimeout.toMillis()) { + expressEvent.`do` { + if (status() == NexusExpressEventStatus.WAITING) { + val removed = localQueue(shard).remove(this) + if (Nexus.configuration.express.showExpiredWarnings) { + Nexus.logger.warn( + "An express request that was specified " + + "for shard $shard has expired after ${maximumTimeout.toMillis()} milliseconds " + + "without the shard connecting with Nexus. [acknowledged=$removed]" + ) + } + + expire() + } + } + } + } + } else { + Nexus.launcher.launch { expressEvent.process(Nexus.sharding[shard]!!) } + } + + return expressEvent + } + + override fun queue(predicate: Predicate, event: NexusExpressRequest): NexusExpressEvent { + val expressEvent = NexusExpressEventCore(event) + val shard = Nexus.sharding.find { shard2 -> predicate.test(shard2) } + + if (shard == null){ + val pair = predicate to expressEvent + predicateQueue.add(pair) + + val maximumTimeout = Nexus.configuration.express.maximumTimeout + if (!maximumTimeout.isZero && !maximumTimeout.isNegative) { + Nexus.launch.scheduler.launch(maximumTimeout.toMillis()) { + expressEvent.`do` { + if (status() == NexusExpressEventStatus.WAITING) { + val removed = predicateQueue.remove(pair) + if (Nexus.configuration.express.showExpiredWarnings) { + Nexus.logger.warn( + "An express request that was specified " + + "for a predicate has expired after ${maximumTimeout.toMillis()} milliseconds " + + "without any matching shard connecting with Nexus. [acknowledged=$removed]" + ) + } + + expire() + } + } + } + } + } else { + Nexus.launcher.launch { expressEvent.process(shard) } + } + + return expressEvent + } + + override fun queue(event: NexusExpressRequest): NexusExpressEvent { + val expressEvent = NexusExpressEventCore(event) + + if (Nexus.sharding.size == 0){ + globalQueue.add(expressEvent) + + val maximumTimeout = Nexus.configuration.express.maximumTimeout + if (!maximumTimeout.isZero && !maximumTimeout.isNegative) { + Nexus.launch.scheduler.launch(maximumTimeout.toMillis()) { + expressEvent.`do` { + if (status() == NexusExpressEventStatus.WAITING) { + val removed = globalQueue.remove(this) + if (Nexus.configuration.express.showExpiredWarnings) { + Nexus.logger.warn( + "An express request that was specified " + + "for any available shards has expired after ${maximumTimeout.toMillis()} milliseconds " + + "without any shard connecting with Nexus. [acknowledged=$removed]" + ) + } + + expire() + } + } + } + } + } else { + Nexus.launcher.launch { expressEvent.process(Nexus.sharding.collection().first()) } + } + + return expressEvent + } + + override fun await(shard: Int): CompletableFuture { + val shardA = Nexus.sharding[shard] + + if (shardA != null) { + return CompletableFuture.completedFuture(shardA) + } + + val future = CompletableFuture() + failFutureOnExpire(queue(shard, future::complete), future) + + return future + } + + override fun await(server: Long): CompletableFuture { + val serverA = Nexus.sharding.server(server) + + if (serverA != null) { + return CompletableFuture.completedFuture(serverA) + } + + val future = CompletableFuture() + failFutureOnExpire( + queue( + { shard -> shard.getServerById(server).isPresent }, + { shard -> future.complete(shard.getServerById(server).get()) } + ), + future + ) + + return future + } + + override fun awaitAvailable(): CompletableFuture { + val shardA = Nexus.sharding.collection().firstOrNull() + + if (shardA != null) { + return CompletableFuture.completedFuture(shardA) + } + + val future = CompletableFuture() + failFutureOnExpire(queue(future::complete), future) + + return future + } + + override fun failFutureOnExpire(event: NexusExpressEvent, future: CompletableFuture) { + event.addStatusChangeListener { _, _, newStatus -> + if (newStatus == NexusExpressEventStatus.EXPIRED || newStatus == NexusExpressEventStatus.STOPPED) { + future.completeExceptionally( + NexusFailedActionException("Failed to connect with the shard that was being waited, it's possible " + + "that the maximum timeout has been reached or the event has been somehow cancelled.") + ) + } + } + } + + override fun broadcast(event: Consumer) { + Nexus.sharding.collection().forEach { shard -> + Nexus.launcher.launch { + try { + event.accept(shard) + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught from Nexus Express Broadcaster.", exception) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/express/event/NexusExpressEvent.kt b/src/main/java/pw/mihou/nexus/express/event/NexusExpressEvent.kt new file mode 100644 index 00000000..7316a7e4 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/event/NexusExpressEvent.kt @@ -0,0 +1,84 @@ +package pw.mihou.nexus.express.event + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.express.event.listeners.NexusExpressEventStatusChange +import pw.mihou.nexus.express.event.listeners.NexusExpressEventStatusListener +import pw.mihou.nexus.express.event.status.NexusExpressEventStatus + +interface NexusExpressEvent { + + /** + * Signals the event to be cancelled. This method changes the status of the + * event from [NexusExpressEventStatus.WAITING] to [NexusExpressEventStatus.STOPPED]. + * + * The signal will be ignored if the cancel was executed when the status is not equivalent to [NexusExpressEventStatus.WAITING]. + */ + fun cancel() + + /** + * Gets the current status of the event. + * @return the current status of the event. + */ + fun status(): NexusExpressEventStatus + + /** + * Adds a [NexusExpressEventStatusChange] listener to the event, allowing you to receive events + * about when the status of the event changes. + * @param event the listener to use to listen to the event. + */ + fun addStatusChangeListener(event: NexusExpressEventStatusChange) + + /** + * Adds a specific [NexusExpressEventStatusListener] that can listen to specific, multiple status changes + * and react to it. This is a short-hand method of [addStatusChangeListener] and is used to handle specific + * status change events. + * @param ev the listener to use to listen to the event. + * @param statuses the new statuses that will trigger the listener. + */ + fun addStatusChangeListener(ev: NexusExpressEventStatusListener, vararg statuses: NexusExpressEventStatus) { + this.addStatusChangeListener status@{ event, _, newStatus -> + for (status in statuses) { + if (newStatus == status) { + try { + ev.onStatusChange(event) + } catch (ex: Exception) { + Nexus.logger.error("Caught an uncaught exception in a Express Way listener.", ex) + } + } + } + } + } + + /** + * Adds a [NexusExpressEventStatusListener] that listens specifically to events that causes the listener to + * cancel, such in the case of a [cancel] or an expire. If you want to listen specifically to a call to [cancel], + * we recommend using [addStoppedListener] listener instead. + * @param ev the listener to use to listen to the event. + */ + fun addCancelListener(ev: NexusExpressEventStatusListener) = + this.addStatusChangeListener(ev, NexusExpressEventStatus.STOPPED, NexusExpressEventStatus.EXPIRED) + + /** + * Adds a [NexusExpressEventStatusListener] that listens specifically to when the event is finished processing. + * @param ev the listener to use to listen to the event. + */ + fun addFinishedListener(ev: NexusExpressEventStatusListener) = + this.addStatusChangeListener(ev, NexusExpressEventStatus.FINISHED) + + /** + * Adds a [NexusExpressEventStatusListener] that listens specifically to when the event expired while waiting. + * @param ev the listener to use to listen to the event. + */ + fun addExpiredListener(ev: NexusExpressEventStatusListener) = + this.addStatusChangeListener(ev, NexusExpressEventStatus.EXPIRED) + + /** + * Adds a [NexusExpressEventStatusListener] that listens specifically to events that causes the listener to + * [cancel]. If you want to listen specifically to a call to [cancel] and expire, we recommend using + * [addCancelListener] listener instead which listens to both cancel and expire. + * @param ev the listener to use to listen to the event. + */ + fun addStoppedListener(ev: NexusExpressEventStatusListener) = + this.addStatusChangeListener(ev, NexusExpressEventStatus.STOPPED) + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/express/event/core/NexusExpressEventCore.kt b/src/main/java/pw/mihou/nexus/express/event/core/NexusExpressEventCore.kt new file mode 100644 index 00000000..7b430cda --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/event/core/NexusExpressEventCore.kt @@ -0,0 +1,74 @@ +package pw.mihou.nexus.express.event.core + +import org.javacord.api.DiscordApi +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.express.event.NexusExpressEvent +import pw.mihou.nexus.express.event.listeners.NexusExpressEventStatusChange +import pw.mihou.nexus.express.event.status.NexusExpressEventStatus +import pw.mihou.nexus.express.request.NexusExpressRequest + +internal class NexusExpressEventCore(val request: NexusExpressRequest): NexusExpressEvent { + + @Volatile private var status = NexusExpressEventStatus.WAITING + private val listeners = mutableListOf() + + override fun cancel() { + synchronized(this) { + if (status == NexusExpressEventStatus.WAITING) { + change(status = NexusExpressEventStatus.STOPPED) + } + } + } + + fun expire() { + synchronized (this) { + if (status == NexusExpressEventStatus.WAITING) { + change(status = NexusExpressEventStatus.EXPIRED) + } + } + } + + fun `do`(task: NexusExpressEventCore.() -> Unit) { + synchronized(this) { + task() + } + } + + fun process(shard: DiscordApi) { + synchronized(this) { + try { + if (status == NexusExpressEventStatus.STOPPED || status == NexusExpressEventStatus.EXPIRED) { + return@synchronized + } + + change(status = NexusExpressEventStatus.PROCESSING) + request.onEvent(shard) + } catch (exception: Exception) { + Nexus.configuration.global.logger.error("An uncaught exception was caught by Nexus Express Way.", exception) + } finally { + change(status = NexusExpressEventStatus.FINISHED) + } + } + } + + private fun change(status: NexusExpressEventStatus) { + synchronized(this) { + val oldStatus = this.status + this.status = status + + listeners.forEach { listener -> listener.onStatusChange(this, oldStatus, status) } + } + } + + override fun status(): NexusExpressEventStatus { + synchronized(this) { + return status + } + } + + override fun addStatusChangeListener(event: NexusExpressEventStatusChange) { + synchronized(this) { + listeners.add(event) + } + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/express/event/listeners/NexusExpressEventStatusChange.kt b/src/main/java/pw/mihou/nexus/express/event/listeners/NexusExpressEventStatusChange.kt new file mode 100644 index 00000000..1c1b8169 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/event/listeners/NexusExpressEventStatusChange.kt @@ -0,0 +1,10 @@ +package pw.mihou.nexus.express.event.listeners + +import pw.mihou.nexus.express.event.NexusExpressEvent +import pw.mihou.nexus.express.event.status.NexusExpressEventStatus + +fun interface NexusExpressEventStatusChange { + + fun onStatusChange(event: NexusExpressEvent, oldStatus: NexusExpressEventStatus, newStatus: NexusExpressEventStatus) + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/express/event/listeners/NexusExpressEventStatusListener.kt b/src/main/java/pw/mihou/nexus/express/event/listeners/NexusExpressEventStatusListener.kt new file mode 100644 index 00000000..cd3ea565 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/event/listeners/NexusExpressEventStatusListener.kt @@ -0,0 +1,7 @@ +package pw.mihou.nexus.express.event.listeners + +import pw.mihou.nexus.express.event.NexusExpressEvent + +fun interface NexusExpressEventStatusListener { + fun onStatusChange(event: NexusExpressEvent) +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/express/event/status/NexusExpressEventStatus.kt b/src/main/java/pw/mihou/nexus/express/event/status/NexusExpressEventStatus.kt new file mode 100644 index 00000000..4bd2e389 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/event/status/NexusExpressEventStatus.kt @@ -0,0 +1,5 @@ +package pw.mihou.nexus.express.event.status + +enum class NexusExpressEventStatus { + EXPIRED, STOPPED, WAITING, PROCESSING, FINISHED +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/express/request/NexusExpressRequest.kt b/src/main/java/pw/mihou/nexus/express/request/NexusExpressRequest.kt new file mode 100644 index 00000000..35c08010 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/request/NexusExpressRequest.kt @@ -0,0 +1,9 @@ +package pw.mihou.nexus.express.request + +import org.javacord.api.DiscordApi + +fun interface NexusExpressRequest { + + fun onEvent(shard: DiscordApi) + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/annotation/IdentifiableAs.kt b/src/main/java/pw/mihou/nexus/features/command/annotation/IdentifiableAs.kt new file mode 100644 index 00000000..a922cd69 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/annotation/IdentifiableAs.kt @@ -0,0 +1,13 @@ +package pw.mihou.nexus.features.command.annotation + +import pw.mihou.nexus.Nexus + +/** + * An annotation that tells [Nexus] that the command should use the given key as its unique identifier. + * + * This tends to be used when a command's name is common among other commands, which therefore causes an index-identifier conflict, + * and needs to be resolved by using this annotation to change the identifier of the command. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class IdentifiableAs(val key: String) diff --git a/src/main/java/pw/mihou/nexus/features/command/annotation/NexusAttach.java b/src/main/java/pw/mihou/nexus/features/command/annotation/NexusAttach.java deleted file mode 100755 index 1ff6ce8b..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/annotation/NexusAttach.java +++ /dev/null @@ -1,16 +0,0 @@ -package pw.mihou.nexus.features.command.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * This tells Nexus to attach the command onto the Nexus command directory - * once it is finished processing the command. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Deprecated(forRemoval = true) -public @interface NexusAttach { -} diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandCore.java b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandCore.java index d7568e10..bfbe9839 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandCore.java +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandCore.java @@ -3,12 +3,12 @@ import org.javacord.api.entity.permission.PermissionType; import org.javacord.api.interaction.DiscordLocale; import org.javacord.api.interaction.SlashCommandOption; -import pw.mihou.nexus.core.NexusCore; +import org.jetbrains.annotations.NotNull; import pw.mihou.nexus.core.reflective.annotations.*; +import pw.mihou.nexus.features.command.validation.OptionValidation; import pw.mihou.nexus.features.command.facade.NexusCommand; import pw.mihou.nexus.features.command.facade.NexusHandler; -import java.time.Duration; import java.util.*; import java.util.stream.Stream; @@ -28,6 +28,7 @@ public class NexusCommandCore implements NexusCommand { private Map nexusCustomFields; @Required + @Uuid public String name; @WithDefault @@ -43,35 +44,34 @@ public class NexusCommandCore implements NexusCommand { public List options = Collections.emptyList(); @WithDefault - public Duration cooldown = Duration.ofSeconds(5); + public List> validators = Collections.emptyList(); @WithDefault public List middlewares = Collections.emptyList(); @WithDefault public List afterwares = Collections.emptyList(); - @WithDefault public List serverIds = new ArrayList<>(); - @WithDefault public boolean defaultEnabledForEveryone = true; - @WithDefault public boolean enabledInDms = true; - @WithDefault public boolean defaultDisabled = false; - + @WithDefault + public boolean nsfw = false; @WithDefault public List defaultEnabledForPermissions = Collections.emptyList(); - - @InjectNexusCore - public NexusCore core; - @InjectReferenceClass public NexusHandler handler; + @NotNull + @Override + public String getUuid() { + return uuid; + } + @Override public String getName() { return name; @@ -87,28 +87,22 @@ public List getOptions() { return options; } - @Override - public Duration getCooldown() { - return cooldown; - } - @Override public List getServerIds() { return serverIds; } @Override - public NexusCommand addSupportFor(Long... serverIds) { + public NexusCommand associate(Long... serverIds) { this.serverIds = Stream.concat(this.serverIds.stream(), Stream.of(serverIds)).toList(); return this; } @Override - public NexusCommand removeSupportFor(Long... serverIds) { - List mutableList = new ArrayList<>(this.serverIds); - mutableList.removeAll(Arrays.stream(serverIds).toList()); + public NexusCommand disassociate(Long... serverIds) { + List excludedSnowflakes = Arrays.asList(serverIds); + this.serverIds = this.serverIds.stream().filter(snowflake -> !excludedSnowflakes.contains(snowflake)).toList(); - this.serverIds = mutableList.stream().toList(); return this; } @@ -143,6 +137,11 @@ public boolean isDefaultDisabled() { return defaultDisabled; } + @Override + public boolean isNsfw() { + return nsfw; + } + @Override public List getDefaultEnabledForPermissions() { return defaultEnabledForPermissions; @@ -158,19 +157,23 @@ public Map getDescriptionLocalizations() { return descriptionLocalizations; } - @Override - public long getServerId() { - return serverIds.get(0); - } - @Override public String toString() { return "NexusCommandCore{" + "name='" + name + '\'' + ", description='" + description + '\'' + ", options=" + options + - ", cooldown=" + cooldown + ", serverId=" + getServerIds().toString() + + ", middlewares=" + middlewares.toString() + + ", afterwares=" + afterwares.toString() + + ", nameLocalizations=" + nameLocalizations.toString() + + ", descriptionLocalizations=" + descriptionLocalizations.toString() + + ", defaultEnabledForPermissions=" + defaultEnabledForPermissions.toString() + + ", shared=" + nexusCustomFields.toString() + + ", defaultDisabled=" + defaultDisabled + + ", defaultEnabledForEveryone=" + defaultEnabledForEveryone + + ", enabledInDms=" + enabledInDms + + ", nsfw=" + nsfw + '}'; } } diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.java b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.java deleted file mode 100755 index 7241d317..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.java +++ /dev/null @@ -1,70 +0,0 @@ -package pw.mihou.nexus.features.command.core; - -import org.javacord.api.event.interaction.SlashCommandCreateEvent; -import org.javacord.api.util.logging.ExceptionLogger; -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.core.threadpool.NexusThreadPool; -import pw.mihou.nexus.features.command.facade.NexusCommandEvent; -import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore; -import pw.mihou.nexus.features.command.interceptors.core.NexusMiddlewareGateCore; -import pw.mihou.nexus.features.messages.core.NexusMessageCore; - -import java.util.ArrayList; -import java.util.List; - -public class NexusCommandDispatcher { - - /** - * Dispatches one slash command create event of a command onto the given {@link NexusCommandCore}. - * This performs the necessary middleware handling, dispatching to the listener and afterware handling. - *
- * This is synchronous by nature except when the event is dispatched to its respective listener and also - * when the afterwares are executed. - * - * @param instance The {@link NexusCommandCore} instance to dispatch the event towards. - * @param event The {@link SlashCommandCreateEvent} event to dispatch. - */ - public static void dispatch(NexusCommandCore instance, SlashCommandCreateEvent event) { - NexusCommandEvent nexusEvent = new NexusCommandEventCore(event, instance); - List middlewares = new ArrayList<>(); - middlewares.addAll(instance.core.getGlobalMiddlewares()); - middlewares.addAll(instance.middlewares); - - List afterwares = new ArrayList<>(); - afterwares.addAll(instance.core.getGlobalAfterwares()); - afterwares.addAll(instance.afterwares); - - NexusMiddlewareGateCore middlewareGate = (NexusMiddlewareGateCore) NexusCommandInterceptorCore.interceptWithMany(middlewares, nexusEvent); - - if (middlewareGate != null) { - NexusMessageCore middlewareResponse = ((NexusMessageCore) middlewareGate.response()); - if (middlewareResponse != null) { - middlewareResponse - .convertTo(nexusEvent.respondNow()) - .respond() - .exceptionally(ExceptionLogger.get()); - } - return; - } - - if (event.getSlashCommandInteraction().getChannel().isEmpty()) { - NexusCore.logger.error( - "The channel of a slash command event is somehow not present; this is possibly a change in Discord's side " + - "and may need to be addressed, please send an issue @ https://github.com/ShindouMihou/Nexus" - ); - } - - NexusThreadPool.executorService.submit(() -> { - try { - instance.handler.onEvent(nexusEvent); - } catch (Throwable throwable) { - NexusCore.logger.error("An uncaught exception was received by Nexus Command Dispatcher for the " + - "command " + instance.name + " with the following stacktrace."); - throwable.printStackTrace(); - } - }); - - NexusThreadPool.executorService.submit(() -> NexusCommandInterceptorCore.interceptWithMany(afterwares, nexusEvent)); - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.kt new file mode 100755 index 00000000..d73fc09c --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.kt @@ -0,0 +1,115 @@ +package pw.mihou.nexus.features.command.core + +import org.javacord.api.event.interaction.SlashCommandCreateEvent +import org.javacord.api.util.logging.ExceptionLogger +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.Nexus.globalAfterwares +import pw.mihou.nexus.Nexus.globalMiddlewares +import pw.mihou.nexus.Nexus.logger +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore +import pw.mihou.nexus.features.command.interceptors.core.NexusMiddlewareGateCore +import pw.mihou.nexus.features.command.validation.OptionValidation +import pw.mihou.nexus.features.command.validation.middleware.OptionValidationMiddleware +import pw.mihou.nexus.features.command.validation.result.ValidationResult +import java.time.Instant +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean + +object NexusCommandDispatcher { + /** + * Dispatches one slash command create event of a command onto the given [NexusCommandCore]. + * This performs the necessary middleware handling, dispatching to the listener and afterware handling. + *

+ * This is synchronous by nature except when the event is dispatched to its respective listener and also + * when the afterwares are executed. + * + * @param instance The [NexusCommandCore] instance to dispatch the event towards. + * @param event The [SlashCommandCreateEvent] event to dispatch. + */ + fun dispatch(instance: NexusCommandCore, event: SlashCommandCreateEvent) { + val nexusEvent = NexusCommandEventCore(event, instance) + + val middlewares: MutableList = ArrayList() + middlewares.add(OptionValidationMiddleware.NAME) + middlewares.addAll(globalMiddlewares) + middlewares.addAll(instance.middlewares) + + val afterwares: MutableList = ArrayList() + afterwares.addAll(globalAfterwares) + afterwares.addAll(instance.afterwares) + + var dispatched = false + + try { + val middlewareGate: NexusMiddlewareGateCore? = if (Nexus.configuration.interceptors.autoDeferMiddlewareResponses) { + val future = CompletableFuture.supplyAsync { + NexusCommandInterceptorCore.execute(nexusEvent, NexusCommandInterceptorCore.middlewares(middlewares)) + } + val timeUntil = Instant.now().toEpochMilli() - + event.interaction.creationTimestamp.minusMillis(Nexus.configuration.global.autoDeferAfterMilliseconds).toEpochMilli() + val deferredTaskRan = AtomicBoolean(false) + val task = Nexus.launch.scheduler.launch(timeUntil) { + deferredTaskRan.set(true) + if (future.isDone) { + return@launch + } + nexusEvent.respondLaterEphemerallyIf(Nexus.configuration.interceptors.autoDeferAsEphemeral) + .exceptionally(ExceptionLogger.get()) + } + val gate = future.join() + if (!deferredTaskRan.get()) { + task.cancel(false) + } + gate + } else { + NexusCommandInterceptorCore.execute(nexusEvent, NexusCommandInterceptorCore.middlewares(middlewares)) + } + + if (middlewareGate != null) { + val middlewareResponse = middlewareGate.response() + if (middlewareResponse != null) { + val updaterFuture = nexusEvent.updater.get() + if (updaterFuture != null) { + val updater = updaterFuture.join() + middlewareResponse.into(updater).update().exceptionally(ExceptionLogger.get()) + } else { + var responder = nexusEvent.respondNow() + if (middlewareResponse.ephemeral) { + responder = nexusEvent.respondNowEphemerally() + } + middlewareResponse.into(responder).respond().exceptionally(ExceptionLogger.get()) + } + } + return + } + + if (event.slashCommandInteraction.channel.isEmpty) { + logger.error( + "The channel of a slash command event is somehow not present; this is possibly a change in Discord's side " + + "and may need to be addressed, please send an issue @ https://github.com/ShindouMihou/Nexus" + ) + } + + dispatched = true + Nexus.launcher.launch { + try { + instance.handler.onEvent(nexusEvent) + } catch (throwable: Throwable) { + logger.error("An uncaught exception was received by Nexus Command Dispatcher for the " + + "command ${instance.name} with the following stacktrace.", throwable) + } + } + + Nexus.launcher.launch { + NexusCommandInterceptorCore.execute(nexusEvent, NexusCommandInterceptorCore.afterwares(afterwares)) + } + } catch (exception: Exception) { + logger.error("An uncaught exception occurred within Nexus' dispatcher for command ${instance.name}.", exception) + } finally { + if (!dispatched) { + NexusCommandInterceptorCore.execute(nexusEvent, NexusCommandInterceptorCore.afterwares(afterwares), dispatched = false) + } + } + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.java b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.java deleted file mode 100755 index dbca6c07..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.java +++ /dev/null @@ -1,42 +0,0 @@ -package pw.mihou.nexus.features.command.core; - -import org.javacord.api.event.interaction.SlashCommandCreateEvent; -import pw.mihou.nexus.features.command.facade.NexusCommand; -import pw.mihou.nexus.features.command.facade.NexusCommandEvent; - -import java.util.HashMap; -import java.util.Map; - -public class NexusCommandEventCore implements NexusCommandEvent { - - private final SlashCommandCreateEvent event; - private final NexusCommand command; - private final Map store = new HashMap<>(); - - /** - * Creates a new Nexus Event Core that is sent along with the Command Interceptors - * and other sorts of handlers. - * - * @param event The base event received from Javacord. - * @param command The command instance that is used. - */ - public NexusCommandEventCore(SlashCommandCreateEvent event, NexusCommand command) { - this.event = event; - this.command = command; - } - - @Override - public SlashCommandCreateEvent getBaseEvent() { - return event; - } - - @Override - public NexusCommand getCommand() { - return command; - } - - @Override - public Map store() { - return store; - } -} diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt new file mode 100755 index 00000000..0ae6eb7f --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -0,0 +1,82 @@ +package pw.mihou.nexus.features.command.core + +import org.javacord.api.entity.message.MessageFlag +import org.javacord.api.event.interaction.SlashCommandCreateEvent +import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater +import org.javacord.api.util.logging.ExceptionLogger +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.configuration.modules.Cancellable +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.responses.NexusAutoResponse +import pw.mihou.nexus.features.messages.NexusMessage +import java.time.Instant +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Function + +class NexusCommandEventCore(override val event: SlashCommandCreateEvent, override val command: NexusCommand) : NexusCommandEvent { + private val store: MutableMap = HashMap() + var updater: AtomicReference?> = AtomicReference(null) + + override fun store(): MutableMap = store + + override fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture { + var task: Cancellable? = null + val deferredTaskRan = AtomicBoolean(false) + if (updater.get() == null) { + val timeUntil = Instant.now().toEpochMilli() - event.interaction.creationTimestamp + .minusMillis(Nexus.configuration.global.autoDeferAfterMilliseconds) + .toEpochMilli() + + task = Nexus.launch.scheduler.launch(timeUntil) { + if (updater.get() == null) { + respondLaterEphemerallyIf(ephemeral).exceptionally(ExceptionLogger.get()) + } + deferredTaskRan.set(true) + } + } + val future = CompletableFuture() + Nexus.launcher.launch { + try { + val message = response.apply(null) + if (!deferredTaskRan.get() && task != null) { + task.cancel(false) + } + val updater = updater.get() + if (updater == null) { + val responder = respondNow() + if (ephemeral) { + responder.setFlags(MessageFlag.EPHEMERAL) + } + message.into(responder).respond() + .thenAccept { r -> future.complete(NexusAutoResponse(r, null)) } + .exceptionally { exception -> + future.completeExceptionally(exception) + return@exceptionally null + } + } else { + val completedUpdater = updater.join() + message.into(completedUpdater).update() + .thenAccept { r -> future.complete(NexusAutoResponse(null, r)) } + .exceptionally { exception -> + future.completeExceptionally(exception) + return@exceptionally null + } + } + } catch (exception: Exception) { + future.completeExceptionally(exception) + } + } + return future + } + + override fun respondLater(): CompletableFuture { + return updater.updateAndGet { interaction.respondLater() }!! + } + + override fun respondLaterEphemerally(): CompletableFuture { + return updater.updateAndGet { interaction.respondLater(true) }!! + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.java b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.java deleted file mode 100644 index 31811432..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.java +++ /dev/null @@ -1,27 +0,0 @@ -package pw.mihou.nexus.features.command.core; - -import org.javacord.api.event.interaction.SlashCommandCreateEvent; -import pw.mihou.nexus.features.command.facade.NexusCommand; -import pw.mihou.nexus.features.command.facade.NexusCommandEvent; -import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent; - -import java.util.Map; - -public record NexusMiddlewareEventCore(NexusCommandEvent event) implements NexusMiddlewareEvent { - - @Override - public SlashCommandCreateEvent getBaseEvent() { - return event.getBaseEvent(); - } - - @Override - public NexusCommand getCommand() { - return event.getCommand(); - } - - @Override - public Map store() { - return event.store(); - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt new file mode 100644 index 00000000..2989e49b --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt @@ -0,0 +1,28 @@ +package pw.mihou.nexus.features.command.core + +import org.javacord.api.event.interaction.SlashCommandCreateEvent +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent +import pw.mihou.nexus.features.command.interceptors.core.NexusMiddlewareGateCore +import pw.mihou.nexus.features.command.responses.NexusAutoResponse +import pw.mihou.nexus.features.messages.NexusMessage +import java.util.concurrent.CompletableFuture +import java.util.function.Function + +class NexusMiddlewareEventCore(private val _event: NexusCommandEvent, private val gate: NexusMiddlewareGateCore): NexusMiddlewareEvent { + override val event: SlashCommandCreateEvent get() = _event.event + override val command: NexusCommand get() = _event.command + override fun store(): MutableMap = _event.store() + override fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture { + return _event.autoDefer(ephemeral, response) + } + + override fun next() { + gate.next() + } + + override fun stop(response: NexusMessage?) { + gate.stop(response) + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommand.java b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommand.java index 5f89077d..fe19fb32 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommand.java +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommand.java @@ -1,15 +1,69 @@ package pw.mihou.nexus.features.command.facade; +import kotlin.Pair; import org.javacord.api.entity.permission.PermissionType; import org.javacord.api.interaction.*; -import pw.mihou.nexus.commons.Pair; +import pw.mihou.nexus.core.exceptions.NoSuchAfterwareException; +import pw.mihou.nexus.core.exceptions.NoSuchMiddlewareException; +import pw.mihou.nexus.features.commons.NexusApplicationCommand; +import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore; +import pw.mihou.nexus.features.command.validation.OptionValidation; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; -public interface NexusCommand { +public interface NexusCommand extends NexusApplicationCommand { + + long PLACEHOLDER_SERVER_ID = 0L; + + static Map createLocalized(Pair... entries) { + return Arrays.stream(entries).collect(Collectors.toMap(Pair::component1, Pair::component2)); + } + + static List createOptions(SlashCommandOption... options) { + return Arrays.stream(options).toList(); + } + + static List> createValidators(OptionValidation... validators) { + return Arrays.stream(validators).toList(); + } + + static List createMiddlewares(String... middlewares) { + List list = new ArrayList<>(); + for (String middleware : middlewares) { + if (!NexusCommandInterceptorCore.hasMiddleware(middleware)) throw new NoSuchMiddlewareException(middleware); + list.add(middleware); + } + return list; + } + + static List createAfterwares(String... afterwares) { + List list = new ArrayList<>(); + for (String afterware : afterwares) { + if (!NexusCommandInterceptorCore.hasAfterware(afterware)) throw new NoSuchAfterwareException(afterware); + list.add(afterware); + } + return list; + } + + static List with(long... servers) { + return Arrays.stream(servers).boxed().toList(); + } + + static List createDefaultEnabledForPermissions(PermissionType... permissions) { + return Arrays.stream(permissions).toList(); + } + + /** + * Gets the unique identifier of the command, this tends to be the command name unless the + * command has an override using the {@link pw.mihou.nexus.features.command.annotation.IdentifiableAs} annotation. + *
+ * There are many use-cases for this unique identifier such as retrieving the index (application command id) of the + * command from the index store to be used for mentioning the command, and some other more. + * + * @return the unique identifier of the command. + */ + String getUuid(); /** * Gets the name of the command. @@ -33,40 +87,50 @@ public interface NexusCommand { List getOptions(); /** - * Gets the cooldown of the command. + * Gets all the servers that this command is for. * - * @return The cooldown of the command. + * @return A list of server ids that this command is for. */ - Duration getCooldown(); + List getServerIds(); /** - * Gets all the servers that this command is for. + * Checks whether this command is a server command. + *

+ * A server command can be a server that does have an entry on its associated server ids. * - * @return A list of server ids that this command is for. + * @return Is this a server command. */ - List getServerIds(); + default boolean isServerCommand() { + return !getServerIds().isEmpty(); + } /** - * Adds the specified server to the list of servers to - * register this command with. If {@link pw.mihou.nexus.Nexus} has the - * autoApplySupportedServersChangesOnCommands option enabled then it - * will automatically update this change. + * Associates this command from the given servers, this can be used to include this command into the command list + * of the server after batching updating. + *

+ * This does not perform any changes onto Discord. + *

+ * If you want to update changes then please use + * the {@link pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer#batchUpdate(long)} method + * after using this method. * - * @param serverIds The server ids to add support for this command. - * @return {@link NexusCommand} instance for chain-calling methods. + * @param serverIds The snowflakes of the servers to disassociate this command from. + * @return the current and updated {@link NexusCommand} instance for chain-calling methods. */ - NexusCommand addSupportFor(Long... serverIds); + NexusCommand associate(Long... serverIds); /** - * Removes the specified server to the list of servers to - * register this command with. If {@link pw.mihou.nexus.Nexus} has the - * autoApplySupportedServersChangesOnCommands option enabled then it - * will automatically update this change. + * Disassociates this command from the given servers, removing any form of association with + * the given servers. + *

+ * This does not perform any changes onto Discord. If you want to update changes then please use + * the {@link pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer#batchUpdate(long)} method + * after using this method. * - * @param serverIds The server ids to remove support for this command. - * @return {@link NexusCommand} instance for chain-calling methods. + * @param serverIds The snowflakes of the servers to disassociate this command from. + * @return the current and updated {@link NexusCommand} instance for chain-calling methods. */ - NexusCommand removeSupportFor(Long... serverIds); + NexusCommand disassociate(Long... serverIds); /** * Gets a specific custom field that is annotated with {@link pw.mihou.nexus.core.reflective.annotations.Share} from @@ -109,6 +173,12 @@ public interface NexusCommand { */ boolean isDefaultDisabled(); + /** + * Checks whether the command is not-safe-for-work or not. + * @return Whether the command is not-safe-for-work or not. + */ + boolean isNsfw(); + /** * Gets the permission types required to have this command enabled for that user. * @@ -152,19 +222,8 @@ default Optional getNameLocalization(DiscordLocale locale) { } /** - * Gets the server id of the command. - * - * @return The server ID of the command. - */ - @Deprecated - long getServerId(); - - /** - * Transforms this into a slash command builder that can be used to create - * the slash command yourself, it returns this via a {@link Pair} containing the server id - * and the builder. - * - * @return The server id of the server this is intended (nullable) and the slash command builder. + * Transforms this into a slash command builder that can be used to create the slash command. + * @return the {@link SlashCommandBuilder}. */ default SlashCommandBuilder asSlashCommand() { SlashCommandBuilder builder = SlashCommand.with(getName().toLowerCase(), getDescription()) @@ -173,6 +232,7 @@ default SlashCommandBuilder asSlashCommand() { getNameLocalizations().forEach(builder::addNameLocalization); getDescriptionLocalizations().forEach(builder::addDescriptionLocalization); + builder.setNsfw(isNsfw()); if (isDefaultDisabled()) { builder.setDefaultDisabled(); } @@ -193,12 +253,10 @@ default SlashCommandBuilder asSlashCommand() { } /** - * Transforms this into a slash command updater that can be used to update - * the slash command yourself, it returns this via a {@link Pair} containing the server id - * and the updater. + * Transforms this into a slash command updater that can be used to update the slash command yourself. * * @param commandId The ID of the command to update. - * @return The server id of the server this is intended (nullable) and the slash command updater. + * @return the {@link SlashCommandUpdater}. */ default SlashCommandUpdater asSlashCommandUpdater(long commandId) { SlashCommandUpdater updater = new SlashCommandUpdater(commandId) @@ -209,6 +267,7 @@ default SlashCommandUpdater asSlashCommandUpdater(long commandId) { getNameLocalizations().forEach(updater::addNameLocalization); getDescriptionLocalizations().forEach(updater::addDescriptionLocalization); + // TODO: Add support for `nsfw` update once new version out. Refers to (https://github.com/Javacord/Javacord/pull/1225). if (isDefaultDisabled()) { updater.setDefaultDisabled(); } diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java deleted file mode 100755 index 96ad0923..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java +++ /dev/null @@ -1,275 +0,0 @@ -package pw.mihou.nexus.features.command.facade; - -import org.javacord.api.DiscordApi; -import org.javacord.api.entity.channel.ServerTextChannel; -import org.javacord.api.entity.channel.TextChannel; -import org.javacord.api.entity.message.MessageFlag; -import org.javacord.api.entity.server.Server; -import org.javacord.api.entity.user.User; -import org.javacord.api.event.interaction.SlashCommandCreateEvent; -import org.javacord.api.interaction.SlashCommandInteraction; -import org.javacord.api.interaction.SlashCommandInteractionOption; -import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; -import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.core.managers.NexusShardManager; -import pw.mihou.nexus.features.command.core.NexusCommandCore; - -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.function.Predicate; - -public interface NexusCommandEvent { - - /** - * Gets the base event that was received from Javacord. This is usually nothing - * of use to the end-user. - * - * @return The base event that was received. - */ - SlashCommandCreateEvent getBaseEvent(); - - /** - * Gets the interaction received from Javacord. - * - * @return The interaction that was received from Javacord. - */ - default SlashCommandInteraction getInteraction() { - return getBaseEvent().getSlashCommandInteraction(); - } - - /** - * Gets the user received from Javacord. - * - * @return The user that was received from Javacord. - */ - default User getUser() { - return getInteraction().getUser(); - } - - /** - * Gets the channel that isn't an optional. This exists primarily because there is no currently possible - * way for the text channel to be optional in Discord Slash Commands. - * - * @return The text channel that is being used. - */ - default TextChannel getChannel() { - return getInteraction().getChannel().orElseThrow(() -> new NoSuchElementException( - "It seems like the text channel is no longer always available. Please create an issue on https://github.com/ShindouMihou/Nexus" - )); - } - - /** - * Gets the server instance. - * - * @return The server instance. - */ - default Optional getServer() { - return getInteraction().getServer(); - } - - /** - * Gets the ID of the server. - * - * @return The ID of the server. - */ - default Optional getServerId() { - return getInteraction().getServer().map(Server::getId); - } - - /** - * Gets the ID of the text channel where the command was executed. - * - * @return The ID of the text channel where the command was executed. - */ - default Long getChannelId() { - return getChannel().getId(); - } - - /** - * Gets the ID of the user that executed the command. - * - * @return The ID of the user who executed this command. - */ - default Long getUserId() { - return getUser().getId(); - } - - /** - * Gets the text channel as a server text channel. - * - * @return The text channel as a server text channel. - */ - default Optional getServerTextChannel() { - return getChannel().asServerTextChannel(); - } - /** - * Gets the command that was executed. - * - * @return The command that was executed. - */ - NexusCommand getCommand(); - - /** - * Gets the {@link Nexus} instance that was in charge - * of handling this command. - * - * @return The Nexus instance that was in charge of this command. - */ - default Nexus getNexus() { - return ((NexusCommandCore) getCommand()).core; - } - - /** - * Gets the Discord API shard that was in charge of this event. - * - * @return The Discord API shard that was in charge of this event. - */ - default DiscordApi getApi() { - return getBaseEvent().getApi(); - } - - /** - * Gets the {@link NexusShardManager} that is associated with the - * {@link Nexus} instance. - * - * @return The Nexus Shard Manager associated with this Nexus instance. - */ - default NexusShardManager getShardManager() { - return getNexus().getShardManager(); - } - - /** - * Gets the options that were brought with this command. - * - * @return All the options of this command. - */ - default List getOptions() { - return getInteraction().getOptions(); - } - - /** - * Gets the options of a subcommand. - * - * @param name The name of the subcommand to search for. - * @return The options of a subcommand, if present. - */ - default Optional> getSubcommandOptions(String name) { - return getInteraction().getOptionByName(name).map(SlashCommandInteractionOption::getOptions); - } - - /** - * Gets the immediate response for this command. - * - * @return The immediate response builder associated with this command. - */ - default InteractionImmediateResponseBuilder respondNow() { - return getInteraction().createImmediateResponder(); - } - - /** - * Gets the immediate response builder for this command and adds the - * {@link MessageFlag#EPHEMERAL} flag ahead of time. - * - * @return The immediate response builder associated with this command with the - * ephemeral flag added. - */ - default InteractionImmediateResponseBuilder respondNowAsEphemeral() { - return respondNow().setFlags(MessageFlag.EPHEMERAL); - } - - /** - * Gets the {@link InteractionOriginalResponseUpdater} associated with this command. - * - * @return The {@link InteractionOriginalResponseUpdater}. - */ - default CompletableFuture respondLater() { - return getNexus() - .getResponderRepository() - .get(getBaseEvent().getInteraction()); - } - - /** - * Gets the {@link InteractionOriginalResponseUpdater} associated with this command with the - * ephemeral flag attached. - * - * @return The {@link InteractionOriginalResponseUpdater} with the ephemeral flag attached. - */ - default CompletableFuture respondLaterAsEphemeral() { - return getNexus() - .getResponderRepository() - .getEphemereal(getBaseEvent().getInteraction()); - } - - /** - * Gets the {@link InteractionOriginalResponseUpdater} associated with this command with the ephemeral flag - * attached if the predicate is true. - * - * @param predicate The predicate to determine whether to use ephemeral response or not. - * @return The {@link InteractionOriginalResponseUpdater} for this interaction. - */ - default CompletableFuture respondLaterAsEphemeralIf(boolean predicate) { - if (predicate) { - return respondLaterAsEphemeral(); - } - - return respondLater(); - } - - /** - * Gets the {@link InteractionOriginalResponseUpdater} associated with this command with the ephemeral flag - * attached if the predicate is true. - * - * @param predicate The predicate to determine whether to use ephemeral response or not. - * @return The {@link InteractionOriginalResponseUpdater} for this interaction. - */ - default CompletableFuture respondLaterAsEphemeralIf(Predicate predicate) { - return respondLaterAsEphemeralIf(predicate.test(null)); - } - - /** - * Gets the event local store that is used to contain shared fields accessible from middlewares, command and - * afterwares themselves. You can use this to store data such as whether the command was completed successfully - * or other related. - * - * @return The event-local store from this event. - */ - Map store(); - - /** - * Gets the value of the given key from the {@link NexusCommandEvent#store()} and maps it into the type given - * if possible, otherwise returns null. - * - * @param key The key to get from the {@link NexusCommandEvent#store()}. - * @param type The type expected of the value. - * @param The type expected of the value. - * - * @return The value mapped with the key in {@link NexusCommandEvent#store()} mapped to the type, otherwise null. - */ - default T get(String key, Class type) { - if (!store().containsKey(key)) - return null; - - Object object = store().get(key); - - if (type.isAssignableFrom(object.getClass())) { - return type.cast(object); - } - - return null; - } - - /** - * A short-hand expression for placing a key-value pair to {@link NexusCommandEvent#store()}. - * - * @param key The key to insert to the store. - * @param value The value to insert to the store. - */ - default void store(String key, Object value) { - store().put(key, value); - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt new file mode 100755 index 00000000..33a02deb --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt @@ -0,0 +1,185 @@ +package pw.mihou.nexus.features.command.facade + +import org.javacord.api.entity.message.MessageFlag +import org.javacord.api.event.interaction.SlashCommandCreateEvent +import org.javacord.api.interaction.SlashCommandInteraction +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder +import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater +import org.javacord.api.util.logging.ExceptionLogger +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.Nexus.sharding +import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore.execute +import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore.middlewares +import pw.mihou.nexus.features.command.responses.NexusAutoResponse +import pw.mihou.nexus.features.commons.NexusInteractionEvent +import pw.mihou.nexus.features.messages.NexusMessage +import pw.mihou.nexus.sharding.NexusShardingManager +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer +import java.util.function.Function +import java.util.function.Predicate + +@JvmDefaultWithCompatibility +interface NexusCommandEvent : NexusInteractionEvent { + + /** + * Gets the base event that was received from Javacord. This is usually nothing + * of use to the end-user. + * + * @return The base event that was received. + * @see NexusCommandEvent.event + */ + @get:Deprecated("Standardized methods across the framework.", ReplaceWith("getEvent()")) + val baseEvent: SlashCommandCreateEvent get() = event + + override val interaction: SlashCommandInteraction get() = event.slashCommandInteraction + override val event: SlashCommandCreateEvent + + /** + * Gets the command that was executed. + * + * @return The command that was executed. + */ + val command: NexusCommand + + /** + * Gets the [NexusShardingManager] that is associated with the [Nexus] instance. + * + * @return The Nexus Shard Manager associated with this Nexus instance. + */ + val shardManager: NexusShardingManager get() = sharding + + /** + * Gets the immediate response builder for this command and adds the + * [MessageFlag.EPHEMERAL] flag ahead of time. + * + * @return The immediate response builder associated with this command with the + * ephemeral flag added. + * @see NexusCommandEvent.respondNowEphemerally + */ + @Deprecated("Standardized methods across the framework.", ReplaceWith("respondNowEphemerally()")) + fun respondNowAsEphemeral(): InteractionImmediateResponseBuilder { + return respondNowEphemerally() + } + + /** + * Gets the [InteractionOriginalResponseUpdater] associated with this command with the + * ephemeral flag attached. + * + * @return The [InteractionOriginalResponseUpdater] with the ephemeral flag attached. + * @see NexusCommandEvent.respondLaterEphemerally + */ + @Deprecated("Standardized methods across the framework.", ReplaceWith("respondLaterEphemerally()")) + fun respondLaterAsEphemeral(): CompletableFuture { + return respondLaterEphemerally() + } + + /** + * Gets the [InteractionOriginalResponseUpdater] associated with this command with the ephemeral flag + * attached if the predicate is true. + * + * @param predicate The predicate to determine whether to use ephemeral response or not. + * @return The [InteractionOriginalResponseUpdater] for this interaction. + * @see NexusCommandEvent.respondLaterEphemerallyIf + */ + @Deprecated("Standardized methods across the framework.", ReplaceWith("respondLaterEphemerallyIf()")) + fun respondLaterAsEphemeralIf(predicate: Boolean): CompletableFuture { + return respondLaterEphemerallyIf(predicate) + } + + /** + * Gets the [InteractionOriginalResponseUpdater] associated with this command with the ephemeral flag + * attached if the predicate is true. + * + * @param predicate The predicate to determine whether to use ephemeral response or not. + * @return The [InteractionOriginalResponseUpdater] for this interaction. + * @see NexusCommandEvent.respondLaterEphemerallyIf + */ + @Deprecated("Standardized methods across the framework.", ReplaceWith("respondLaterEphemerallyIf()")) + fun respondLaterAsEphemeralIf(predicate: Predicate): CompletableFuture { + return respondLaterEphemerallyIf(predicate.test(null)) + } + + /** + * Gets the event local store that is used to contain shared fields accessible from middlewares, command and + * afterwares themselves. You can use this to store data such as whether the command was completed successfully + * or other related. + * + * @return The event-local store from this event. + */ + fun store(): MutableMap + + /** + * Gets the value of the given key from the [NexusCommandEvent.store] and maps it into the type given + * if possible, otherwise returns null. + * + * @param key The key to get from the [NexusCommandEvent.store]. + * @param type The type expected of the value. + * @param The type expected of the value. + * + * @return The value mapped with the key in [NexusCommandEvent.store] mapped to the type, otherwise null. + */ + operator fun get(key: String, type: Class): T? { + if (!store().containsKey(key)) return null + val `object` = store()[key] + return if (type.isAssignableFrom(`object`!!.javaClass)) { type.cast(`object`) } else null + } + + /** + * Gets the value of the given key from the [NexusCommandEvent.store]. + * + * @param key The key to get from the [NexusCommandEvent.store].* + * @return The value mapped with the key in [NexusCommandEvent.store], otherwise null. + */ + operator fun get(key: String): Any? { + return store()[key] + } + + /** + * A short-hand expression for placing a key-value pair to [NexusCommandEvent.store]. + * + * @param key The key to insert to the store. + * @param value The value to insert to the store. + */ + fun store(key: String, value: Any) { + store()[key] = value + } + + /** + * Activates one or more middlewares and executes the success consumer once all middlewares succeed without + * throwing an issue. This does not support [InteractionOriginalResponseUpdater] or related, it will use + * [InteractionImmediateResponseBuilder] instead. + * + * @param middlewares the middlewares to activate. + * @param success what to do when all the middlewares succeed. + */ + fun middlewares(middlewares: List, success: Consumer) { + val middlewareGate = execute( + this, + middlewares(middlewares) + ) + if (middlewareGate == null) { + success.accept(null) + return + } + val response = middlewareGate.response() + if (response != null) { + var responder = respondNow() + if (response.ephemeral) { + responder = respondNowEphemerally() + } + response.into(responder).respond().exceptionally(ExceptionLogger.get()) + } + } + + /** + * Automatically answers either deferred or non-deferred based on circumstances, to configure the time that it should + * consider before deferring (this is based on time now - (interaction creation time - auto defer time)), you can + * modify [pw.mihou.nexus.configuration.modules.NexusGlobalConfiguration.autoDeferAfterMilliseconds]. + * + * @param ephemeral whether to respond ephemerally or not. + * @param response the response to send to Discord. + * @return the response from Discord. + */ + fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture +} diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.java b/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.java deleted file mode 100644 index d0a4fcc3..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.java +++ /dev/null @@ -1,142 +0,0 @@ -package pw.mihou.nexus.features.command.facade; - -import pw.mihou.nexus.features.command.interceptors.repositories.NexusMiddlewareGateRepository; -import pw.mihou.nexus.features.messages.facade.NexusMessage; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.concurrent.CompletableFuture; -import java.util.function.Predicate; - -public interface NexusMiddlewareEvent extends NexusCommandEvent { - - /** - * A middleware-only function that tells Discord that the response will be - * taking more than three-seconds because the middleware has to process tasks - * that can take more than three-second limit. - * @return The future to determine whether the response was accepted or not. - */ - default CompletableFuture askDelayedResponse() { - return CompletableFuture.allOf( - getNexus().getResponderRepository().peek(getBaseEvent().getInteraction()) - ); - } - - /** - * A middleware-only function that tells Discord that the response will be - * taking more than three-seconds because the middleware has to process tasks - * that can take more than three-second limit. - * @return The future to determine whether the response was accepted or not. - */ - default CompletableFuture askDelayedResponseAsEphemeral() { - return CompletableFuture.allOf( - getNexus().getResponderRepository().peekEphemeral(getBaseEvent().getInteraction()) - ); - } - - /** - * A middleware-only function that tells Discord that the response will be - * taking more than three-seconds because the middleware has to process tasks - * that can take more than three-second limit. - * - * @param predicate The predicate to determine whether the response should be ephemeral or not. - * @return The future to determine whether the response was accepted or not. - */ - default CompletableFuture askDelayedResponseAsEphemeralIf(boolean predicate) { - if (predicate) { - return askDelayedResponseAsEphemeral(); - } - - return askDelayedResponse(); - } - - /** - * A middleware-only function that tells Discord that the response will be - * taking more than three-seconds because the middleware has to process tasks - * that can take more than three-second limit. - * - * @param predicate The predicate to determine whether the response should be ephemeral or not. - * @return The future to determine whether the response was accepted or not. - */ - default CompletableFuture askDelayedResponseAsEphemeralIf(Predicate predicate) { - return askDelayedResponseAsEphemeralIf(predicate.test(null)); - } - - /** - * Tells the command interceptor handler to move forward with the next - * middleware if there is any, otherwise executes the command code. - */ - default void next() { - NexusMiddlewareGateRepository - .get(getBaseEvent().getInteraction()) - .next(); - } - - /** - * Stops further command execution. This cancels the command from executing - * without sending a notice. - */ - default void stop() { - stop(null); - } - - /** - * Stops further command execution. This cancels the command from executing - * and sends a notice to the user. - */ - default void stop(NexusMessage response) { - NexusMiddlewareGateRepository - .get(getBaseEvent().getInteraction()) - .stop(response); - } - - /** - * Stops further command execution if the predicate returns a value - * of {@link Boolean#TRUE} and allows execution if the predicate is a - * {@link Boolean#FALSE}. - * - * @param predicate The predicate to evaluate. - * @param response The response to send if the evaluation is false. - */ - default void stopIf(boolean predicate, @Nullable NexusMessage response) { - if (predicate) { - stop(response); - } - } - - /** - * Stops further command execution if the predicate returns a value - * of {@link Boolean#TRUE} and allows execution if the predicate is a - * {@link Boolean#FALSE}. - * - * @param predicate The predicate to evaluate. - */ - default void stopIf(boolean predicate) { - stopIf(predicate, null); - } - - /** - * Stops further command execution if the predicate returns a value - * of {@link Boolean#TRUE} and allows execution if the predicate is a - * {@link Boolean#FALSE}. - * - * @param predicate The predicate to evaluate. - */ - default void stopIf(@Nonnull Predicate predicate) { - stopIf(predicate, null); - } - - /** - * Stops further command execution if the predicate returns a value - * of {@link Boolean#TRUE} and allows execution if the predicate is a - * {@link Boolean#FALSE}. - * - * @param predicate The predicate to evaluate. - * @param response The response to send if the evaluation is false. - */ - default void stopIf(@Nonnull Predicate predicate, @Nullable NexusMessage response) { - stopIf(predicate.test(null), response); - } - - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.kt b/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.kt new file mode 100644 index 00000000..eb5397fd --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.kt @@ -0,0 +1,79 @@ +package pw.mihou.nexus.features.command.facade + +import pw.mihou.nexus.features.messages.NexusMessage +import java.util.function.Predicate +import kotlin.Boolean + +@JvmDefaultWithCompatibility +interface NexusMiddlewareEvent: NexusCommandEvent { + fun defer(ephemeral: Boolean = false) { + this.respondLaterEphemerallyIf(ephemeral).join() + } + + /** + * Tells the command interceptor handler to move forward with the next + * middleware if there is any, otherwise executes the command code. + */ + fun next() + + /** + * Stops further command execution. This cancels the command from executing + * without sending a notice. + */ + fun stop() { + stop(null) + } + + /** + * Stops further command execution. This cancels the command from executing + * and sends a notice to the user. + */ + fun stop(response: NexusMessage?) + + /** + * Stops further command execution if the predicate returns a value + * of `true` and allows execution if the predicate is a `false`. + * + * @param predicate The predicate to evaluate. + * @param response The response to send if the evaluation is false. + */ + fun stopIf(predicate: Boolean, response: NexusMessage?) { + if (predicate) { + stop(response) + } + } + + /** + * Stops further command execution if the predicate returns a value + * of `true` and allows execution if the predicate is a + * `false`. + * + * @param predicate The predicate to evaluate. + */ + fun stopIf(predicate: Boolean) { + stopIf(predicate, null) + } + + /** + * Stops further command execution if the predicate returns a value + * of `true` and allows execution if the predicate is a + * `false`. + * + * @param predicate The predicate to evaluate. + */ + fun stopIf(predicate: Predicate) { + stopIf(predicate, null) + } + + /** + * Stops further command execution if the predicate returns a value + * of `true` and allows execution if the predicate is a + * `false`. + * + * @param predicate The predicate to evaluate. + * @param response The response to send if the evaluation is false. + */ + fun stopIf(predicate: Predicate, response: NexusMessage?) { + stopIf(predicate.test(null), response) + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/NexusCommandInterceptors.kt b/src/main/java/pw/mihou/nexus/features/command/interceptors/NexusCommandInterceptors.kt new file mode 100644 index 00000000..476c0e70 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/NexusCommandInterceptors.kt @@ -0,0 +1,75 @@ +package pw.mihou.nexus.features.command.interceptors + +import pw.mihou.nexus.core.assignment.NexusUuidAssigner +import pw.mihou.nexus.core.reflective.NexusReflection +import pw.mihou.nexus.features.command.interceptors.annotations.Name +import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore +import pw.mihou.nexus.features.command.interceptors.facades.NexusAfterware +import pw.mihou.nexus.features.command.interceptors.facades.NexusMiddleware + +object NexusCommandInterceptors { + private val interceptors = NexusCommandInterceptorCore + + + /** + * Adds the provided middleware into the registry, when there is no name provided, the framework + * will generate one on your behalf using [NexusUuidAssigner]. + * + * @param name the name of the middleware, null to auto-generate a random one. + * @param middleware the middleware to add. + * @return the name of the middleware. + */ + @JvmOverloads + fun middleware(name: String? = null, middleware: NexusMiddleware): String { + val uuid = name ?: NexusUuidAssigner.request() + if (name == null && NexusCommandInterceptorCore.has(uuid)) { + return middleware(null, middleware) + } + interceptors.addMiddleware(uuid, middleware) + return uuid + } + + /** + * Adds the provided afterware into the registry, when there is no name provided, the framework + * will generate one on your behalf using [NexusUuidAssigner]. + * + * @param name the name of the afterware, null to auto-generate a random one. + * @param afterware the afterware to add. + * @return the name of the afterware. + */ + @JvmOverloads + fun afterware(name: String? = null, afterware: NexusAfterware): String { + val uuid = name ?: NexusUuidAssigner.request() + if (name == null && NexusCommandInterceptorCore.has(uuid)) { + return afterware(null, afterware) + } + interceptors.addAfterware(uuid, afterware) + return uuid + } + + /** + * Adds the provided repository to the registry. In this new mechanism, we load all the [NexusAfterware] and + * [NexusMiddleware] variables in the class into the registry with a name based on their field name or the name + * provided using [Name] annotation. + * + * @param repository the repository to add. + */ + fun add(repository: Any) { + NexusReflection.accumulate(repository) { field -> + if (field.type != NexusMiddleware::class.java && field.type != NexusAfterware::class.java) return@accumulate + val name = + if (field.isAnnotationPresent(Name::class.java)) field.getAnnotation(Name::class.java).value + else field.name + + if (field.type == NexusMiddleware::class.java || field.type.isAssignableFrom(NexusMiddleware::class.java)) { + interceptors.addMiddleware(name, field.get(repository) as NexusMiddleware) + return@accumulate + } + + if (field.type == NexusAfterware::class.java || field.type.isAssignableFrom(NexusAfterware::class.java)) { + interceptors.addAfterware(name, field.get(repository) as NexusAfterware) + return@accumulate + } + } + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/annotations/Name.kt b/src/main/java/pw/mihou/nexus/features/command/interceptors/annotations/Name.kt new file mode 100644 index 00000000..ead02f1c --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/annotations/Name.kt @@ -0,0 +1,5 @@ +package pw.mihou.nexus.features.command.interceptors.annotations + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class Name(val value: String) diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/NexusCommonInterceptors.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/NexusCommonInterceptors.java deleted file mode 100755 index ac2ac935..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/NexusCommonInterceptors.java +++ /dev/null @@ -1,19 +0,0 @@ -package pw.mihou.nexus.features.command.interceptors.commons; - -/** - * This class exists purely to separate the static function of adding - * all the common interceptors. You can use this as a reference for all built-in Nexus middlewares - * and afterwares. - */ -public class NexusCommonInterceptors { - - public static final String NEXUS_AUTH_BOT_OWNER_MIDDLEWARE = "nexus.auth.bot.owner"; - public static final String NEXUS_AUTH_SERVER_OWNER_MIDDLEWARE = "nexus.auth.server.owner"; - public static final String NEXUS_AUTH_PERMISSIONS_MIDDLEWARE = "nexus.auth.permissions"; - public static final String NEXUS_AUTH_ROLES_MIDDLEWARE = "nexus.auth.roles"; - public static final String NEXUS_AUTH_USER_MIDDLEWARE = "nexus.auth.user"; - public static final String NEXUS_GATE_SERVER = "nexus.gate.server"; - public static final String NEXUS_GATE_DMS = "nexus.gate.dms"; - public static final String NEXUS_RATELIMITER = "nexus.ratelimiter"; - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/NexusCommonInterceptors.kt b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/NexusCommonInterceptors.kt new file mode 100644 index 00000000..67bf58a9 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/NexusCommonInterceptors.kt @@ -0,0 +1,16 @@ +package pw.mihou.nexus.features.command.interceptors.commons + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.features.command.interceptors.commons.modules.NexusLogAfterware +import pw.mihou.nexus.features.command.interceptors.commons.modules.ratelimiter.core.NexusRatelimiterCore + +object NexusCommonInterceptors { + @JvmField val NEXUS_GATE_SERVER = Nexus.interceptors.middleware("nexus.gate.server") { + event -> event.stopIf(event.server.isEmpty) + } + @JvmField val NEXUS_GATE_DMS = Nexus.interceptors.middleware("nexus.gate.dms") { event -> + event.stopIf(event.server.isPresent) + } + @JvmField val NEXUS_RATELIMITER = Nexus.interceptors.middleware("nexus.ratelimiter", NexusRatelimiterCore()) + @JvmField val NEXUS_LOG = Nexus.interceptors.afterware("nexus.log", NexusLogAfterware) +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/core/NexusCommonInterceptorsCore.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/core/NexusCommonInterceptorsCore.java deleted file mode 100644 index f431bff6..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/core/NexusCommonInterceptorsCore.java +++ /dev/null @@ -1,23 +0,0 @@ -package pw.mihou.nexus.features.command.interceptors.commons.core; - -import pw.mihou.nexus.features.command.interceptors.commons.modules.auth.NexusAuthMiddleware; -import pw.mihou.nexus.features.command.interceptors.facades.NexusInterceptorRepository; -import pw.mihou.nexus.features.command.interceptors.commons.modules.ratelimiter.core.NexusRatelimiterCore; - -import static pw.mihou.nexus.features.command.interceptors.commons.NexusCommonInterceptors.*; - -public class NexusCommonInterceptorsCore extends NexusInterceptorRepository { - - @Override - public void define() { - middleware(NEXUS_AUTH_BOT_OWNER_MIDDLEWARE, NexusAuthMiddleware::onBotOwnerAuthenticationMiddleware); - middleware(NEXUS_AUTH_SERVER_OWNER_MIDDLEWARE, NexusAuthMiddleware::onServerOwnerAuthenticationMiddleware); - middleware(NEXUS_AUTH_PERMISSIONS_MIDDLEWARE, NexusAuthMiddleware::onPermissionsAuthenticationMiddleware); - middleware(NEXUS_AUTH_ROLES_MIDDLEWARE, NexusAuthMiddleware::onRoleAuthenticationMiddleware); - middleware(NEXUS_AUTH_USER_MIDDLEWARE, NexusAuthMiddleware::onUserAuthenticationMiddleware); - middleware(NEXUS_GATE_SERVER, event -> event.stopIf(event.getServer().isEmpty())); - middleware(NEXUS_GATE_DMS, event -> event.stopIf(event.getServer().isPresent())); - middleware(NEXUS_RATELIMITER, new NexusRatelimiterCore()); - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/NexusLogAfterware.kt b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/NexusLogAfterware.kt new file mode 100644 index 00000000..ccec923e --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/NexusLogAfterware.kt @@ -0,0 +1,54 @@ +package pw.mihou.nexus.features.command.interceptors.commons.modules + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.interceptors.facades.NexusAfterware +import java.text.NumberFormat +import java.time.Duration +import java.time.Instant + +object NexusLogAfterware: NexusAfterware { + + private const val RESET = "\u001B[0m" + private const val CYAN = "\u001B[36m" + private const val RED = "\u001B[31m" + private const val GREEN = "\u001B[32m" + override fun onAfterCommandExecution(event: NexusCommandEvent) { + val elapsed = Instant.now().toEpochMilli() - event.interaction.creationTimestamp.toEpochMilli() + val ratelimited = if ((event["nexus::is_ratelimited"] as? Boolean != null)) "${CYAN}ratelimited=${RESET}false" else "" + Nexus.logger.info("${GREEN}DISPATCHED: $RESET" + + "${CYAN}command=$RESET${event.interaction.fullCommandName} " + + "${CYAN}user=$RESET${event.user.discriminatedName} " + + "$ratelimited " + + "Dispatched within ${if (elapsed > 2500) RED else GREEN}${NumberFormat.getInstance().format(elapsed)}ms" + + "$RESET.") + } + + override fun onFailedDispatch(event: NexusCommandEvent) { + val elapsed = Instant.now().toEpochMilli() - event.interaction.creationTimestamp.toEpochMilli() + + val isRatelimited = event["nexus::is_ratelimited"] as? Boolean + val ratelimitRemaining = event["nexus::ratelimit_remaining"] as? Long + + val ratelimited = + if (isRatelimited != null) "${CYAN}ratelimited=${if(isRatelimited) RED else GREEN}${isRatelimited}$RESET " + else "" + + val ratelimitedUntil = + if (isRatelimited != null && ratelimitRemaining != null && isRatelimited) + "${CYAN}ratelimited_until=$RESET${NumberFormat.getInstance().format(ratelimitRemaining)}s$RESET " + else "" + + val blocker = event[NexusAfterware.BLOCKING_MIDDLEWARE_KEY] as? String + val blocked = if (blocker != null) "${CYAN}blocker=$RESET$blocker " else "" + + Nexus.logger.info("${RED}FAILED_DISPATCH: $RESET" + + "${CYAN}command=$RESET${event.interaction.fullCommandName} " + + "${CYAN}user=$RESET${event.user.discriminatedName} " + + "$ratelimited$ratelimitedUntil" + + blocked + + "Failed to dispatch, likely due to a middleware rejecting the request. " + + "It took ${if (elapsed > 2500) RED else GREEN}${NumberFormat.getInstance().format(elapsed)}ms" + + "$RESET.") + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java deleted file mode 100644 index dce83c33..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java +++ /dev/null @@ -1,156 +0,0 @@ -package pw.mihou.nexus.features.command.interceptors.commons.modules.auth; - -import org.javacord.api.entity.permission.PermissionType; -import org.javacord.api.entity.permission.Role; -import org.javacord.api.entity.server.Server; -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent; -import pw.mihou.nexus.features.messages.facade.NexusMessage; - -import java.util.List; - -public class NexusAuthMiddleware { - - /** - * Executed when the auth middleware is dedicated to the permissions - * authentication. - * - * @param event The event to manage. - */ - @SuppressWarnings("unchecked") - public static void onPermissionsAuthenticationMiddleware(NexusMiddlewareEvent event) { - if (event.getServer().isEmpty()) { - event.stop(); - return; - } - - Object field = event.getCommand().get("requiredPermissions").orElseThrow(() -> new IllegalStateException( - event.getCommand().getName() + " has permission authentication middleware but doesn't have requiredPermissions shared field." - )); - - List permissionTypes = null; - if (field instanceof List && !((List) field).isEmpty()) { - if (((List) field).get(0) instanceof PermissionType) { - permissionTypes = (List) field; - } - } - - Server server = event.getServer().get(); - if (permissionTypes == null) { - throw new IllegalStateException( - event.getCommand().getName() + " has permission authentication middleware but doesn't have requiredPermissions " + - "that matches List type." - ); - } - - event.stopIf( - !server.getPermissions(event.getUser()).getAllowedPermission().containsAll(permissionTypes), - ((NexusCore) event.getNexus()).getMessageConfiguration().onMissingPermission(event, permissionTypes) - ); - } - - /** - * Executed when the auth middleware is dedicated to the roles - * authentication. - * - * @param event The event to manage. - */ - @SuppressWarnings("unchecked") - public static void onRoleAuthenticationMiddleware(NexusMiddlewareEvent event) { - if (event.getServer().isEmpty()) { - event.stop(); - return; - } - - Object field = event.getCommand().get("requiredRoles").orElseThrow(() -> new IllegalStateException( - event.getCommand().getName() + " has role authentication middleware but doesn't have requiredRoles shared field." - )); - - List roles = null; - if (field instanceof List && !((List) field).isEmpty()) { - if (((List) field).get(0) instanceof Long) { - roles = (List) field; - } - } - - Server server = event.getServer().get(); - if (roles == null) { - throw new IllegalStateException( - event.getCommand().getName() + " has role authentication middleware but doesn't have requiredRoles " + - "that matches List type." - ); - } - - event.stopIf( - roles.stream().noneMatch(roleId -> - server.getRoles(event.getUser()) - .stream() - .map(Role::getId) - .anyMatch(userRoleId -> userRoleId.equals(roleId)) - ), - ((NexusCore) event.getNexus()).getMessageConfiguration().onRoleLockedCommand(event, roles) - ); - } - - /** - * Executed when the auth middleware is dedicated to the user - * authentication. - * - * @param event The event to manage. - */ - @SuppressWarnings("unchecked") - public static void onUserAuthenticationMiddleware(NexusMiddlewareEvent event) { - if (event.getServer().isEmpty()) { - event.stop(); - return; - } - - Object field = event.getCommand().get("requiredUsers").orElseThrow(() -> new IllegalStateException( - event.getCommand().getName() + " has user authentication middleware but doesn't have requiredUsers shared field." - )); - - List users = null; - if (field instanceof List && !((List) field).isEmpty()) { - if (((List) field).get(0) instanceof Long) { - users = (List) field; - } - } - - Server server = event.getServer().get(); - if (users == null) { - throw new IllegalStateException( - event.getCommand().getName() + " has user authentication middleware but doesn't have requiredUsers " + - "that matches List type." - ); - } - - event.stopIf(!users.contains(event.getUserId())); - } - - /** - * Executed when the auth middleware is dedicated to the bot owner - * authentication. - * - * @param event The event to manage. - */ - public static void onBotOwnerAuthenticationMiddleware(NexusMiddlewareEvent event) { - event.stopIf( - !event.getUser().isBotOwner(), - NexusMessage.fromEphemereal("**PERMISSION DENIED**\nYou need to be the bot owner to execute this command.") - ); - } - - /** - * Executed when the auth middleware is dedicated to the server owner - * authentication. - * - * @param event The event to manage. - */ - public static void onServerOwnerAuthenticationMiddleware(NexusMiddlewareEvent event) { - event.stopIf( - event.getServer().isPresent() && event.getServer().get().isOwner(event.getUser()), - NexusMessage.fromEphemereal("**PERMISSION DENIED**\nYou need to be the server owner to execute this command.") - ); - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.java deleted file mode 100755 index 41f48f98..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.java +++ /dev/null @@ -1,117 +0,0 @@ -package pw.mihou.nexus.features.command.interceptors.commons.modules.ratelimiter.core; - -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.features.command.core.NexusCommandCore; -import pw.mihou.nexus.features.command.facade.NexusCommand; -import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent; -import pw.mihou.nexus.features.command.interceptors.commons.modules.ratelimiter.facade.NexusRatelimitData; -import pw.mihou.nexus.features.command.interceptors.commons.modules.ratelimiter.facade.NexusRatelimiter; - -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -public class NexusRatelimiterCore implements NexusRatelimiter { - - private final Map ratelimits = new ConcurrentHashMap<>(); - - /** - * Ratelimits a user on a specific server, or on their private channel for a - * specific command. - * - * @param user The user to rate-limit. - * @param server The server or private channel to rate-limit on. - * @param command The command to rate-limit on. - * @return The results from the rate-limit attempt. - */ - private AccessorRatelimitData ratelimit(long user, long server, NexusCommandCore command) { - Entity key = new Entity(command.uuid, user); - - if (!ratelimits.containsKey(key)) { - ratelimits.put(key, new NexusRatelimitDataCore(user)); - } - - NexusRatelimitDataCore entity = (NexusRatelimitDataCore) ratelimits.get(key); - if (entity.isRatelimitedOn(server)) { - // This if-statement defines the section where the user - // is rate-limited. - if (getRemainingSecondsFrom(server, entity, command) > 0) { - if (!entity.isNotifiedOn(server)) { - entity.notified(server); - return new AccessorRatelimitData(false, true, getRemainingSecondsFrom(server, entity, command)); - } - - return new AccessorRatelimitData(true, true, getRemainingSecondsFrom(server, entity, command)); - } - - // This statement means that the user is still regarded as rate-limited by the - // rate-limiter despite the cooldown expiring. - entity.release(server); - } - - entity.ratelimit(server); - return new AccessorRatelimitData(false, false, -1); - } - - /** - * Gets the remaining seconds from the data provided. - * - * @param server The ID of the server. - * @param dataCore The rate-limit core data. - * @param commandCore The Nexus Command Core data. - * @return The remaining seconds before the rate-limit should be released. - */ - private long getRemainingSecondsFrom(long server, NexusRatelimitDataCore dataCore, NexusCommandCore commandCore) { - return (TimeUnit.MILLISECONDS.toSeconds( - dataCore.getRatelimitedTimestampInMillisOn(server) - + - commandCore.cooldown.toMillis() - )) - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); - } - - @Override - public Optional get(NexusCommand command, long user) { - return Optional.ofNullable(ratelimits.getOrDefault(new Entity(((NexusCommandCore) command).uuid, user), null)); - } - - @Override - public void onBeforeCommand(NexusMiddlewareEvent event) { - AccessorRatelimitData ratelimitData = ratelimit(event.getUserId(), event.getServerId().orElse(event.getUserId()), - (NexusCommandCore) event.getCommand()); - - if (ratelimitData.ratelimited()) { - if (!ratelimitData.notified()) { - event.stop - (((NexusCore) event.getNexus()) - .getMessageConfiguration().onRatelimited(event, ratelimitData.remaining()) - ); - return; - } - - event.stop(); - } - } - - private record AccessorRatelimitData(boolean notified, boolean ratelimited, long remaining) { - } - - private record Entity(String commandUUID, long user) { - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Entity entity = (Entity) o; - return user == entity.user && Objects.equals(commandUUID, entity.commandUUID); - } - - @Override - public int hashCode() { - return Objects.hash(commandUUID, user); - } - - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.kt b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.kt new file mode 100755 index 00000000..adbb411e --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.kt @@ -0,0 +1,119 @@ +package pw.mihou.nexus.features.command.interceptors.commons.modules.ratelimiter.core + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.features.command.core.NexusCommandCore +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent +import pw.mihou.nexus.features.command.interceptors.commons.modules.ratelimiter.facade.NexusRatelimitData +import pw.mihou.nexus.features.command.interceptors.commons.modules.ratelimiter.facade.NexusRatelimiter +import java.time.Duration +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import kotlin.jvm.optionals.getOrDefault + +class NexusRatelimiterCore internal constructor(): NexusRatelimiter { + + private val ratelimits: MutableMap = ConcurrentHashMap() + + /** + * Ratelimits a user on a specific server, or on their private channel for a + * specific command. + * + * @param user The user to rate-limit. + * @param server The server or private channel to rate-limit on. + * @param command The command to rate-limit on. + * @return The results from the rate-limit attempt. + */ + private fun ratelimit(user: Long, server: Long, command: NexusCommandCore): AccessorRatelimitData { + val key = Entity(command.uuid, user) + + if (!ratelimits.containsKey(key)) { + ratelimits[key] = NexusRatelimitDataCore(user) + } + + val entity = ratelimits[key] as NexusRatelimitDataCore? + if (entity!!.isRatelimitedOn(server)) { + if (getRemainingSecondsFrom(server, entity, command) > 0) { + if (!entity.isNotifiedOn(server)) { + entity.notified(server) + return AccessorRatelimitData( + notified = false, + ratelimited = true, + remaining = getRemainingSecondsFrom(server, entity, command) + ) + } + return AccessorRatelimitData( + notified = true, + ratelimited = true, + remaining = getRemainingSecondsFrom(server, entity, command) + ) + } + + entity.release(server) + } + entity.ratelimit(server) + return AccessorRatelimitData( + notified = false, + ratelimited = false, + remaining = -1 + ) + } + + /** + * Gets the remaining seconds from the data provided. + * + * @param server The ID of the server. + * @param dataCore The rate-limit core data. + * @param commandCore The Nexus Command Core data. + * @return The remaining seconds before the rate-limit should be released. + */ + private fun getRemainingSecondsFrom( + server: Long, + dataCore: NexusRatelimitDataCore?, + commandCore: NexusCommandCore + ): Long { + return TimeUnit.MILLISECONDS.toSeconds( + dataCore!!.getRatelimitedTimestampInMillisOn(server) + + + commandCore.get("cooldown", Duration::class.java).getOrDefault(Duration.ofSeconds(5)).toMillis() + ) - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + } + + override fun get(command: NexusCommand, user: Long): Optional { + return Optional.ofNullable(ratelimits.getOrDefault(Entity((command as NexusCommandCore).uuid, user), null)) + } + + override fun onBeforeCommand(event: NexusMiddlewareEvent) { + val ratelimitData = ratelimit( + event.userId, event.serverId.orElse(event.userId), + event.command as NexusCommandCore + ) + event.store("nexus::is_ratelimited", ratelimitData.ratelimited) + event.store("nexus::ratelimit_remaining", ratelimitData.remaining) + if (ratelimitData.ratelimited) { + if (!ratelimitData.notified) { + event.stop( + Nexus.configuration.commonsInterceptors.messages.ratelimited(event, ratelimitData.remaining) + ) + return + } + event.stop() + } + } + + private inner class AccessorRatelimitData(val notified: Boolean, val ratelimited: Boolean, val remaining: Long) + private inner class Entity(val command: String, val user: Long) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val entity = other as Entity + return user == entity.user && command == entity.command + } + + override fun hashCode(): Int { + return Objects.hash(command, user) + } + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusCommandInterceptorCore.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusCommandInterceptorCore.java deleted file mode 100755 index 771aa591..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusCommandInterceptorCore.java +++ /dev/null @@ -1,103 +0,0 @@ -package pw.mihou.nexus.features.command.interceptors.core; - -import pw.mihou.nexus.features.command.core.NexusMiddlewareEventCore; -import pw.mihou.nexus.features.command.facade.NexusCommandEvent; -import pw.mihou.nexus.features.command.interceptors.facades.*; -import pw.mihou.nexus.features.command.interceptors.repositories.NexusMiddlewareGateRepository; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class NexusCommandInterceptorCore { - - private static final Map interceptors = new HashMap<>(); - - /** - * Adds the middleware into the command interceptor storage. - * - * @param name The name of the middleware to add. - * @param middleware The middleware functionality. - */ - public static void addMiddleware(String name, NexusMiddleware middleware) { - interceptors.put(name, middleware); - } - - /** - * Adds a repository of command interceptors to the command interceptor - * storage. - * - * @param repository The repository to add. - */ - public static void addRepository(NexusInterceptorRepository repository) { - repository.define(); - } - - /** - * Adds the afterware into the command interceptor storage. - * - * @param name The name of the afterware to add. - * @param afterware The afterware functionality. - */ - public static void addAfterware(String name, NexusAfterware afterware) { - interceptors.put(name, afterware); - } - - /** - * Checks whether there is an interceptor with the given name. - * - * @param name The name of the interceptor. - * @return Is there an interceptor with the following name? - */ - public static boolean has(String name) { - return interceptors.containsKey(name); - } - - /** - * An internal method that is used to execute a command interceptor based on its - * type. - * - * @param name The name of the command interceptor. - * @param event The event being intercepted. - */ - private static void interceptWith(String name, NexusCommandEvent event) { - try { - NexusCommandInterceptor interceptor = interceptors.getOrDefault(name, null); - - if (interceptor == null) { - return; - } - - if (interceptor instanceof NexusMiddleware) { - ((NexusMiddleware) interceptor).onBeforeCommand(new NexusMiddlewareEventCore(event)); - } else if (interceptor instanceof NexusAfterware){ - ((NexusAfterware) interceptor).onAfterCommandExecution(event); - } - } catch (Exception exception) { - exception.printStackTrace(); - } - } - - /** - * An internal method that assesses multiple command interceptors and expects a - * proper boolean response. - * - * @param names The names of the command interceptors to execute. - * @param event The event to execute. - * @return Are all interceptors agreeing with the command execution? - */ - public static NexusMiddlewareGate interceptWithMany(List names, NexusCommandEvent event) { - // This is intentionally a for-loop since we want to stop at a specific point. - NexusMiddlewareGateCore gate = NexusMiddlewareGateRepository.get(event.getBaseEvent().getInteraction()); - for (String name : names) { - interceptWith(name, event); - if (!gate.allowed()) { - return NexusMiddlewareGateRepository.release(event.getBaseEvent().getInteraction()); - } - } - - NexusMiddlewareGateRepository.release(event.getBaseEvent().getInteraction()); - return null; - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusCommandInterceptorCore.kt b/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusCommandInterceptorCore.kt new file mode 100644 index 00000000..294d5e86 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusCommandInterceptorCore.kt @@ -0,0 +1,89 @@ +package pw.mihou.nexus.features.command.interceptors.core + +import pw.mihou.nexus.features.command.interceptors.facades.NexusAfterware +import pw.mihou.nexus.features.command.interceptors.facades.NexusCommandInterceptor +import pw.mihou.nexus.features.command.interceptors.facades.NexusMiddleware +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.features.command.core.NexusMiddlewareEventCore +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.validation.middleware.OptionValidationMiddleware + +internal object NexusCommandInterceptorCore { + + private val interceptors: MutableMap = mutableMapOf( + OptionValidationMiddleware.NAME to OptionValidationMiddleware + ) + + /** + * Adds one middleware to [Nexus]. + * @param name the name of the middleware. + * @param middleware the middleware to add. + */ + @JvmStatic + fun addMiddleware(name: String, middleware: NexusMiddleware) { + interceptors[name] = middleware + } + + /** + * Adds one afterware to [Nexus]. + * @param name the name of the afterware. + * @param afterware the afterware to add. + */ + @JvmStatic + fun addAfterware(name: String, afterware: NexusAfterware) { + interceptors[name] = afterware + } + + @JvmStatic + fun has(name: String) = interceptors.containsKey(name) + + @JvmStatic + fun hasAfterware(name: String) = interceptors.containsKey(name) && interceptors[name] is NexusAfterware + + + @JvmStatic + fun hasMiddleware(name: String) = interceptors.containsKey(name) && interceptors[name] is NexusMiddleware + + @JvmStatic + fun middlewares(names: List): Map = names + .map { it to interceptors[it] } + .filter { it.second != null && it.second is NexusMiddleware } + .associate { it.first to it.second as NexusMiddleware } + + internal fun afterwares(names: List): List = names + .map { interceptors[it] } + .filter { it != null && it is NexusAfterware } + .map { it as NexusAfterware } + + @JvmStatic + fun execute(event: NexusCommandEvent, middlewares: Map): NexusMiddlewareGateCore? { + val gate = NexusMiddlewareGateCore() + for ((name, middleware) in middlewares) { + try { + middleware.onBeforeCommand(NexusMiddlewareEventCore(event, gate)) + if (!gate.isAllowed) { + event.store(NexusAfterware.BLOCKING_MIDDLEWARE_KEY, name) + return gate + } + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught while trying to execute a middleware.", exception) + } + } + return null + } + + internal fun execute(event: NexusCommandEvent, afterwares: List, dispatched: Boolean = true) { + for (afterware in afterwares) { + try { + if (dispatched) { + afterware.onAfterCommandExecution(event) + } else { + afterware.onFailedDispatch(event) + } + } catch (exception: Exception) { + Nexus.logger.error("An uncaught exception was caught while trying to execute an afterware.", exception) + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusMiddlewareGateCore.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusMiddlewareGateCore.java index 1bac789d..525244b7 100755 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusMiddlewareGateCore.java +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/core/NexusMiddlewareGateCore.java @@ -1,7 +1,7 @@ package pw.mihou.nexus.features.command.interceptors.core; import pw.mihou.nexus.features.command.interceptors.facades.NexusMiddlewareGate; -import pw.mihou.nexus.features.messages.facade.NexusMessage; +import pw.mihou.nexus.features.messages.NexusMessage; import javax.annotation.Nullable; import java.util.concurrent.atomic.AtomicBoolean; @@ -38,7 +38,7 @@ private void setResponse(@Nullable NexusMessage response) { * * @return Is the command allowed to execute any further? */ - public boolean allowed() { + public boolean isAllowed() { return state.get(); } diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusAfterware.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusAfterware.java index e3f9a967..8b5638bb 100755 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusAfterware.java +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusAfterware.java @@ -4,6 +4,12 @@ public interface NexusAfterware extends NexusCommandInterceptor { + /** + * The key in {@link NexusCommandEvent#get(String)} to identify which middleware blocked the execution. + * This should only be used in {@link NexusAfterware#onFailedDispatch(NexusCommandEvent)}. + */ + public static final String BLOCKING_MIDDLEWARE_KEY = "$.engine::blocker"; + /** * This is executed after the command was executed. You can expect functionality such as * `respondNow` and similar to be expired. @@ -12,4 +18,11 @@ public interface NexusAfterware extends NexusCommandInterceptor { */ void onAfterCommandExecution(NexusCommandEvent event); + /** + * This is executed when the command failed to dispatch, an example of this scenario is when a middleware + * prevented the dispatching of the command. + * @param event the event to execute. + */ + default void onFailedDispatch(NexusCommandEvent event) {} + } diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusCommandInterceptor.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusCommandInterceptor.java index 91caecc0..48e1773c 100755 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusCommandInterceptor.java +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusCommandInterceptor.java @@ -1,84 +1,3 @@ package pw.mihou.nexus.features.command.interceptors.facades; -import pw.mihou.nexus.core.assignment.NexusUuidAssigner; -import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore; - -public interface NexusCommandInterceptor { - - /** - * Creates an anonymous middleware where the name of the middleware is generated randomly - * by the framework at runtime. - * - * @param middleware The middleware functionality. - * @return The name of the middleware to generated. - */ - static String middleware(NexusMiddleware middleware) { - String uuid = NexusUuidAssigner.request(); - - if (NexusCommandInterceptorCore.has(uuid)) { - NexusUuidAssigner.deny(uuid); - return middleware(middleware); - } - - NexusCommandInterceptorCore.addMiddleware(uuid, middleware); - - return uuid; - } - - /** - * Creates an anonymous afterware where the name of the middleware is generated randomly - * by the framework at runtime. - * - * @param afterware The afterware functionality. - * @return The name of the afterware to generated. - */ - static String afterware(NexusAfterware afterware) { - String uuid = NexusUuidAssigner.request(); - - if (NexusCommandInterceptorCore.has(uuid)) { - NexusUuidAssigner.deny(uuid); - return afterware(afterware); - } - - NexusCommandInterceptorCore.addAfterware(uuid, afterware); - - return uuid; - } - - /** - * Adds the middleware into the command interceptor storage. - * - * @param name The name of the middleware to add. - * @param middleware The middleware functionality. - * @return The name of the middleware for quick-use. - */ - static String addMiddleware(String name, NexusMiddleware middleware) { - NexusCommandInterceptorCore.addMiddleware(name, middleware); - - return name; - } - - /** - * Adds the afterware into the command interceptor storage. - * - * @param name The name of the afterware to add. - * @param afterware The afterware functionality. - * @return The name of the afterware for quick-use. - */ - static String addAfterware(String name, NexusAfterware afterware) { - NexusCommandInterceptorCore.addAfterware(name, afterware); - - return name; - } - - /** - * Adds a repository of command interceptors to the command interceptor - * storage. - * - * @param repository The repository to add. - */ - static void addRepository(NexusInterceptorRepository repository) { - NexusCommandInterceptorCore.addRepository(repository); - } - -} +public interface NexusCommandInterceptor {} diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusInterceptorRepository.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusInterceptorRepository.java deleted file mode 100644 index c2b65e59..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusInterceptorRepository.java +++ /dev/null @@ -1,37 +0,0 @@ -package pw.mihou.nexus.features.command.interceptors.facades; - -import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore; - -/** - * {@link NexusInterceptorRepository} is an extendable interface that can be used - * to define one or more interceptors which calling the register function. - */ -public abstract class NexusInterceptorRepository { - - /** - * Defines one or more interceptors. This method will be called - * upon startup. - */ - public abstract void define(); - - /** - * Adds the middleware into the command interceptor storage. - * - * @param name The name of the middleware to add. - * @param middleware The middleware functionality. - */ - public void middleware(String name, NexusMiddleware middleware) { - NexusCommandInterceptorCore.addMiddleware(name, middleware); - } - - /** - * Adds the afterware into the command interceptor storage. - * - * @param name The name of the afterware to add. - * @param afterware The afterware functionality. - */ - public void afterware(String name, NexusAfterware afterware) { - NexusCommandInterceptorCore.addAfterware(name, afterware); - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddleware.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddleware.java index 6c1ef46d..18e21836 100755 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddleware.java +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddleware.java @@ -1,7 +1,7 @@ package pw.mihou.nexus.features.command.interceptors.facades; import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent; -import pw.mihou.nexus.features.messages.facade.NexusMessage; +import pw.mihou.nexus.features.messages.NexusMessage; public interface NexusMiddleware extends NexusCommandInterceptor { diff --git a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddlewareGate.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddlewareGate.java index 801dceab..13a85c2f 100755 --- a/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddlewareGate.java +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/facades/NexusMiddlewareGate.java @@ -1,6 +1,6 @@ package pw.mihou.nexus.features.command.interceptors.facades; -import pw.mihou.nexus.features.messages.facade.NexusMessage; +import pw.mihou.nexus.features.messages.NexusMessage; import javax.annotation.Nullable; diff --git a/src/main/java/pw/mihou/nexus/features/command/responders/NexusResponderRepository.java b/src/main/java/pw/mihou/nexus/features/command/responders/NexusResponderRepository.java deleted file mode 100644 index 648dbda4..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/responders/NexusResponderRepository.java +++ /dev/null @@ -1,94 +0,0 @@ -package pw.mihou.nexus.features.command.responders; - -import org.javacord.api.interaction.Interaction; -import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater; - -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; - -public class NexusResponderRepository { - - private final Map responders = new ConcurrentHashMap<>(); - - /** - * Gets the current {@link InteractionOriginalResponseUpdater} for the specific interaction - * if available from another middleware otherwise requests for a new {@link InteractionOriginalResponseUpdater} - * that can be used instead. - *

- * Not to be confused with {@link NexusResponderRepository#get(Interaction)} which deliberately - * destroys the interaction that is being stored after being requested. This is intended for middlewares to - * prevent Discord's Interaction has failed while processing a heavy task. - * - * @param interaction The interaction to reference. - * @return The {@link InteractionOriginalResponseUpdater} if present otherwise requests for one. - */ - public CompletableFuture peek(Interaction interaction) { - if (responders.containsKey(interaction.getId())) { - return CompletableFuture.completedFuture(responders.get(interaction.getId())); - } - - return interaction.respondLater() - .thenApply(responseUpdater -> { - responders.put(interaction.getId(), responseUpdater); - return responseUpdater; - }); - } - - /** - * Gets the current {@link InteractionOriginalResponseUpdater} for the specific interaction - * if available from another middleware otherwise requests for a new {@link InteractionOriginalResponseUpdater} - * that can be used instead. - *

- * Not to be confused with {@link NexusResponderRepository#get(Interaction)} which deliberately - * destroys the interaction that is being stored after being requested. This is intended for middlewares to - * prevent Discord's Interaction has failed while processing a heavy task. - * - * @param interaction The interaction to reference. - * @return The {@link InteractionOriginalResponseUpdater} if present otherwise requests for one. - */ - public CompletableFuture peekEphemeral(Interaction interaction) { - if (responders.containsKey(interaction.getId())) { - return CompletableFuture.completedFuture(responders.get(interaction.getId())); - } - - return interaction.respondLater(true) - .thenApply(responseUpdater -> { - responders.put(interaction.getId(), responseUpdater); - return responseUpdater; - }); - } - - /** - * Gets the current {@link InteractionOriginalResponseUpdater} for the specific interaction if - * available from another middleware otherwise requests for a new {@link InteractionOriginalResponseUpdater} - * that can be used instead. - * - * @param interaction The interaction to reference for. - * @return The {@link InteractionOriginalResponseUpdater} if present otherwise requests for one. - */ - public CompletableFuture get(Interaction interaction) { - if (responders.containsKey(interaction.getId())) { - return CompletableFuture.completedFuture(responders.remove(interaction.getId())); - } - - return interaction.respondLater(); - } - - /** - * Gets the current {@link InteractionOriginalResponseUpdater} for the specific interaction if - * available from another middleware otherwise requests for a new {@link InteractionOriginalResponseUpdater} - * that can be used instead. - * - * @param interaction The interaction to reference for. - * @return The {@link InteractionOriginalResponseUpdater} if present otherwise requests for one. - */ - public CompletableFuture getEphemereal(Interaction interaction) { - if (responders.containsKey(interaction.getId())) { - return CompletableFuture.completedFuture(responders.remove(interaction.getId())); - } - - return interaction.respondLater(true); - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/responses/NexusAutoResponse.kt b/src/main/java/pw/mihou/nexus/features/command/responses/NexusAutoResponse.kt new file mode 100644 index 00000000..51815934 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/responses/NexusAutoResponse.kt @@ -0,0 +1,14 @@ +package pw.mihou.nexus.features.command.responses + +import org.javacord.api.entity.message.Message +import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater +import java.util.concurrent.CompletableFuture + +data class NexusAutoResponse internal constructor( + val updater: InteractionOriginalResponseUpdater?, + val message: Message? +) { + fun getOrRequestMessage(): CompletableFuture = + if (message == null && updater != null) updater.update() + else CompletableFuture.completedFuture(message) +} diff --git a/src/main/java/pw/mihou/nexus/features/command/router/SubcommandRouter.kt b/src/main/java/pw/mihou/nexus/features/command/router/SubcommandRouter.kt new file mode 100644 index 00000000..115534c6 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/router/SubcommandRouter.kt @@ -0,0 +1,41 @@ +package pw.mihou.nexus.features.command.router + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.router.types.Routeable +import java.util.NoSuchElementException + +class SubcommandRouter { + + private val routes: MutableMap = mutableMapOf() + + companion object { + @JvmSynthetic + fun create(modifier: RouteBuilder.() -> Unit): SubcommandRouter { + val router = SubcommandRouter() + modifier(RouteBuilder { name, route -> router.routes[name] = route }) + return router + } + + fun create(vararg routes: Pair): SubcommandRouter { + val router = SubcommandRouter() + router.routes.putAll(routes) + return router + } + } + + fun accept(event: NexusCommandEvent, modifier: NexusCommandEvent.() -> Unit = {}) { + for ((key, value) in routes) { + val option = event.interaction.getOptionByName(key).orElse(null) ?: continue + modifier(event) + value.accept(event, option) + return + } + + throw NoSuchElementException("No routes can be found for ${event.event.slashCommandInteraction.fullCommandName}.") + } + + class RouteBuilder(private val modifier: (name: String, route: Routeable) -> Unit) { + fun route(name: String, route: Routeable) = modifier(name, route) + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/router/types/Routeable.kt b/src/main/java/pw/mihou/nexus/features/command/router/types/Routeable.kt new file mode 100644 index 00000000..e504e98c --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/router/types/Routeable.kt @@ -0,0 +1,13 @@ +package pw.mihou.nexus.features.command.router.types + +import org.javacord.api.interaction.SlashCommandInteractionOption +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import java.util.function.Consumer + +interface Routeable { + fun accept(event: NexusCommandEvent, option: SlashCommandInteractionOption) + fun subcommand(name: String, from: SlashCommandInteractionOption, then: Consumer) { + val subcommand = from.getOptionByName(name).orElse(null) ?: return + then.accept(subcommand) + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java b/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java deleted file mode 100644 index d0b9e4d2..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java +++ /dev/null @@ -1,202 +0,0 @@ -package pw.mihou.nexus.features.command.synchronizer; - -import org.javacord.api.DiscordApi; -import org.javacord.api.entity.server.Server; -import org.javacord.api.interaction.SlashCommandBuilder; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.core.enginex.facade.NexusEngineX; -import pw.mihou.nexus.core.managers.facade.NexusCommandManager; -import pw.mihou.nexus.features.command.facade.NexusCommand; -import pw.mihou.nexus.features.command.synchronizer.overwrites.NexusSynchronizeMethods; -import pw.mihou.nexus.features.command.synchronizer.overwrites.defaults.NexusDefaultSynchronizeMethods; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; - -public record NexusSynchronizer( - Nexus nexus -) { - - public static final AtomicReference SYNCHRONIZE_METHODS = new AtomicReference<>(new NexusDefaultSynchronizeMethods()); - - /** - * Deletes a command to a specific server. - * - * @param command The command to delete. - * @param serverIds The servers to delete the command towards. - * @param totalShards The total amount of shards for this bot. This is used to - * for sharding formula. - * @return A future to indicate progress of this task. - */ - public CompletableFuture delete(NexusCommand command, int totalShards, long... serverIds) { - Map> serverMappedFutures = new HashMap<>(); - NexusEngineX engineX = ((NexusCore) nexus).getEngineX(); - for (long serverId : serverIds) { - if (!serverMappedFutures.containsKey(serverId)) { - serverMappedFutures.put(serverId, new CompletableFuture<>()); - } - - engineX.queue( - (int) ((serverId >> 22) % totalShards), - (api, store) -> SYNCHRONIZE_METHODS.get().deleteForServer(api, command, serverId, serverMappedFutures.get(serverId)) - ); - } - - return CompletableFuture.allOf(serverMappedFutures.values().toArray(new CompletableFuture[0])); - } - - /** - * Batch updates all commands that supports a specific server. This completely overrides the - * server command list and can be used to clear any server slash commands of the bot for that - * specific server. - * - * @param totalShards The total amount of shards for this bot. This is used to - * for sharding formula. - * @param serverId The server to batch upsert the commands onto. - * @return A future to indicate progress of this task. - */ - public CompletableFuture batchUpdate(long serverId, int totalShards) { - CompletableFuture future = new CompletableFuture<>(); - - NexusEngineX engineX = ((NexusCore) nexus).getEngineX(); - engineX.queue( - (int) ((serverId >> 22) % totalShards), - (api, store) -> batchUpdate(serverId, api) - .thenAccept(unused -> future.complete(null)) - .exceptionally(throwable -> { - future.completeExceptionally(throwable); - return null; - }) - ); - - return future; - } - - /** - * Batch updates all commands that supports a specific server. This completely overrides the - * server command list and can be used to clear any server slash commands of the bot for that - * specific server. - * - * @param shard The shard to use for updating the server's commands. - * @param serverId The server to batch upsert the commands onto. - * @return A future to indicate progress of this task. - */ - public CompletableFuture batchUpdate(long serverId, DiscordApi shard) { - NexusCommandManager manager = nexus.getCommandManager(); - CompletableFuture future = new CompletableFuture<>(); - List serverCommands = manager.getCommands() - .stream() - .filter(nexusCommand -> !nexusCommand.getServerIds().isEmpty() && nexusCommand.getServerIds().contains(serverId)) - .toList(); - - List slashCommandBuilders = new ArrayList<>(); - serverCommands.forEach(command -> slashCommandBuilders.add(command.asSlashCommand())); - SYNCHRONIZE_METHODS.get().bulkOverwriteServer(shard, slashCommandBuilders, serverId, future); - - return future; - - } - - /** - * Upserts a command to a specific server. - * - * @param command The command to upsert. - * @param serverIds The servers to upsert the command towards. - * @param totalShards The total amount of shards for this bot. This is used to - * for sharding formula. - * @return A future to indicate progress of this task. - */ - public CompletableFuture upsert(NexusCommand command, int totalShards, long... serverIds) { - Map> serverMappedFutures = new HashMap<>(); - NexusEngineX engineX = ((NexusCore) nexus).getEngineX(); - for (long serverId : serverIds) { - if (!serverMappedFutures.containsKey(serverId)) { - serverMappedFutures.put(serverId, new CompletableFuture<>()); - } - - engineX.queue( - (int) ((serverId >> 22) % totalShards), - (api, store) -> SYNCHRONIZE_METHODS.get().updateForServer(api, command, serverId, serverMappedFutures.get(serverId)) - ); - } - - return CompletableFuture.allOf(serverMappedFutures.values().toArray(new CompletableFuture[0])); - } - - /** - * Synchronizes all the server commands and global commands with the use of - * {@link org.javacord.api.DiscordApi#bulkOverwriteGlobalApplicationCommands(List)} and - * {@link org.javacord.api.DiscordApi#bulkOverwriteServerApplicationCommands(Server, List)}. This does not - * take any regards to any changes and pushes an override without any care. - * - * @param totalShards The total amount of shards on the bot, used for sharding formula. - * @return A future to indicate the progress of the synchronization task. - */ - public CompletableFuture synchronize(int totalShards) { - NexusCommandManager manager = nexus.getCommandManager(); - CompletableFuture globalFuture = new CompletableFuture<>(); - - // 0L is acceptable as a placeholder definition for "This is a server command but only register when a server other than zero is up". - List serverCommands = manager.getCommands() - .stream() - .filter(nexusCommand -> !nexusCommand.getServerIds().isEmpty() - && !(nexusCommand.getServerIds().size() == 1 && nexusCommand.getServerIds().get(0) == 0) - ) - .toList(); - - List globalCommands = manager.getCommands() - .stream() - .filter(nexusCommand -> nexusCommand.getServerIds().isEmpty()) - .map(NexusCommand::asSlashCommand) - .toList(); - - NexusEngineX engineX = ((NexusCore) nexus).getEngineX(); - engineX.queue( - (api, store) -> - SYNCHRONIZE_METHODS.get().bulkOverwriteGlobal(api, globalCommands) - .thenAccept(unused -> globalFuture.complete(null)) - .exceptionally(throwable -> { - globalFuture.completeExceptionally(throwable); - return null; - }) - ); - - Map> serverMappedCommands = new HashMap<>(); - Map> serverMappedFutures = new HashMap<>(); - serverCommands.forEach(nexusCommand -> nexusCommand.getServerIds().forEach(serverId -> { - if (serverId == 0L) { - return; - } - - if (!serverMappedCommands.containsKey(serverId)) { - serverMappedCommands.put(serverId, new ArrayList<>()); - } - - serverMappedCommands.get(serverId).add(nexusCommand.asSlashCommand()); - })); - - serverMappedCommands.forEach((serverId, slashCommandBuilders) -> { - if (serverId == 0L) { - return; - } - - if (!serverMappedFutures.containsKey(serverId)) { - serverMappedFutures.put(serverId, new CompletableFuture<>()); - } - - engineX.queue((int) ( - (serverId >> 22) % totalShards), - (api, store) -> SYNCHRONIZE_METHODS.get().bulkOverwriteServer(api, slashCommandBuilders, serverId, serverMappedFutures.get(serverId)) - ); - }); - - List> futures = new ArrayList<>(); - futures.add(globalFuture); - futures.addAll(serverMappedFutures.values()); - - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); - } - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.kt b/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.kt new file mode 100644 index 00000000..3dced5a3 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.kt @@ -0,0 +1,184 @@ +package pw.mihou.nexus.features.command.synchronizer + +import org.javacord.api.interaction.ApplicationCommand +import org.javacord.api.interaction.ApplicationCommandBuilder +import org.javacord.api.interaction.SlashCommandBuilder +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.core.async.NexusLaunchable +import pw.mihou.nexus.core.managers.facade.NexusCommandManager +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.command.synchronizer.exceptions.NexusSynchronizerException +import pw.mihou.nexus.features.command.synchronizer.overwrites.NexusSynchronizeMethods +import pw.mihou.nexus.features.command.synchronizer.overwrites.defaults.NexusDefaultSynchronizeMethods +import java.util.concurrent.CompletableFuture + +class NexusSynchronizer internal constructor() { + + @Volatile var methods: NexusSynchronizeMethods = NexusDefaultSynchronizeMethods + private val inclusions: MutableMap>> = mutableMapOf() + + companion object { + private const val GLOBAL_SCOPE = -550L + } + + /** + * Includes the given builders to the [batchUpdate] and [synchronize], this is to accommodate situations wherein + * we may have stuff, such as User and Message Context Menus, that we want to include, but keeps getting overridden, + * by the [NexusSynchronizer]. + * + * @param server The server to link these builders, use null to indicate global. + * @param commands All the command builders that are linked to the server or global (if server is null). + */ + fun include(server: Long? = null, vararg commands: ApplicationCommandBuilder<*, *, *>) { + inclusions.computeIfAbsent(server ?: GLOBAL_SCOPE) { mutableListOf() }.addAll(commands) + } + + /** + * Deletes a command from a specific server. + * + * @param command The command to delete. + * @param servers The servers to delete the command towards. + * @param totalShards The total amount of shards for this bot. This is used to + * for sharding formula. + * @return A future to indicate the completion of this task. + */ + fun delete(command: NexusCommand, totalShards: Int, vararg servers: Long): NexusLaunchable = NexusLaunchable { + for (serverId in servers) { + try { + Nexus.express + .await(Nexus.sharding.calculate(serverId, totalShards)) + .thenCompose { shard -> methods.deleteForServer(shard, command, serverId) } + .join() + } catch (exception: Exception) { + error(NexusSynchronizerException(serverId, command, exception)) + } + } + } + + /** + * Batch updates all commands that supports a specific server. This completely overrides the + * server command list and can be used to clear any server slash commands of the bot for that + * specific server. + * + * @param server the given guild snowflake to perform updates upon. + * @return A future to indicate progress of this task. + */ + fun batchUpdate(server: Long): CompletableFuture { + val manager: NexusCommandManager = Nexus.commandManager + val serverCommands = manager.commandsAssociatedWith(server) + .map { command -> command.asSlashCommand() as ApplicationCommandBuilder<*, *, *> } + .toHashSet() + + inclusions[server]?.let { serverCommands += it } + manager.contextMenusAssociatedWith(server) + .map { contextMenu -> contextMenu.builder } + .forEach { serverCommands += it } + + return Nexus.express.awaitAvailable() + .thenCompose { shard -> methods.bulkOverwriteServer(shard, serverCommands, server) } + .thenAccept(manager::index) + } + + /** + * Upserts a command to a specific server. + * + * @param command The command to upsert. + * @param servers The servers to upsert the command towards. + * @param totalShards The total amount of shards for this bot. This is used to + * for sharding formula. + * @return A future to indicate progress of this task. + */ + fun upsert(command: NexusCommand, totalShards: Int, vararg servers: Long): NexusLaunchable = NexusLaunchable { + val serverMappedFutures = mutableMapOf>() + + for (server in servers) { + if (serverMappedFutures.containsKey(server)) continue + + try { + Nexus.express + .await(Nexus.sharding.calculate(server, totalShards)) + .thenCompose { shard -> methods.updateForServer(shard, command, server) } + .thenApply { complete(it); it } + .thenAccept { `$command` -> Nexus.commandManager.index(command, `$command`.applicationId, `$command`.serverId.orElse(null)) } + .join() + } catch (exception: Exception) { + error(NexusSynchronizerException(server, command, exception)) + } + } + } + + /** + * Synchronizes all the server commands and global commands with the use of + * [org.javacord.api.DiscordApi.bulkOverwriteGlobalApplicationCommands] and + * [org.javacord.api.DiscordApi.bulkOverwriteServerApplicationCommands]. This does not + * take any regards to any changes and pushes an override without any care. + * + * @return A future to indicate the progress of the synchronization task. + */ + fun synchronize(): NexusLaunchable> = NexusLaunchable { + val manager: NexusCommandManager = Nexus.commandManager + + val serverCommands = manager.serverCommands + val serverContextMenus = manager.serverContextMenus + val globalCommands = manager.globalCommands + .map { command -> command.asSlashCommand() as ApplicationCommandBuilder<*, *, *> } + .toHashSet() + + inclusions[GLOBAL_SCOPE]?.let { globalCommands += it } + manager.globalContextMenus + .map { contextMenu -> contextMenu.builder } + .forEach { globalCommands += it } + + try { + Nexus.express + .awaitAvailable() + .thenCompose { shard -> methods.bulkOverwriteGlobal(shard, globalCommands) } + .thenApply { complete(it); it } + .thenAccept(manager::index) + .join() + } catch (exception: Exception) { + error(NexusSynchronizerException(null, null, exception)) + } + + if (serverCommands.isEmpty() && serverContextMenus.isEmpty()) { + return@NexusLaunchable + } + + val serverMappedCommands: MutableMap>> = mutableMapOf() + + for ((id, value) in inclusions.entries) { + if (id == GLOBAL_SCOPE) continue + serverMappedCommands.computeIfAbsent(id) { HashSet() } += value + } + + for (`$command` in serverCommands) { + for (serverId in `$command`.serverIds) { + if (serverId == NexusCommand.PLACEHOLDER_SERVER_ID) continue + serverMappedCommands.computeIfAbsent(serverId) { HashSet() } += `$command`.asSlashCommand() + } + } + + for (contextMenu in serverContextMenus) { + for (serverId in contextMenu.serverIds) { + if (serverId == NexusCommand.PLACEHOLDER_SERVER_ID) continue + serverMappedCommands.computeIfAbsent(serverId) { HashSet() } += contextMenu.builder + } + } + + serverMappedCommands.forEach { (server, builders) -> + if (server == NexusCommand.PLACEHOLDER_SERVER_ID) return@forEach + + try { + Nexus.express + .awaitAvailable() + .thenCompose { shard -> methods.bulkOverwriteServer(shard, builders, server) } + .thenApply { complete(it); it } + .thenAccept(manager::index) + .join() + } catch (exception: Exception) { + error(NexusSynchronizerException(server, null, exception)) + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/exceptions/NexusSynchronizerException.kt b/src/main/java/pw/mihou/nexus/features/command/synchronizer/exceptions/NexusSynchronizerException.kt new file mode 100644 index 00000000..5ada9117 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/synchronizer/exceptions/NexusSynchronizerException.kt @@ -0,0 +1,9 @@ +package pw.mihou.nexus.features.command.synchronizer.exceptions + +import pw.mihou.nexus.features.command.facade.NexusCommand +import java.lang.Exception + +class NexusSynchronizerException(val server: Long?, val command: NexusCommand?, val exception: Exception): + RuntimeException("An exception occurred while trying to perform command synchronization. " + + "{server=${server ?: "N/A"}, command=${command?.name ?: "N/A"}}: " + + exception) \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/NexusSynchronizeMethods.java b/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/NexusSynchronizeMethods.java deleted file mode 100644 index 1e8bbe46..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/NexusSynchronizeMethods.java +++ /dev/null @@ -1,24 +0,0 @@ -package pw.mihou.nexus.features.command.synchronizer.overwrites; - -import org.javacord.api.DiscordApi; -import org.javacord.api.interaction.ApplicationCommand; -import org.javacord.api.interaction.SlashCommandBuilder; -import pw.mihou.nexus.features.command.facade.NexusCommand; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -public interface NexusSynchronizeMethods { - - CompletableFuture bulkOverwriteGlobal(DiscordApi shard, List slashCommands); - - void bulkOverwriteServer(DiscordApi shard, List slashCommands, - long serverId, CompletableFuture future); - - void deleteForServer(DiscordApi shard, NexusCommand command, long serverId, CompletableFuture future); - - void updateForServer(DiscordApi shard, NexusCommand command, long serverId, CompletableFuture future); - - void createForServer(DiscordApi shard, NexusCommand command, long serverId, CompletableFuture future); - -} diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/NexusSynchronizeMethods.kt b/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/NexusSynchronizeMethods.kt new file mode 100644 index 00000000..7d11d4a0 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/NexusSynchronizeMethods.kt @@ -0,0 +1,34 @@ +package pw.mihou.nexus.features.command.synchronizer.overwrites + +import org.javacord.api.DiscordApi +import org.javacord.api.interaction.ApplicationCommand +import org.javacord.api.interaction.ApplicationCommandBuilder +import org.javacord.api.interaction.SlashCommandBuilder +import pw.mihou.nexus.features.command.facade.NexusCommand +import java.util.concurrent.CompletableFuture + +interface NexusSynchronizeMethods { + fun bulkOverwriteGlobal( + shard: DiscordApi, + applicationCommands: Set> + ): CompletableFuture> + + fun bulkOverwriteServer( + shard: DiscordApi, + applicationCommands: Set>, + serverId: Long + ): CompletableFuture> + + fun deleteForServer(shard: DiscordApi, command: NexusCommand, serverId: Long): CompletableFuture + fun updateForServer( + shard: DiscordApi, + command: NexusCommand, + serverId: Long + ): CompletableFuture + + fun createForServer( + shard: DiscordApi, + command: NexusCommand, + serverId: Long + ): CompletableFuture +} diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/defaults/NexusDefaultSynchronizeMethods.java b/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/defaults/NexusDefaultSynchronizeMethods.java deleted file mode 100644 index d0da5edb..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/defaults/NexusDefaultSynchronizeMethods.java +++ /dev/null @@ -1,141 +0,0 @@ -package pw.mihou.nexus.features.command.synchronizer.overwrites.defaults; - -import org.javacord.api.DiscordApi; -import org.javacord.api.entity.server.Server; -import org.javacord.api.interaction.SlashCommand; -import org.javacord.api.interaction.SlashCommandBuilder; -import pw.mihou.nexus.core.NexusCore; -import pw.mihou.nexus.features.command.facade.NexusCommand; -import pw.mihou.nexus.features.command.synchronizer.overwrites.NexusSynchronizeMethods; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -public class NexusDefaultSynchronizeMethods implements NexusSynchronizeMethods { - - @Override - public CompletableFuture bulkOverwriteGlobal(DiscordApi shard, List slashCommands) { - CompletableFuture globalFuture = new CompletableFuture<>(); - - shard.bulkOverwriteGlobalApplicationCommands(slashCommands).thenAccept(applicationCommands -> { - NexusCore.logger.debug("Global commands completed synchronization. [size={}]", applicationCommands.size()); - globalFuture.complete(null); - }).exceptionally(throwable -> { - globalFuture.completeExceptionally(throwable); - return null; - }); - - return globalFuture; - } - - @Override - public void bulkOverwriteServer(DiscordApi shard, List slashCommands, - long serverId, CompletableFuture future) { - if (shard.getServerById(serverId).isEmpty()) { - future.completeExceptionally( - new IllegalStateException( - "Failed to synchronize commands for server, not found on the shard calculated. Is the total shard number value wrong? " + - "[shard=" + shard.getCurrentShard() + ";id=" + serverId + "]" - ) - ); - return; - } - - Server server = shard.getServerById(serverId).orElseThrow(); - shard.bulkOverwriteServerApplicationCommands(server, slashCommands) - .thenAccept(applicationCommands -> { - NexusCore.logger.debug("A server has completed synchronization. [server={}, size={}]", serverId, applicationCommands.size()); - future.complete(null); - }) - .exceptionally(throwable -> { - future.completeExceptionally(throwable); - return null; - }); - } - - @Override - public void deleteForServer(DiscordApi api, NexusCommand command, long serverId, CompletableFuture future) { - if (api.getServerById(serverId).isEmpty()) { - future.completeExceptionally( - new IllegalStateException( - "Failed to synchronize commands for server, not found on the shard calculated. Is the total shard number value wrong? " + - "[shard=" + api.getCurrentShard() + ";id=" + serverId + "]" - ) - ); - return; - } - - Server server = api.getServerById(serverId).orElseThrow(); - server.getSlashCommands() - .join() - .stream() - .filter(slashCommand -> slashCommand.getName().equalsIgnoreCase(command.getName())) - .findFirst() - .ifPresent(slashCommand -> slashCommand.deleteForServer(server).thenAccept(unused -> { - NexusCore.logger.debug("A command has completed deletion. [server={}, command={}]", serverId, slashCommand.getName()); - future.complete(null); - }).exceptionally(throwable -> { - future.completeExceptionally(throwable); - return null; - })); - } - - @Override - public void updateForServer(DiscordApi api, NexusCommand command, long serverId, CompletableFuture future) { - if (api.getServerById(serverId).isEmpty()) { - future.completeExceptionally( - new IllegalStateException( - "Failed to synchronize commands for server, not found on the shard calculated. Is the total shard number value wrong? " + - "[shard=" + api.getCurrentShard() + ";id=" + serverId + "]" - ) - ); - return; - } - - Server server = api.getServerById(serverId).orElseThrow(); - List commands = server.getSlashCommands().join(); - - Optional matchingCommand = commands.stream() - .filter(slashCommand -> slashCommand.getName().equalsIgnoreCase(command.getName())) - .findFirst(); - - if (matchingCommand.isPresent()) { - command.asSlashCommandUpdater(matchingCommand.get().getId()).updateForServer(server) - .thenAccept(slashCommand -> { - NexusCore.logger.debug("A command has completed synchronization. [server={}, command={}]", serverId, slashCommand.getName()); - future.complete(null); - }) - .exceptionally(throwable -> { - future.completeExceptionally(throwable); - return null; - }); - } else { - createForServer(api, command, serverId, future); - } - } - - @Override - public void createForServer(DiscordApi api, NexusCommand command, long serverId, CompletableFuture future) { - if (api.getServerById(serverId).isEmpty()) { - future.completeExceptionally( - new IllegalStateException( - "Failed to synchronize commands for server, not found on the shard calculated. Is the total shard number value wrong? " + - "[shard=" + api.getCurrentShard() + ";id=" + serverId + "]" - ) - ); - return; - } - - Server server = api.getServerById(serverId).orElseThrow(); - command.asSlashCommand().createForServer(server) - .thenAccept(slashCommand -> { - NexusCore.logger.debug("A command has completed synchronization. [server={}, command={}]", serverId, slashCommand.getName()); - future.complete(null); - }) - .exceptionally(throwable -> { - future.completeExceptionally(throwable); - return null; - }); - } -} diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/defaults/NexusDefaultSynchronizeMethods.kt b/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/defaults/NexusDefaultSynchronizeMethods.kt new file mode 100644 index 00000000..f64d7f95 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/synchronizer/overwrites/defaults/NexusDefaultSynchronizeMethods.kt @@ -0,0 +1,75 @@ +package pw.mihou.nexus.features.command.synchronizer.overwrites.defaults + +import org.javacord.api.DiscordApi +import org.javacord.api.interaction.ApplicationCommand +import org.javacord.api.interaction.ApplicationCommandBuilder +import org.javacord.api.interaction.SlashCommand +import org.javacord.api.interaction.SlashCommandBuilder +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.configuration.modules.debug +import pw.mihou.nexus.core.exceptions.NexusFailedActionException +import pw.mihou.nexus.features.command.facade.NexusCommand +import pw.mihou.nexus.features.command.synchronizer.overwrites.NexusSynchronizeMethods +import java.util.concurrent.CompletableFuture + +object NexusDefaultSynchronizeMethods : NexusSynchronizeMethods { + + override fun bulkOverwriteGlobal(shard: DiscordApi, applicationCommands: Set>) = + shard + .bulkOverwriteGlobalApplicationCommands(applicationCommands) + .and { Nexus.configuration.loggingTemplates.GLOBAL_COMMANDS_SYNCHRONIZED(it).debug() } + + override fun bulkOverwriteServer(shard: DiscordApi, applicationCommands: Set>, serverId: Long) = + shard + .bulkOverwriteServerApplicationCommands(serverId, applicationCommands) + .and { Nexus.configuration.loggingTemplates.SERVER_COMMANDS_SYNCHRONIZED(serverId, it).debug() } + + override fun deleteForServer(shard: DiscordApi, command: NexusCommand, serverId: Long): CompletableFuture { + if (shard.getServerById(serverId).isEmpty) { + return getServerNotFoundErrorFrom(shard, serverId) + } + + val server = shard.getServerById(serverId).orElseThrow() + return server.slashCommands.thenCompose { slashCommands -> + val slashCommand = find(command, from = slashCommands) ?: return@thenCompose CompletableFuture.completedFuture(null) + return@thenCompose slashCommand.delete().thenAccept { + Nexus.configuration.loggingTemplates.SERVER_COMMAND_DELETED(serverId, slashCommand).debug() + } + } + } + + override fun updateForServer(shard: DiscordApi, command: NexusCommand, serverId: Long): CompletableFuture { + if (shard.getServerById(serverId).isEmpty) { + return getServerNotFoundErrorFrom(shard, serverId) + } + + val server = shard.getServerById(serverId).orElseThrow() + return server.slashCommands.thenCompose { slashCommands -> + val slashCommand = find(command, from = slashCommands) ?: return@thenCompose createForServer(shard, command, serverId) + return@thenCompose command.asSlashCommandUpdater(slashCommand.id) + .updateForServer(shard, serverId) + .and { Nexus.configuration.loggingTemplates.SERVER_COMMAND_UPDATED(serverId, it).debug() } + .thenApply { it as ApplicationCommand } + } + } + + override fun createForServer(shard: DiscordApi, command: NexusCommand, serverId: Long) = + command + .asSlashCommand() + .createForServer(shard, serverId) + .and { Nexus.configuration.loggingTemplates.SERVER_COMMAND_CREATED(serverId, it).debug() } + .thenApply { it as ApplicationCommand } + + private fun find(command: NexusCommand, from: Set): SlashCommand? = + from.firstOrNull { `$command` -> `$command`.name.equals(command.name, ignoreCase = true) } + + private fun CompletableFuture.and(`do`: (Type) -> Unit): CompletableFuture = + this.thenApply { `do`(it); return@thenApply it } + + private fun getServerNotFoundErrorFrom(shard: DiscordApi, serverId: Long): CompletableFuture = + CompletableFuture.failedFuture(NexusFailedActionException( + "An action failed for Nexus Synchronizer. The server (" + serverId + ")" + + " cannot be found on the shard calculated (" + shard.currentShard + "). " + + "Is the total shard number value wrong?" + )) +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/requests/NexusSynchronizeRequest.java b/src/main/java/pw/mihou/nexus/features/command/synchronizer/requests/NexusSynchronizeRequest.java deleted file mode 100644 index da7ce8d3..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/synchronizer/requests/NexusSynchronizeRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package pw.mihou.nexus.features.command.synchronizer.requests; - -import org.javacord.api.DiscordApi; -import org.javacord.api.interaction.SlashCommandBuilder; -import java.util.List; - -public record NexusSynchronizeRequest( - DiscordApi shard, - List slashCommands -) { } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/synchronizer/requests/NexusSynchronizeServerRequest.java b/src/main/java/pw/mihou/nexus/features/command/synchronizer/requests/NexusSynchronizeServerRequest.java deleted file mode 100644 index 6d146e10..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/synchronizer/requests/NexusSynchronizeServerRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package pw.mihou.nexus.features.command.synchronizer.requests; - -import org.javacord.api.DiscordApi; -import org.javacord.api.interaction.ApplicationCommand; -import org.javacord.api.interaction.SlashCommandBuilder; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -public record NexusSynchronizeServerRequest( - DiscordApi shard, - List slashCommands, - long serverId, - CompletableFuture> future -) { } diff --git a/src/main/java/pw/mihou/nexus/features/command/validation/OptionValidation.kt b/src/main/java/pw/mihou/nexus/features/command/validation/OptionValidation.kt new file mode 100644 index 00000000..767a4479 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/validation/OptionValidation.kt @@ -0,0 +1,149 @@ +package pw.mihou.nexus.features.command.validation + +import pw.mihou.nexus.features.command.validation.errors.ValidationError +import pw.mihou.nexus.features.command.validation.result.ValidationResult +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import java.util.* + +typealias OptionCollector