From 9c7186dccf84a1e4c37102ba977675715e273ee3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 28 Aug 2022 04:23:36 +0800 Subject: [PATCH 001/113] Adds the baseline new features for beta release (read full commit message). This adds baseline new features and improvements for the beta release of Nexus which includes many breaking changes to introduce much better functionality. As part of this change, the entire NexusSynchronizer and its default methods have been modified to create indexes on-the-go on anything that effects command additions. Nexus' EngineX has new methods such as `queue(predicate, event)`, `await(shard)`, `await(server)`, `awaitAvailable()` and `failFutureOnExpire(event, future)` which are explained deeper in the pull request (#8). All references to a writeable record store in Nexus EngineX have been removed because it has no clear purpose. Nexus Shard Manager now has a conveience method for the shard id calculation `((server >> 22) % totalShards)` which is called `shardOf(server, totalShards)` Nexus Command Manager can now support importing indexes from the database and exporting its current in-memory indexes, more details on PR (#8). `Nexus.start()` has been removed and methods related to creating shards and login with shards in `NexusBuilder` has been removed. --- pom.xml | 2 +- src/main/java/pw/mihou/nexus/Nexus.java | 8 - .../java/pw/mihou/nexus/core/NexusCore.java | 42 +---- .../nexus/core/builder/NexusBuilder.java | 43 +---- .../core/enginex/core/NexusEngineXCore.java | 167 ++++++++++++++--- .../enginex/event/NexusEngineQueuedEvent.java | 5 +- .../event/core/NexusEngineEventCore.java | 15 +- .../media/NexusEngineEventReadableStore.java | 55 ------ .../media/NexusEngineEventWriteableStore.java | 31 --- .../core/enginex/facade/NexusEngineX.java | 50 +++++ .../NexusFailedActionException.java | 9 + .../core/managers/NexusShardManager.java | 14 ++ .../core/NexusCommandManagerCore.java | 130 ++++++------- .../managers/facade/NexusCommandManager.java | 136 +++++++++++++- .../managers/records/NexusCommandIndex.java | 5 + .../features/command/facade/NexusCommand.java | 13 ++ .../synchronizer/NexusSynchronizer.java | 156 ++++++--------- .../overwrites/NexusSynchronizeMethods.java | 15 +- .../NexusDefaultSynchronizeMethods.java | 177 ++++++++---------- 19 files changed, 582 insertions(+), 491 deletions(-) delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/event/media/NexusEngineEventReadableStore.java delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/event/media/NexusEngineEventWriteableStore.java create mode 100644 src/main/java/pw/mihou/nexus/core/exceptions/NexusFailedActionException.java create mode 100644 src/main/java/pw/mihou/nexus/core/managers/records/NexusCommandIndex.java diff --git a/pom.xml b/pom.xml index 2f6bfe41..94c116da 100755 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ org.javacord javacord - 3.5.0 + 3.6.0-SNAPSHOT pom diff --git a/src/main/java/pw/mihou/nexus/Nexus.java b/src/main/java/pw/mihou/nexus/Nexus.java index 8c9f3714..a44b13ee 100755 --- a/src/main/java/pw/mihou/nexus/Nexus.java +++ b/src/main/java/pw/mihou/nexus/Nexus.java @@ -158,12 +158,4 @@ static void setLogger(NexusLoggingAdapter adapter) { */ 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/core/NexusCore.java b/src/main/java/pw/mihou/nexus/core/NexusCore.java index b1cfbf12..9e6943fd 100755 --- a/src/main/java/pw/mihou/nexus/core/NexusCore.java +++ b/src/main/java/pw/mihou/nexus/core/NexusCore.java @@ -1,7 +1,5 @@ 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; @@ -30,18 +28,15 @@ import java.util.*; -import java.util.function.Consumer; public class NexusCore implements Nexus { private final NexusCommandManagerCore commandManager = new NexusCommandManagerCore(this); - private NexusShardManager shardManager; + private final 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); @@ -52,17 +47,9 @@ public class NexusCore implements Nexus { * 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. + * @param nexusConfiguration The configuration to use for Nexus. */ - public NexusCore( - NexusMessageConfiguration messageConfiguration, - DiscordApiBuilder builder, - Consumer onShardLogin, - NexusConfiguration nexusConfiguration - ) { - this.builder = builder; - this.onShardLogin = onShardLogin; + public NexusCore(NexusMessageConfiguration messageConfiguration, NexusConfiguration nexusConfiguration) { this.shardManager = new NexusShardManager(this); this.nexusConfiguration = nexusConfiguration; this.messageConfiguration = Objects.requireNonNullElseGet(messageConfiguration, NexusDefaultMessageConfiguration::new); @@ -165,29 +152,6 @@ public Nexus addGlobalAfterwares(String... 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 diff --git a/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java b/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java index 2eb56e2b..68410fd8 100755 --- a/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java +++ b/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java @@ -1,21 +1,15 @@ 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) ); @@ -33,41 +27,6 @@ public NexusBuilder setMessageConfiguration(NexusMessageConfiguration configurat 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. * @@ -86,6 +45,6 @@ public NexusBuilder setNexusConfiguration(NexusConfiguration nexusConfiguration) * @return The new {@link Nexus} instance. */ public Nexus build() { - return new NexusCore(messageConfiguration, builder, onShardLogin, nexusConfiguration); + return new NexusCore(messageConfiguration, nexusConfiguration); } } 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 index be7c3f31..284d8bf3 100644 --- a/src/main/java/pw/mihou/nexus/core/enginex/core/NexusEngineXCore.java +++ b/src/main/java/pw/mihou/nexus/core/enginex/core/NexusEngineXCore.java @@ -1,26 +1,34 @@ package pw.mihou.nexus.core.enginex.core; import org.javacord.api.DiscordApi; +import org.javacord.api.entity.server.Server; import pw.mihou.nexus.Nexus; +import pw.mihou.nexus.commons.Pair; 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.exceptions.NexusFailedActionException; import pw.mihou.nexus.core.threadpool.NexusThreadPool; import java.time.Duration; import java.util.Map; +import java.util.Objects; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import java.util.function.Predicate; 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 BlockingQueue, NexusEngineEvent>> predicateQueue = new LinkedBlockingQueue<>(); + private final ReentrantLock predicateQueueLock = new ReentrantLock(); + private final ReentrantLock globalProcessingLock = new ReentrantLock(); private final Nexus nexus; /** @@ -34,13 +42,14 @@ public NexusEngineXCore(Nexus nexus) { } /** - * Gets the global blocking queue that is dedicated for all shards to - * accept at any point of time when needed. + * 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. * - * @return The blocking queue that any shard can accept. + * @param shard The shard to get the queue of. + * @return The blocking queue for this shard. */ - public BlockingQueue getGlobalQueue() { - return globalQueue; + private BlockingQueue getLocalQueue(int shard) { + return localQueue.computeIfAbsent(shard, key -> new LinkedBlockingQueue<>()); } /** @@ -50,7 +59,7 @@ public BlockingQueue getGlobalQueue() { * @param shard The shard to process the events. */ public void onShardReady(DiscordApi shard) { - CompletableFuture.runAsync(() -> { + NexusThreadPool.executorService.submit(() -> { while (!getLocalQueue(shard.getCurrentShard()).isEmpty()) { try { NexusEngineEvent event = getLocalQueue(shard.getCurrentShard()).poll(); @@ -63,15 +72,18 @@ public void onShardReady(DiscordApi shard) { exception.printStackTrace(); } } - }); - if (!hasGlobalProcessed.get() && !getGlobalQueue().isEmpty()) { - hasGlobalProcessed.set(true); - CompletableFuture.runAsync(() -> { - while (!getGlobalQueue().isEmpty()) { + predicateQueueLock.lock(); + try { + while (!predicateQueue.isEmpty()) { try { - NexusEngineEvent event = getGlobalQueue().poll(); + Predicate predicate = predicateQueue.peek().getLeft(); + + if (!predicate.test(shard)) { + continue; + } + NexusEngineEvent event = Objects.requireNonNull(predicateQueue.poll()).getRight(); if (event != null) { ((NexusEngineEventCore) event).process(shard); } @@ -80,24 +92,30 @@ public void onShardReady(DiscordApi shard) { exception.printStackTrace(); } } - hasGlobalProcessed.set(false); - }); - } - } + } finally { + predicateQueueLock.unlock(); + } + }); - /** - * 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<>()); - } + NexusThreadPool.executorService.submit(() -> { + globalProcessingLock.lock(); + try { + while (!globalQueue.isEmpty()) { + try { + NexusEngineEvent event = globalQueue.poll(); - return localQueue.get(shard); + 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(); + } + } + } finally { + globalProcessingLock.unlock(); + } + }); } @Override @@ -128,6 +146,36 @@ public NexusEngineEvent queue(int shard, NexusEngineQueuedEvent event) { return engineEvent; } + @Override + public NexusEngineEvent queue(Predicate predicate, NexusEngineQueuedEvent event) { + NexusEngineEventCore engineEvent = new NexusEngineEventCore(event); + DiscordApi $shard = nexus.getShardManager().asStream().filter(predicate).findFirst().orElse(null); + + if ($shard == null) { + Pair, NexusEngineEvent> queuedPair = Pair.of(predicate, engineEvent); + predicateQueue.add(queuedPair); + + Duration expiration = nexus.getConfiguration().timeBeforeExpiringEngineRequests(); + if (!(expiration.isZero() || expiration.isNegative())) { + NexusThreadPool.schedule(() -> { + if (engineEvent.status() == NexusEngineEventStatus.WAITING) { + boolean removeFromQueue = predicateQueue.remove(queuedPair); + 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. " + + "[sacknowledged={}]", + removeFromQueue + ); + engineEvent.expire(); + } + }, expiration.toMillis(), TimeUnit.MILLISECONDS); + } + } else { + engineEvent.process($shard); + } + + return engineEvent; + } + @Override public NexusEngineEvent queue(NexusEngineQueuedEvent event) { NexusEngineEventCore engineEvent = new NexusEngineEventCore(event); @@ -157,6 +205,65 @@ public NexusEngineEvent queue(NexusEngineQueuedEvent event) { return engineEvent; } + @Override + public CompletableFuture await(int shard) { + DiscordApi $shard = nexus.getShardManager().getShard(shard); + + if ($shard != null) { + return CompletableFuture.completedFuture($shard); + } + + CompletableFuture future = new CompletableFuture<>(); + failFutureOnExpire(queue(shard, future::complete), future); + + return future; + } + + @Override + public CompletableFuture await(long server) { + DiscordApi $shard = nexus.getShardManager().getShardOf(server).orElse(null); + + if ($shard != null) { + return CompletableFuture.completedFuture($shard.getServerById(server).orElseThrow()); + } + + CompletableFuture future = new CompletableFuture<>(); + failFutureOnExpire( + queue( + (shard) -> shard.getServerById(server).isPresent(), + (shard) -> future.complete(shard.getServerById(server).orElseThrow()) + ), + future + ); + + return future; + } + + @Override + public CompletableFuture awaitAvailable() { + DiscordApi $shard = nexus.getShardManager().asStream().findFirst().orElse(null); + + if ($shard != null) { + return CompletableFuture.completedFuture($shard); + } + + CompletableFuture future = new CompletableFuture<>(); + failFutureOnExpire(queue(future::complete), future); + + return future; + } + + @Override + public void failFutureOnExpire(NexusEngineEvent event, CompletableFuture future) { + event.addStatusChangeListener(($event, oldStatus, newStatus) -> { + if (newStatus == NexusEngineEventStatus.EXPIRED || newStatus == NexusEngineEventStatus.STOPPED) { + future.completeExceptionally( + new NexusFailedActionException("Failed to connect with the shard that was waited to complete this action.") + ); + } + }); + } + @Override public void broadcast(Consumer event) { nexus.getShardManager() 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 index 8c504da1..4c135f27 100644 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineQueuedEvent.java +++ b/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineQueuedEvent.java @@ -1,7 +1,6 @@ package pw.mihou.nexus.core.enginex.event; import org.javacord.api.DiscordApi; -import pw.mihou.nexus.core.enginex.event.media.NexusEngineEventWriteableStore; public interface NexusEngineQueuedEvent { @@ -10,9 +9,7 @@ public interface NexusEngineQueuedEvent { * 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); + void onEvent(DiscordApi api); } 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 index f7aefbf8..dd534f6e 100644 --- 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 @@ -4,14 +4,12 @@ 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 { @@ -19,8 +17,6 @@ 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; } @@ -33,8 +29,9 @@ public void cancel() { } /** - * Expires this event and stops it from proceeding in any - * form of way. + * Expires this event which will cancel any actions that will be attempted, this will + * also trigger the {@link NexusEngineEventCore#changeStatus(NexusEngineEventStatus)} method which will invoke + * all listeners listening to a status change. */ public void expire() { if (status() == NexusEngineEventStatus.WAITING) { @@ -50,8 +47,8 @@ private void changeStatus(NexusEngineEventStatus newStatus) { NexusEngineEventStatus oldStatus = status.get(); status.set(newStatus); - listeners.forEach(listener -> CompletableFuture - .runAsync(() -> listener.onStatusChange(this, oldStatus, newStatus), NexusThreadPool.executorService)); + listeners.forEach(listener -> NexusThreadPool.executorService.submit(() -> + listener.onStatusChange(this, oldStatus, newStatus))); } /** @@ -65,7 +62,7 @@ public void process(DiscordApi api) { } changeStatus(NexusEngineEventStatus.PROCESSING); - CompletableFuture.runAsync(() -> event.onEvent(api, store), NexusThreadPool.executorService) + CompletableFuture.runAsync(() -> event.onEvent(api), NexusThreadPool.executorService) .thenAccept(unused -> changeStatus(NexusEngineEventStatus.FINISHED)); } 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/facade/NexusEngineX.java b/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java index b2bf6978..ed5b60b8 100644 --- a/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java +++ b/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java @@ -1,10 +1,13 @@ package pw.mihou.nexus.core.enginex.facade; import org.javacord.api.DiscordApi; +import org.javacord.api.entity.server.Server; import pw.mihou.nexus.core.enginex.event.NexusEngineEvent; import pw.mihou.nexus.core.enginex.event.NexusEngineQueuedEvent; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import java.util.function.Predicate; public interface NexusEngineX { @@ -18,6 +21,15 @@ public interface NexusEngineX { */ NexusEngineEvent queue(int shard, NexusEngineQueuedEvent event); + /** + * 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. + */ + NexusEngineEvent queue(Predicate predicate, NexusEngineQueuedEvent event); + /** * Queues an event to be executed by any specific shard that is available * to take the event. @@ -27,6 +39,44 @@ public interface NexusEngineX { */ NexusEngineEvent queue(NexusEngineQueuedEvent event); + /** + * 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 {@link DiscordApi} instance of the given shard. + */ + CompletableFuture await(int shard); + + /** + * 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 {@link DiscordApi} instance of the given shard. + */ + CompletableFuture await(long server); + + /** + * Creates an awaiting listener to wait for any available shards to be ready and returns + * the shard that is available. + * + * @return The {@link DiscordApi} available to take any action. + */ + CompletableFuture awaitAvailable(); + + /** + * 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 EngineX 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. + */ + void failFutureOnExpire(NexusEngineEvent event, CompletableFuture future); + /** * 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. 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/managers/NexusShardManager.java b/src/main/java/pw/mihou/nexus/core/managers/NexusShardManager.java index 7615d819..2e01311f 100755 --- a/src/main/java/pw/mihou/nexus/core/managers/NexusShardManager.java +++ b/src/main/java/pw/mihou/nexus/core/managers/NexusShardManager.java @@ -58,6 +58,20 @@ public Optional getShardOf(long server) { .findFirst(); } + /** + * Calculates which shard the given server belongs to, given a total number of shards. + *

+ * This uses the formula ((serverId >> 22) % totalShards) which is the specified formula for + * calculating the shard of a "guild" by Discord. + * + * @param serverId The id of the server to calculate. + * @param totalShards The total number of shards as per formula. + * @return The shard which the given server should belong to, given a total number of shards. + */ + public int shardOf(long serverId, int totalShards) { + return (int) ((serverId >> 22) % totalShards); + } + /** * Gets the given server from any of the shards if there is any shard * responsible for that given server. 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 index 49f06320..c703bf4b 100755 --- a/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.java +++ b/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.java @@ -1,5 +1,6 @@ package pw.mihou.nexus.core.managers.core; +import org.javacord.api.entity.server.Server; import org.javacord.api.event.interaction.SlashCommandCreateEvent; import org.javacord.api.interaction.ApplicationCommand; import org.javacord.api.interaction.SlashCommand; @@ -9,18 +10,16 @@ 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.core.managers.records.NexusCommandIndex; 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; @@ -56,13 +55,6 @@ 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() @@ -72,6 +64,14 @@ public Optional getCommandByName(String name, long server) { .findFirst(); } + @Override + public List export() { + return indexes.entrySet() + .stream() + .map(entry -> new NexusCommandIndex(commands.get(entry.getValue()), entry.getKey())) + .toList(); + } + /** * This performs indexing based on the data analyzed from the * {@link SlashCommandCreateEvent} and returns the results for post-processing @@ -109,59 +109,63 @@ public Optional acceptEvent(SlashCommandCreateEvent event) { 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(); + nexusCore.getEngineX().awaitAvailable().thenAcceptAsync(shard -> { + Set slashCommands = shard.getGlobalSlashCommands().join(); + + // Clearing the entire in-memory index mapping to make sure that we don't have any outdated indexes. + indexes.clear(); + + for (SlashCommand slashCommand : slashCommands) { + index(slashCommand); + } + + Set servers = new HashSet<>(); + for (NexusCommand serverCommand : getServerCommands()) { + servers.addAll(serverCommand.getServerIds()); + } + + for (long server : servers) { + if (server == 0L) continue; + + Set $slashCommands = nexusCore.getEngineX().await(server) + .thenComposeAsync(Server::getSlashCommands).join(); + + for (SlashCommand slashCommand : $slashCommands) { + index(slashCommand); + } + } + + logger.info("All global and server slash commands are now indexed. It took {} milliseconds to complete indexing.", System.currentTimeMillis() - start); + }).exceptionally(ExceptionLogger.get()).join(); + } + + @Override + public void index(NexusCommand command, long snowflake) { + indexes.put(snowflake, ((NexusCommandCore) command).uuid); + } + + @Override + public void index(ApplicationCommand applicationCommand) { + long serverId = applicationCommand.getServerId().orElse(-1L); + + if (serverId == -1L) { + for (NexusCommand command : commands.values()) { + if (!command.getServerIds().isEmpty()) continue; + if (!command.getName().equalsIgnoreCase(applicationCommand.getName())) continue; + + index(command, applicationCommand.getApplicationId()); + break; + } + return; + } + + for (NexusCommand command : commands.values()) { + if (command.getServerIds().contains(serverId)) continue; + if (!command.getName().equalsIgnoreCase(applicationCommand.getName())) continue; + + index(command, applicationCommand.getApplicationId()); + break; + } } } 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 index d31502b1..d6b548f1 100755 --- a/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java +++ b/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java @@ -1,13 +1,13 @@ package pw.mihou.nexus.core.managers.facade; +import org.javacord.api.interaction.ApplicationCommand; import org.javacord.api.interaction.SlashCommand; import pw.mihou.nexus.Nexus; +import pw.mihou.nexus.core.managers.records.NexusCommandIndex; 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; +import java.util.*; +import java.util.function.Function; public interface NexusCommandManager { @@ -19,6 +19,61 @@ public interface NexusCommandManager { */ Collection getCommands(); + /** + * 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. + */ + default Set getGlobalCommands() { + Set commands = new HashSet<>(); + for (NexusCommand command : getCommands()) { + if (!command.isServerCommand()) continue; + commands.add(command); + } + + return commands; + } + + /** + * 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. + */ + default Set getServerCommands() { + Set commands = new HashSet<>(); + for (NexusCommand command : getCommands()) { + if (command.isServerCommand()) continue; + commands.add(command); + } + + return commands; + } + + /** + * 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 + * {@link List#contains(Object)} predicate over its server ids. + * + * @param server The server to find all associated commands of. + * @return All associated commands of the given server. + */ + default Set getCommandsAssociatedWith(long server) { + Set commands = new HashSet<>(); + for (NexusCommand command : getCommands()) { + if (!command.getServerIds().contains(server)) continue; + commands.add(command); + } + + return commands; + } + /** * Adds a command to the registry. * @@ -52,7 +107,9 @@ public interface NexusCommandManager { * @param name The name of the command to look for. * @return The first command that matches the name specified. */ - Optional getCommandByName(String name); + default Optional getCommandByName(String name) { + return getCommands().stream().filter(nexusCommand -> nexusCommand.getName().equalsIgnoreCase(name)).findFirst(); + } /** * Gets the command that matches the {@link NexusCommand#getName()} and {@link NexusCommand#getServerIds()}. This is a precise @@ -64,10 +121,79 @@ public interface NexusCommandManager { */ Optional getCommandByName(String name, long server); + /** + * 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 in-memory indexes that the command manager has. + */ + List export(); + + + /** + * Creates a new fresh in-memory index map by using the {@link NexusCommandManager#index()} method before creating + * an export of the given indexes with the {@link NexusCommandManager#export()} command. + * + * @return A snapshot of the in-memory indexes that the command manager has. + */ + default List indexThenExport() { + index(); + return export(); + } + /** * This indexes all the commands whether it'd be global or server commands to increase * performance and precision of slash commands. */ void index(); + /** + * Creates an in-memory 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. + */ + void index(NexusCommand command, long snowflake); + + /** + * Massively creates an in-memory index mapping for all the given command using the indexer reducer + * provided below. + *

+ * This is a completely synchronous operation and could cause major blocking on the application if you use + * it daringly. + * + * @param indexer The indexer reducer to collectively find the index of the commands. + */ + default void index(Function indexer) { + for (NexusCommand command : getCommands()) { + index(command, indexer.apply(command)); + } + } + + /** + * Creates an in-memory 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. + */ + default void index(Set applicationCommandList) { + for (ApplicationCommand applicationCommand : applicationCommandList) { + index(applicationCommand); + } + } + + /** + * Creates an in-memory 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 command The command to index. + */ + void index(ApplicationCommand command); + } diff --git a/src/main/java/pw/mihou/nexus/core/managers/records/NexusCommandIndex.java b/src/main/java/pw/mihou/nexus/core/managers/records/NexusCommandIndex.java new file mode 100644 index 00000000..f629930d --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/records/NexusCommandIndex.java @@ -0,0 +1,5 @@ +package pw.mihou.nexus.core.managers.records; + +import pw.mihou.nexus.features.command.facade.NexusCommand; + +public record NexusCommandIndex(NexusCommand command, long applicationCommandId) {} 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..0dba374d 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 @@ -11,6 +11,8 @@ public interface NexusCommand { + long PLACEHOLDER_SERVER_ID = 0L; + /** * Gets the name of the command. * @@ -46,6 +48,17 @@ public interface NexusCommand { */ List getServerIds(); + /** + * 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 Is this a server command. + */ + 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 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 index d0b9e4d2..1d55c717 100644 --- a/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java +++ b/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java @@ -4,7 +4,6 @@ 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; @@ -13,35 +12,32 @@ import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; -public record NexusSynchronizer( - Nexus nexus -) { +public record NexusSynchronizer(Nexus nexus) { - public static final AtomicReference SYNCHRONIZE_METHODS = new AtomicReference<>(new NexusDefaultSynchronizeMethods()); + public static volatile NexusSynchronizeMethods SYNCHRONIZE_METHODS = new NexusDefaultSynchronizeMethods(); /** - * Deletes a command to a specific server. + * Deletes a command from 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. + * @return A future to indicate the completion 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<>()); - } + if (serverMappedFutures.containsKey(serverId)) continue; - engineX.queue( - (int) ((serverId >> 22) % totalShards), - (api, store) -> SYNCHRONIZE_METHODS.get().deleteForServer(api, command, serverId, serverMappedFutures.get(serverId)) - ); + CompletableFuture future = nexus.getEngineX() + .await(nexus.getShardManager().shardOf(serverId, totalShards)) + .thenCompose(shard -> SYNCHRONIZE_METHODS.deleteForServer(shard, command, serverId)); + + serverMappedFutures.put(serverId, future); } return CompletableFuture.allOf(serverMappedFutures.values().toArray(new CompletableFuture[0])); @@ -58,20 +54,8 @@ public CompletableFuture delete(NexusCommand command, int totalShards, lon * @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; + return nexus.getEngineX().await(nexus.getShardManager().shardOf(serverId, totalShards)) + .thenCompose(shard -> batchUpdate(serverId, shard)); } /** @@ -85,18 +69,12 @@ public CompletableFuture batchUpdate(long serverId, int totalShards) { */ 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; + Set serverCommands = manager.getCommandsAssociatedWith(serverId).stream() + .map(NexusCommand::asSlashCommand) + .collect(Collectors.toSet()); + return SYNCHRONIZE_METHODS.bulkOverwriteServer(shard, serverCommands, serverId).thenAccept(manager::index); } /** @@ -110,16 +88,16 @@ public CompletableFuture batchUpdate(long serverId, DiscordApi shard) { */ 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<>()); - } + if (serverMappedFutures.containsKey(serverId)) continue; + + CompletableFuture future = nexus.getEngineX() + .await(nexus.getShardManager().shardOf(serverId, totalShards)) + .thenCompose(shard -> SYNCHRONIZE_METHODS.updateForServer(shard, command, serverId)) + .thenAccept($command -> nexus.getCommandManager().index(command, $command.getApplicationId())); - engineX.queue( - (int) ((serverId >> 22) % totalShards), - (api, store) -> SYNCHRONIZE_METHODS.get().updateForServer(api, command, serverId, serverMappedFutures.get(serverId)) - ); + serverMappedFutures.put(serverId, future); } return CompletableFuture.allOf(serverMappedFutures.values().toArray(new CompletableFuture[0])); @@ -127,8 +105,8 @@ public CompletableFuture upsert(NexusCommand command, int totalShards, lon /** * 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 + * {@link org.javacord.api.DiscordApi#bulkOverwriteGlobalApplicationCommands(Set)} and + * {@link org.javacord.api.DiscordApi#bulkOverwriteServerApplicationCommands(Server, Set)}. 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. @@ -136,63 +114,47 @@ public CompletableFuture upsert(NexusCommand command, int totalShards, lon */ 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<>(); + + Set serverCommands = manager.getServerCommands(); + Set globalCommands = manager.getGlobalCommands().stream().map(NexusCommand::asSlashCommand) + .collect(Collectors.toSet()); + + NexusEngineX engineX = nexus.getEngineX(); + CompletableFuture globalFuture = engineX.awaitAvailable() + .thenCompose(shard -> SYNCHRONIZE_METHODS.bulkOverwriteGlobal(shard, globalCommands)) + .thenAccept(manager::index); + + if (serverCommands.isEmpty()) { + return globalFuture; + } + + 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<>()); - } + for (NexusCommand $command : serverCommands) { + for (long serverId : $command.getServerIds()) { + if (serverId == NexusCommand.PLACEHOLDER_SERVER_ID) continue; - serverMappedCommands.get(serverId).add(nexusCommand.asSlashCommand()); - })); + if (!serverMappedCommands.containsKey(serverId)) { + serverMappedCommands.put(serverId, new HashSet<>()); + } - serverMappedCommands.forEach((serverId, slashCommandBuilders) -> { - if (serverId == 0L) { - return; + serverMappedCommands.get(serverId).add($command.asSlashCommand()); } + } - if (!serverMappedFutures.containsKey(serverId)) { - serverMappedFutures.put(serverId, new CompletableFuture<>()); - } + serverMappedCommands.forEach((server, builders) -> { + if (server == NexusCommand.PLACEHOLDER_SERVER_ID) return; - engineX.queue((int) ( - (serverId >> 22) % totalShards), - (api, store) -> SYNCHRONIZE_METHODS.get().bulkOverwriteServer(api, slashCommandBuilders, serverId, serverMappedFutures.get(serverId)) - ); + CompletableFuture future = engineX.await(nexus.getShardManager().shardOf(server, totalShards)) + .thenCompose(shard -> SYNCHRONIZE_METHODS.bulkOverwriteServer(shard, builders, server)) + .thenAccept(manager::index); + + serverMappedFutures.put(server, future); }); List> futures = new ArrayList<>(); + futures.add(globalFuture); futures.addAll(serverMappedFutures.values()); 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 index 1e8bbe46..7c00519a 100644 --- 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 @@ -5,20 +5,21 @@ import org.javacord.api.interaction.SlashCommandBuilder; import pw.mihou.nexus.features.command.facade.NexusCommand; -import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; public interface NexusSynchronizeMethods { - CompletableFuture bulkOverwriteGlobal(DiscordApi shard, List slashCommands); + CompletableFuture> bulkOverwriteGlobal(DiscordApi shard, + Set slashCommands); - void bulkOverwriteServer(DiscordApi shard, List slashCommands, - long serverId, CompletableFuture future); + CompletableFuture> bulkOverwriteServer(DiscordApi shard, Set slashCommands, + long serverId); - void deleteForServer(DiscordApi shard, NexusCommand command, long serverId, CompletableFuture future); + CompletableFuture deleteForServer(DiscordApi shard, NexusCommand command, long serverId); - void updateForServer(DiscordApi shard, NexusCommand command, long serverId, CompletableFuture future); + CompletableFuture updateForServer(DiscordApi shard, NexusCommand command, long serverId); - void createForServer(DiscordApi shard, NexusCommand command, long serverId, CompletableFuture future); + CompletableFuture createForServer(DiscordApi shard, NexusCommand command, long serverId); } 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 index d0da5edb..5f712003 100644 --- 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 @@ -2,140 +2,117 @@ import org.javacord.api.DiscordApi; import org.javacord.api.entity.server.Server; +import org.javacord.api.interaction.ApplicationCommand; import org.javacord.api.interaction.SlashCommand; import org.javacord.api.interaction.SlashCommandBuilder; import pw.mihou.nexus.core.NexusCore; +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.List; -import java.util.Optional; +import java.util.Set; 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; + public CompletableFuture> bulkOverwriteGlobal(DiscordApi shard, Set slashCommands) { + return shard.bulkOverwriteGlobalApplicationCommands(slashCommands).thenApply(applicationCommands -> { + NexusCore.logger.debug("All global commands have been synchronized. [size={}]", applicationCommands.size()); + return applicationCommands; }); - - return globalFuture; } @Override - public void bulkOverwriteServer(DiscordApi shard, List slashCommands, - long serverId, CompletableFuture future) { + public CompletableFuture> bulkOverwriteServer(DiscordApi shard, Set slashCommands, long serverId) { 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; + return getServerNotFoundErrorFrom(shard, serverId); } 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; - }); + return shard.bulkOverwriteServerApplicationCommands(server, slashCommands).thenApply(applicationCommands -> { + NexusCore.logger.debug("All commands with relation for server {} have been synchronized.", serverId); + return applicationCommands; + }); } @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; + public CompletableFuture deleteForServer(DiscordApi shard, NexusCommand command, long serverId) { + if (shard.getServerById(serverId).isEmpty()) { + return getServerNotFoundErrorFrom(shard, serverId); } - 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; - })); + Server server = shard.getServerById(serverId).orElseThrow(); + return server.getSlashCommands().thenCompose(slashCommands -> { + SlashCommand slashCommand = slashCommands.stream() + .filter($command -> $command.getName().equalsIgnoreCase(command.getName())) + .findAny() + .orElse(null); + + if (slashCommand == null) { + return CompletableFuture.completedFuture(null); + } + + return slashCommand.deleteForServer(serverId) + .thenAccept((unused) -> + NexusCore.logger.debug("The command ({}) has been removed from the server ({}).", + command.getName(), serverId) + ); + }); } @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; + public CompletableFuture updateForServer(DiscordApi shard, NexusCommand command, long serverId) { + if (shard.getServerById(serverId).isEmpty()) { + return getServerNotFoundErrorFrom(shard, serverId); } - 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; + Server server = shard.getServerById(serverId).orElseThrow(); + return server.getSlashCommands().thenCompose(slashCommands -> { + SlashCommand slashCommand = slashCommands.stream() + .filter($command -> $command.getName().equalsIgnoreCase(command.getName())) + .findAny() + .orElse(null); + + if (slashCommand == null) { + return createForServer(shard, command, serverId); + } + + return command.asSlashCommandUpdater(slashCommand.getId()) + .updateForServer(shard, serverId) + .thenApply(resultingCommand -> { + NexusCore.logger.debug("The command ({}) has been updated for the server ({}).", command.getName(), serverId); + return resultingCommand; }); - } 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; + public CompletableFuture createForServer(DiscordApi shard, NexusCommand command, long serverId) { + if (shard.getServerById(serverId).isEmpty()) { + return getServerNotFoundErrorFrom(shard, serverId); } - 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; - }); + return command.asSlashCommand().createForServer(shard, serverId).thenApply(slashCommand -> { + NexusCore.logger.debug("The command ({}) has been created for the server ({}).", command.getName(), + serverId); + return slashCommand; + }); + } + + /** + * Creates a simple {@link CompletableFuture} with a failed future that indicates the server cannot be found. + * + * @return A failed future indicating the server cannot be found. + * @param The return type intended to contain. + */ + private CompletableFuture getServerNotFoundErrorFrom(DiscordApi shard, long serverId) { + return CompletableFuture.failedFuture( + new NexusFailedActionException( + "An action failed for Nexus Synchronizer. The server (" + serverId + ")" + + " cannot be found on the shard calculated (" + shard.getCurrentShard() +"). " + + "Is the total shard number value wrong?" + ) + ); } } From d4ac5491fb45c27e210625a0d0612cd4df57506e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 28 Aug 2022 19:47:50 +0800 Subject: [PATCH 002/113] Adds synchronized keyword to `process`, `expire` and `cancel` in NexusEngineEventCore. --- .../nexus/core/enginex/event/core/NexusEngineEventCore.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index dd534f6e..ea148160 100644 --- 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 @@ -22,7 +22,7 @@ public NexusEngineEventCore(NexusEngineQueuedEvent event) { } @Override - public void cancel() { + public synchronized void cancel() { if (status() == NexusEngineEventStatus.WAITING) { changeStatus(NexusEngineEventStatus.STOPPED); } @@ -33,7 +33,7 @@ public void cancel() { * also trigger the {@link NexusEngineEventCore#changeStatus(NexusEngineEventStatus)} method which will invoke * all listeners listening to a status change. */ - public void expire() { + public synchronized void expire() { if (status() == NexusEngineEventStatus.WAITING) { changeStatus(NexusEngineEventStatus.EXPIRED); } @@ -56,7 +56,7 @@ private void changeStatus(NexusEngineEventStatus newStatus) { * * @param api The shard that will be processing the event. */ - public void process(DiscordApi api) { + public synchronized void process(DiscordApi api) { if (status.get() == NexusEngineEventStatus.STOPPED || status.get() == NexusEngineEventStatus.EXPIRED) { return; } From 35550e8a966abb11de2f659475db432b24b72791 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 28 Aug 2022 19:51:36 +0800 Subject: [PATCH 003/113] Adds uncaught exception handling inside NexusEngineEventCore. --- .../enginex/event/core/NexusEngineEventCore.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 index ea148160..536de48f 100644 --- 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 @@ -1,6 +1,7 @@ package pw.mihou.nexus.core.enginex.event.core; import org.javacord.api.DiscordApi; +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.listeners.NexusEngineEventStatusChange; @@ -9,7 +10,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; public class NexusEngineEventCore implements NexusEngineEvent { @@ -62,8 +62,16 @@ public synchronized void process(DiscordApi api) { } changeStatus(NexusEngineEventStatus.PROCESSING); - CompletableFuture.runAsync(() -> event.onEvent(api), NexusThreadPool.executorService) - .thenAccept(unused -> changeStatus(NexusEngineEventStatus.FINISHED)); + NexusThreadPool.executorService.submit(() -> { + try { + event.onEvent(api); + } catch (Exception exception) { + NexusCore.logger.error("An uncaught exception was caught inside a Nexus Engine Event."); + exception.printStackTrace(); + } finally { + changeStatus(NexusEngineEventStatus.FINISHED); + } + }); } @Override From d2a161a55c35f53db9fbec64f8c11dd881f0d065 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 28 Aug 2022 20:10:54 +0800 Subject: [PATCH 004/113] Adds alternative names for `removeSupportFor(serverIds)` and `addSupprtFor(serverIds)` in NexusCommand. This change is correlated with making Nexus a bit more clearer to understand. It's more preferred to use the word disassociate and associate when it comes to servers. --- .../command/core/NexusCommandCore.java | 17 ++++++-- .../features/command/facade/NexusCommand.java | 41 ++++++++++++++++--- 2 files changed, 48 insertions(+), 10 deletions(-) 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..642aacc8 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 @@ -99,19 +99,28 @@ public List getServerIds() { @Override public NexusCommand addSupportFor(Long... serverIds) { + return associate(serverIds); + } + + @Override + 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; } + @Override + public NexusCommand removeSupportFor(Long... serverIds) { + return disassociate(serverIds); + } + @Override public Optional get(String field, Class type) { return get(field).map(object -> { 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 0dba374d..64197b07 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,5 +1,6 @@ package pw.mihou.nexus.features.command.facade; +import org.javacord.api.DiscordApi; import org.javacord.api.entity.permission.PermissionType; import org.javacord.api.interaction.*; import pw.mihou.nexus.commons.Pair; @@ -61,24 +62,52 @@ default boolean isServerCommand() { /** * 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. + * register this command with. * * @param serverIds The server ids to add support for this command. * @return {@link NexusCommand} instance for chain-calling methods. + * @see NexusCommand#associate(Long...) */ + @Deprecated() NexusCommand addSupportFor(Long... serverIds); + /** + * 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, DiscordApi)} method + * after using this method. + * + * @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 associate(Long... serverIds); + + /** + * 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, DiscordApi)} method + * after using this method. + * + * @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 disassociate(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. + * register this command with. * * @param serverIds The server ids to remove support for this command. * @return {@link NexusCommand} instance for chain-calling methods. + * @see NexusCommand#disassociate(Long...) */ + @Deprecated() NexusCommand removeSupportFor(Long... serverIds); /** From ebcf3b907724187fdca5067806f967473a336a14 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 31 Aug 2022 23:22:20 +0800 Subject: [PATCH 005/113] Fixes flipped condition for `getGlobalCommands` and `getServerCommands` --- .../mihou/nexus/core/managers/facade/NexusCommandManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index d6b548f1..c05afb06 100755 --- a/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java +++ b/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java @@ -30,7 +30,7 @@ public interface NexusCommandManager { default Set getGlobalCommands() { Set commands = new HashSet<>(); for (NexusCommand command : getCommands()) { - if (!command.isServerCommand()) continue; + if (command.isServerCommand()) continue; commands.add(command); } @@ -48,7 +48,7 @@ default Set getGlobalCommands() { default Set getServerCommands() { Set commands = new HashSet<>(); for (NexusCommand command : getCommands()) { - if (command.isServerCommand()) continue; + if (!command.isServerCommand()) continue; commands.add(command); } From 12b1069abd30fd26447d60c0a6c7abd1e91e7ed5 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 13 Sep 2022 07:33:23 +0800 Subject: [PATCH 006/113] Removes the need for server-specific `DiscordApi` in `batchUpdate(serverId)` and `synchronize`. --- .../synchronizer/NexusSynchronizer.java | 30 +++++-------------- .../NexusDefaultSynchronizeMethods.java | 11 +------ 2 files changed, 8 insertions(+), 33 deletions(-) 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 index 1d55c717..385599d1 100644 --- a/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java +++ b/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java @@ -1,6 +1,5 @@ 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; @@ -48,33 +47,19 @@ public CompletableFuture delete(NexusCommand command, int totalShards, lon * 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) { - return nexus.getEngineX().await(nexus.getShardManager().shardOf(serverId, totalShards)) - .thenCompose(shard -> batchUpdate(serverId, shard)); - } - - /** - * 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. + * @param serverId the given guild snowflake to perform updates upon. * @return A future to indicate progress of this task. */ - public CompletableFuture batchUpdate(long serverId, DiscordApi shard) { + public CompletableFuture batchUpdate(long serverId) { NexusCommandManager manager = nexus.getCommandManager(); Set serverCommands = manager.getCommandsAssociatedWith(serverId).stream() .map(NexusCommand::asSlashCommand) .collect(Collectors.toSet()); - return SYNCHRONIZE_METHODS.bulkOverwriteServer(shard, serverCommands, serverId).thenAccept(manager::index); + return nexus.getEngineX().awaitAvailable() + .thenCompose(shard -> SYNCHRONIZE_METHODS.bulkOverwriteServer(shard, serverCommands, serverId)) + .thenAccept(manager::index); } /** @@ -109,10 +94,9 @@ public CompletableFuture upsert(NexusCommand command, int totalShards, lon * {@link org.javacord.api.DiscordApi#bulkOverwriteServerApplicationCommands(Server, Set)}. 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) { + public CompletableFuture synchronize() { NexusCommandManager manager = nexus.getCommandManager(); Set serverCommands = manager.getServerCommands(); @@ -146,7 +130,7 @@ public CompletableFuture synchronize(int totalShards) { serverMappedCommands.forEach((server, builders) -> { if (server == NexusCommand.PLACEHOLDER_SERVER_ID) return; - CompletableFuture future = engineX.await(nexus.getShardManager().shardOf(server, totalShards)) + CompletableFuture future = engineX.awaitAvailable() .thenCompose(shard -> SYNCHRONIZE_METHODS.bulkOverwriteServer(shard, builders, server)) .thenAccept(manager::index); 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 index 5f712003..68999f1c 100644 --- 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 @@ -25,12 +25,7 @@ public CompletableFuture> bulkOverwriteGlobal(DiscordApi @Override public CompletableFuture> bulkOverwriteServer(DiscordApi shard, Set slashCommands, long serverId) { - if (shard.getServerById(serverId).isEmpty()) { - return getServerNotFoundErrorFrom(shard, serverId); - } - - Server server = shard.getServerById(serverId).orElseThrow(); - return shard.bulkOverwriteServerApplicationCommands(server, slashCommands).thenApply(applicationCommands -> { + return shard.bulkOverwriteServerApplicationCommands(serverId, slashCommands).thenApply(applicationCommands -> { NexusCore.logger.debug("All commands with relation for server {} have been synchronized.", serverId); return applicationCommands; }); @@ -89,10 +84,6 @@ public CompletableFuture updateForServer(DiscordApi shard, N @Override public CompletableFuture createForServer(DiscordApi shard, NexusCommand command, long serverId) { - if (shard.getServerById(serverId).isEmpty()) { - return getServerNotFoundErrorFrom(shard, serverId); - } - return command.asSlashCommand().createForServer(shard, serverId).thenApply(slashCommand -> { NexusCore.logger.debug("The command ({}) has been created for the server ({}).", command.getName(), serverId); From 14f54f9752a5709f65e00ae64b55c44af7d344d1 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 13 Sep 2022 07:41:42 +0800 Subject: [PATCH 007/113] Updates examples to reflect newer state. --- examples/synchronization/Main.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/synchronization/Main.java b/examples/synchronization/Main.java index f457e0d0..c22aea93 100644 --- a/examples/synchronization/Main.java +++ b/examples/synchronization/Main.java @@ -40,8 +40,7 @@ public static void main(String[] args) { // 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) + nexus.getSynchronizer().synchronize() .thenAccept(unused -> System.out.println("Synchronization with Discord's and Nexus' command repository is now complete.")) .exceptionally(ExceptionLogger.get()); @@ -55,8 +54,8 @@ public static void main(String[] args) { // 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. @@ -70,7 +69,7 @@ public static void main(String[] args) { // 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); @@ -84,11 +83,10 @@ public static void main(String[] args) { * 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) { + private static void batchUpdate(long serverId) { nexus.getSynchronizer() - .batchUpdate(serverId, totalShards) + .batchUpdate(serverId) .thenAccept(unused -> System.out.println("A batch update was complete. [server="+serverId+"]")) .exceptionally(ExceptionLogger.get()); } From 931138420385312e5ab2d403ead6498d8b0ce7dc Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 13 Sep 2022 07:42:13 +0800 Subject: [PATCH 008/113] Additional resolutions in the examples. --- examples/synchronization/Main.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/synchronization/Main.java b/examples/synchronization/Main.java index c22aea93..aadfdb6c 100644 --- a/examples/synchronization/Main.java +++ b/examples/synchronization/Main.java @@ -79,7 +79,7 @@ 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. @@ -93,7 +93,7 @@ private static void batchUpdate(long serverId) { /** * 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. @@ -109,7 +109,7 @@ private static void singleUpdate(NexusCommand command, int totalShards, long... /** * 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. From 143076258a7f26676674f893d7d4d4ac7e74ab62 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 13 Sep 2022 07:44:13 +0800 Subject: [PATCH 009/113] Adds a tiny bit of change in the examples. --- examples/synchronization/commands/ADynamicCommand.java | 6 +++--- .../synchronization/commands/ASpecificServerCommand.java | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/synchronization/commands/ADynamicCommand.java b/examples/synchronization/commands/ADynamicCommand.java index b365895e..36f49ef5 100644 --- a/examples/synchronization/commands/ADynamicCommand.java +++ b/examples/synchronization/commands/ADynamicCommand.java @@ -12,9 +12,9 @@ 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 = List.of(NexusCommand.PLACEHOLDER_SERVER_ID); @Override public void onEvent(NexusCommandEvent event) { diff --git a/examples/synchronization/commands/ASpecificServerCommand.java b/examples/synchronization/commands/ASpecificServerCommand.java index 9d34b337..e809d2f0 100644 --- a/examples/synchronization/commands/ASpecificServerCommand.java +++ b/examples/synchronization/commands/ASpecificServerCommand.java @@ -9,9 +9,7 @@ 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 = List.of(807084089013174272L); @Override public void onEvent(NexusCommandEvent event) { From e486c9fa3f3822735ce3060dbdbf1a601f330f2e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 13 Sep 2022 07:55:40 +0800 Subject: [PATCH 010/113] Adds more respond methods to enable more straightforward command responding. --- .../command/facade/NexusCommandEvent.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) 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 index 96ad0923..53b3d642 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java @@ -4,6 +4,7 @@ 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.message.embed.EmbedBuilder; import org.javacord.api.entity.server.Server; import org.javacord.api.entity.user.User; import org.javacord.api.event.interaction.SlashCommandCreateEvent; @@ -170,6 +171,50 @@ default InteractionImmediateResponseBuilder respondNow() { return getInteraction().createImmediateResponder(); } + /** + * A short-hand expression of sending a non-ephemeral response to Discord with the given content. This does not + * handle the exceptions, please handle the exceptions accordingly. + * + * @param content the content to send as a response. + * @return A future that contains the updater to update the response when needed. + */ + default CompletableFuture respondNowWith(String content) { + return respondNow().setContent(content).respond(); + } + + /** + * A short-hand expression of sending a non-ephemeral response to Discord with the given embeds. This does not + * handle the exceptions, please handle the exceptions accordingly. + * + * @param embeds the embeds to send as a response. + * @return A future that contains the updater to update the response when needed. + */ + default CompletableFuture respondNowWith(EmbedBuilder... embeds) { + return respondNow().addEmbeds(embeds).respond(); + } + + /** + * A short-hand expression of sending an ephemeral response to Discord with the given content. This does not + * handle the exceptions, please handle the exceptions accordingly. + * + * @param content the content to send as a response. + * @return A future that contains the updater to update the response when needed. + */ + default CompletableFuture respondNowEphemerallyWith(String content) { + return respondNowAsEphemeral().setContent(content).respond(); + } + + /** + * A short-hand expression of sending an ephemeral response to Discord with the given embeds. This does not + * handle the exceptions, please handle the exceptions accordingly. + * + * @param embeds the embeds to send as a response. + * @return A future that contains the updater to update the response when needed. + */ + default CompletableFuture respondNowEphemerallyWith(EmbedBuilder... embeds) { + return respondNowAsEphemeral().addEmbeds(embeds).respond(); + } + /** * Gets the immediate response builder for this command and adds the * {@link MessageFlag#EPHEMERAL} flag ahead of time. From f5a428893c70284c15b747cfb5bcce68f5e21466 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 13 Sep 2022 07:57:24 +0800 Subject: [PATCH 011/113] Removes `getOptions()` and `getSubcommandOptions()` methods in `NexusCommandEvent`. --- .../command/facade/NexusCommandEvent.java | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) 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 index 53b3d642..f8e0bad0 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java @@ -9,14 +9,12 @@ 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; @@ -86,7 +84,7 @@ default Optional getServerId() { * * @return The ID of the text channel where the command was executed. */ - default Long getChannelId() { + default long getChannelId() { return getChannel().getId(); } @@ -95,7 +93,7 @@ default Long getChannelId() { * * @return The ID of the user who executed this command. */ - default Long getUserId() { + default long getUserId() { return getUser().getId(); } @@ -143,25 +141,6 @@ 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. * From 905b67212f1964b30dbc9666c601d326c01818b2 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 13 Sep 2022 08:00:18 +0800 Subject: [PATCH 012/113] Resolves examples to use newer methods for responding. --- examples/authentication/permissions/PingCommand.java | 2 +- examples/authentication/roles/PingCommand.java | 2 +- examples/authentication/user/PingCommand.java | 2 +- examples/shared_fields/PingCommand.java | 2 +- examples/synchronization/commands/ADynamicCommand.java | 4 +--- examples/synchronization/commands/AGlobalCommand.java | 4 +--- examples/synchronization/commands/ASpecificServerCommand.java | 4 +--- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/examples/authentication/permissions/PingCommand.java b/examples/authentication/permissions/PingCommand.java index d7d950a2..f7f12883 100644 --- a/examples/authentication/permissions/PingCommand.java +++ b/examples/authentication/permissions/PingCommand.java @@ -18,6 +18,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/authentication/roles/PingCommand.java b/examples/authentication/roles/PingCommand.java index 44c4b4fa..287bbc6e 100644 --- a/examples/authentication/roles/PingCommand.java +++ b/examples/authentication/roles/PingCommand.java @@ -17,6 +17,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/authentication/user/PingCommand.java b/examples/authentication/user/PingCommand.java index cf695132..184097a7 100644 --- a/examples/authentication/user/PingCommand.java +++ b/examples/authentication/user/PingCommand.java @@ -17,6 +17,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/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/commands/ADynamicCommand.java b/examples/synchronization/commands/ADynamicCommand.java index 36f49ef5..7f86e044 100644 --- a/examples/synchronization/commands/ADynamicCommand.java +++ b/examples/synchronization/commands/ADynamicCommand.java @@ -18,8 +18,6 @@ public class ADynamicCommand implements NexusHandler { @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 e809d2f0..3bc7fcd7 100644 --- a/examples/synchronization/commands/ASpecificServerCommand.java +++ b/examples/synchronization/commands/ASpecificServerCommand.java @@ -13,8 +13,6 @@ public class ASpecificServerCommand implements NexusHandler { @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!"); } } From 8ac3de5a65d317f6dd4b26e0df2433d3efff943c Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 2 Nov 2022 21:46:58 +0800 Subject: [PATCH 013/113] Kotlin-ification, Persistent Indexing, One Nexus, Express Way and Option Validators! --- .github/workflows/maven.yml | 6 +- build.gradle | 29 ++ .../permissions/PingCommand.java | 23 -- .../authentication/roles/PingCommand.java | 22 -- examples/authentication/user/PingCommand.java | 22 -- pom.xml | 147 ---------- src/main/java/pw/mihou/nexus/Nexus.java | 161 ---------- src/main/java/pw/mihou/nexus/Nexus.kt | 207 +++++++++++++ .../nexus/configuration/NexusConfiguration.kt | 13 + .../NexusCommonsInterceptorsConfiguration.kt | 7 + ...CommonsInterceptorsMessageConfiguration.kt | 16 + .../modules/NexusExpressConfiguration.kt | 14 + .../modules/NexusGlobalConfiguration.kt | 35 +++ .../java/pw/mihou/nexus/core/NexusCore.java | 194 ------------- .../nexus/core/builder/NexusBuilder.java | 50 ---- .../core/NexusConfiguration.java | 11 - .../core/enginex/core/NexusEngineXCore.java | 274 ------------------ .../core/enginex/event/NexusEngineEvent.java | 30 -- .../enginex/event/NexusEngineQueuedEvent.java | 15 - .../event/core/NexusEngineEventCore.java | 87 ------ .../NexusEngineEventStatusChange.java | 23 -- .../event/status/NexusEngineEventStatus.java | 11 - .../enginex/facade/NexusDiscordShard.java | 15 - .../NexusConsoleLoggingConfiguration.java | 1 - .../core/managers/NexusShardManager.java | 135 --------- .../core/NexusCommandManagerCore.java | 171 ----------- .../managers/core/NexusCommandManagerCore.kt | 162 +++++++++++ .../managers/facade/NexusCommandManager.java | 199 ------------- .../managers/facade/NexusCommandManager.kt | 158 ++++++++++ .../nexus/core/managers/indexes/IndexStore.kt | 52 ++++ .../indexes/defaults/InMemoryIndexStore.kt | 28 ++ .../IndexIdentifierConflictException.kt | 8 + .../managers/records/NexusCommandIndex.java | 5 - .../core/managers/records/NexusMetaIndex.kt | 8 + .../core/reflective/NexusReflectiveCore.java | 16 +- .../NexusExpress.kt} | 49 ++-- .../nexus/express/core/NexusExpressCore.kt | 244 ++++++++++++++++ .../nexus/express/event/NexusExpressEvent.kt | 18 ++ .../event/core/NexusExpressEventCore.kt | 74 +++++ .../NexusExpressEventStatusChange.kt | 10 + .../event/status/NexusExpressEventStatus.kt | 5 + .../express/request/NexusExpressRequest.kt | 9 + .../mihou/nexus/extensions/NexusExtensions.kt | 17 ++ .../command/annotation/IdentifiableAs.kt | 13 + .../command/annotation/NexusAttach.java | 16 - .../command/core/NexusCommandCore.java | 19 +- .../command/core/NexusCommandDispatcher.java | 70 ----- .../command/core/NexusCommandDispatcher.kt | 95 ++++++ .../features/command/facade/NexusCommand.java | 16 +- .../command/facade/NexusCommandEvent.java | 24 +- .../command/facade/NexusMiddlewareEvent.java | 5 +- .../commons/NexusCommonInterceptors.java | 5 - .../core/NexusCommonInterceptorsCore.java | 6 - .../modules/auth/NexusAuthMiddleware.java | 156 ---------- .../core/NexusRatelimiterCore.java | 117 -------- .../ratelimiter/core/NexusRatelimiterCore.kt | 115 ++++++++ .../core/NexusCommandInterceptorCore.java | 2 + .../synchronizer/NexusSynchronizer.java | 148 ---------- .../command/synchronizer/NexusSynchronizer.kt | 141 +++++++++ .../NexusDefaultSynchronizeMethods.java | 12 +- .../command/validation/OptionValidation.kt | 111 +++++++ .../validation/errors/ValidationError.kt | 58 ++++ .../validation/result/ValidationResult.kt | 5 + .../messages/core/NexusMessageCore.java | 17 ++ .../NexusDefaultMessageConfiguration.java | 45 --- .../facade/NexusMessageConfiguration.java | 40 --- .../nexus/sharding/NexusShardingManager.kt | 109 +++++++ src/test/java/CommandGenerationTests.java | 51 ++-- .../conflict/AlsoConflictedPingCommand.java | 13 + .../conflict/ConflictedPingCommand.java | 13 + .../conflict/NotConflictedPingCommand.java | 15 + 71 files changed, 1919 insertions(+), 2299 deletions(-) create mode 100644 build.gradle delete mode 100644 examples/authentication/permissions/PingCommand.java delete mode 100644 examples/authentication/roles/PingCommand.java delete mode 100644 examples/authentication/user/PingCommand.java delete mode 100755 pom.xml delete mode 100755 src/main/java/pw/mihou/nexus/Nexus.java create mode 100644 src/main/java/pw/mihou/nexus/Nexus.kt create mode 100644 src/main/java/pw/mihou/nexus/configuration/NexusConfiguration.kt create mode 100644 src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsConfiguration.kt create mode 100644 src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsMessageConfiguration.kt create mode 100644 src/main/java/pw/mihou/nexus/configuration/modules/NexusExpressConfiguration.kt create mode 100644 src/main/java/pw/mihou/nexus/configuration/modules/NexusGlobalConfiguration.kt delete mode 100755 src/main/java/pw/mihou/nexus/core/NexusCore.java delete mode 100755 src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java delete mode 100644 src/main/java/pw/mihou/nexus/core/configuration/core/NexusConfiguration.java delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/core/NexusEngineXCore.java delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineEvent.java delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineQueuedEvent.java delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/event/core/NexusEngineEventCore.java delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/event/listeners/NexusEngineEventStatusChange.java delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/event/status/NexusEngineEventStatus.java delete mode 100644 src/main/java/pw/mihou/nexus/core/enginex/facade/NexusDiscordShard.java delete mode 100755 src/main/java/pw/mihou/nexus/core/managers/NexusShardManager.java delete mode 100755 src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.java create mode 100755 src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.kt delete mode 100755 src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java create mode 100755 src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.kt create mode 100644 src/main/java/pw/mihou/nexus/core/managers/indexes/IndexStore.kt create mode 100644 src/main/java/pw/mihou/nexus/core/managers/indexes/defaults/InMemoryIndexStore.kt create mode 100644 src/main/java/pw/mihou/nexus/core/managers/indexes/exceptions/IndexIdentifierConflictException.kt delete mode 100644 src/main/java/pw/mihou/nexus/core/managers/records/NexusCommandIndex.java create mode 100644 src/main/java/pw/mihou/nexus/core/managers/records/NexusMetaIndex.kt rename src/main/java/pw/mihou/nexus/{core/enginex/facade/NexusEngineX.java => express/NexusExpress.kt} (62%) create mode 100644 src/main/java/pw/mihou/nexus/express/core/NexusExpressCore.kt create mode 100644 src/main/java/pw/mihou/nexus/express/event/NexusExpressEvent.kt create mode 100644 src/main/java/pw/mihou/nexus/express/event/core/NexusExpressEventCore.kt create mode 100644 src/main/java/pw/mihou/nexus/express/event/listeners/NexusExpressEventStatusChange.kt create mode 100644 src/main/java/pw/mihou/nexus/express/event/status/NexusExpressEventStatus.kt create mode 100644 src/main/java/pw/mihou/nexus/express/request/NexusExpressRequest.kt create mode 100644 src/main/java/pw/mihou/nexus/extensions/NexusExtensions.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/annotation/IdentifiableAs.kt delete mode 100755 src/main/java/pw/mihou/nexus/features/command/annotation/NexusAttach.java delete mode 100755 src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.java create mode 100755 src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.kt delete mode 100644 src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/auth/NexusAuthMiddleware.java delete mode 100755 src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.java create mode 100755 src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.kt delete mode 100644 src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java create mode 100644 src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/validation/OptionValidation.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/validation/errors/ValidationError.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/validation/result/ValidationResult.kt delete mode 100755 src/main/java/pw/mihou/nexus/features/messages/defaults/NexusDefaultMessageConfiguration.java delete mode 100755 src/main/java/pw/mihou/nexus/features/messages/facade/NexusMessageConfiguration.java create mode 100644 src/main/java/pw/mihou/nexus/sharding/NexusShardingManager.kt create mode 100644 src/test/java/commands/conflict/AlsoConflictedPingCommand.java create mode 100644 src/test/java/commands/conflict/ConflictedPingCommand.java create mode 100644 src/test/java/commands/conflict/NotConflictedPingCommand.java 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/build.gradle b/build.gradle new file mode 100644 index 00000000..8ff6c92a --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.7.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.6.0' + implementation 'org.slf4j:slf4j-api:2.0.1' + testImplementation 'org.jetbrains.kotlin:kotlin-test' + compileOnly 'com.google.code.findbugs:jsr305:3.0.2' +} + +test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} \ 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 f7f12883..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.respondNowWith("Pong!"); - } -} \ 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 287bbc6e..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.respondNowWith("Pong!"); - } -} \ 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 184097a7..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.respondNowWith("Pong!"); - } -} \ No newline at end of file diff --git a/pom.xml b/pom.xml deleted file mode 100755 index 94c116da..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.6.0-SNAPSHOT - 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 a44b13ee..00000000 --- a/src/main/java/pw/mihou/nexus/Nexus.java +++ /dev/null @@ -1,161 +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); - -} 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..8bf78111 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/Nexus.kt @@ -0,0 +1,207 @@ +package pw.mihou.nexus + +import org.javacord.api.event.interaction.ButtonClickEvent +import org.javacord.api.event.interaction.SlashCommandCreateEvent +import org.javacord.api.listener.interaction.ButtonClickListener +import org.javacord.api.listener.interaction.SlashCommandCreateListener +import pw.mihou.nexus.configuration.NexusConfiguration +import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter +import pw.mihou.nexus.core.managers.core.NexusCommandManagerCore +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.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.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.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 { + + init { + NexusCommandInterceptor.addRepository(NexusCommonInterceptorsCore()) + } + + /** + * 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 + val logger: NexusLoggingAdapter get() = configuration.global.logger + + /** + * [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/Understanding-Nexus-Express). + */ + @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/Understanding-Nexus-Sharding-Manager). + */ + @JvmStatic + 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/Understanding-Command-Interceptors). + */ + @JvmStatic + val globalAfterwares: Set get() = configuration.global.afterwares + + /** + * [NexusResponderRepository] is an internal repository that is used by the framework to manage responders such as InteractionImmediateResponder and + * so forth to enable more synchronization between the middlewares and the command execution. It is also the reason that we discourage the use of + * the native Javacord responding methods. + */ + @JvmStatic + val responderRepository = NexusResponderRepository() + + /** + * Configures the [NexusConfiguration] in a more Kotlin way. + * @param modifier the modifier to modify the state of the framework. + */ + @JvmStatic + 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 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) } + } + + /** + * 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 (NexusReflectiveCore.command(model) as NexusCommand) + } + + /** + * Adds one or more global middlewares. + * + * You can learn more about middlewares at [Understanding Command Interceptors](https://github.com/ShindouMihou/Nexus/wiki/Understanding-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/Understanding-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 + NexusThreadPool.executorService.submit { 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 + + NexusThreadPool.executorService.submit { + 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.printStackTrace() + } + } + } + +} \ 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..42fea9b5 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/NexusConfiguration.kt @@ -0,0 +1,13 @@ +package pw.mihou.nexus.configuration + +import pw.mihou.nexus.configuration.modules.NexusCommonsInterceptorsConfiguration +import pw.mihou.nexus.configuration.modules.NexusExpressConfiguration +import pw.mihou.nexus.configuration.modules.NexusGlobalConfiguration + +class NexusConfiguration internal constructor() { + + val express = NexusExpressConfiguration() + val global = NexusGlobalConfiguration() + val commonsInterceptors = NexusCommonsInterceptorsConfiguration() + +} \ 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..18994fd6 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsConfiguration.kt @@ -0,0 +1,7 @@ +package pw.mihou.nexus.configuration.modules + +class NexusCommonsInterceptorsConfiguration internal constructor() { + + 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..d2c21c9b --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusCommonsInterceptorsMessageConfiguration.kt @@ -0,0 +1,16 @@ +package pw.mihou.nexus.configuration.modules + +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.messages.facade.NexusMessage + +class NexusCommonsInterceptorsMessageConfiguration internal constructor() { + + @Volatile var ratelimited: (event: NexusCommandEvent, remainingSeconds: Long) -> NexusMessage = { _, remainingSeconds -> + NexusMessage.fromEphemereal( + "**SLOW DOWN***!" + + "\n" + + "You 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..613e9e55 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusExpressConfiguration.kt @@ -0,0 +1,14 @@ +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) + +} \ 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..50989e3f --- /dev/null +++ b/src/main/java/pw/mihou/nexus/configuration/modules/NexusGlobalConfiguration.kt @@ -0,0 +1,35 @@ +package pw.mihou.nexus.configuration.modules + +import pw.mihou.nexus.core.logger.adapters.NexusLoggingAdapter +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 = NexusDefaultLoggingAdapter() +} \ 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 9e6943fd..00000000 --- a/src/main/java/pw/mihou/nexus/core/NexusCore.java +++ /dev/null @@ -1,194 +0,0 @@ -package pw.mihou.nexus.core; - -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.*; - -public class NexusCore implements Nexus { - - private final NexusCommandManagerCore commandManager = new NexusCommandManagerCore(this); - private final 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 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 nexusConfiguration The configuration to use for Nexus. - */ - public NexusCore(NexusMessageConfiguration messageConfiguration, NexusConfiguration nexusConfiguration) { - 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 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/builder/NexusBuilder.java b/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java deleted file mode 100755 index 68410fd8..00000000 --- a/src/main/java/pw/mihou/nexus/core/builder/NexusBuilder.java +++ /dev/null @@ -1,50 +0,0 @@ -package pw.mihou.nexus.core.builder; - -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; - -public class NexusBuilder { - - private NexusMessageConfiguration messageConfiguration; - 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 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, 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 284d8bf3..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/core/NexusEngineXCore.java +++ /dev/null @@ -1,274 +0,0 @@ -package pw.mihou.nexus.core.enginex.core; - -import org.javacord.api.DiscordApi; -import org.javacord.api.entity.server.Server; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.commons.Pair; -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.exceptions.NexusFailedActionException; -import pw.mihou.nexus.core.threadpool.NexusThreadPool; - -import java.time.Duration; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.*; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Consumer; -import java.util.function.Predicate; - -public class NexusEngineXCore implements NexusEngineX { - - private final BlockingQueue globalQueue = new LinkedBlockingQueue<>(); - private final Map> localQueue = new ConcurrentHashMap<>(); - - private final BlockingQueue, NexusEngineEvent>> predicateQueue = new LinkedBlockingQueue<>(); - private final ReentrantLock predicateQueueLock = new ReentrantLock(); - private final ReentrantLock globalProcessingLock = new ReentrantLock(); - 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 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. - */ - private BlockingQueue getLocalQueue(int shard) { - return localQueue.computeIfAbsent(shard, key -> new LinkedBlockingQueue<>()); - } - - /** - * 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) { - NexusThreadPool.executorService.submit(() -> { - 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(); - } - } - - predicateQueueLock.lock(); - try { - while (!predicateQueue.isEmpty()) { - try { - Predicate predicate = predicateQueue.peek().getLeft(); - - if (!predicate.test(shard)) { - continue; - } - - NexusEngineEvent event = Objects.requireNonNull(predicateQueue.poll()).getRight(); - 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(); - } - } - } finally { - predicateQueueLock.unlock(); - } - }); - - NexusThreadPool.executorService.submit(() -> { - globalProcessingLock.lock(); - try { - while (!globalQueue.isEmpty()) { - try { - NexusEngineEvent event = globalQueue.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(); - } - } - } finally { - globalProcessingLock.unlock(); - } - }); - } - - @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(Predicate predicate, NexusEngineQueuedEvent event) { - NexusEngineEventCore engineEvent = new NexusEngineEventCore(event); - DiscordApi $shard = nexus.getShardManager().asStream().filter(predicate).findFirst().orElse(null); - - if ($shard == null) { - Pair, NexusEngineEvent> queuedPair = Pair.of(predicate, engineEvent); - predicateQueue.add(queuedPair); - - Duration expiration = nexus.getConfiguration().timeBeforeExpiringEngineRequests(); - if (!(expiration.isZero() || expiration.isNegative())) { - NexusThreadPool.schedule(() -> { - if (engineEvent.status() == NexusEngineEventStatus.WAITING) { - boolean removeFromQueue = predicateQueue.remove(queuedPair); - 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. " + - "[sacknowledged={}]", - removeFromQueue - ); - engineEvent.expire(); - } - }, expiration.toMillis(), TimeUnit.MILLISECONDS); - } - } else { - engineEvent.process($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 CompletableFuture await(int shard) { - DiscordApi $shard = nexus.getShardManager().getShard(shard); - - if ($shard != null) { - return CompletableFuture.completedFuture($shard); - } - - CompletableFuture future = new CompletableFuture<>(); - failFutureOnExpire(queue(shard, future::complete), future); - - return future; - } - - @Override - public CompletableFuture await(long server) { - DiscordApi $shard = nexus.getShardManager().getShardOf(server).orElse(null); - - if ($shard != null) { - return CompletableFuture.completedFuture($shard.getServerById(server).orElseThrow()); - } - - CompletableFuture future = new CompletableFuture<>(); - failFutureOnExpire( - queue( - (shard) -> shard.getServerById(server).isPresent(), - (shard) -> future.complete(shard.getServerById(server).orElseThrow()) - ), - future - ); - - return future; - } - - @Override - public CompletableFuture awaitAvailable() { - DiscordApi $shard = nexus.getShardManager().asStream().findFirst().orElse(null); - - if ($shard != null) { - return CompletableFuture.completedFuture($shard); - } - - CompletableFuture future = new CompletableFuture<>(); - failFutureOnExpire(queue(future::complete), future); - - return future; - } - - @Override - public void failFutureOnExpire(NexusEngineEvent event, CompletableFuture future) { - event.addStatusChangeListener(($event, oldStatus, newStatus) -> { - if (newStatus == NexusEngineEventStatus.EXPIRED || newStatus == NexusEngineEventStatus.STOPPED) { - future.completeExceptionally( - new NexusFailedActionException("Failed to connect with the shard that was waited to complete this action.") - ); - } - }); - } - - @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 4c135f27..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/NexusEngineQueuedEvent.java +++ /dev/null @@ -1,15 +0,0 @@ -package pw.mihou.nexus.core.enginex.event; - -import org.javacord.api.DiscordApi; - -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. - */ - void onEvent(DiscordApi api); - -} 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 536de48f..00000000 --- a/src/main/java/pw/mihou/nexus/core/enginex/event/core/NexusEngineEventCore.java +++ /dev/null @@ -1,87 +0,0 @@ -package pw.mihou.nexus.core.enginex.event.core; - -import org.javacord.api.DiscordApi; -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.listeners.NexusEngineEventStatusChange; -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.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<>(); - public NexusEngineEventCore(NexusEngineQueuedEvent event) { - this.event = event; - } - - @Override - public synchronized void cancel() { - if (status() == NexusEngineEventStatus.WAITING) { - changeStatus(NexusEngineEventStatus.STOPPED); - } - } - - /** - * Expires this event which will cancel any actions that will be attempted, this will - * also trigger the {@link NexusEngineEventCore#changeStatus(NexusEngineEventStatus)} method which will invoke - * all listeners listening to a status change. - */ - public synchronized 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 -> NexusThreadPool.executorService.submit(() -> - listener.onStatusChange(this, oldStatus, newStatus))); - } - - /** - * Proceeds to process the event if it is still available to process. - * - * @param api The shard that will be processing the event. - */ - public synchronized void process(DiscordApi api) { - if (status.get() == NexusEngineEventStatus.STOPPED || status.get() == NexusEngineEventStatus.EXPIRED) { - return; - } - - changeStatus(NexusEngineEventStatus.PROCESSING); - NexusThreadPool.executorService.submit(() -> { - try { - event.onEvent(api); - } catch (Exception exception) { - NexusCore.logger.error("An uncaught exception was caught inside a Nexus Engine Event."); - exception.printStackTrace(); - } finally { - 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/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/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 2e01311f..00000000 --- a/src/main/java/pw/mihou/nexus/core/managers/NexusShardManager.java +++ /dev/null @@ -1,135 +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(); - } - - /** - * Calculates which shard the given server belongs to, given a total number of shards. - *

- * This uses the formula ((serverId >> 22) % totalShards) which is the specified formula for - * calculating the shard of a "guild" by Discord. - * - * @param serverId The id of the server to calculate. - * @param totalShards The total number of shards as per formula. - * @return The shard which the given server should belong to, given a total number of shards. - */ - public int shardOf(long serverId, int totalShards) { - return (int) ((serverId >> 22) % totalShards); - } - - /** - * 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 c703bf4b..00000000 --- a/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.java +++ /dev/null @@ -1,171 +0,0 @@ -package pw.mihou.nexus.core.managers.core; - -import org.javacord.api.entity.server.Server; -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.core.managers.records.NexusCommandIndex; -import pw.mihou.nexus.features.command.core.NexusCommandCore; -import pw.mihou.nexus.features.command.facade.NexusCommand; - -import java.util.*; - -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, long server) { - return getCommands().stream() - .filter(nexusCommand -> - nexusCommand.getName().equalsIgnoreCase(name) && nexusCommand.getServerIds().contains(server) - ) - .findFirst(); - } - - @Override - public List export() { - return indexes.entrySet() - .stream() - .map(entry -> new NexusCommandIndex(commands.get(entry.getValue()), entry.getKey())) - .toList(); - } - - /** - * 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.getEngineX().awaitAvailable().thenAcceptAsync(shard -> { - Set slashCommands = shard.getGlobalSlashCommands().join(); - - // Clearing the entire in-memory index mapping to make sure that we don't have any outdated indexes. - indexes.clear(); - - for (SlashCommand slashCommand : slashCommands) { - index(slashCommand); - } - - Set servers = new HashSet<>(); - for (NexusCommand serverCommand : getServerCommands()) { - servers.addAll(serverCommand.getServerIds()); - } - - for (long server : servers) { - if (server == 0L) continue; - - Set $slashCommands = nexusCore.getEngineX().await(server) - .thenComposeAsync(Server::getSlashCommands).join(); - - for (SlashCommand slashCommand : $slashCommands) { - index(slashCommand); - } - } - - logger.info("All global and server slash commands are now indexed. It took {} milliseconds to complete indexing.", System.currentTimeMillis() - start); - }).exceptionally(ExceptionLogger.get()).join(); - } - - @Override - public void index(NexusCommand command, long snowflake) { - indexes.put(snowflake, ((NexusCommandCore) command).uuid); - } - - @Override - public void index(ApplicationCommand applicationCommand) { - long serverId = applicationCommand.getServerId().orElse(-1L); - - if (serverId == -1L) { - for (NexusCommand command : commands.values()) { - if (!command.getServerIds().isEmpty()) continue; - if (!command.getName().equalsIgnoreCase(applicationCommand.getName())) continue; - - index(command, applicationCommand.getApplicationId()); - break; - } - return; - } - - for (NexusCommand command : commands.values()) { - if (command.getServerIds().contains(serverId)) continue; - if (!command.getName().equalsIgnoreCase(applicationCommand.getName())) continue; - - index(command, applicationCommand.getApplicationId()); - break; - } - } - -} 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..d044683b --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/core/NexusCommandManagerCore.kt @@ -0,0 +1,162 @@ +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.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 java.util.* + +class NexusCommandManagerCore internal constructor() : NexusCommandManager { + private val commandsDelegate: MutableMap = HashMap() + override val commands: Collection + get() = commandsDelegate.values + + private val indexStore: IndexStore = InMemoryIndexStore() + + override fun add(command: NexusCommand): NexusCommandManager { + if (commandsDelegate[command.uuid] != null) + throw IndexIdentifierConflictException(command) + + commandsDelegate[(command as NexusCommandCore).uuid] = command + return this + } + + override operator fun get(applicationId: Long): NexusCommand? = indexStore[applicationId]?.take() + 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 export(): List { + return indexStore.all() + } + + private fun toIndex(applicationCommandId: Long, command: NexusCommand, server: Long?): NexusMetaIndex { + return NexusMetaIndex(command = command.uuid, applicationCommandId = applicationCommandId, server = server) + } + + /** + * This performs indexing based on the data analyzed from the [SlashCommandCreateEvent] and + * returns the results for post-processing. + * + * @param event The event to handle. + */ + fun acceptEvent(event: SlashCommandCreateEvent): NexusCommand? { + val interaction = event.slashCommandInteraction + + if (get(interaction.commandId) != null) { + return get(interaction.commandId) + } + + 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) + } + + /** + * An internal method that is used by [acceptEvent] as a short-hand to index the command if the command is + * present and also return the command at the same time. + * + * @param interaction the interaction received from the event. + * @param command the command that was identified. + * @param server the server that the command is associated or was called. + * + * @return the command. + */ + private fun index(interaction: SlashCommandInteraction, command: NexusCommand?, server: Long?): NexusCommand? { + return command?.apply { indexStore.add(toIndex(interaction.commandId, this, server)) } + } + + override fun index() { + Nexus.configuration.global.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...") + + val start = System.currentTimeMillis() + Nexus.express + .awaitAvailable() + .thenAcceptAsync { shard -> + val slashCommands: Set = shard.globalSlashCommands.join() + + // Clearing the entire index store to make sure that we don't have any outdated indexes. + indexStore.clear() + for (slashCommand in slashCommands) { + index(slashCommand) + } + + val servers: MutableSet = HashSet() + for (serverCommand in serverCommands) { + servers.addAll(serverCommand.serverIds) + } + + for (server in servers) { + if (server == 0L) continue + + val slashCommandSet: Set = Nexus.express + .await(server) + .thenComposeAsync { it.slashCommands } + .join() + + for (slashCommand in slashCommandSet) { + index(slashCommand) + } + } + + Nexus.configuration.global.logger.info("All global and server slash commands are now indexed." + + " It took ${System.currentTimeMillis() - start} milliseconds to complete indexing.") + } + .exceptionally(ExceptionLogger.get()) + .join() + } + + override fun index(command: NexusCommand, snowflake: Long, server: Long?) { + indexStore.add(toIndex(applicationCommandId = snowflake, command = command, server = server)) + } + + override fun index(applicationCommand: ApplicationCommand) { + val serverId = applicationCommand.serverId.orElse(-1L) + + if (serverId == -1L) { + for (command in commands) { + if (command.serverIds.isNotEmpty()) continue + if (!command.name.equals(command.name, ignoreCase = true)) continue + + index(command, applicationCommand.applicationId, applicationCommand.serverId.orElse(null)) + break + } + return + } + + for (command in commands) { + if (command.serverIds.contains(serverId)) continue + if (!command.name.equals(command.name, ignoreCase = true)) continue + + index(command, applicationCommand.applicationId, applicationCommand.serverId.orElse(null)) + 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 c05afb06..00000000 --- a/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.java +++ /dev/null @@ -1,199 +0,0 @@ -package pw.mihou.nexus.core.managers.facade; - -import org.javacord.api.interaction.ApplicationCommand; -import org.javacord.api.interaction.SlashCommand; -import pw.mihou.nexus.Nexus; -import pw.mihou.nexus.core.managers.records.NexusCommandIndex; -import pw.mihou.nexus.features.command.facade.NexusCommand; - -import java.util.*; -import java.util.function.Function; - -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(); - - /** - * 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. - */ - default Set getGlobalCommands() { - Set commands = new HashSet<>(); - for (NexusCommand command : getCommands()) { - if (command.isServerCommand()) continue; - commands.add(command); - } - - return commands; - } - - /** - * 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. - */ - default Set getServerCommands() { - Set commands = new HashSet<>(); - for (NexusCommand command : getCommands()) { - if (!command.isServerCommand()) continue; - commands.add(command); - } - - return commands; - } - - /** - * 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 - * {@link List#contains(Object)} predicate over its server ids. - * - * @param server The server to find all associated commands of. - * @return All associated commands of the given server. - */ - default Set getCommandsAssociatedWith(long server) { - Set commands = new HashSet<>(); - for (NexusCommand command : getCommands()) { - if (!command.getServerIds().contains(server)) continue; - commands.add(command); - } - - return commands; - } - - /** - * 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. - */ - default Optional getCommandByName(String name) { - return getCommands().stream().filter(nexusCommand -> nexusCommand.getName().equalsIgnoreCase(name)).findFirst(); - } - - /** - * 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); - - /** - * 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 in-memory indexes that the command manager has. - */ - List export(); - - - /** - * Creates a new fresh in-memory index map by using the {@link NexusCommandManager#index()} method before creating - * an export of the given indexes with the {@link NexusCommandManager#export()} command. - * - * @return A snapshot of the in-memory indexes that the command manager has. - */ - default List indexThenExport() { - index(); - return export(); - } - - /** - * This indexes all the commands whether it'd be global or server commands to increase - * performance and precision of slash commands. - */ - void index(); - - /** - * Creates an in-memory 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. - */ - void index(NexusCommand command, long snowflake); - - /** - * Massively creates an in-memory index mapping for all the given command using the indexer reducer - * provided below. - *

- * This is a completely synchronous operation and could cause major blocking on the application if you use - * it daringly. - * - * @param indexer The indexer reducer to collectively find the index of the commands. - */ - default void index(Function indexer) { - for (NexusCommand command : getCommands()) { - index(command, indexer.apply(command)); - } - } - - /** - * Creates an in-memory 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. - */ - default void index(Set applicationCommandList) { - for (ApplicationCommand applicationCommand : applicationCommandList) { - index(applicationCommand); - } - } - - /** - * Creates an in-memory 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 command The command to index. - */ - void index(ApplicationCommand command); - -} 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..2ccd416e --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/facade/NexusCommandManager.kt @@ -0,0 +1,158 @@ +package pw.mihou.nexus.core.managers.facade + +import org.javacord.api.interaction.ApplicationCommand +import pw.mihou.nexus.core.managers.records.NexusMetaIndex +import pw.mihou.nexus.features.command.facade.NexusCommand +import java.util.* +import java.util.function.Function + +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 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 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 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 + } + + fun add(command: NexusCommand): NexusCommandManager + operator fun get(applicationId: Long): NexusCommand? + operator fun get(uuid: String): NexusCommand? + operator fun get(name: String, server: Long? = null): NexusCommand? + + /** + * 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 in-memory indexes that the command manager has. + */ + fun export(): List + + /** + * Creates a new fresh in-memory index map by using the [NexusCommandManager.index] method before creating + * an export of the given indexes with the [NexusCommandManager.export] command. + * + * @return A snapshot of the in-memory indexes that the command manager has. + */ + fun indexThenExport(): List { + index() + return export() + } + + /** + * 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 in-memory 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: NexusCommand, snowflake: Long, server: Long?) + + /** + * Massively creates an in-memory index mapping for all the given command using the indexer reducer + * provided below. + *



+ * This is a completely synchronous operation and could cause major blocking on the application if you use + * it daringly. + * + * @param indexer The indexer reducer to collectively find the index of the commands. + */ + fun index(indexer: Function) { + for (command in commands) { + val metaIndex = indexer.apply(command) + index(command, metaIndex.applicationCommandId, metaIndex.server) + } + } + + /** + * Creates an in-memory 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) { + for (applicationCommand in applicationCommandList) { + index(applicationCommand) + } + } + + /** + * Creates an in-memory 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) +} \ 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..949fea41 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/indexes/IndexStore.kt @@ -0,0 +1,52 @@ +package pw.mihou.nexus.core.managers.indexes + +import pw.mihou.nexus.core.managers.records.NexusMetaIndex +import pw.mihou.nexus.features.command.annotation.IdentifiableAs + +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] from the store or an in-memory cache by the Nexus unique identifier. + * @param command the unique identifier of the command, tends to be of the same value always unless the name changed or + * the unique identifier was changed via the [IdentifiableAs] annotation. + * @return the [NexusMetaIndex] that was caught otherwise none. + */ + operator fun get(command: String): NexusMetaIndex? + + /** + * 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..52afddbe --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/indexes/defaults/InMemoryIndexStore.kt @@ -0,0 +1,28 @@ +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 operator fun get(command: String): NexusMetaIndex? = indexes.values.firstOrNull { it.command == command } + + 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..4052d9df --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/indexes/exceptions/IndexIdentifierConflictException.kt @@ -0,0 +1,8 @@ +package pw.mihou.nexus.core.managers.indexes.exceptions + +import pw.mihou.nexus.features.command.facade.NexusCommand + +class IndexIdentifierConflictException(command: NexusCommand): + RuntimeException("An index-identifier conflict was identified between commands with the name ${command.name}. We do not " + + "recommend having commands with the same name that have the same unique identifier, please change one of the commands' identifier " + + "by using the @IdentifiableAs annotation. (https://github.com/ShindouMihou/Nexus/wiki/Index-Identifier-Conflict)") \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/core/managers/records/NexusCommandIndex.java b/src/main/java/pw/mihou/nexus/core/managers/records/NexusCommandIndex.java deleted file mode 100644 index f629930d..00000000 --- a/src/main/java/pw/mihou/nexus/core/managers/records/NexusCommandIndex.java +++ /dev/null @@ -1,5 +0,0 @@ -package pw.mihou.nexus.core.managers.records; - -import pw.mihou.nexus.features.command.facade.NexusCommand; - -public record NexusCommandIndex(NexusCommand command, long applicationCommandId) {} 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..a74d9375 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/core/managers/records/NexusMetaIndex.kt @@ -0,0 +1,8 @@ +package pw.mihou.nexus.core.managers.records + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.features.command.facade.NexusCommand + +data class NexusMetaIndex(val command: String, val applicationCommandId: Long, val server: Long?) { + fun take(): NexusCommand? = Nexus.commandManager[command] +} diff --git a/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectiveCore.java b/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectiveCore.java index c178aac6..e4492e97 100755 --- a/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectiveCore.java +++ b/src/main/java/pw/mihou/nexus/core/reflective/NexusReflectiveCore.java @@ -1,10 +1,9 @@ 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.annotation.IdentifiableAs; import pw.mihou.nexus.features.command.core.NexusCommandCore; import java.util.*; @@ -13,7 +12,7 @@ public class NexusReflectiveCore { private static final Class REFERENCE_CLASS = NexusCommandCore.class; - public static NexusCommandCore command(Object object, NexusCore core) { + public static NexusCommandCore command(Object object) { NexusCommandCore reference = new NexusCommandCore(); NexusReflectiveVariableFacade facade = new NexusReflectiveVariableCore(object, reference); @@ -28,6 +27,13 @@ public static NexusCommandCore command(Object object, NexusCore core) { } } + String temporaryUuid = facade.getWithType("name", String.class).orElseThrow(); + + if (object.getClass().isAnnotationPresent(IdentifiableAs.class)) { + temporaryUuid = object.getClass().getAnnotation(IdentifiableAs.class).key(); + } + + final String uuid = temporaryUuid; Arrays.stream(reference.getClass().getDeclaredFields()) .forEach(field -> { @@ -37,9 +43,7 @@ public static NexusCommandCore command(Object object, NexusCore core) { 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); + field.set(reference, uuid); } else if (field.isAnnotationPresent(Stronghold.class)){ field.set(reference, facade.getSharedFields()); } else { diff --git a/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java b/src/main/java/pw/mihou/nexus/express/NexusExpress.kt similarity index 62% rename from src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java rename to src/main/java/pw/mihou/nexus/express/NexusExpress.kt index ed5b60b8..7015cbd0 100644 --- a/src/main/java/pw/mihou/nexus/core/enginex/facade/NexusEngineX.java +++ b/src/main/java/pw/mihou/nexus/express/NexusExpress.kt @@ -1,15 +1,14 @@ -package pw.mihou.nexus.core.enginex.facade; +package pw.mihou.nexus.express -import org.javacord.api.DiscordApi; -import org.javacord.api.entity.server.Server; -import pw.mihou.nexus.core.enginex.event.NexusEngineEvent; -import pw.mihou.nexus.core.enginex.event.NexusEngineQueuedEvent; +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 -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -import java.util.function.Predicate; - -public interface NexusEngineX { +interface NexusExpress { /** * Queues an event to be executed by the specific shard that @@ -19,7 +18,7 @@ public interface NexusEngineX { * @param event The event to execute for this shard. * @return The controller and status viewer for the event. */ - NexusEngineEvent queue(int shard, NexusEngineQueuedEvent event); + fun queue(shard: Int, event: NexusExpressRequest): NexusExpressEvent /** * Queues an event to be executed by the shard that matches the given predicate. @@ -28,7 +27,7 @@ public interface NexusEngineX { * @param event The event to execute for this shard. * @return The controller and status viewer for the event. */ - NexusEngineEvent queue(Predicate predicate, NexusEngineQueuedEvent event); + fun queue(predicate: Predicate, event: NexusExpressRequest): NexusExpressEvent /** * Queues an event to be executed by any specific shard that is available @@ -37,45 +36,45 @@ public interface NexusEngineX { * @param event The event to execute by a shard. * @return The controller and status viewer for the event. */ - NexusEngineEvent queue(NexusEngineQueuedEvent 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 {@link DiscordApi} instance of the given shard. + * @return The [DiscordApi] instance of the given shard. */ - CompletableFuture await(int 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 {@link DiscordApi} instance of the given shard. + * @return The [DiscordApi] instance of the given shard. */ - CompletableFuture await(long server); + 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 {@link DiscordApi} available to take any action. + * @return The [DiscordApi] available to take any action. */ - CompletableFuture awaitAvailable(); + 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 EngineX with CompletableFutures as this provides an extra kill-switch + *



+ * 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. - */ - void failFutureOnExpire(NexusEngineEvent event, CompletableFuture future); + */ + fun failFutureOnExpire(event: NexusExpressEvent, future: CompletableFuture) /** * Broadcasts the event for all shards to execute, this doesn't wait for any shards @@ -83,6 +82,6 @@ public interface NexusEngineX { * * @param event The event to broadcast to all shards. */ - void broadcast(Consumer event); + 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..d9dae90b --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/core/NexusExpressCore.kt @@ -0,0 +1,244 @@ +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.core.threadpool.NexusThreadPool +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) { + NexusThreadPool.executorService.submit { + val local = localQueue(shard.currentShard) + while (!local.isEmpty()) { + try { + val event = local.poll() + + if (event != null) { + NexusThreadPool.executorService.submit { + (event as NexusExpressEventCore).process(shard) + } + } + } catch (exception: Exception) { + Nexus.configuration.global.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() + + NexusThreadPool.executorService.submit { + (event as NexusExpressEventCore).process(shard) + } + } catch (exception: Exception) { + Nexus.configuration.global.logger.error("An uncaught exception was caught from Nexus Express Way.", exception) + } + } + } + } + + NexusThreadPool.executorService.submit { + globalQueueProcessingLock.withLock { + try { + val event = globalQueue.poll() + + if (event != null) { + NexusThreadPool.executorService.submit { + (event as NexusExpressEventCore).process(shard) + } + } + } catch (exception: Exception) { + Nexus.configuration.global.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) { + NexusThreadPool.schedule({ + expressEvent.`do` { + if (status() == NexusExpressEventStatus.WAITING) { + val removed = localQueue(shard).remove(this) + Nexus.configuration.global.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() + } + } + }, maximumTimeout.toMillis(), TimeUnit.MILLISECONDS) + } + } else { + NexusThreadPool.executorService.submit { 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) { + NexusThreadPool.schedule({ + expressEvent.`do` { + if (status() == NexusExpressEventStatus.WAITING) { + val removed = predicateQueue.remove(pair) + Nexus.configuration.global.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() + } + } + }, maximumTimeout.toMillis(), TimeUnit.MILLISECONDS) + } + } else { + NexusThreadPool.executorService.submit { 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) { + NexusThreadPool.schedule({ + expressEvent.`do` { + if (status() == NexusExpressEventStatus.WAITING) { + val removed = globalQueue.remove(this) + Nexus.configuration.global.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() + } + } + }, maximumTimeout.toMillis(), TimeUnit.MILLISECONDS) + } + } else { + NexusThreadPool.executorService.submit { 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 -> + NexusThreadPool.executorService.submit { + try { + event.accept(shard) + } catch (exception: Exception) { + Nexus.configuration.global.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..e7426a87 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/express/event/NexusExpressEvent.kt @@ -0,0 +1,18 @@ +package pw.mihou.nexus.express.event + +import pw.mihou.nexus.express.event.listeners.NexusExpressEventStatusChange +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() + fun status(): NexusExpressEventStatus + fun addStatusChangeListener(event: NexusExpressEventStatusChange) + +} \ 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..9d7cf6bc --- /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.WAITING) + } + } + } + + 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/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/extensions/NexusExtensions.kt b/src/main/java/pw/mihou/nexus/extensions/NexusExtensions.kt new file mode 100644 index 00000000..aeea1a2d --- /dev/null +++ b/src/main/java/pw/mihou/nexus/extensions/NexusExtensions.kt @@ -0,0 +1,17 @@ +package pw.mihou.nexus.extensions + +import org.javacord.api.entity.message.embed.EmbedBuilder + +/** + * Builds an [EmbedBuilder] like a regular [EmbedBuilder] but with a fancier + * developer experience. + * + * @param modifier the modifier to modify the [EmbedBuilder]. + * @return the [EmbedBuilder] that is being used. + */ +fun embed(modifier: EmbedBuilder.() -> Unit): EmbedBuilder { + val embed = EmbedBuilder() + modifier(embed) + + return embed +} \ 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 642aacc8..27ecd2fe 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,8 +3,8 @@ 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 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; @@ -42,6 +42,9 @@ public class NexusCommandCore implements NexusCommand { @WithDefault public List options = Collections.emptyList(); + @WithDefault + public List> validators = Collections.emptyList(); + @WithDefault public Duration cooldown = Duration.ofSeconds(5); @@ -50,28 +53,24 @@ public class NexusCommandCore implements NexusCommand { @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 List defaultEnabledForPermissions = Collections.emptyList(); - - @InjectNexusCore - public NexusCore core; - @InjectReferenceClass public NexusHandler handler; + @Override + public String getUuid() { + return uuid; + } + @Override public String getName() { return name; 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..795f475f --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandDispatcher.kt @@ -0,0 +1,95 @@ +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.globalAfterwares +import pw.mihou.nexus.Nexus.globalMiddlewares +import pw.mihou.nexus.Nexus.logger +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.command.validation.OptionValidation +import pw.mihou.nexus.features.command.validation.result.ValidationResult +import pw.mihou.nexus.features.messages.core.NexusMessageCore + +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: NexusCommandEvent = NexusCommandEventCore(event, instance) + + val middlewares: MutableList = ArrayList() + middlewares.addAll(globalMiddlewares) + middlewares.addAll(instance.middlewares) + + val afterwares: MutableList = ArrayList() + afterwares.addAll(globalAfterwares) + afterwares.addAll(instance.afterwares) + + val middlewareGate = NexusCommandInterceptorCore.interceptWithMany(middlewares, nexusEvent) as NexusMiddlewareGateCore? + + if (middlewareGate != null) { + val middlewareResponse = middlewareGate.response() as NexusMessageCore? + middlewareResponse?.convertTo(nexusEvent.respondNow())?.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" + ) + } + + val validationResult = validate(validations = instance.validators, nexusEvent) + + if (validationResult != null) { + if (validationResult.error == null) { + return + } + + val responder = nexusEvent.respondNow() + validationResult.error.convertTo(responder) + + responder.respond().exceptionally(ExceptionLogger.get()) + return + } + + NexusThreadPool.executorService.submit { + 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.printStackTrace() + } + } + + NexusThreadPool.executorService.submit { NexusCommandInterceptorCore.interceptWithMany(afterwares, nexusEvent) } + } +} + +private fun validate(validations: List>, event: NexusCommandEvent): ValidationResult? { + var blocker: ValidationResult? = null + + for (validation in validations) { + val result = validation.validate(event) + + if (!result.hasPassed) { + blocker = result + break + } + } + + return blocker +} \ 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 64197b07..3df165a8 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,6 +1,5 @@ package pw.mihou.nexus.features.command.facade; -import org.javacord.api.DiscordApi; import org.javacord.api.entity.permission.PermissionType; import org.javacord.api.interaction.*; import pw.mihou.nexus.commons.Pair; @@ -14,6 +13,17 @@ public interface NexusCommand { long PLACEHOLDER_SERVER_ID = 0L; + /** + * 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. * @@ -78,7 +88,7 @@ default boolean isServerCommand() { * 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, DiscordApi)} method + * the {@link pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer#batchUpdate(long)} method * after using this method. * * @param serverIds The snowflakes of the servers to disassociate this command from. @@ -91,7 +101,7 @@ default boolean isServerCommand() { * 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, DiscordApi)} method + * the {@link pw.mihou.nexus.features.command.synchronizer.NexusSynchronizer#batchUpdate(long)} method * after using this method. * * @param serverIds The snowflakes of the servers to disassociate this command from. 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 index f8e0bad0..f829e043 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.java @@ -12,8 +12,7 @@ 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 pw.mihou.nexus.sharding.NexusShardingManager; import java.util.Map; import java.util.NoSuchElementException; @@ -112,16 +111,6 @@ default Optional getServerTextChannel() { */ 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. * @@ -132,13 +121,12 @@ default DiscordApi getApi() { } /** - * Gets the {@link NexusShardManager} that is associated with the - * {@link Nexus} instance. + * Gets the {@link NexusShardingManager} that is associated with the {@link Nexus} instance. * * @return The Nexus Shard Manager associated with this Nexus instance. */ - default NexusShardManager getShardManager() { - return getNexus().getShardManager(); + default NexusShardingManager getShardManager() { + return Nexus.getSharding(); } /** @@ -211,7 +199,7 @@ default InteractionImmediateResponseBuilder respondNowAsEphemeral() { * @return The {@link InteractionOriginalResponseUpdater}. */ default CompletableFuture respondLater() { - return getNexus() + return Nexus .getResponderRepository() .get(getBaseEvent().getInteraction()); } @@ -223,7 +211,7 @@ default CompletableFuture respondLater() { * @return The {@link InteractionOriginalResponseUpdater} with the ephemeral flag attached. */ default CompletableFuture respondLaterAsEphemeral() { - return getNexus() + return Nexus .getResponderRepository() .getEphemereal(getBaseEvent().getInteraction()); } 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 index d0a4fcc3..0fc8fec7 100644 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.java +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusMiddlewareEvent.java @@ -1,5 +1,6 @@ package pw.mihou.nexus.features.command.facade; +import pw.mihou.nexus.Nexus; import pw.mihou.nexus.features.command.interceptors.repositories.NexusMiddlewareGateRepository; import pw.mihou.nexus.features.messages.facade.NexusMessage; @@ -18,7 +19,7 @@ public interface NexusMiddlewareEvent extends NexusCommandEvent { */ default CompletableFuture askDelayedResponse() { return CompletableFuture.allOf( - getNexus().getResponderRepository().peek(getBaseEvent().getInteraction()) + Nexus.getResponderRepository().peek(getBaseEvent().getInteraction()) ); } @@ -30,7 +31,7 @@ default CompletableFuture askDelayedResponse() { */ default CompletableFuture askDelayedResponseAsEphemeral() { return CompletableFuture.allOf( - getNexus().getResponderRepository().peekEphemeral(getBaseEvent().getInteraction()) + Nexus.getResponderRepository().peekEphemeral(getBaseEvent().getInteraction()) ); } 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 index ac2ac935..6a661bc7 100755 --- 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 @@ -7,11 +7,6 @@ */ 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/core/NexusCommonInterceptorsCore.java b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/core/NexusCommonInterceptorsCore.java index f431bff6..18cc50a9 100644 --- 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 @@ -1,6 +1,5 @@ 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; @@ -10,11 +9,6 @@ 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/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..2b4b1d51 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/interceptors/commons/modules/ratelimiter/core/NexusRatelimiterCore.kt @@ -0,0 +1,115 @@ +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.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +internal class NexusRatelimiterCore : 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.cooldown.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 + ) + 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 index 771aa591..c507ca70 100755 --- 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 @@ -5,6 +5,7 @@ import pw.mihou.nexus.features.command.interceptors.facades.*; import pw.mihou.nexus.features.command.interceptors.repositories.NexusMiddlewareGateRepository; +import javax.annotation.Nullable; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -86,6 +87,7 @@ private static void interceptWith(String name, NexusCommandEvent event) { * @param event The event to execute. * @return Are all interceptors agreeing with the command execution? */ + @Nullable 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()); 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 385599d1..00000000 --- a/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.java +++ /dev/null @@ -1,148 +0,0 @@ -package pw.mihou.nexus.features.command.synchronizer; - -import org.javacord.api.entity.server.Server; -import org.javacord.api.interaction.SlashCommandBuilder; -import pw.mihou.nexus.Nexus; -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.stream.Collectors; - -public record NexusSynchronizer(Nexus nexus) { - - public static volatile NexusSynchronizeMethods SYNCHRONIZE_METHODS = new NexusDefaultSynchronizeMethods(); - - /** - * Deletes a command from 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 the completion of this task. - */ - public CompletableFuture delete(NexusCommand command, int totalShards, long... serverIds) { - Map> serverMappedFutures = new HashMap<>(); - - for (long serverId : serverIds) { - if (serverMappedFutures.containsKey(serverId)) continue; - - CompletableFuture future = nexus.getEngineX() - .await(nexus.getShardManager().shardOf(serverId, totalShards)) - .thenCompose(shard -> SYNCHRONIZE_METHODS.deleteForServer(shard, command, serverId)); - - serverMappedFutures.put(serverId, future); - } - - 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 serverId the given guild snowflake to perform updates upon. - * @return A future to indicate progress of this task. - */ - public CompletableFuture batchUpdate(long serverId) { - NexusCommandManager manager = nexus.getCommandManager(); - - Set serverCommands = manager.getCommandsAssociatedWith(serverId).stream() - .map(NexusCommand::asSlashCommand) - .collect(Collectors.toSet()); - - return nexus.getEngineX().awaitAvailable() - .thenCompose(shard -> SYNCHRONIZE_METHODS.bulkOverwriteServer(shard, serverCommands, serverId)) - .thenAccept(manager::index); - } - - /** - * 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<>(); - - for (long serverId : serverIds) { - if (serverMappedFutures.containsKey(serverId)) continue; - - CompletableFuture future = nexus.getEngineX() - .await(nexus.getShardManager().shardOf(serverId, totalShards)) - .thenCompose(shard -> SYNCHRONIZE_METHODS.updateForServer(shard, command, serverId)) - .thenAccept($command -> nexus.getCommandManager().index(command, $command.getApplicationId())); - - serverMappedFutures.put(serverId, future); - } - - 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(Set)} and - * {@link org.javacord.api.DiscordApi#bulkOverwriteServerApplicationCommands(Server, Set)}. 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. - */ - public CompletableFuture synchronize() { - NexusCommandManager manager = nexus.getCommandManager(); - - Set serverCommands = manager.getServerCommands(); - Set globalCommands = manager.getGlobalCommands().stream().map(NexusCommand::asSlashCommand) - .collect(Collectors.toSet()); - - NexusEngineX engineX = nexus.getEngineX(); - CompletableFuture globalFuture = engineX.awaitAvailable() - .thenCompose(shard -> SYNCHRONIZE_METHODS.bulkOverwriteGlobal(shard, globalCommands)) - .thenAccept(manager::index); - - if (serverCommands.isEmpty()) { - return globalFuture; - } - - Map> serverMappedCommands = new HashMap<>(); - Map> serverMappedFutures = new HashMap<>(); - - for (NexusCommand $command : serverCommands) { - for (long serverId : $command.getServerIds()) { - if (serverId == NexusCommand.PLACEHOLDER_SERVER_ID) continue; - - if (!serverMappedCommands.containsKey(serverId)) { - serverMappedCommands.put(serverId, new HashSet<>()); - } - - serverMappedCommands.get(serverId).add($command.asSlashCommand()); - } - } - - serverMappedCommands.forEach((server, builders) -> { - if (server == NexusCommand.PLACEHOLDER_SERVER_ID) return; - - CompletableFuture future = engineX.awaitAvailable() - .thenCompose(shard -> SYNCHRONIZE_METHODS.bulkOverwriteServer(shard, builders, server)) - .thenAccept(manager::index); - - serverMappedFutures.put(server, future); - }); - - 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..fd8f429d --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/synchronizer/NexusSynchronizer.kt @@ -0,0 +1,141 @@ +package pw.mihou.nexus.features.command.synchronizer + +import org.javacord.api.interaction.SlashCommandBuilder +import pw.mihou.nexus.Nexus +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.concurrent.CompletableFuture + +class NexusSynchronizer internal constructor() { + + @Volatile var methods: NexusSynchronizeMethods = NexusDefaultSynchronizeMethods() + + /** + * 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): CompletableFuture { + val serverMappedFutures: MutableMap> = HashMap() + + for (serverId in servers) { + if (serverMappedFutures.containsKey(serverId)) continue + + val future: CompletableFuture = Nexus.express + .await(Nexus.sharding.calculate(serverId, totalShards)) + .thenCompose { shard -> methods.deleteForServer(shard, command, serverId) } + + serverMappedFutures[serverId] = future + } + + return CompletableFuture.allOf(*serverMappedFutures.values.toTypedArray>()) + } + + /** + * 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() }.toHashSet() + + 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): CompletableFuture { + val serverMappedFutures = mutableMapOf>() + + for (server in servers) { + if (serverMappedFutures.containsKey(server)) continue + + val future: CompletableFuture = Nexus.express + .await(Nexus.sharding.calculate(server, totalShards)) + .thenCompose { shard -> methods.updateForServer(shard, command, server) } + .thenAccept { `$command` -> Nexus.commandManager.index(command, `$command`.applicationId, `$command`.serverId.orElse(null)) } + + serverMappedFutures[server] = future + } + + return CompletableFuture.allOf(*serverMappedFutures.values.toTypedArray>()) + } + + /** + * 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(): CompletableFuture { + val manager: NexusCommandManager = Nexus.commandManager + + val serverCommands = manager.serverCommands + val globalCommands = manager.globalCommands.map { command -> command.asSlashCommand() }.toHashSet() + + val globalFuture: CompletableFuture = Nexus.express + .awaitAvailable() + .thenCompose { shard -> methods.bulkOverwriteGlobal(shard, globalCommands) } + .thenAccept(manager::index) + + if (serverCommands.isEmpty()) { + return globalFuture + } + + val serverMappedCommands: MutableMap> = mutableMapOf() + val serverMappedFutures: MutableMap> = mutableMapOf() + + for (`$command` in serverCommands) { + for (serverId in `$command`.serverIds) { + if (serverId == NexusCommand.PLACEHOLDER_SERVER_ID) continue + if (!serverMappedCommands.containsKey(serverId)) { + serverMappedCommands[serverId] = HashSet() + } + + serverMappedCommands[serverId]!!.add(`$command`.asSlashCommand()) + } + } + + serverMappedCommands.forEach { (server, builders) -> + if (server == NexusCommand.PLACEHOLDER_SERVER_ID) return@forEach + + val future: CompletableFuture = Nexus.express + .awaitAvailable() + .thenCompose { shard -> methods.bulkOverwriteServer(shard, builders, server) } + .thenAccept(manager::index) + + serverMappedFutures[server] = future + } + + val futures: List> = mutableListOf>().let { + it.add(globalFuture) + it.addAll(serverMappedFutures.values) + + it.toList() + } + + return CompletableFuture.allOf(*futures.toTypedArray>()) + } + +} \ No newline at end of file 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 index 68999f1c..6910e38e 100644 --- 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 @@ -5,7 +5,7 @@ import org.javacord.api.interaction.ApplicationCommand; import org.javacord.api.interaction.SlashCommand; import org.javacord.api.interaction.SlashCommandBuilder; -import pw.mihou.nexus.core.NexusCore; +import pw.mihou.nexus.Nexus; import pw.mihou.nexus.core.exceptions.NexusFailedActionException; import pw.mihou.nexus.features.command.facade.NexusCommand; import pw.mihou.nexus.features.command.synchronizer.overwrites.NexusSynchronizeMethods; @@ -18,7 +18,7 @@ public class NexusDefaultSynchronizeMethods implements NexusSynchronizeMethods { @Override public CompletableFuture> bulkOverwriteGlobal(DiscordApi shard, Set slashCommands) { return shard.bulkOverwriteGlobalApplicationCommands(slashCommands).thenApply(applicationCommands -> { - NexusCore.logger.debug("All global commands have been synchronized. [size={}]", applicationCommands.size()); + Nexus.getLogger().debug("All global commands have been synchronized. [size={}]", applicationCommands.size()); return applicationCommands; }); } @@ -26,7 +26,7 @@ public CompletableFuture> bulkOverwriteGlobal(DiscordApi @Override public CompletableFuture> bulkOverwriteServer(DiscordApi shard, Set slashCommands, long serverId) { return shard.bulkOverwriteServerApplicationCommands(serverId, slashCommands).thenApply(applicationCommands -> { - NexusCore.logger.debug("All commands with relation for server {} have been synchronized.", serverId); + Nexus.getLogger().debug("All commands with relation for server {} have been synchronized.", serverId); return applicationCommands; }); } @@ -50,7 +50,7 @@ public CompletableFuture deleteForServer(DiscordApi shard, NexusCommand co return slashCommand.deleteForServer(serverId) .thenAccept((unused) -> - NexusCore.logger.debug("The command ({}) has been removed from the server ({}).", + Nexus.getLogger().debug("The command ({}) has been removed from the server ({}).", command.getName(), serverId) ); }); @@ -76,7 +76,7 @@ public CompletableFuture updateForServer(DiscordApi shard, N return command.asSlashCommandUpdater(slashCommand.getId()) .updateForServer(shard, serverId) .thenApply(resultingCommand -> { - NexusCore.logger.debug("The command ({}) has been updated for the server ({}).", command.getName(), serverId); + Nexus.getLogger().debug("The command ({}) has been updated for the server ({}).", command.getName(), serverId); return resultingCommand; }); }); @@ -85,7 +85,7 @@ public CompletableFuture updateForServer(DiscordApi shard, N @Override public CompletableFuture createForServer(DiscordApi shard, NexusCommand command, long serverId) { return command.asSlashCommand().createForServer(shard, serverId).thenApply(slashCommand -> { - NexusCore.logger.debug("The command ({}) has been created for the server ({}).", command.getName(), + Nexus.getLogger().debug("The command ({}) has been created for the server ({}).", command.getName(), serverId); return slashCommand; }); 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..56133f51 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/validation/OptionValidation.kt @@ -0,0 +1,111 @@ +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.* + +class OptionValidation