From 9f5d8dbdc9432b802dd902310da9017f3d47db6e Mon Sep 17 00:00:00 2001 From: Ashley Date: Fri, 25 Aug 2023 21:24:33 -0500 Subject: [PATCH 1/4] initial commit --- assets/META-INF/defaults/watamebot.ini | 24 +- assets/META-INF/integrated.ini | 5 + src/module-info.java | 1 + .../database/AConnectionProvider.java | 12 +- .../foxgenesis/database/DatabaseManager.java | 6 +- .../providers/MySQLConnectionProvider.java | 6 +- .../executor/PrefixedThreadFactory.java | 12 +- .../property/ImmutableProperty.java | 4 +- src/net/foxgenesis/property/Property.java | 8 +- src/net/foxgenesis/property/PropertyInfo.java | 2 +- .../foxgenesis/property/PropertyResolver.java | 8 +- .../UnmodifiablePropertyException.java | 2 +- .../property/lck/ImmutableLCKProperty.java | 2 +- .../foxgenesis/property/lck/LCKProperty.java | 3 +- src/net/foxgenesis/util/ArrayUtils.java | 2 +- src/net/foxgenesis/util/MethodTimer.java | 50 ++- .../{watame => util}/PushBullet.java | 4 +- .../foxgenesis/util/SingleInstanceUtil.java | 22 +- src/net/foxgenesis/util/StreamUtils.java | 4 + .../util/function/QuadFunction.java | 71 ++-- .../foxgenesis/util/function/TriConsumer.java | 9 +- .../foxgenesis/util/resource/ConfigType.java | 4 +- .../util/resource/ResourceUtils.java | 6 +- src/net/foxgenesis/watame/Context.java | 12 +- src/net/foxgenesis/watame/ExitCode.java | 34 +- src/net/foxgenesis/watame/Main.java | 41 +-- src/net/foxgenesis/watame/Settings.java | 84 +++++ .../foxgenesis/watame/SettingsBuilder.java | 37 ++ src/net/foxgenesis/watame/State.java | 33 ++ src/net/foxgenesis/watame/WatameBot.java | 258 ++++++++----- .../foxgenesis/watame/WatameBotSettings.java | 127 ------- .../watame/command/ConfigCommand.java | 6 +- .../watame/command/IntegratedCommands.java | 33 +- .../foxgenesis/watame/plugin/EventStore.java | 59 ++- .../foxgenesis/watame/plugin/IEventStore.java | 1 + src/net/foxgenesis/watame/plugin/Plugin.java | 289 +++++---------- .../watame/plugin/PluginHandler.java | 344 +++++++++++++----- .../watame/plugin/PluginInformation.java | 31 ++ .../watame/plugin/ServiceStartup.java | 124 +++++++ .../watame/plugin/SeverePluginException.java | 20 +- .../plugin/{ => require}/CommandProvider.java | 6 +- .../{ => require}/PluginConfiguration.java | 13 +- .../watame/plugin/require/RequiresCache.java | 10 + .../plugin/require/RequiresIntents.java | 10 + .../require/RequiresMemberCachePolicy.java | 8 + .../property/PluginPropertyMapping.java | 8 +- .../property/impl/CachedPluginProperty.java | 4 +- .../impl/PluginPropertyProviderImpl.java | 11 +- 48 files changed, 1151 insertions(+), 719 deletions(-) create mode 100644 assets/META-INF/integrated.ini rename src/net/foxgenesis/{watame => util}/PushBullet.java (95%) create mode 100644 src/net/foxgenesis/watame/Settings.java create mode 100644 src/net/foxgenesis/watame/SettingsBuilder.java create mode 100644 src/net/foxgenesis/watame/State.java delete mode 100644 src/net/foxgenesis/watame/WatameBotSettings.java create mode 100644 src/net/foxgenesis/watame/plugin/PluginInformation.java create mode 100644 src/net/foxgenesis/watame/plugin/ServiceStartup.java rename src/net/foxgenesis/watame/plugin/{ => require}/CommandProvider.java (81%) rename src/net/foxgenesis/watame/plugin/{ => require}/PluginConfiguration.java (92%) create mode 100644 src/net/foxgenesis/watame/plugin/require/RequiresCache.java create mode 100644 src/net/foxgenesis/watame/plugin/require/RequiresIntents.java create mode 100644 src/net/foxgenesis/watame/plugin/require/RequiresMemberCachePolicy.java diff --git a/assets/META-INF/defaults/watamebot.ini b/assets/META-INF/defaults/watamebot.ini index 88f2c8c..f5509f1 100644 --- a/assets/META-INF/defaults/watamebot.ini +++ b/assets/META-INF/defaults/watamebot.ini @@ -1,25 +1,25 @@ -[Token] -# Path to login token file -tokenFile = config/token.txt +[WatameBot.Token] +# Relative path to discord bot token file +tokenFile = token.txt -[WatameBot.Status] +[Startup.Status] +# Startup activity status startup = Initalizing... +# Activity status after startup online = https://github.com/FoxGenesis/Watamebot -[SingleInstance] +[Startup.SingleInstance] # Allow only one instance to run at a time enabled = true # Amount of times to attempt to obtain instance lock retries = 5 -[Runtime] -# -ansiConsole = true +[PushBullet] +# Pushbullet token +token = [Logging] # Logging level (info | debug | trace) logLevel = info - -[PushBullet] -# Pushbullet token -token = \ No newline at end of file +# Display ANSI colors in console +ansiConsole = true \ No newline at end of file diff --git a/assets/META-INF/integrated.ini b/assets/META-INF/integrated.ini new file mode 100644 index 0000000..2e41428 --- /dev/null +++ b/assets/META-INF/integrated.ini @@ -0,0 +1,5 @@ +[IntegratedPlugin] +# Enable/Disable the '/options configuration' command +enableOptionsCommand = false +# Enable/Disable the '/ping' command +enablePingCommand = true \ No newline at end of file diff --git a/src/module-info.java b/src/module-info.java index 20efcc4..e0e0b39 100644 --- a/src/module-info.java +++ b/src/module-info.java @@ -32,6 +32,7 @@ exports net.foxgenesis.log; exports net.foxgenesis.watame; exports net.foxgenesis.watame.plugin; + exports net.foxgenesis.watame.plugin.require; exports net.foxgenesis.util; exports net.foxgenesis.util.resource; exports net.foxgenesis.util.function; diff --git a/src/net/foxgenesis/database/AConnectionProvider.java b/src/net/foxgenesis/database/AConnectionProvider.java index 28c27a0..7001a68 100644 --- a/src/net/foxgenesis/database/AConnectionProvider.java +++ b/src/net/foxgenesis/database/AConnectionProvider.java @@ -19,7 +19,7 @@ public abstract class AConnectionProvider implements AutoCloseable { private final String name; private final String database; - public AConnectionProvider( @NotNull String name, @NotNull Properties properties) { + public AConnectionProvider(@NotNull String name, @NotNull Properties properties) { this.name = Objects.requireNonNull(name); logger = LoggerFactory.getLogger(name); this.properties = Objects.requireNonNull(properties); @@ -43,16 +43,17 @@ public AConnectionProvider( @NotNull String name, @NotNull Properties properties @NotNull protected abstract Connection openConnection() throws SQLException; - + @NotNull protected Optional openAutoClosedConnection(@NotNull ConnectionConsumer consumer) throws SQLException { - try(Connection conn = openConnection()) { + try (Connection conn = openConnection()) { return Optional.ofNullable(consumer.applyConnection(conn)); } } @NotNull - protected Optional openAutoClosedConnection(@NotNull ConnectionConsumer consumer, Consumer error) { + protected Optional openAutoClosedConnection(@NotNull ConnectionConsumer consumer, + Consumer error) { try (Connection conn = openConnection()) { return Optional.ofNullable(consumer.applyConnection(conn)); } catch (Exception e) { @@ -78,6 +79,7 @@ public void close() throws Exception { @FunctionalInterface public interface ConnectionConsumer { - @SuppressWarnings("exports") U applyConnection(@NotNull Connection connection) throws SQLException; + @SuppressWarnings("exports") + U applyConnection(@NotNull Connection connection) throws SQLException; } } diff --git a/src/net/foxgenesis/database/DatabaseManager.java b/src/net/foxgenesis/database/DatabaseManager.java index 3d676ed..fa6ab99 100644 --- a/src/net/foxgenesis/database/DatabaseManager.java +++ b/src/net/foxgenesis/database/DatabaseManager.java @@ -64,7 +64,7 @@ public DatabaseManager(@NotNull String name) { public boolean register(@NotNull Plugin plugin, @NotNull AbstractDatabase database) throws IOException { Objects.requireNonNull(database); - if (!plugin.needsDatabase) + if (!plugin.getInfo().requiresDatabase()) throw new IllegalArgumentException("Plugin does not declare that it needs database connection!"); if (isDatabaseRegistered(database)) @@ -95,13 +95,13 @@ public boolean register(@NotNull Plugin plugin, @NotNull AbstractDatabase databa * been unloaded. {@code false} otherwise */ public boolean unload(Plugin owner) { - if (!owner.needsDatabase) + if (!owner.getInfo().requiresDatabase()) return false; if (databases.containsKey(owner)) { synchronized (databases) { if (databases.containsKey(owner)) { - logger.info("Unloading databases from {}", owner.friendlyName); + logger.info("Unloading databases from {}", owner.getInfo().getDisplayName()); Set databases = this.databases.remove(owner); for (AbstractDatabase database : databases) diff --git a/src/net/foxgenesis/database/providers/MySQLConnectionProvider.java b/src/net/foxgenesis/database/providers/MySQLConnectionProvider.java index 0a589dd..1df4cdd 100644 --- a/src/net/foxgenesis/database/providers/MySQLConnectionProvider.java +++ b/src/net/foxgenesis/database/providers/MySQLConnectionProvider.java @@ -30,11 +30,13 @@ public MySQLConnectionProvider(Properties properties) throws ConnectException { try { source = new HikariDataSource(new HikariConfig(properties)); - } catch(Exception e) { + } catch (Exception e) { throw new ConnectException("Failed to connect to database"); } } @Override - protected Connection openConnection() throws SQLException { return source.getConnection(); } + protected Connection openConnection() throws SQLException { + return source.getConnection(); + } } diff --git a/src/net/foxgenesis/executor/PrefixedThreadFactory.java b/src/net/foxgenesis/executor/PrefixedThreadFactory.java index 4f19b50..468868e 100644 --- a/src/net/foxgenesis/executor/PrefixedThreadFactory.java +++ b/src/net/foxgenesis/executor/PrefixedThreadFactory.java @@ -14,7 +14,9 @@ public class PrefixedThreadFactory implements ThreadFactory { private final boolean daemon; - public PrefixedThreadFactory(@NotNull String prefix) { this(prefix, true); } + public PrefixedThreadFactory(@NotNull String prefix) { + this(prefix, true); + } @SuppressWarnings("null") public PrefixedThreadFactory(@NotNull String prefix, boolean daemon) { @@ -30,7 +32,11 @@ public Thread newThread(Runnable r) { } @NotNull - public String getPrefix() { return prefix; } + public String getPrefix() { + return prefix; + } - public boolean isDaemon() { return daemon; } + public boolean isDaemon() { + return daemon; + } } diff --git a/src/net/foxgenesis/property/ImmutableProperty.java b/src/net/foxgenesis/property/ImmutableProperty.java index 3c8e106..a7564fe 100644 --- a/src/net/foxgenesis/property/ImmutableProperty.java +++ b/src/net/foxgenesis/property/ImmutableProperty.java @@ -22,10 +22,10 @@ public interface ImmutableProperty { /** * Get the current value of this property if present. Otherwise get the current * value of the specified {@code fallback}. - * + * * @param lookup - property lookup * @param fallback - fallback property - * + * * @return Returns a {@link Optional} {@link PropertyMapping} containing the raw * data retrieved */ diff --git a/src/net/foxgenesis/property/Property.java b/src/net/foxgenesis/property/Property.java index 8b6ad0f..97fb0ea 100644 --- a/src/net/foxgenesis/property/Property.java +++ b/src/net/foxgenesis/property/Property.java @@ -63,7 +63,7 @@ default boolean set(@NotNull L lookup, byte[] data, boolean isUserInput) { /** * Check if this property can be modified by user input. - * + * * @return Returns {@code true} if the user is allowed to modify this property. * {@code false} otherwise */ @@ -76,9 +76,9 @@ default boolean isUserModifiable() { * {@code user} is {@code true} and {@link #isUserModifiable()} is * {@code false}, this method will throw a {@link UnmodifiablePropertyException} * to stop further execution. - * + * * @param user - if this call is from the user - * + * * @throws UnmodifiablePropertyException Thrown if {@code user} is {@code true} * and {@link #isUserModifiable()} is * {@code false} @@ -120,7 +120,7 @@ static byte[] serialize(@NotNull PropertyInfo info, @NotNull Serializable obj) { return ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(i).array(); if (obj instanceof Long f) return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(f).array(); - else if (obj instanceof Double d) + if (obj instanceof Double d) return ByteBuffer.allocate(Double.SIZE / Byte.SIZE).putDouble(d).array(); else if (obj instanceof Float f) return ByteBuffer.allocate(Float.SIZE / Byte.SIZE).putFloat(f).array(); diff --git a/src/net/foxgenesis/property/PropertyInfo.java b/src/net/foxgenesis/property/PropertyInfo.java index 2c4d320..fb39d4f 100644 --- a/src/net/foxgenesis/property/PropertyInfo.java +++ b/src/net/foxgenesis/property/PropertyInfo.java @@ -7,7 +7,7 @@ public record PropertyInfo(int id, String category, String name, boolean modifia public String getDisplayString() { return DISPLAY_FORMAT.formatted(id, type, category, name); } - + @Override public String toString() { return getDisplayString(); diff --git a/src/net/foxgenesis/property/PropertyResolver.java b/src/net/foxgenesis/property/PropertyResolver.java index 8d9253f..e41569a 100644 --- a/src/net/foxgenesis/property/PropertyResolver.java +++ b/src/net/foxgenesis/property/PropertyResolver.java @@ -48,10 +48,10 @@ PropertyInfo createPropertyInfo(@NotNull C category, @NotNull K key, boolean mod /** * Remove a property inside the configuration. - * + * * @param category - category of the property * @param key - property key - * + * * @return Returns {@code true} if the property was deleted. {@code false} * otherwise */ @@ -59,9 +59,9 @@ PropertyInfo createPropertyInfo(@NotNull C category, @NotNull K key, boolean mod /** * Remove a property inside the configuration. - * + * * @param info - property information - * + * * @return Returns {@code true} if the property was deleted. {@code false} * otherwise */ diff --git a/src/net/foxgenesis/property/UnmodifiablePropertyException.java b/src/net/foxgenesis/property/UnmodifiablePropertyException.java index 8a8f950..23a1e91 100644 --- a/src/net/foxgenesis/property/UnmodifiablePropertyException.java +++ b/src/net/foxgenesis/property/UnmodifiablePropertyException.java @@ -11,7 +11,7 @@ public UnmodifiablePropertyException(String msg, Throwable t) { public UnmodifiablePropertyException(String msg) { super(msg); } - + public UnmodifiablePropertyException(Throwable t) { super(t); } diff --git a/src/net/foxgenesis/property/lck/ImmutableLCKProperty.java b/src/net/foxgenesis/property/lck/ImmutableLCKProperty.java index d274bc6..5251839 100644 --- a/src/net/foxgenesis/property/lck/ImmutableLCKProperty.java +++ b/src/net/foxgenesis/property/lck/ImmutableLCKProperty.java @@ -3,6 +3,6 @@ import net.foxgenesis.property.ImmutableProperty; import net.foxgenesis.property.lck.impl.BlobMapping; -public interface ImmutableLCKProperty extends ImmutableProperty{ +public interface ImmutableLCKProperty extends ImmutableProperty { } diff --git a/src/net/foxgenesis/property/lck/LCKProperty.java b/src/net/foxgenesis/property/lck/LCKProperty.java index d2168cf..d8311d5 100644 --- a/src/net/foxgenesis/property/lck/LCKProperty.java +++ b/src/net/foxgenesis/property/lck/LCKProperty.java @@ -3,5 +3,4 @@ import net.foxgenesis.property.Property; import net.foxgenesis.property.lck.impl.BlobMapping; -public interface LCKProperty extends Property { -} +public interface LCKProperty extends Property {} diff --git a/src/net/foxgenesis/util/ArrayUtils.java b/src/net/foxgenesis/util/ArrayUtils.java index 533d383..c25f621 100644 --- a/src/net/foxgenesis/util/ArrayUtils.java +++ b/src/net/foxgenesis/util/ArrayUtils.java @@ -12,7 +12,7 @@ public static String commaSeparated(T[] arr) { @SuppressWarnings("rawtypes") public static String commaSeparated(int[] arr) { - return arr == null ? null : sep((Stream)Arrays.stream(arr)); + return arr == null ? null : sep((Stream) Arrays.stream(arr)); } private static String sep(Stream s) { diff --git a/src/net/foxgenesis/util/MethodTimer.java b/src/net/foxgenesis/util/MethodTimer.java index febe720..b6276ab 100644 --- a/src/net/foxgenesis/util/MethodTimer.java +++ b/src/net/foxgenesis/util/MethodTimer.java @@ -15,12 +15,15 @@ public static double runNano(Runnable r) { r.run(); return System.nanoTime() - n; } + /** * Time how long it takes to execute {@link Runnable} {@code r}. Time is * calculated in nano seconds and returned as milliseconds. * * @param r - {@link Runnable} to time + * * @return elapsed time in milliseconds + * * @see #run(Runnable, int) */ public static double run(Runnable r) { @@ -33,7 +36,9 @@ public static double run(Runnable r) { * * @param r - {@link Runnable} to time * @param n - Amount of times to run + * * @return average elapsed time milliseconds + * * @see #run(Runnable) */ public static double run(Runnable r, int n) { @@ -43,8 +48,8 @@ public static double run(Runnable r, int n) { s[i] = runNano(r); System.out.printf("==== %,.2fms ====\n", s[i] / 1_000_000D); } - return Arrays.stream(s).reduce((a,b) -> (a + b) / 2D).orElseThrow() / 1_000_000D; - //return (sum / n) / 1_000_000D; + return Arrays.stream(s).reduce((a, b) -> (a + b) / 2D).orElseThrow() / 1_000_000D; + // return (sum / n) / 1_000_000D; } /** @@ -61,11 +66,15 @@ public static double run(Runnable r, int n) { * * * @param r - {@link Runnable} to time + * * @return formatted string with two decimal places + * * @see #runFormatMS(Runnable, int) * @see #runFormatMS(Runnable, int, int) */ - public static String runFormatMS(Runnable r) { return runFormatMS(r, 2); } + public static String runFormatMS(Runnable r) { + return runFormatMS(r, 2); + } /** * Time how long it takes to execute {@link Runnable} {@code r}. Time is @@ -82,7 +91,9 @@ public static double run(Runnable r, int n) { * * @param r - {@link Runnable} to time * @param decimals - Amount of decimal places to format + * * @return formatted string with {@code decimals} decimal places + * * @see #runFormatMS(Runnable) * @see #runFormatMS(Runnable, int, int) */ @@ -106,7 +117,9 @@ public static String runFormatMS(Runnable r, int decimals) { * @param r - {@link Runnable} to time * @param n - Amount of times to execute {@code r} * @param decimals - Amount of decimal places to format + * * @return formatted string with {@code decimals} decimal places + * * @see #runFormatMS(Runnable) * @see #runFormatMS(Runnable, int, int) */ @@ -128,11 +141,15 @@ public static String runFormatMS(Runnable r, int n, int decimals) { * * * @param r - {@link Runnable} to time + * * @return formatted string with two decimal places + * * @see #runFormatMS(Runnable, int) * @see #runFormatMS(Runnable, int, int) */ - public static String runFormatSec(Runnable r) { return runFormatSec(r, 2); } + public static String runFormatSec(Runnable r) { + return runFormatSec(r, 2); + } /** * Time how long it takes to execute {@link Runnable} {@code r}. Time is @@ -149,7 +166,9 @@ public static String runFormatMS(Runnable r, int n, int decimals) { * * @param r - {@link Runnable} to time * @param decimals - Amount of decimal places to format + * * @return formatted string with {@code decimals} decimal places + * * @see #runFormatMS(Runnable) * @see #runFormatMS(Runnable, int, int) */ @@ -173,7 +192,9 @@ public static String runFormatSec(Runnable r, int decimals) { * @param r - {@link Runnable} to time * @param n - Amount of times to execute {@code r} * @param decimals - Amount of decimal places to format + * * @return formatted string with {@code decimals} decimal places + * * @see #runFormatMS(Runnable) * @see #runFormatMS(Runnable, int, int) */ @@ -185,35 +206,47 @@ public static String runFormatSec(Runnable r, int n, int decimals) { * NEED_JAVADOC * * @param time + * * @return formatted string */ - public static String formatToMilli(long time) { return formatToMilli(time, 2); } + public static String formatToMilli(long time) { + return formatToMilli(time, 2); + } /** * NEED_JAVADOC * * @param time * @param decimals + * * @return formatted string */ - public static String formatToMilli(long time, int decimals) { return format(time, decimals, 1_000_000D); } + public static String formatToMilli(long time, int decimals) { + return format(time, decimals, 1_000_000D); + } /** * NEED_JAVADOC * * @param time + * * @return formatted string */ - public static String formatToSeconds(long time) { return formatToSeconds(time, 2); } + public static String formatToSeconds(long time) { + return formatToSeconds(time, 2); + } /** * NEED_JAVADOC * * @param time * @param decimals + * * @return formatted string */ - public static String formatToSeconds(long time, int decimals) { return format(time, decimals, 1_000_000_000D); } + public static String formatToSeconds(long time, int decimals) { + return format(time, decimals, 1_000_000_000D); + } /** * NEED_JAVADOC @@ -221,6 +254,7 @@ public static String runFormatSec(Runnable r, int n, int decimals) { * @param time * @param decimals * @param div + * * @return formatted string */ public static String format(long time, int decimals, double div) { diff --git a/src/net/foxgenesis/watame/PushBullet.java b/src/net/foxgenesis/util/PushBullet.java similarity index 95% rename from src/net/foxgenesis/watame/PushBullet.java rename to src/net/foxgenesis/util/PushBullet.java index 75c1013..3707dcd 100644 --- a/src/net/foxgenesis/watame/PushBullet.java +++ b/src/net/foxgenesis/util/PushBullet.java @@ -1,4 +1,4 @@ -package net.foxgenesis.watame; +package net.foxgenesis.util; import java.io.IOException; import java.util.HashMap; @@ -32,7 +32,7 @@ public PushBullet(String token) { this(new OkHttpClient().newBuilder().build(), token); } - public PushBullet(@NotNull OkHttpClient client, String token) { + public PushBullet(@SuppressWarnings("exports") @NotNull OkHttpClient client, String token) { this.client = Objects.requireNonNull(client); this.token = token; } diff --git a/src/net/foxgenesis/util/SingleInstanceUtil.java b/src/net/foxgenesis/util/SingleInstanceUtil.java index ead36c2..803dee2 100644 --- a/src/net/foxgenesis/util/SingleInstanceUtil.java +++ b/src/net/foxgenesis/util/SingleInstanceUtil.java @@ -33,11 +33,15 @@ public final class SingleInstanceUtil { * * * @param amt - Amount of retries before failing to obtain lock + * * @throws SingleInstanceLockException Thrown if try count equals or exceeds * {@code amt} + * * @see #waitAndGetLock(File, int) */ - public static void waitAndGetLock(int amt) { waitAndGetLock(new File(".pid"), amt, 10_000); } + public static void waitAndGetLock(int amt) { + waitAndGetLock(new File(".pid"), amt, 10_000); + } /** * Attempt to obtain lock on {@code PID} with {@code amt} retries and 10 second @@ -53,11 +57,15 @@ public final class SingleInstanceUtil { * * @param file - location of PID file should attempt to lock * @param amt - Amount of retries before failing to obtain lock + * * @throws SingleInstanceLockException Thrown if try count equals or exceeds * {@code amt} + * * @see #waitAndGetLock(File, int, int) */ - public static void waitAndGetLock(File file, int amt) { waitAndGetLock(file, amt, 10_000); } + public static void waitAndGetLock(File file, int amt) { + waitAndGetLock(file, amt, 10_000); + } /** * Attempt to obtain lock on PID file {@code pid}, {@code amt} times with @@ -66,8 +74,10 @@ public final class SingleInstanceUtil { * @param file - location of PID file * @param amt - Amount of retries before failing to obtain lock * @param delay - Delay between retries + * * @throws SingleInstanceLockException Thrown if try count equals or exceeds * {@code amt} + * * @see #waitAndGetLock(File, int) */ public static void waitAndGetLock(File file, int amt, int delay) { @@ -192,10 +202,14 @@ private void ensureFile() { * @return Returns {@code true} if the PID file channel is not {@code null} and * is open */ - public boolean isValid() { return channel != null && channel.isOpen(); } + public boolean isValid() { + return channel != null && channel.isOpen(); + } @Override - public void close() throws IOException { channel.close(); } + public void close() throws IOException { + channel.close(); + } } /** diff --git a/src/net/foxgenesis/util/StreamUtils.java b/src/net/foxgenesis/util/StreamUtils.java index 8b212d6..fb79d91 100644 --- a/src/net/foxgenesis/util/StreamUtils.java +++ b/src/net/foxgenesis/util/StreamUtils.java @@ -15,9 +15,11 @@ public final class StreamUtils { * * @param - stream type * @param col - collection to get stream for + * * @return Returns a {@link Stream} that will be possibly parallel if * {@link Collection#size()} is greater than 1024. Otherwise returns a * sequential stream + * * @see #getEffectiveStream(Stream) */ public static Stream getEffectiveStream(Collection col) { @@ -29,9 +31,11 @@ public static Stream getEffectiveStream(Collection col) { * * @param - stream type * @param stream - stream to use + * * @return Returns a {@link Stream} that will be possibly parallel if * {@link Stream#count()} is greater than 1024. Otherwise returns the * input stream + * * @see #getEffectiveStream(Collection) */ public static Stream getEffectiveStream(Stream stream) { diff --git a/src/net/foxgenesis/util/function/QuadFunction.java b/src/net/foxgenesis/util/function/QuadFunction.java index 9f95f36..3abb4fd 100644 --- a/src/net/foxgenesis/util/function/QuadFunction.java +++ b/src/net/foxgenesis/util/function/QuadFunction.java @@ -4,50 +4,55 @@ import java.util.function.Function; /** - * Represents a function that accepts two arguments and produces a result. - * This is the two-arity specialization of {@link Function}. + * Represents a function that accepts two arguments and produces a result. This + * is the two-arity specialization of {@link Function}. * - *

This is a functional interface - * whose functional method is {@link #apply(Object, Object, Object, Object)}. + *

+ * This is a functional interface whose + * functional method is {@link #apply(Object, Object, Object, Object)}. * * @param the type of the first argument to the function * @param the type of the second argument to the function - * @param - * @param + * @param + * @param * @param the type of the result of the function * * @see Function + * * @since 1.8 */ @FunctionalInterface public interface QuadFunction { - /** - * Applies this function to the given arguments. - * - * @param t the first function argument - * @param u the second function argument - * @param v - * @param w - * @return the function result - */ - R apply(T t, U u, V v, W w); + /** + * Applies this function to the given arguments. + * + * @param t the first function argument + * @param u the second function argument + * @param v + * @param w + * + * @return the function result + */ + R apply(T t, U u, V v, W w); - /** - * Returns a composed function that first applies this function to - * its input, and then applies the {@code after} function to the result. - * If evaluation of either function throws an exception, it is relayed to - * the caller of the composed function. - * - * @param the type of output of the {@code after} function, and of the - * composed function - * @param after the function to apply after this function is applied - * @return a composed function that first applies this function and then - * applies the {@code after} function - * @throws NullPointerException if after is null - */ - default QuadFunction andThen(Function after) { - Objects.requireNonNull(after); - return (T t, U u, V v, W w) -> after.apply(apply(t, u, v, w)); - } + /** + * Returns a composed function that first applies this function to its input, + * and then applies the {@code after} function to the result. If evaluation of + * either function throws an exception, it is relayed to the caller of the + * composed function. + * + * @param the type of output of the {@code after} function, and of the + * composed function + * @param after the function to apply after this function is applied + * + * @return a composed function that first applies this function and then applies + * the {@code after} function + * + * @throws NullPointerException if after is null + */ + default QuadFunction andThen(Function after) { + Objects.requireNonNull(after); + return (T t, U u, V v, W w) -> after.apply(apply(t, u, v, w)); + } } diff --git a/src/net/foxgenesis/util/function/TriConsumer.java b/src/net/foxgenesis/util/function/TriConsumer.java index 079b184..9672ccc 100644 --- a/src/net/foxgenesis/util/function/TriConsumer.java +++ b/src/net/foxgenesis/util/function/TriConsumer.java @@ -4,9 +4,9 @@ /** * Represents an operation that accepts three input arguments and returns no - * result. This is the three-arity specialization of Consumer. Unlike - * most other functional interfaces, {@code TriConsumer} is expected to operate - * via side-effects. + * result. This is the three-arity specialization of Consumer. Unlike most other + * functional interfaces, {@code TriConsumer} is expected to operate via + * side-effects. * *

* This is a functional interface whose @@ -17,6 +17,7 @@ * @param the type of the third argument to the operation * * @since 1.8 + * * @author Seth Steinberg */ @FunctionalInterface @@ -39,8 +40,10 @@ public interface TriConsumer { * {@code after} operation will not be performed. * * @param after the operation to perform after this operation + * * @return a composed {@code TriConsumer} that performs in sequence this * operation followed by the {@code after} operation + * * @throws NullPointerException if {@code after} is null */ default TriConsumer andThen(TriConsumer after) { diff --git a/src/net/foxgenesis/util/resource/ConfigType.java b/src/net/foxgenesis/util/resource/ConfigType.java index 839e317..43fdc9c 100644 --- a/src/net/foxgenesis/util/resource/ConfigType.java +++ b/src/net/foxgenesis/util/resource/ConfigType.java @@ -2,9 +2,9 @@ /** * Enumeration of all supported configuration file types. - * + * * @author Ashley - * + * * @since 1.1.1 */ public enum ConfigType { diff --git a/src/net/foxgenesis/util/resource/ResourceUtils.java b/src/net/foxgenesis/util/resource/ResourceUtils.java index 7c464ce..9e5fc8a 100644 --- a/src/net/foxgenesis/util/resource/ResourceUtils.java +++ b/src/net/foxgenesis/util/resource/ResourceUtils.java @@ -118,7 +118,7 @@ public static Properties getProperties(Path path, ModuleResource defaults) throw *

* This method is effectively equivalent to: *

- * + * *
 	 * Configuration configuration = switch (type) {
 	 * 	case PROPERTIES -> loadProperties(defaults, directory, output);
@@ -128,7 +128,7 @@ public static Properties getProperties(Path path, ModuleResource defaults) throw
 	 * 	default -> throw new IllegalArgumentException("Unknown type: " + type);
 	 * };
 	 * 
- * + * * @param type - configuration type * @param defaults - resource containing the configuration defaults * @param directory - the directory containing the configuration file @@ -139,7 +139,7 @@ public static Properties getProperties(Path path, ModuleResource defaults) throw * @throws IOException If an I/O error occurs * @throws ConfigurationException If an error occurs * @throws SecurityException Thrown if the specified file is not readable - * + * * @see #loadProperties(ModuleResource, Path, String) * @see #loadINI(ModuleResource, Path, String) * @see #loadJSON(ModuleResource, Path, String) diff --git a/src/net/foxgenesis/watame/Context.java b/src/net/foxgenesis/watame/Context.java index 22b4b93..d6b7523 100644 --- a/src/net/foxgenesis/watame/Context.java +++ b/src/net/foxgenesis/watame/Context.java @@ -8,7 +8,6 @@ import java.util.function.BiConsumer; import net.foxgenesis.database.DatabaseManager; -import net.foxgenesis.watame.WatameBot.State; import net.foxgenesis.watame.plugin.EventStore; import org.jetbrains.annotations.NotNull; @@ -22,17 +21,14 @@ public class Context implements Closeable { private static final Logger logger = LoggerFactory.getLogger(Context.class); - private final WatameBot bot; - private final EventStore eventStore; private final ExecutorService executor; private final BiConsumer pbConsumer; - public Context(@NotNull WatameBot bot, @NotNull JDABuilder builder, @Nullable ExecutorService executor, + public Context(@NotNull JDABuilder builder, @Nullable ExecutorService executor, @NotNull BiConsumer pbConsumer) { - this.bot = Objects.requireNonNull(bot); this.executor = Objects.requireNonNullElse(executor, ForkJoinPool.commonPool()); this.pbConsumer = Objects.requireNonNull(pbConsumer); eventStore = new EventStore(builder); @@ -40,7 +36,7 @@ public Context(@NotNull WatameBot bot, @NotNull JDABuilder builder, @Nullable Ex @NotNull public DatabaseManager getDatabaseManager() { - return (DatabaseManager) bot.getDatabaseManager(); + return (DatabaseManager) WatameBot.getDatabaseManager(); } @NotNull @@ -59,12 +55,12 @@ public void pushNotification(String title, String message) { @Nullable public JDA getJDA() { - return bot.getJDA(); + return WatameBot.getJDA(); } @NotNull public State getState() { - return bot.getState(); + return WatameBot.getState(); } void onJDABuilder(JDA jda) { diff --git a/src/net/foxgenesis/watame/ExitCode.java b/src/net/foxgenesis/watame/ExitCode.java index 945b40c..d664e62 100644 --- a/src/net/foxgenesis/watame/ExitCode.java +++ b/src/net/foxgenesis/watame/ExitCode.java @@ -29,14 +29,18 @@ public enum ExitCode { * * @param statusCode - exit code number */ - ExitCode(int statusCode) { this.statusCode = statusCode; } + ExitCode(int statusCode) { + this.statusCode = statusCode; + } /** * Returns the {@link ExitCode}'s number. * * @return exit code */ - public Integer getCode() { return statusCode; } + public Integer getCode() { + return statusCode; + } /** * Exit the program with a specific {@code message} and {@link Throwable}. @@ -54,16 +58,17 @@ public enum ExitCode { * * * @see #programExit() + * * @param exitMessage - Exit message to log * @param thrown - Throwable to log + * * @throws Exception */ public void programExit(String exitMessage, Exception thrown) throws Exception { // if(exitMessage != null || thrown != null) - LoggerFactory.getLogger(Logger.GLOBAL_LOGGER_NAME).error(exitMessage == null ? name() : exitMessage, - thrown); + LoggerFactory.getLogger(Logger.GLOBAL_LOGGER_NAME).error(exitMessage == null ? name() : exitMessage, thrown); programExit(); - if(thrown != null) + if (thrown != null) throw thrown; } @@ -82,10 +87,14 @@ public void programExit(String exitMessage, Exception thrown) throws Exception { * * * @see #programExit(String, Exception) + * * @param thrown - Throwable to log + * * @throws Exception */ - public void programExit(Exception thrown) throws Exception { programExit(null, thrown); } + public void programExit(Exception thrown) throws Exception { + programExit(null, thrown); + } /** * Exit the program with a specific {@code message}. @@ -103,10 +112,14 @@ public void programExit(String exitMessage, Exception thrown) throws Exception { * * * @see #programExit(String, Exception) + * * @param exitMessage - Exit message to log + * * @throws Exception */ - public void programExit(String exitMessage) throws Exception { programExit(exitMessage, null); } + public void programExit(String exitMessage) throws Exception { + programExit(exitMessage, null); + } /** * Exit the program with this {@link ExitCode}'s {@code "exit code"}. @@ -121,8 +134,11 @@ public void programExit(String exitMessage, Exception thrown) throws Exception { * * * - * @throws Exception + * @throws RuntimeException + * * @see #programExit(String, Exception) */ - public void programExit() throws Exception { System.exit(getCode()); } + public void programExit() throws RuntimeException { + System.exit(getCode()); + } } diff --git a/src/net/foxgenesis/watame/Main.java b/src/net/foxgenesis/watame/Main.java index 720963b..45bf97b 100644 --- a/src/net/foxgenesis/watame/Main.java +++ b/src/net/foxgenesis/watame/Main.java @@ -6,6 +6,7 @@ import java.util.concurrent.ForkJoinPool; import net.foxgenesis.util.SingleInstanceUtil; +import net.foxgenesis.watame.Settings.LogLevel; import org.apache.commons.configuration2.ImmutableConfiguration; import org.apache.commons.lang3.StringUtils; @@ -29,7 +30,7 @@ public class Main { private final static Logger logger = LoggerFactory.getLogger(Main.class); - private static WatameBotSettings settings; + private static Settings settings; /** * Program entry point. @@ -41,10 +42,8 @@ public class Main { public static void main(String[] args) throws Exception { System.setProperty("watame.status", "START-UP"); - Path configPath = Path.of("config/"); - String logLevel = null; - String pbToken = null; - Path tokenFile = null; + SettingsBuilder builder = new SettingsBuilder(); + builder.setConfigPath(Path.of("config")); int length = args.length; for (int i = 0; i < args.length; i++) { @@ -54,7 +53,7 @@ public static void main(String[] args) throws Exception { case "-config" -> { if (hasArg(i, length, "-config")) { i++; - configPath = Path.of(StringUtils.strip(args[i], "\"")); + builder.setConfigPath(Path.of(StringUtils.strip(args[i], "\""))); } } @@ -63,8 +62,10 @@ public static void main(String[] args) throws Exception { i++; String tmp = args[i]; if (StringUtils.equalsAnyIgnoreCase(tmp, "info", "debug", "trace")) { - logLevel = tmp.toUpperCase(); - logger.info("Setting logging level to: " + logLevel); + LogLevel level = LogLevel.valueOf(tmp.toUpperCase()); + + builder.setLogLevel(level); + logger.info("Setting logging level to: " + level); } } } @@ -72,40 +73,35 @@ public static void main(String[] args) throws Exception { case "-tokenfile" -> { if (hasArg(i, length, "-tokenfile")) { i++; - tokenFile = Path.of(StringUtils.strip(args[i], "\"")); + builder.setTokenFile(StringUtils.strip(args[i], "\"")); } } case "-pbToken" -> { if (hasArg(i, length, "-pbToken")) { i++; - pbToken = args[i]; + builder.setPushbulletToken(args[i]); } } } } // Load settings - settings = new WatameBotSettings(configPath, tokenFile, pbToken); + settings = new Settings(builder); ImmutableConfiguration config = settings.getConfiguration(); - pbToken = settings.getPBToken(); try { // Enable ANSI console - if (config.getBoolean("Runtime.ansiConsole", true)) { + if (config.getBoolean("Logging.ansiConsole", true)) { logger.info("Installing ANSI console"); AnsiConsole.systemInstall(); } - // Set our log level - if (logLevel == null) - logLevel = config.getString("Logging.logLevel", "info"); - System.setProperty("LOG_LEVEL", logLevel); restartLogging(); // Attempt to obtain instance lock - if (config.getBoolean("SingleInstance.enabled", true)) - getLock(config.getInt("SingleInstance.retries", 5)); + if (config.getBoolean("Startup.SingleInstance.enabled", true)) + getLock(config.getInt("Startup.SingleInstance.retries", 5)); // Print out our current multi-threading information logger.info("Checking multi-threading"); @@ -113,9 +109,6 @@ public static void main(String[] args) throws Exception { Runtime.getRuntime().availableProcessors(), ForkJoinPool.commonPool().getParallelism(), ForkJoinPool.getCommonPoolParallelism()); - if (pbToken != null) - logger.info("Loaded PushBullet token"); - // First call of WatameBot class. Will cause instance creation WatameBot watame = WatameBot.INSTANCE; @@ -123,7 +116,7 @@ public static void main(String[] args) throws Exception { watame.start(); } catch (Exception e) { - new PushBullet(pbToken).pushPBMessage("An Error Occurred in Watame", ExceptionUtils.getStackTrace(e)); + settings.getPushbullet().pushPBMessage("An Error Occurred in Watame", ExceptionUtils.getStackTrace(e)); throw e; } } @@ -170,7 +163,7 @@ private static void restartLogging() { } @NotNull - static WatameBotSettings getSettings() { + static Settings getSettings() { return settings; } } diff --git a/src/net/foxgenesis/watame/Settings.java b/src/net/foxgenesis/watame/Settings.java new file mode 100644 index 0000000..e6895e7 --- /dev/null +++ b/src/net/foxgenesis/watame/Settings.java @@ -0,0 +1,84 @@ +package net.foxgenesis.watame; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import net.foxgenesis.util.PushBullet; +import net.foxgenesis.util.resource.ModuleResource; +import net.foxgenesis.util.resource.ResourceUtils; + +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.ex.ConfigurationException; + +public class Settings { + + private final Path configPath; + private final Path tokenFile; + private final PushBullet pb; + private final Configuration config; + + public Settings(SettingsBuilder builder) throws IOException, ConfigurationException { + this.configPath = Objects.requireNonNull(builder.configPath); + + // Load config file + this.config = ResourceUtils.loadINI( + new ModuleResource(getClass().getModule(), "/META-INF/defaults/watamebot.ini"), configPath, + "watamebot.ini"); + + // Get token file + this.tokenFile = configPath.resolve(builder.tokenFile != null ? builder.tokenFile + : config.getString("WatameBot.Token.tokenFile", "token.txt")); + + // Setup push bullet + String tmp = config.getString("PushBullet.token", builder.pbToken); + tmp = !(tmp == null || tmp.isBlank()) ? tmp.trim() : null; + this.pb = new PushBullet(tmp); + + // Set our log level + System.setProperty("LOG_LEVEL", builder.logLevel != null ? builder.logLevel.name().toLowerCase() + : config.getString("Logging.logLevel", "info")); + } + + public Path getConfigPath() { + return configPath; + } + + public Path tokenFile() { + return tokenFile; + } + + public Configuration getConfiguration() { + return config; + } + + public PushBullet getPushbullet() { + return pb; + } + + public String getToken() { + return readToken(tokenFile); + } + + /** + * NEED_JAVADOC + * + * @param filepath + * + * @return Returns the read token + * + */ + private static String readToken(Path filepath) { + try { + return Files.lines(filepath).filter(s -> !s.startsWith("#")).map(String::trim).findFirst().orElse(""); + } catch (IOException e) { + ExitCode.NO_TOKEN.programExit(); + return null; + } + } + + public static enum LogLevel { + TRACE, DEBUG, INFO, ERROR + } +} diff --git a/src/net/foxgenesis/watame/SettingsBuilder.java b/src/net/foxgenesis/watame/SettingsBuilder.java new file mode 100644 index 0000000..aac95d5 --- /dev/null +++ b/src/net/foxgenesis/watame/SettingsBuilder.java @@ -0,0 +1,37 @@ +package net.foxgenesis.watame; + +import java.nio.file.Path; + +import net.foxgenesis.watame.Settings.LogLevel; + +import org.jetbrains.annotations.Nullable; + +public class SettingsBuilder { + + Path configPath = Path.of("config"); + + @Nullable + LogLevel logLevel = null; + + @Nullable + String tokenFile = null; + + @Nullable + String pbToken = null; + + public void setConfigPath(Path path) { + this.configPath = path; + } + + public void setTokenFile(String tokenFile) { + this.tokenFile = tokenFile; + } + + public void setLogLevel(LogLevel level) { + this.logLevel = level; + } + + public void setPushbulletToken(String token) { + this.pbToken = token; + } +} diff --git a/src/net/foxgenesis/watame/State.java b/src/net/foxgenesis/watame/State.java new file mode 100644 index 0000000..cdd80f9 --- /dev/null +++ b/src/net/foxgenesis/watame/State.java @@ -0,0 +1,33 @@ +package net.foxgenesis.watame; + +/** + * States {@link WatameBot} goes through on startup. + * + * @author Ashley + */ +public enum State { + /** + * NEED_JAVADOC + */ + CONSTRUCTING, + /** + * NEED_JAVADOC + */ + PRE_INIT, + /** + * NEED_JAVADOC + */ + INIT, + /** + * NEED_JAVADOC + */ + POST_INIT, + /** + * WatameBot has finished all loading stages and is running + */ + RUNNING, + /** + * WatameBot is shutting down + */ + SHUTDOWN; +} \ No newline at end of file diff --git a/src/net/foxgenesis/watame/WatameBot.java b/src/net/foxgenesis/watame/WatameBot.java index 5a1ff5a..080af86 100644 --- a/src/net/foxgenesis/watame/WatameBot.java +++ b/src/net/foxgenesis/watame/WatameBot.java @@ -1,8 +1,11 @@ package net.foxgenesis.watame; +import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; + import java.io.IOException; import java.net.ConnectException; import java.nio.file.Path; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Properties; import java.util.concurrent.ExecutorService; @@ -16,6 +19,7 @@ import net.foxgenesis.property.PropertyType; import net.foxgenesis.property.database.LCKConfigurationDatabase; import net.foxgenesis.util.MethodTimer; +import net.foxgenesis.util.PushBullet; import net.foxgenesis.util.resource.ResourceUtils; import net.foxgenesis.watame.plugin.Plugin; import net.foxgenesis.watame.plugin.PluginHandler; @@ -30,8 +34,6 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.Marker; -import org.slf4j.MarkerFactory; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA.Status; @@ -56,22 +58,27 @@ public class WatameBot { /** * General purpose logger */ - public static final Logger logger = LoggerFactory.getLogger(WatameBot.class); + private static final Logger logger = LoggerFactory.getLogger(WatameBot.class); /** - * Singleton instance of class + * Path pointing to the configuration directory */ - public static final WatameBot INSTANCE; + public static final Path CONFIG_PATH; /** - * Path pointing to the configuration directory + * Utility class to retrieve information from the stack */ - public static final Path CONFIG_PATH; + private static final StackWalker walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE); + + /** + * Push notifications helper + */ + private static final PushBullet pushbullet; /** * Settings that were parsed at startup */ - private static final WatameBotSettings settings; + private static final Settings settings; /** * watame.ini configuration file @@ -79,25 +86,147 @@ public class WatameBot { private static final ImmutableConfiguration config; /** - * Push notifications helper + * Singleton instance of class */ - private static final PushBullet pushbullet; + static final WatameBot INSTANCE; static { settings = Main.getSettings(); config = settings.getConfiguration(); - pushbullet = new PushBullet(settings.getPBToken()); + pushbullet = settings.getPushbullet(); RestAction.setDefaultFailure( err -> pushbullet.pushPBMessage("An Error Occurred in Watame", ExceptionUtils.getStackTrace(err))); // Initialize our configuration path - CONFIG_PATH = settings.configurationPath; + CONFIG_PATH = settings.getConfigPath(); // Initialize the main bot object with token INSTANCE = new WatameBot(settings.getToken()); } + /** + * Get the {@link Plugin} of the calling method. + *

+ * This method uses a {@link StackWalker} to retrieve the calling class. As + * such, it will only retrieve the loaded {@link Plugin} of the caller's + * {@link Module}. + *

+ * + * @param Wanted plugin type + * @param pluginClass - class of the desired {@link Plugin} + * + * @return Returns the found {@link Plugin} + * + * @throws NoSuchElementException Thrown if there is no plugin loaded for the + * calling module + * @throws ClassCastException Thrown if the found plugin is not assignable + * to the specified {@code pluginClass} + * + * @see #getSelfPlugin() + */ + public static U getSelfPlugin(Class pluginClass) { + Class callerClass = walker.getCallerClass(); + Module module = callerClass.getModule(); + Plugin p = INSTANCE.pluginHandler.getPluginForModule(module); + + if (p == null) + throw new NoSuchElementException("No plugin is loaded for module: " + module.getName()); + + return pluginClass.cast(p); + } + + /** + * Get the {@link Plugin} of the calling method. + *

+ * This method uses a {@link StackWalker} to retrieve the calling class. As + * such, it will only retrieve the loaded {@link Plugin} of the caller's + * {@link Module}. + *

+ * + * @return Returns the found {@link Plugin} + * + * @see #getSelfPlugin(Class) + */ + public static Plugin getSelfPlugin() { + Class callerClass = walker.getCallerClass(); + Module module = callerClass.getModule(); + return INSTANCE.pluginHandler.getPluginForModule(module); + } + + /** + * Get the plugin specified by the {@code identifier}. + * + * @param identifier - plugin identifier + * + * @return Returns the {@link Plugin} with the specified {@code identifier} + */ + public static Plugin getPlugin(String identifier) { + return INSTANCE.pluginHandler.getPlugin(identifier); + } + + /** + * Get a plugin by class. + * + * @param pluginClass - plugin class + * + * @return Returns the found {@link Plugin} if found, otherwise {@code null} + */ + public static Plugin getPluginByClass(Class pluginClass) { + return INSTANCE.pluginHandler.getPlugin(pluginClass); + } + + /** + * Get the database manager used to register custom databases. + * + * @return Returns the {@link IDatabaseManager} used to register custom + * databases + */ + public static IDatabaseManager getDatabaseManager() { + return INSTANCE.manager; + } + + /** + * Get the {@link PluginPropertyProvider} used to register/retrieve + * {@link PluginProperty PluginProperties}. + * + * @return Returns the {@link PluginPropertyProvider} + */ + public static PluginPropertyProvider getPropertyProvider() { + return INSTANCE.propertyProvider; + } + + /** + * Get the modlog (Moderation Logging) channel property. + * + * @return Returns the an {@link ImmutablePluginProperty} used to retrieve the + * set modlog for a {@link net.dv8tion.jda.api.entities.Guild + * Guild} + */ + public static ImmutablePluginProperty getLoggingChannel() { + return INSTANCE.loggingChannel; + } + + /** + * Get the current state of the bot. + * + * @return Returns the {@link State} of the bot + * + * @see State + */ + public static State getState() { + return INSTANCE.state; + } + + /** + * Get the {@link JDA} instance. + * + * @return the current instance of {@link JDA} + */ + public static JDA getJDA() { + return INSTANCE.discord; + } + // ------------------------------- INSTNACE ==================== /** @@ -183,7 +312,7 @@ private WatameBot(String token) { builder = createJDA(token, null); // Set our instance context - context = new Context(this, builder, null, pushbullet::pushPBMessage); + context = new Context(builder, null, pushbullet::pushPBMessage); // Create our plugin handler pluginHandler = new PluginHandler<>(context, getClass().getModule().getLayer(), Plugin.class); @@ -287,7 +416,7 @@ private void postInit() throws Exception { */ // Post-initialize all plugins - pluginHandler.postInit(this); + pluginHandler.postInit(); discord = buildJDA(); context.onJDABuilder(discord); @@ -311,13 +440,13 @@ private void postInit() throws Exception { } private void ready() { + // Fire on ready event + pluginHandler.onReady(); + // Display our game as ready logger.debug("Setting presence to ready"); - discord.getPresence().setPresence(OnlineStatus.ONLINE, Activity - .playing(config.getString("WatameBot.Status.online", "https://github.com/FoxGenesis/Watamebot"))); - - // Fire on ready event - pluginHandler.onReady(this); + discord.getPresence().setPresence(OnlineStatus.ONLINE, + Activity.playing(config.getString("Startup.Status.online", "https://github.com/FoxGenesis/Watamebot"))); } /** @@ -376,7 +505,7 @@ private JDABuilder createJDA(String token, ExecutorService eventExecutor) { .disableCache(CacheFlag.ACTIVITY, CacheFlag.EMOJI, CacheFlag.STICKER, CacheFlag.CLIENT_STATUS, CacheFlag.ONLINE_STATUS, CacheFlag.SCHEDULED_EVENTS) .setChunkingFilter(ChunkingFilter.ALL).setAutoReconnect(true) - .setActivity(Activity.playing(config.getString("WatameBot.Status.startup", "Initalizing..."))) + .setActivity(Activity.playing(config.getString("Startup.Status.startup", "Initalizing..."))) .setMemberCachePolicy(MemberCachePolicy.ALL).setStatus(OnlineStatus.DO_NOT_DISTURB) .setEnableShutdownHook(false); @@ -387,10 +516,19 @@ private JDABuilder createJDA(String token, ExecutorService eventExecutor) { } private JDA buildJDA() throws Exception { - JDA discordTmp = null; + // Finalize required intents and caches + logger.info("Collecting gateway intents"); + builder.enableIntents(pluginHandler.getGatewayIntents()); + + logger.info("Setting up caches"); + builder.enableCache(pluginHandler.getCaches()); + + logger.info("Getting all required cache policies"); + builder.setMemberCachePolicy(pluginHandler.getRequiredCachePolicy()); // Attempt to connect to discord. If failed because no Internet, wait 5 seconds // and retry. + JDA discordTmp = null; int tries = 0; int maxTries = 5; double delay = 2; @@ -429,7 +567,7 @@ private AConnectionProvider getConnectionProvider() throws Exception { int tries = 0; int maxTries = 5; double delay = 2; - Properties properties = ResourceUtils.getProperties(settings.configurationPath.resolve("database.properties"), + Properties properties = ResourceUtils.getProperties(settings.getConfigPath().resolve("database.properties"), Constants.DATABASE_SETTINGS_FILE); while (tries < maxTries && provider == null) { @@ -463,85 +601,9 @@ public boolean isConnectedToDiscord() { return discord != null && discord.getStatus() == Status.CONNECTED; } - /** - * NEED_JAVADOC - * - * @return Returns the {@link IDatabaseManager} used to register custom - * databases - */ - public IDatabaseManager getDatabaseManager() { - return manager; - } - - public PluginPropertyProvider getPropertyProvider() { - return propertyProvider; - } - - public ImmutablePluginProperty getLoggingChannel() { - return loggingChannel; - } - - /** - * NEED_JAVADOC - * - * @return the current instance of {@link JDA} - */ - public JDA getJDA() { - return discord; - } - - /** - * Get the current state of the bot. - * - * @return Returns the {@link State} of the bot - * - * @see State - */ - public State getState() { - return state; - } - private void updateState(State state) { this.state = state; logger.trace("STATE = " + state); System.setProperty("watame.status", state.name()); } - - /** - * States {@link WatameBot} goes through on startup. - * - * @author Ashley - */ - public enum State { - /** - * NEED_JAVADOC - */ - CONSTRUCTING, - /** - * NEED_JAVADOC - */ - PRE_INIT, - /** - * NEED_JAVADOC - */ - INIT, - /** - * NEED_JAVADOC - */ - POST_INIT, - /** - * WatameBot has finished all loading stages and is running - */ - RUNNING, - /** - * WatameBot is shutting down - */ - SHUTDOWN; - - public final Marker marker; - - State() { - marker = MarkerFactory.getMarker(name()); - } - } } diff --git a/src/net/foxgenesis/watame/WatameBotSettings.java b/src/net/foxgenesis/watame/WatameBotSettings.java deleted file mode 100644 index 17428ab..0000000 --- a/src/net/foxgenesis/watame/WatameBotSettings.java +++ /dev/null @@ -1,127 +0,0 @@ -package net.foxgenesis.watame; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; - -import net.foxgenesis.util.resource.ModuleResource; -import net.foxgenesis.util.resource.ResourceUtils; - -import org.apache.commons.configuration2.INIConfiguration; -import org.apache.commons.configuration2.ImmutableConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class WatameBotSettings { - - @NotNull - public final Path configurationPath; - - private final INIConfiguration config; - - /** - * Authentication token (passwords should be stored as char[]) - */ - private final String token; - - private final String pbToken; - - WatameBotSettings(@NotNull Path configPath) throws Exception { - this(configPath, null, null); - } - - WatameBotSettings(@NotNull Path configPath, @Nullable Path tokenFile2, String pbToken2) throws Exception { - configurationPath = Objects.requireNonNull(configPath); - - if (!isValidDirectory(configPath)) - throw new IOException("Invalid configuration directory"); - - config = ResourceUtils.loadINI(new ModuleResource(getClass().getModule(), "/META-INF/defaults/watamebot.ini"), - configurationPath, "watamebot.ini"); - - // Get the token file - Path tokenFile = tokenFile2 == null ? Path.of(config.getString("Token.tokenFile", "token.txt")) - : configPath.resolve(tokenFile2); - - if (!isValidFile(tokenFile)) - throw new SettingsException("Invalid token file"); - - // Read token - token = readToken(tokenFile); - - String tmp = config.getString("PushBullet.token", pbToken2); - pbToken = !(tmp == null || tmp.isBlank()) ? tmp.trim() : null; - } - - public ImmutableConfiguration getConfiguration() { - return config; - } - - String getToken() { - return token; - } - - String getPBToken() { - return pbToken; - } - - /** - * NEED_JAVADOC - * - * @param filepath - * - * @return Returns the read token - * - * @throws Exception - */ - private static String readToken(Path filepath) throws Exception { - return Files.lines(filepath).filter(s -> !s.startsWith("#")).map(String::trim).findFirst().orElse(""); - } - - private static boolean isValidDirectory(Path path) throws SettingsException { - if (Files.notExists(path)) - try { - Files.createDirectories(path); - } catch (IOException e) { - e.printStackTrace(); - } - - if (Files.exists(path) && check(Files.isDirectory(path), "Configuration path must be a directory!")) { - return check(Files.isReadable(path), "Unable to read from configuration directory!") - && check(Files.isWritable(path), "Unable to write to configuration directory!"); - } - - return false; - } - - private static boolean isValidFile(Path path) throws SettingsException { - if (Files.notExists(path)) - try { - Files.createFile(path); - } catch (IOException e) { - e.printStackTrace(); - } - - if (Files.exists(path)) - if (check(Files.isRegularFile(path), "Token file must be a regular file!")) - return check(Files.isReadable(path), "Unable to read from token file!"); - - return false; - } - - private static boolean check(boolean toTest, String err) throws SettingsException { - if (toTest) - return true; - throw new SettingsException(err); - } - - public static class SettingsException extends Exception { - - private static final long serialVersionUID = 5228408925488573319L; - - public SettingsException(String msg) { - super(msg); - } - } -} diff --git a/src/net/foxgenesis/watame/command/ConfigCommand.java b/src/net/foxgenesis/watame/command/ConfigCommand.java index f9664b8..6876562 100644 --- a/src/net/foxgenesis/watame/command/ConfigCommand.java +++ b/src/net/foxgenesis/watame/command/ConfigCommand.java @@ -65,7 +65,7 @@ public void onCommandAutoCompleteInteraction(CommandAutoCompleteInteractionEvent Guild guild = event.getGuild(); if (event.isFromGuild() && guild != null) { if (event.getFullCommandName().startsWith("options configuration") && option.getName().equals("key")) { - PluginPropertyProvider provider = WatameBot.INSTANCE.getPropertyProvider(); + PluginPropertyProvider provider = WatameBot.getPropertyProvider(); String value = option.getValue().toLowerCase(); @SuppressWarnings("null") List choices = provider.getPropertyList().stream() @@ -95,7 +95,7 @@ private static void handleOptions(SlashCommandInteractionEvent event) { * @param event - slash command event */ private static void handleConfiguration(SlashCommandInteractionEvent event) { - PluginPropertyProvider provider = WatameBot.INSTANCE.getPropertyProvider(); + PluginPropertyProvider provider = WatameBot.getPropertyProvider(); String sub = Objects.requireNonNull(event.getSubcommandName()); InteractionHook hook = event.getHook(); @@ -305,7 +305,7 @@ private static MessageEmbed response(int color, @NotNull String title, @Nullable * @param value - new property value */ private static void logChange(Member user, String key, String oldValue, String value) { - GuildMessageChannel channel = WatameBot.INSTANCE.getLoggingChannel().get(user.getGuild(), + GuildMessageChannel channel = WatameBot.getLoggingChannel().get(user.getGuild(), PluginPropertyMapping::getAsMessageChannel); if (channel != null) { diff --git a/src/net/foxgenesis/watame/command/IntegratedCommands.java b/src/net/foxgenesis/watame/command/IntegratedCommands.java index 34a2ee7..64dec4a 100644 --- a/src/net/foxgenesis/watame/command/IntegratedCommands.java +++ b/src/net/foxgenesis/watame/command/IntegratedCommands.java @@ -5,10 +5,13 @@ import java.util.Collection; import java.util.List; -import net.foxgenesis.watame.WatameBot; -import net.foxgenesis.watame.plugin.CommandProvider; +import net.foxgenesis.util.resource.ConfigType; import net.foxgenesis.watame.plugin.IEventStore; import net.foxgenesis.watame.plugin.Plugin; +import net.foxgenesis.watame.plugin.require.CommandProvider; +import net.foxgenesis.watame.plugin.require.PluginConfiguration; + +import org.apache.commons.configuration2.Configuration; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; @@ -19,24 +22,44 @@ import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +@PluginConfiguration(defaultFile = "/META-INF/integrated.ini", identifier = "integrated", outputFile = "integrated.ini", type = ConfigType.INI) public class IntegratedCommands extends Plugin implements CommandProvider { + private final boolean enableConfigCommand; + private final boolean enablePingCommand; + + public IntegratedCommands() { + super(); + + if (hasConfiguration("integrated")) { + Configuration config = getConfiguration("integrated"); + enableConfigCommand = config.getBoolean("IntegratedPlugin.enableOptionsCommand", false); + enablePingCommand = config.getBoolean("IntegratedPlugin.enablePingCommand", true); + } else { + enableConfigCommand = false; + enablePingCommand = true; + } + } + @Override public void preInit() {} @Override public void init(IEventStore builder) { + if (enableConfigCommand) + builder.registerListeners(this, new ConfigCommand()); - builder.registerListeners(this, new ConfigCommand(), new PingCommand()); + if (enablePingCommand) + builder.registerListeners(this, new PingCommand()); } @Override - public void postInit(WatameBot bot) { + public void postInit() { } @Override - public void onReady(WatameBot bot) {} + public void onReady() {} @Override public void close() {} diff --git a/src/net/foxgenesis/watame/plugin/EventStore.java b/src/net/foxgenesis/watame/plugin/EventStore.java index 393081f..3c44011 100644 --- a/src/net/foxgenesis/watame/plugin/EventStore.java +++ b/src/net/foxgenesis/watame/plugin/EventStore.java @@ -1,5 +1,6 @@ package net.foxgenesis.watame.plugin; +import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; @@ -34,7 +35,7 @@ public void unregister(Plugin plugin) { if (!(objs == null || objs.isEmpty())) { Object[] l = objs.toArray(); - logger.debug("Removing {} listeners from {}", l.length, plugin.friendlyName); + logger.debug("Removing {} listeners from {}", l.length, plugin.getInfo().getDisplayName()); builder.removeEventListeners(l); if (jda != null) @@ -49,41 +50,39 @@ public void registerListeners(Plugin plugin, Object... listener) { Objects.requireNonNull(plugin); Objects.requireNonNull(listener); - if (store.containsKey(plugin)) { - Set listeners = store.get(plugin); - synchronized (listeners) { - logger.debug("Adding {} listeners from {}", listener.length, plugin.friendlyName); - for (Object l : listener) - listeners.add(l); - builder.addEventListeners(listener); - - if (jda != null) { - logger.debug("Adding {} listeners from {} to JDA", listener.length, plugin.friendlyName); - jda.addEventListener(listener); - } - } - } else + if (!store.containsKey(plugin)) throw new IllegalArgumentException("Provided plugin is not registered!"); + Set listeners = store.get(plugin); + synchronized (listeners) { + logger.debug("Adding {} listeners from {}", listener.length, plugin.getInfo().getDisplayName()); + Collections.addAll(listeners, listener); + builder.addEventListeners(listener); + + if (jda != null) { + logger.debug("Adding {} listeners from {} to JDA", listener.length, plugin.getInfo().getDisplayName()); + jda.addEventListener(listener); + } + } } @Override public void unregisterListeners(Plugin plugin, Object... listener) { - if (store.containsKey(plugin)) { - Set listeners = store.get(plugin); - synchronized (listeners) { - logger.debug("Removing {} listeners from {}", listener.length, plugin.friendlyName); - for (Object l : listener) - listeners.remove(l); - builder.removeEventListeners(listener); - - if (jda != null) { - logger.debug("Removing {} listeners from {} in JDA", listener.length, plugin.friendlyName); - jda.removeEventListener(listener); - } - - } - } else + if (!store.containsKey(plugin)) throw new IllegalArgumentException("Provided plugin is not registered!"); + Set listeners = store.get(plugin); + synchronized (listeners) { + logger.debug("Removing {} listeners from {}", listener.length, plugin.getInfo().getDisplayName()); + for (Object l : listener) + listeners.remove(l); + builder.removeEventListeners(listener); + + if (jda != null) { + logger.debug("Removing {} listeners from {} in JDA", listener.length, + plugin.getInfo().getDisplayName()); + jda.removeEventListener(listener); + } + + } } public synchronized void setJDA(JDA jda) { diff --git a/src/net/foxgenesis/watame/plugin/IEventStore.java b/src/net/foxgenesis/watame/plugin/IEventStore.java index 0bf15be..6ca9ead 100644 --- a/src/net/foxgenesis/watame/plugin/IEventStore.java +++ b/src/net/foxgenesis/watame/plugin/IEventStore.java @@ -2,5 +2,6 @@ public interface IEventStore { void registerListeners(Plugin plugin, Object... listener); + void unregisterListeners(Plugin plugin, Object... listener); } diff --git a/src/net/foxgenesis/watame/plugin/Plugin.java b/src/net/foxgenesis/watame/plugin/Plugin.java index 8ce2ab3..cc54d44 100644 --- a/src/net/foxgenesis/watame/plugin/Plugin.java +++ b/src/net/foxgenesis/watame/plugin/Plugin.java @@ -5,6 +5,7 @@ import java.lang.module.ModuleDescriptor.Version; import java.nio.file.Path; import java.util.HashMap; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Properties; import java.util.Set; @@ -17,6 +18,8 @@ import net.foxgenesis.util.resource.ModuleResource; import net.foxgenesis.util.resource.ResourceUtils; import net.foxgenesis.watame.WatameBot; +import net.foxgenesis.watame.plugin.require.CommandProvider; +import net.foxgenesis.watame.plugin.require.PluginConfiguration; import net.foxgenesis.watame.property.PluginProperty; import net.foxgenesis.watame.property.PluginPropertyProvider; @@ -28,8 +31,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.dv8tion.jda.api.interactions.commands.build.CommandData; - /** * A service providing functionality to {@link WatameBot}. *

@@ -37,11 +38,11 @@ * {@code public static Plugin provider()} method in accordance to * {@link java.util.ServiceLoader.Provider#get() Provider.get()} *

- * + * * @author Ashley * */ -public abstract class Plugin { +public abstract class Plugin extends ServiceStartup { /** * Plugin logger @@ -56,44 +57,10 @@ public abstract class Plugin { private final HashMap configs = new HashMap<>(); /** - * Path to the plugin's configuration folder - */ - @NotNull - public final Path configurationPath; - - /** - * Name identifier of the plugin. - */ - @NotNull - public final String name; - - /** - * Friendly identifier of the plugin. - */ - @NotNull - public final String friendlyName; - - /** - * Description of the plugin. - */ - @NotNull - public final String description; - - /** - * Version of the plugin. + * Information about the plugin */ @NotNull - public final Version version; - - /** - * Does this plugin provide commands. - */ - public final boolean providesCommands; - - /** - * Does this plugin require access to the database. - */ - public final boolean needsDatabase; + private final PluginInformation info; /** * No-argument constructor called by the {@link java.util.ServiceLoader @@ -107,7 +74,7 @@ public abstract class Plugin { * * Anything beyond the previous should be loaded in the {@link #preInit()} * method. - * + * * @throws SeverePluginException if the plugin is not in a named module or there * was a problem while loading the * {@code plugin.properties} file @@ -116,35 +83,27 @@ public Plugin() throws SeverePluginException { Class c = getClass(); Module module = c.getModule(); - if (!module.isNamed()) - throw new SeverePluginException("Plugin is not in a named module!"); - // Load plugin properties Properties properties = new Properties(); try (InputStream stream = module.getResourceAsStream("/plugin.properties")) { properties.load(stream); - - this.name = Objects.requireNonNull(properties.getProperty("name"), "name must not be null!"); - this.friendlyName = Objects.requireNonNull(properties.getProperty("friendlyName"), - "friendlyName must not be null!"); - - this.version = Version - .parse(Objects.requireNonNull(properties.getProperty("version"), "version must not be null!")); - this.description = properties.getProperty("description", "No description provided"); - this.providesCommands = this instanceof CommandProvider; - this.needsDatabase = properties.getProperty("needsDatabase", "false").equalsIgnoreCase("true"); - - this.configurationPath = WatameBot.CONFIG_PATH.resolve(this.name); } catch (IOException e) { throw new SeverePluginException(e, true); } + // Parse properties file + info = parseInfo(properties, this); + // Load configurations if they are present if (c.isAnnotationPresent(PluginConfiguration.class)) { PluginConfiguration[] configDeclares = c.getDeclaredAnnotationsByType(PluginConfiguration.class); for (PluginConfiguration pluginConfig : configDeclares) { + String id = pluginConfig.identifier(); + + // FIXME: sanitize PluginConfiguration IDs + // Skip over duplicate identifiers if (configs.containsKey(id)) continue; @@ -155,7 +114,7 @@ public Plugin() throws SeverePluginException { // Parse the configuration logger.debug("Loading {} configuration for {}", type.name(), pluginConfig.outputFile()); - Configuration config = ResourceUtils.loadConfiguration(type, defaults, this.configurationPath, + Configuration config = ResourceUtils.loadConfiguration(type, defaults, info.getConfigurationPath(), pluginConfig.outputFile()); configs.put(id, config); @@ -170,7 +129,7 @@ public Plugin() throws SeverePluginException { /** * Get a list of all loaded configuration {@code ID}s. - * + * * @return Returns a {@link java.util.Set Set} containing the {@code ID}s of all * configurations loaded */ @@ -180,7 +139,7 @@ protected Set configurationKeySet() { /** * Loop through all loaded {@link Configuration} files. - * + * * @param consumer - {@link PluginConfiguration#identifier()} and the * constructed {@link Configuration} */ @@ -190,9 +149,9 @@ protected void forEachConfiguration(BiConsumer consumer) /** * Check if a configuration file with the specified {@code identifier} exists. - * + * * @param identifier - the {@link PluginConfiguration#identifier()} - * + * * @return Returns {@code true} if the specified {@code identifier} points to a * valid configuration */ @@ -201,26 +160,30 @@ protected boolean hasConfiguration(String identifier) { } /** - * Get the configuration file that is linked to an {@code identifier} or - * {@code null} if not found. - * + * Get the configuration file that is linked to an {@code identifier}. + * * @param identifier - the {@link PluginConfiguration#identifier()} - * + * * @return Returns the {@link PropertiesConfiguration} linked to the * {@code identifier} + * + * @throws NoSuchElementException Thrown if the configuration with the specified + * {@code identifier} does not exist */ - @Nullable + @NotNull protected Configuration getConfiguration(String identifier) { - return configs.getOrDefault(identifier, null); + if (configs.containsKey(identifier)) + return configs.get(identifier); + throw new NoSuchElementException(); } /** * Register a plugin property inside the database. - * + * * @param name - name of the property * @param modifiable - if this property can be modified by a user * @param type - property storage type - * + * * @return Returns the {@link PropertyInfo} of the registered property */ protected final PropertyInfo registerProperty(@NotNull String name, boolean modifiable, @@ -231,11 +194,11 @@ protected final PropertyInfo registerProperty(@NotNull String name, boolean modi /** * Register a plugin property if it doesn't exist and resolve it into a * {@link PluginProperty}. - * + * * @param name - property name * @param modifiable - if this property can be modified by a user * @param type - property storage type - * + * * @return Returns the resolved {@link PluginProperty} */ protected final PluginProperty upsertProperty(@NotNull String name, boolean modifiable, @@ -245,9 +208,9 @@ protected final PluginProperty upsertProperty(@NotNull String name, boolean modi /** * Resolve a {@link PropertyInfo} into a usable {@link PluginProperty}. - * + * * @param info - property information - * + * * @return Returns the resolved {@link PluginProperty} */ protected final PluginProperty getProperty(PropertyInfo info) { @@ -257,9 +220,9 @@ protected final PluginProperty getProperty(PropertyInfo info) { /** * Resolve the {@link PropertyInfo} by the specified {@code name} into a usable * {@link PluginProperty}. - * + * * @param name - property name - * + * * @return Returns the resolved {@link PluginProperty} */ protected final PluginProperty getProperty(String name) { @@ -269,173 +232,85 @@ protected final PluginProperty getProperty(String name) { /** * Get the provider for registering and getting {@link PluginProperty * PluginProperties}. - * + * * @return Returns the {@link PluginPropertyProvider} used by {@link Plugin * Plugins} to register plugin properties */ protected final PluginPropertyProvider getPropertyProvider() { - return WatameBot.INSTANCE.getPropertyProvider(); + return WatameBot.getPropertyProvider(); } /** * Register an {@link AbstractDatabase} that this {@link Plugin} requires. - * + * * @param database - database to register - * + * * @throws IOException Thrown if there was an error while reading the database * setup files */ protected final void registerDatabase(AbstractDatabase database) throws IOException { - WatameBot.INSTANCE.getDatabaseManager().register(this, database); + WatameBot.getDatabaseManager().register(this, database); } // ========================================================================================================= /** - * Startup method called when resources, needed for functionality - * initialization, are to be loaded. Resources that do not require - * connection to Discord or the database should be loaded here. - *

- * The database and Discord information might not be loaded at the time of - * this method! Use {@link #init(IEventStore)} for functionality that - * requires valid connections. - *

- * - *

- * Typical resources to load here include: - *

- *
    - *
  • Custom database registration
  • - *
  • System Data
  • - *
  • Files
  • - *
  • Images
  • - *
+ * Get the information about this plugin. * - * @throws SeverePluginException Thrown if the plugin has encountered a - * severe exception. If the exception is - * fatal, the plugin will be unloaded - * - * @see #init(IEventStore) - * @see #postInit(WatameBot) - * @see #onReady(WatameBot) - */ - protected abstract void preInit() throws SeverePluginException; - - /** - * Startup method called when methods providing functionality are to be loaded. - * Methods that require connection the database should be called here. - *

- * Discord information might not be loaded at the time of this method! - * Use {@link #onReady(WatameBot)} for functionality that requires valid - * connections. - *

- * - *

- * Typical methods to call here include: - *

- *
    - *
  • Main chunk of program initialization
  • - *
  • {@link PluginProperty} registration/retrieval
  • - *
  • Event listener registration
  • - *
  • Custom database operations
  • - *
- * - * @param builder - discord event listener register - * - * @throws SeverePluginException Thrown if the plugin has encountered a - * severe exception. If the exception is - * fatal, the plugin will be unloaded - * - * @see #preInit() - * @see #postInit(WatameBot) - * @see #onReady(WatameBot) + * @return Returns the {@link PluginInformation} */ - protected abstract void init(IEventStore builder) throws SeverePluginException; - - /** - * Startup method called when {@link net.dv8tion.jda.api.JDA JDA} is building a - * connection to discord and all {@link CommandData} is being collected from - * {@link CommandProvider#getCommands()}. - *

- * Discord information might not be loaded at the time of this method! - * Use {@link #onReady(WatameBot)} for functionality that requires valid - * connections. - *

- *

- * Typical methods to call here include: - *

- *
    - *
  • Initialization cleanup
  • - *
- * - * @param bot - reference of {@link WatameBot} - * - * @throws SeverePluginException Thrown if the plugin has encountered a - * severe exception. If the exception is - * fatal, the plugin will be unloaded - */ - protected abstract void postInit(WatameBot bot) throws SeverePluginException; - - /** - * Called by the {@link PluginHandler} when {@link net.dv8tion.jda.api.JDA JDA} - * and all {@link Plugin Plugins} have finished startup and have finished - * loading. - * - * @param bot - reference of {@link WatameBot} - * - * @throws SeverePluginException Thrown if the plugin has encountered a - * severe exception. If the exception is - * fatal, the plugin will be unloaded - * - * @see #preInit() - * @see #init(IEventStore) - * @see #postInit(WatameBot) - */ - protected abstract void onReady(WatameBot bot) throws SeverePluginException; - - /** - * Called when the plugin is to be unloaded. - * - *

- * The shutdown sequence runs as followed: - *

- *
    - *
  • Remove event listeners
  • - *
  • {@link Plugin#close()}
  • - *
  • Close databases
  • - *
- * - * @throws Exception Thrown if an underlying exception is thrown during close - */ - protected abstract void close() throws Exception; - - // ========================================================================================================= - - public String getDisplayInfo() { - return this.friendlyName + " v" + version; + public final PluginInformation getInfo() { + return info; } @Override public String toString() { - return "Plugin [name=" + name + ", friendlyName=" + friendlyName + ", description=" + description + ", version=" - + version + ", providesCommands=" + providesCommands + ", needsDatabase=" + needsDatabase - + ", configurationPath=" + configurationPath + "]"; + return "Plugin [info=" + info + "]"; } @Override public int hashCode() { - return Objects.hash(name, version); + return Objects.hash(info.getID(), info.getVersion()); } @Override public boolean equals(@Nullable Object obj) { if (this == obj) return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) + if ((obj == null) || (getClass() != obj.getClass())) return false; Plugin other = (Plugin) obj; - return Objects.equals(name, other.name) && Objects.equals(version, other.version); + return Objects.equals(info.getID(), other.info.getID()) + && Objects.equals(info.getVersion(), other.info.getVersion()); + } + + // ========================================================================================================= + + /** + * Parse the {@link Properties} file of a {@link Plugin}. + * + * @param properties - plugin.properties file + * @param instance - instance of the plugin + * + * @return Returns the parsed {@link PluginInformation} + */ + private static PluginInformation parseInfo(Properties properties, Plugin instance) { + String id = Objects.requireNonNull(properties.getProperty("name"), "name must not be null!").trim(); + + // FIXME: sanitize plugin IDs + + String friendlyName = Objects.requireNonNull(properties.getProperty("friendlyName"), + "friendlyName must not be null!"); + + Version version = Version + .parse(Objects.requireNonNull(properties.getProperty("version"), "version must not be null!")); + String description = properties.getProperty("description", "No description provided"); + boolean providesCommands = instance instanceof CommandProvider; + boolean needsDatabase = properties.getProperty("needsDatabase", "false").equalsIgnoreCase("true"); + + Path configurationPath = WatameBot.CONFIG_PATH.resolve(id); + + return new PluginInformation(id, friendlyName, version, description, providesCommands, needsDatabase, + configurationPath); } } diff --git a/src/net/foxgenesis/watame/plugin/PluginHandler.java b/src/net/foxgenesis/watame/plugin/PluginHandler.java index a1309e2..f08d539 100644 --- a/src/net/foxgenesis/watame/plugin/PluginHandler.java +++ b/src/net/foxgenesis/watame/plugin/PluginHandler.java @@ -1,22 +1,33 @@ package net.foxgenesis.watame.plugin; import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.ServiceLoader.Provider; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import net.foxgenesis.util.CompletableFutureUtils; import net.foxgenesis.util.MethodTimer; import net.foxgenesis.watame.Context; -import net.foxgenesis.watame.WatameBot; +import net.foxgenesis.watame.plugin.require.CommandProvider; +import net.foxgenesis.watame.plugin.require.RequiresCache; +import net.foxgenesis.watame.plugin.require.RequiresIntents; +import net.foxgenesis.watame.plugin.require.RequiresMemberCachePolicy; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.NotNull; @@ -26,11 +37,14 @@ import org.slf4j.Marker; import org.slf4j.MarkerFactory; +import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction; +import net.dv8tion.jda.api.utils.MemberCachePolicy; +import net.dv8tion.jda.api.utils.cache.CacheFlag; /** * Class used to handle all plugin related tasks. - * + * * @author Ashley * * @param - the plugin class this instance uses @@ -45,7 +59,7 @@ public class PluginHandler<@NotNull T extends Plugin> implements Closeable { * Map of plugins */ @NotNull - private final ConcurrentHashMap plugins = new ConcurrentHashMap<>(); + private final CopyOnWriteArraySet plugins = new CopyOnWriteArraySet<>(); /** * Service loader to load plugins @@ -80,7 +94,7 @@ public class PluginHandler<@NotNull T extends Plugin> implements Closeable { /** * Construct a new {@link PluginHandler} with the specified {@link ModuleLayer} * and plugin {@link Class}. - * + * * @param context - instance context * @param layer - layer the {@link ServiceLoader} should use * @param pluginClass - the plugin {@link Class} to load @@ -97,40 +111,90 @@ public PluginHandler(Context context, ModuleLayer layer, Class pluginClass) { /** * Load all plugins from the service loader */ - @SuppressWarnings("resource") public void loadPlugins() { logger.info("Checking for plugins..."); - List> providers = loader.stream().toList(); + long time = System.nanoTime(); + + Collection> providers = getProviders(); logger.info("Found {} plugins", providers.size()); logger.info("Constructing plugins..."); - long time = System.nanoTime(); + plugins.addAll(construct(providers)); + plugins.forEach(context.getEventRegister()::register); - providers.forEach(provider -> { - logger.debug("Loading {}", provider.type()); + time = System.nanoTime() - time; + logger.info("Constructed all plugins in {}ms", MethodTimer.formatToMilli(time)); + } - try { - T plugin = provider.get(); - plugins.put(plugin.name, plugin); - context.getEventRegister().register(plugin); + /** + * Get all {@link Provider Providers} of {@code T} from the + * {@link ServiceLoader}. This method will only take one {@code T} per + * {@link Module}. + * + * @return Returns a {@link Collection} of {@link Provider Providers} + */ + private Collection> getProviders() { + Map> providers = new HashMap<>(); + for (Provider provider : loader.stream().toList()) { + Module module = provider.type().getModule(); + + // Check if module is already present + if (providers.containsKey(module)) { + logger.warn("A plugin is already registered for module: {}! Skipping...", module); + continue; + } + + providers.put(module, provider); + } + + return providers.values(); + } - logger.info("Loaded {}", plugin.getDisplayInfo()); - } catch (ServiceConfigurationError e) { - logger.error("Failed to load " + provider.type(), e); + private Collection<@NotNull T> construct(Collection> providers) { + // Copy list + List> list = new ArrayList<>(providers); + Set> classes = new HashSet<>(); + + // Filter out duplicate plugin classes + Iterator> i = list.iterator(); + while (i.hasNext()) { + Provider provider = i.next(); + Class c = provider.type(); + + // Check if plugin class is already registered + if (classes.contains(c)) { + logger.error("Plugin class {}(Provider: {}) has already been provided by another plugin! Skipping...", + c, provider); + i.remove(); + continue; } - }); - time = System.nanoTime() - time; - logger.info("Constructed all plugins in {}ms", MethodTimer.formatToMilli(time)); + // Register plugin class + classes.add(c); + } + + // Construct all plugins + return list.parallelStream().mapMulti((Provider provider, Consumer consumer) -> { + Class c = provider.type(); + + try { + // Call plugin constructor + logger.debug("Loading {}", c); + T plugin = provider.get(); + logger.info("Loaded {}", plugin.getInfo().getDisplayInfo()); + consumer.accept(plugin); + } catch (Exception e) { + logger.error("Error while constructing " + c, e); + } + }).toList(); } /** * Pre-Initialize all plugins. */ public void preInit() { - forEach(Plugin::preInit); -// forEachPlugin(Plugin::preInit, null).join(); + forEachPlugin(Plugin::preInit, null).join(); } /** @@ -138,47 +202,39 @@ public void preInit() { */ @NotNull public void init() { - forEach(plugin -> plugin.init(context.getEventRegister())); -// forEachPlugin(plugin -> plugin.init(context.getEventRegister()), null).join(); + forEachPlugin(plugin -> plugin.init(context.getEventRegister()), null).join(); } /** * Post-Initialize all plugins. - * - * @param watamebot - reference to {@link WatameBot} that is passed on to the - * plugin's {@code postInit} */ @NotNull - public void postInit(WatameBot watamebot) { - forEach(plugin -> plugin.postInit(watamebot)); -// forEachPlugin(plugin -> plugin.postInit(watamebot), null).join(); + public void postInit() { + forEachPlugin(Plugin::postInit, null).join(); } /** * Post-Initialize all plugins. - * - * @param watamebot - reference to {@link WatameBot} that is passed on to the - * plugin's {@code onReady}o - * + * * @return Returns a {@link CompletableFuture} that completes when all plugins - * have finished their {@link Plugin#onReady(WatameBot)} + * have finished their {@link Plugin#onReady()} */ @NotNull - public CompletableFuture onReady(WatameBot watamebot) { - return forEachPlugin(plugin -> plugin.onReady(watamebot), null); + public CompletableFuture onReady() { + return forEachPlugin(Plugin::onReady, null); } /** * Fill a {@link CommandListUpdateAction} will all commands specified by the * loaded plugins. - * + * * @param action - update task to fill - * + * * @return Returns the action for chaining */ @NotNull public CommandListUpdateAction updateCommands(CommandListUpdateAction action) { - plugins.values().stream().filter(p -> p instanceof CommandProvider).map(CommandProvider.class::cast) + plugins.stream().filter(p -> p instanceof CommandProvider).map(CommandProvider.class::cast) .map(CommandProvider::getCommands).filter(Objects::nonNull).forEach(action::addCommands); return action; } @@ -187,11 +243,11 @@ public CommandListUpdateAction updateCommands(CommandListUpdateAction action) { * Iterate over all plugins that match the {@code filter} and perform a task. * Additionally, any plugin that fires a fatal * {@link SeverePluginException} will be unloaded. - * + * * @param task - task that is executed for every plugin in the filter * @param filter - filter to select what plugins to use or {@code null} for all * plugins - * + * * @return Returns a {@link CompletableFuture} that completes after all plugins * have finished the {@code task}. */ @@ -199,54 +255,38 @@ public CommandListUpdateAction updateCommands(CommandListUpdateAction action) { private CompletableFuture forEachPlugin(Consumer task, @Nullable Predicate filter) { if (filter == null) filter = p -> true; - return CompletableFutureUtils.allOf(plugins.values().stream().filter(filter).map(plugin -> CompletableFuture + return CompletableFutureUtils.allOf(plugins.stream().filter(filter).map(plugin -> CompletableFuture .runAsync(() -> task.accept(plugin), pluginExecutor).exceptionallyAsync(error -> { pluginError(plugin, error); return null; }, pluginExecutor))); } - private void forEach(Consumer task) { - forEach(null, task); - } - - private void forEach(@Nullable Predicate filter, Consumer task) { - for (T t : plugins.values()) { - if (!(filter == null || filter.test(t))) - continue; - try { - task.accept(t); - } catch (Exception e) { - pluginError(t, e); - } - } - } - /** * Remove a plugin from the managed plugins, closing its resources in the * process. - * + * * @param plugin - the plugin to unload */ @SuppressWarnings("resource") private void unloadPlugin(T plugin) { logger.debug("Unloading {}", plugin.getClass()); - plugins.remove(plugin.name); + plugins.remove(plugin); context.getEventRegister().unregister(plugin); try { plugin.close(); } catch (Exception e) { pluginError(plugin, new SeverePluginException(e, false)); } - if (plugin.needsDatabase) + if (plugin.getInfo().requiresDatabase()) context.getDatabaseManager().unload(plugin); - logger.warn(plugin.getDisplayInfo() + " unloaded"); + logger.warn(plugin.getInfo().getDisplayInfo() + " unloaded"); } /** * Indicate that a plugin has thrown an error during one of its initialization * methods. - * + * * @param plugin - plugin in question * @param error - the error that was thrown */ @@ -257,18 +297,16 @@ private void pluginError(T plugin, Throwable error) { temp = error.getCause(); String header = ""; - if (temp instanceof SeverePluginException) { - SeverePluginException pluginException = (SeverePluginException) temp; - + if (temp instanceof SeverePluginException pluginException) { Marker m = MarkerFactory.getMarker(pluginException.isFatal() ? "FATAL" : "SEVERE"); - header = "Exception in " + plugin.friendlyName; + header = "Exception in " + plugin.getInfo().getDisplayName(); logger.error(m, header, pluginException); if (pluginException.isFatal()) unloadPlugin(plugin); } else { - header = "Error in " + plugin.friendlyName; + header = "Error in " + plugin.getInfo().getDisplayName(); logger.error(header, temp); } @@ -286,21 +324,93 @@ public void close() { } /** - * Check if a plugin is loaded. + * Get all {@link GatewayIntent}s required for all {@code plugins} to operate + * normally. + * + * @return Returns an {@link EnumSet} of all required {@link GatewayIntent}s + */ + public EnumSet getGatewayIntents() { + return collectEnums(plugins, this::getRequiredIntents, GatewayIntent.class); + } + + /** + * Get all {@link CacheFlag}s required for all {@code plugins} to operate + * normally. + * + * @return Returns an {@link EnumSet} of all required {@link CacheFlag}s + */ + public EnumSet getCaches() { + return collectEnums(plugins, this::getRequiredCaches, CacheFlag.class); + } + + /** + * Get the {@link MemberCachePolicy} required by all plugins. * + * @return Returns a {@link MemberCachePolicy} created by combining all declared + * policies into + * {@link MemberCachePolicy#any(MemberCachePolicy, MemberCachePolicy...)} + */ + public MemberCachePolicy getRequiredCachePolicy() { + MemberCachePolicy[] list = plugins.stream() + // Check if plugin declared a policy + .filter(p -> p instanceof RequiresMemberCachePolicy) + // Cast to policy provider + .map(p -> (RequiresMemberCachePolicy) p) + // Get declared policy + .map(RequiresMemberCachePolicy::getPolicy) + // Only use non null policies + .filter(Objects::nonNull) + // Collect into an array + .toArray(MemberCachePolicy[]::new); + return MemberCachePolicy.any(MemberCachePolicy.NONE, list); + } + + /** + * Get the required {@link GatewayIntent}s for the specified {@code plugin}. + * + * @param plugin - plugin to check + * + * @return Returns an {@link EnumSet} of all {@link GatewayIntent}s required for + * normal operation of the {@code plugin} + */ + @SuppressWarnings({ "null" }) + public EnumSet getRequiredCaches(T plugin) { + if (plugin instanceof RequiresCache r) + return r.getRequiredCaches(); + return EnumSet.noneOf(CacheFlag.class); + } + + /** + * Get the required {@link GatewayIntent}s for the specified {@code plugin}. + * + * @param plugin - plugin to check + * + * @return Returns an {@link EnumSet} of all {@link GatewayIntent}s required for + * normal operation of the {@code plugin} + */ + @SuppressWarnings("null") + public EnumSet getRequiredIntents(T plugin) { + if (plugin instanceof RequiresIntents r) + return r.getRequiredIntents(); + return EnumSet.noneOf(GatewayIntent.class); + } + + /** + * Check if a plugin is loaded. + * * @param identifier - plugin identifier - * + * * @return Returns {@code true} if the plugin is loaded */ public boolean isPluginPresent(String identifier) { - return plugins.containsKey(identifier); + return getPlugin(identifier) != null; } /** * Check if a plugin is loaded. - * + * * @param pluginClass - class of the plugin to check - * + * * @return Returns {@code true} if the specified plugin was found */ public boolean isPluginPresent(Class pluginClass) { @@ -308,35 +418,72 @@ public boolean isPluginPresent(Class pluginClass) { } /** - * NEED_JAVADOC - * - * @param identifier - * + * Check if a plugin from the specified module is loaded. + * + * @param module - module to check with + * + * @return Returns {@code true} if the plugin is loaded. {@code false} otherwise + */ + public boolean isPluginPresent(Module module) { + return getPluginForModule(module) != null; + } + + /** + * Get the plugin specified by the {@code identifier}. + * + * @param identifier - plugin identifier + * * @return Returns the {@link Plugin} with the specified {@code identifier} */ @Nullable public T getPlugin(String identifier) { - return plugins.get(identifier); + String temp = identifier.trim().toLowerCase(); + return filterPlugins(p -> p.getInfo().getID().equalsIgnoreCase(temp)); } /** * Get a plugin by class. - * + * * @param pluginClass - plugin class - * + * * @return Returns the found {@link Plugin} if found, otherwise {@code null} */ @Nullable public T getPlugin(Class pluginClass) { - for (T p : plugins.values()) - if (pluginClass.isInstance(p)) - return p; + return filterPlugins(p -> pluginClass.isInstance(p)); + } + + /** + * Get the plugin of a module + * + * @param module - module containing the plugin + * + * @return Returns the found {@link Plugin} if present + */ + @Nullable + public T getPluginForModule(Module module) { + return filterPlugins(p -> p.getClass().getModule().equals(module)); + } + + /** + * Find a plugin based on a {@code filter}. + * + * @param filter - filter to use + * + * @return Returns the first plugin found by the specified {@code filter}. + * Otherwise {@code null}. + */ + @Nullable + private T filterPlugins(Predicate filter) { + for (T plugin : plugins) + if (filter.test(plugin)) + return plugin; return null; } /** * Get the class used by this instance. - * + * * @return Returns a {@link Class} that is used by the {@link ServiceLoader} to * load the plugins */ @@ -347,7 +494,7 @@ public Class getPluginClass() { /** * Get the module layer used by this instance. - * + * * @return Returns a {@link ModuleLayer} that is used by the * {@link ServiceLoader} to load the plugins */ @@ -358,11 +505,32 @@ public ModuleLayer getModuleLayer() { /** * NEED_JAVADOC - * + * * @return Returns the thread pool used for asynchronous execution */ @NotNull public ExecutorService getAsynchronousExecutor() { return pluginExecutor; } + + /** + * Collect all enumerations from a {@link Collection} of objects. + * + * @param

+ * + * @param Enumeration type + * @param coll - collection of objects + * @param func - function to grab enumerations from a plugin + * @param enumClass - class of enumeration + * + * @return Returns a {@link EnumSet} of all enumerations found in the specified + * collection + */ + private static > EnumSet<@Nullable E> collectEnums(Collection

coll, + Function> func, Class enumClass) { + EnumSet set = EnumSet.noneOf(enumClass); + for (P plugin : coll) + set.addAll(func.apply(plugin)); + return set; + } } diff --git a/src/net/foxgenesis/watame/plugin/PluginInformation.java b/src/net/foxgenesis/watame/plugin/PluginInformation.java new file mode 100644 index 0000000..4fed67a --- /dev/null +++ b/src/net/foxgenesis/watame/plugin/PluginInformation.java @@ -0,0 +1,31 @@ +package net.foxgenesis.watame.plugin; + +import java.lang.module.ModuleDescriptor.Version; +import java.nio.file.Path; + +/** + * Record class containing all information about a {@link Plugin}. + * + * @param getID - plugin identifier + * @param getDisplayName - name used for display + * @param getVersion - plugin {@link Version} + * @param getDescription - description of the plugin + * @param providesCommands - if the plugin provides commands + * @param requiresDatabase - if the plugin requires access to the database + * @param getConfigurationPath - the {@link Path} to the plugin's configuration + * folder + * + * @author Ashley + */ +public record PluginInformation(String getID, String getDisplayName, Version getVersion, String getDescription, + boolean providesCommands, boolean requiresDatabase, Path getConfigurationPath) { + + /** + * Get a string displaying the plugin's {@code displayName} and {@code version}. + * + * @return Returns a string representing the plugin's name and version + */ + public String getDisplayInfo() { + return getDisplayName + " v" + getVersion; + } +} diff --git a/src/net/foxgenesis/watame/plugin/ServiceStartup.java b/src/net/foxgenesis/watame/plugin/ServiceStartup.java new file mode 100644 index 0000000..b89d6f9 --- /dev/null +++ b/src/net/foxgenesis/watame/plugin/ServiceStartup.java @@ -0,0 +1,124 @@ +package net.foxgenesis.watame.plugin; + +import net.foxgenesis.watame.plugin.require.CommandProvider; + +/** + * Abstract class defining common methods for the startup of a service. + * + * @author Ashley + */ +public abstract class ServiceStartup { + /** + * Startup method called when resources, needed for functionality + * initialization, are to be loaded. Resources that do not require + * connection to Discord or the database should be loaded here. + *

+ * The database and Discord information might not be loaded at the time of + * this method! Use {@link #init(IEventStore)} for functionality that + * requires valid connections. + *

+ * + *

+ * Typical resources to load here include: + *

+ *
    + *
  • Custom database registration
  • + *
  • System Data
  • + *
  • Files
  • + *
  • Images
  • + *
+ * + * @throws SeverePluginException Thrown if the plugin has encountered a + * severe exception. If the exception is + * fatal, the plugin will be unloaded + * + * @see #init(IEventStore) + * @see #postInit() + * @see #onReady() + */ + protected abstract void preInit() throws SeverePluginException; + + /** + * Startup method called when methods providing functionality are to be loaded. + * Methods that require connection the database should be called here. + *

+ * Discord information might not be loaded at the time of this method! + * Use {@link #onReady()} for functionality that requires valid connections. + *

+ * + *

+ * Typical methods to call here include: + *

+ *
    + *
  • Main chunk of program initialization
  • + *
  • {@link net.foxgenesis.watame.property.PluginProperty PluginProperty} + * registration/retrieval
  • + *
  • Event listener registration
  • + *
  • Custom database operations
  • + *
+ * + * @param builder - discord event listener register + * + * @throws SeverePluginException Thrown if the plugin has encountered a + * severe exception. If the exception is + * fatal, the plugin will be unloaded + * + * @see #preInit() + * @see #postInit() + * @see #onReady() + */ + protected abstract void init(IEventStore builder) throws SeverePluginException; + + /** + * Startup method called when {@link net.dv8tion.jda.api.JDA JDA} is building a + * connection to discord and all + * {@link net.dv8tion.jda.api.interactions.commands.build.CommandData + * CommandData} is being collected from {@link CommandProvider#getCommands()}. + *

+ * Discord information might not be loaded at the time of this method! + * Use {@link #onReady()} for functionality that requires valid connections. + *

+ *

+ * Typical methods to call here include: + *

+ *
    + *
  • Initialization cleanup
  • + *
+ * + * @throws SeverePluginException Thrown if the plugin has encountered a + * severe exception. If the exception is + * fatal, the plugin will be unloaded + */ + protected abstract void postInit() throws SeverePluginException; + + /** + * Called by the {@link PluginHandler} when {@link net.dv8tion.jda.api.JDA JDA} + * and all {@link Plugin Plugins} have finished startup and have finished + * loading. + * + * @throws SeverePluginException Thrown if the plugin has encountered a + * severe exception. If the exception is + * fatal, the plugin will be unloaded + * + * @see #preInit() + * @see #init(IEventStore) + * @see #postInit() + */ + protected abstract void onReady() throws SeverePluginException; + + /** + * Called when the plugin is to be unloaded. + * + *

+ * The shutdown sequence runs as followed: + *

+ *
    + *
  • Remove event listeners
  • + *
  • {@link Plugin#close()}
  • + *
  • Close databases
  • + *
+ * + * @throws Exception Thrown if an underlying exception is thrown during close + */ + protected abstract void close() throws Exception; +} diff --git a/src/net/foxgenesis/watame/plugin/SeverePluginException.java b/src/net/foxgenesis/watame/plugin/SeverePluginException.java index 4d097a9..6ead2de 100644 --- a/src/net/foxgenesis/watame/plugin/SeverePluginException.java +++ b/src/net/foxgenesis/watame/plugin/SeverePluginException.java @@ -3,7 +3,7 @@ public class SeverePluginException extends RuntimeException { /** - * + * */ private static final long serialVersionUID = -1101112080896880561L; @@ -12,11 +12,17 @@ public class SeverePluginException extends RuntimeException { */ private final boolean fatal; - public SeverePluginException(String message, Throwable thrown) { this(message, thrown, true); } + public SeverePluginException(String message, Throwable thrown) { + this(message, thrown, true); + } - public SeverePluginException(String message) { this(message, true); } + public SeverePluginException(String message) { + this(message, true); + } - public SeverePluginException(Throwable thrown) { this(thrown, true); } + public SeverePluginException(Throwable thrown) { + this(thrown, true); + } public SeverePluginException(String message, boolean fatal) { super(message); @@ -35,8 +41,10 @@ public SeverePluginException(String message, Throwable thrown, boolean fatal) { /** * Check if this exception was fatal. - * + * * @return Returns {@code true} if this exception results in a fatal error */ - public boolean isFatal() { return fatal; } + public boolean isFatal() { + return fatal; + } } diff --git a/src/net/foxgenesis/watame/plugin/CommandProvider.java b/src/net/foxgenesis/watame/plugin/require/CommandProvider.java similarity index 81% rename from src/net/foxgenesis/watame/plugin/CommandProvider.java rename to src/net/foxgenesis/watame/plugin/require/CommandProvider.java index 91bdbb9..cd07afb 100644 --- a/src/net/foxgenesis/watame/plugin/CommandProvider.java +++ b/src/net/foxgenesis/watame/plugin/require/CommandProvider.java @@ -1,7 +1,9 @@ -package net.foxgenesis.watame.plugin; +package net.foxgenesis.watame.plugin.require; import java.util.Collection; +import net.foxgenesis.watame.plugin.Plugin; + import org.jetbrains.annotations.NotNull; import net.dv8tion.jda.api.interactions.commands.build.CommandData; @@ -9,7 +11,7 @@ public interface CommandProvider { /** * Register all {@link CommandData} that this plugin provides. - * + * * @return Returns a non-null {@link Collection} of {@link CommandData} that * this {@link Plugin} provides */ diff --git a/src/net/foxgenesis/watame/plugin/PluginConfiguration.java b/src/net/foxgenesis/watame/plugin/require/PluginConfiguration.java similarity index 92% rename from src/net/foxgenesis/watame/plugin/PluginConfiguration.java rename to src/net/foxgenesis/watame/plugin/require/PluginConfiguration.java index 25a29ee..f123101 100644 --- a/src/net/foxgenesis/watame/plugin/PluginConfiguration.java +++ b/src/net/foxgenesis/watame/plugin/require/PluginConfiguration.java @@ -1,4 +1,4 @@ -package net.foxgenesis.watame.plugin; +package net.foxgenesis.watame.plugin.require; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -6,13 +6,14 @@ import java.lang.annotation.Target; import net.foxgenesis.util.resource.ConfigType; +import net.foxgenesis.watame.plugin.Plugin; import org.jetbrains.annotations.NotNull; /** * Annotation used on {@link Plugin} classes to request the loading of custom * configuration files. - * + * * @author Ashley */ @Target({ ElementType.TYPE }) @@ -21,7 +22,7 @@ /** * The {@code ID} of this configuration. - * + * * @return Returns a string representing the {@code ID} of this configuration */ @NotNull @@ -29,7 +30,7 @@ /** * The path to the default configuration file inside the jar. - * + * * @return Returns a string pointing to the default configuration file */ @NotNull @@ -38,7 +39,7 @@ /** * The path to store the configuration file outside the jar file. This * path is relative to the plugin configuration directory. - * + * * @return Returns the path to store the configuration outside the jar relative * to the plugin configuration directory */ @@ -50,7 +51,7 @@ *

* Default: {@link ConfigType#PROPERTIES} *

- * + * * @return Returns the {@link ConfigType} of this configuration */ @NotNull diff --git a/src/net/foxgenesis/watame/plugin/require/RequiresCache.java b/src/net/foxgenesis/watame/plugin/require/RequiresCache.java new file mode 100644 index 0000000..59d54c7 --- /dev/null +++ b/src/net/foxgenesis/watame/plugin/require/RequiresCache.java @@ -0,0 +1,10 @@ +package net.foxgenesis.watame.plugin.require; + +import java.util.EnumSet; + +import net.dv8tion.jda.api.utils.cache.CacheFlag; + +public interface RequiresCache { + + EnumSet getRequiredCaches(); +} diff --git a/src/net/foxgenesis/watame/plugin/require/RequiresIntents.java b/src/net/foxgenesis/watame/plugin/require/RequiresIntents.java new file mode 100644 index 0000000..89ce1d3 --- /dev/null +++ b/src/net/foxgenesis/watame/plugin/require/RequiresIntents.java @@ -0,0 +1,10 @@ +package net.foxgenesis.watame.plugin.require; + +import java.util.EnumSet; + +import net.dv8tion.jda.api.requests.GatewayIntent; + +public interface RequiresIntents { + + EnumSet getRequiredIntents(); +} diff --git a/src/net/foxgenesis/watame/plugin/require/RequiresMemberCachePolicy.java b/src/net/foxgenesis/watame/plugin/require/RequiresMemberCachePolicy.java new file mode 100644 index 0000000..6bd4e58 --- /dev/null +++ b/src/net/foxgenesis/watame/plugin/require/RequiresMemberCachePolicy.java @@ -0,0 +1,8 @@ +package net.foxgenesis.watame.plugin.require; + +import net.dv8tion.jda.api.utils.MemberCachePolicy; + +public interface RequiresMemberCachePolicy { + + public MemberCachePolicy getPolicy(); +} diff --git a/src/net/foxgenesis/watame/property/PluginPropertyMapping.java b/src/net/foxgenesis/watame/property/PluginPropertyMapping.java index 4b4bf39..0753dd1 100644 --- a/src/net/foxgenesis/watame/property/PluginPropertyMapping.java +++ b/src/net/foxgenesis/watame/property/PluginPropertyMapping.java @@ -28,12 +28,14 @@ public PluginPropertyMapping(@NotNull Guild guild, byte[] data, @NotNull Propert } @SuppressWarnings("exports") - public PluginPropertyMapping(long lookup, @NotNull Blob blob, @NotNull PropertyType type) throws IOException, SQLException { + public PluginPropertyMapping(long lookup, @NotNull Blob blob, @NotNull PropertyType type) + throws IOException, SQLException { super(lookup, blob, type); } @SuppressWarnings("exports") - public PluginPropertyMapping(@NotNull Guild guild, @NotNull Blob blob, @NotNull PropertyType type) throws IOException, SQLException { + public PluginPropertyMapping(@NotNull Guild guild, @NotNull Blob blob, @NotNull PropertyType type) + throws IOException, SQLException { super(guild.getIdLong(), blob, type); } @@ -58,6 +60,6 @@ public Member getAsMember() { public Guild getGuild() { // Hard coded to reduce work on end user - return WatameBot.INSTANCE.getJDA().getGuildById(getLookup()); + return WatameBot.getJDA().getGuildById(getLookup()); } } diff --git a/src/net/foxgenesis/watame/property/impl/CachedPluginProperty.java b/src/net/foxgenesis/watame/property/impl/CachedPluginProperty.java index feabe0b..755fdbc 100644 --- a/src/net/foxgenesis/watame/property/impl/CachedPluginProperty.java +++ b/src/net/foxgenesis/watame/property/impl/CachedPluginProperty.java @@ -37,11 +37,11 @@ public boolean set(Guild lookup, byte[] data, boolean isUserInput) { } return false; } - + @Override public boolean remove(Guild lookup, boolean isUserInput) { checkUserInput(isUserInput); - if(super.remove(lookup, isUserInput)) { + if (super.remove(lookup, isUserInput)) { cache.get(lookup.getIdLong()).set(null); return true; } diff --git a/src/net/foxgenesis/watame/property/impl/PluginPropertyProviderImpl.java b/src/net/foxgenesis/watame/property/impl/PluginPropertyProviderImpl.java index 961392a..0af6a6c 100644 --- a/src/net/foxgenesis/watame/property/impl/PluginPropertyProviderImpl.java +++ b/src/net/foxgenesis/watame/property/impl/PluginPropertyProviderImpl.java @@ -27,8 +27,8 @@ public PluginPropertyProviderImpl(@NotNull LCKPropertyResolver database, long ca @Override public PropertyInfo registerProperty(Plugin plugin, String key, boolean modifiable, PropertyType type) { if (!propertyExists(plugin, key)) - return database.createPropertyInfo(plugin.name, key, modifiable, type); - return database.getPropertyInfo(plugin.name, key); + return database.createPropertyInfo(plugin.getInfo().getID(), key, modifiable, type); + return database.getPropertyInfo(plugin.getInfo().getID(), key); } @Override @@ -41,7 +41,7 @@ public PluginProperty getProperty(Plugin plugin, String key) { PluginProperty cached = inCache(plugin, key); if (cached != null) return cached; - return getProperty(database.getPropertyInfo(plugin.name, key)); + return getProperty(database.getPropertyInfo(plugin.getInfo().getID(), key)); } @Override @@ -58,7 +58,7 @@ public PluginProperty getProperty(PropertyInfo info) { public boolean propertyExists(Plugin plugin, String key) { if (inCache(plugin, key) != null) return true; - return database.isRegistered(plugin.name, key); + return database.isRegistered(plugin.getInfo().getID(), key); } @SuppressWarnings("null") @@ -85,7 +85,8 @@ public PluginProperty getPropertyByID(int id) { */ private PluginProperty inCache(Plugin plugin, String key) { for (PluginProperty pair : map) - if (pair.getInfo().category().equalsIgnoreCase(plugin.name) && pair.getInfo().name().equalsIgnoreCase(key)) + if (pair.getInfo().category().equalsIgnoreCase(plugin.getInfo().getID()) + && pair.getInfo().name().equalsIgnoreCase(key)) return pair; return null; } From f69c633924346fd1aeb0c62545d3f0e98e2bbba9 Mon Sep 17 00:00:00 2001 From: Ashley Date: Tue, 21 Nov 2023 13:24:18 -0600 Subject: [PATCH 2/4] added json api --- .classpath | 3 +- assets/META-INF/integrated.ini | 4 +- pom.xml | 3 +- src/module-info.java | 2 + src/net/foxgenesis/http/ApiKey.java | 10 + src/net/foxgenesis/http/JsonApi.java | 506 ++++++++++++++++++ src/net/foxgenesis/http/Method.java | 5 + .../foxgenesis/property2/PropertyData.java | 29 + .../foxgenesis/property2/PropertyType.java | 9 + .../foxgenesis/util/SystemInformation.java | 23 + src/net/foxgenesis/watame/WatameBot.java | 30 ++ .../watame/command/IntegratedCommands.java | 48 +- .../watame/command/UptimeCommand.java | 20 + .../foxgenesis/watame/plugin/EventStore.java | 40 +- .../watame/plugin/PluginHandler.java | 7 + .../watame/util/AttachmentData.java | 144 +++++ .../foxgenesis/watame/util/DiscordUtils.java | 201 ++++++- .../foxgenesis/watame/util/TimeFormat.java | 267 +++++++++ 18 files changed, 1286 insertions(+), 65 deletions(-) create mode 100644 src/net/foxgenesis/http/ApiKey.java create mode 100644 src/net/foxgenesis/http/JsonApi.java create mode 100644 src/net/foxgenesis/http/Method.java create mode 100644 src/net/foxgenesis/property2/PropertyData.java create mode 100644 src/net/foxgenesis/property2/PropertyType.java create mode 100644 src/net/foxgenesis/util/SystemInformation.java create mode 100644 src/net/foxgenesis/watame/command/UptimeCommand.java create mode 100644 src/net/foxgenesis/watame/util/AttachmentData.java create mode 100644 src/net/foxgenesis/watame/util/TimeFormat.java diff --git a/.classpath b/.classpath index 1758b51..bb17aa2 100644 --- a/.classpath +++ b/.classpath @@ -7,7 +7,7 @@ - + @@ -16,7 +16,6 @@ - diff --git a/assets/META-INF/integrated.ini b/assets/META-INF/integrated.ini index 2e41428..a80ccc7 100644 --- a/assets/META-INF/integrated.ini +++ b/assets/META-INF/integrated.ini @@ -2,4 +2,6 @@ # Enable/Disable the '/options configuration' command enableOptionsCommand = false # Enable/Disable the '/ping' command -enablePingCommand = true \ No newline at end of file +enablePingCommand = true +# Enable/Disable the '/uptime' command +enableUptimeCommand = true \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8e0bdbc..9e2615d 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ net.dv8tion JDA - 5.0.0-beta.12 + 5.0.0-beta.18 org.slf4j @@ -97,7 +97,6 @@ annotations 24.0.1 - org.json diff --git a/src/module-info.java b/src/module-info.java index e0e0b39..1fd9544 100644 --- a/src/module-info.java +++ b/src/module-info.java @@ -23,10 +23,12 @@ requires java.sql; requires com.fasterxml.jackson.databind; requires okhttp3; + requires java.management; exports net.foxgenesis.config; exports net.foxgenesis.database; exports net.foxgenesis.executor; + exports net.foxgenesis.http; exports net.foxgenesis.property; exports net.foxgenesis.property.lck; exports net.foxgenesis.log; diff --git a/src/net/foxgenesis/http/ApiKey.java b/src/net/foxgenesis/http/ApiKey.java new file mode 100644 index 0000000..ab36da8 --- /dev/null +++ b/src/net/foxgenesis/http/ApiKey.java @@ -0,0 +1,10 @@ +package net.foxgenesis.http; + +import org.jetbrains.annotations.NotNull; + +public record ApiKey(@NotNull KeyType type, @NotNull String name, @NotNull String token) { + + public static enum KeyType { + HEADER, PARAMETER + } +} \ No newline at end of file diff --git a/src/net/foxgenesis/http/JsonApi.java b/src/net/foxgenesis/http/JsonApi.java new file mode 100644 index 0000000..056021a --- /dev/null +++ b/src/net/foxgenesis/http/JsonApi.java @@ -0,0 +1,506 @@ +package net.foxgenesis.http; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Request.Builder; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * Abstract class defining common methods used to make calls to a JSON web API. + * + * @author Ashley + */ +public abstract class JsonApi implements AutoCloseable { + /** + * Logger + */ + protected final Logger logger; + + /** + * HTTP client + */ + protected final OkHttpClient client; + + /** + * Name of the API + */ + private final String name; + + /** + * Create a new instance with a new HTTP client. + * + * @param name - name of this API + * + * @see #JsonApi(String, OkHttpClient) + */ + public JsonApi(String name) { + this(name, new OkHttpClient().newBuilder().build()); + } + + /** + * Create a new instance using the specified HTTP client. + * + * @param client - {@link OkHttpClient} to use for requests + * @param name - name of this API + * + * @see #JsonApi(String) + */ + @SuppressWarnings("exports") + public JsonApi(String name, @NotNull OkHttpClient client) { + this.logger = LoggerFactory.getLogger(getClass()); + this.name = Objects.requireNonNull(name); + this.client = Objects.requireNonNull(client); + } + + /** + * Send a GET request to the API using the specified {@code end-point}, query + * {@code parameters} and request {@code body}. The response will then be read + * into the specified java {@code bean}. + * + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * request(Method.PUT, endpoint, parameters, body, javaBean)
+	 * 
+ * + * @param - response java bean + * @param endpoint - (optional) end-point of the API + * @param parameters - (optional) query parameters of the request + * @param body - (optional) request body + * @param javaBean - class of the java bean to use + * + * @return Returns a {@link CompletableFuture} that will complete with the + * specified java {@code bean} or exception. + * + * @see #post(String, Map, Supplier, Class) + * @see #delete(String, Map, Supplier, Class) + * @see #update(String, Map, Supplier, Class) + * @see #put(String, Map, Supplier, Class) + * @see #request(Method, String, Map, Supplier, Class) + */ + @NotNull + protected CompletableFuture get(@Nullable String endpoint, @Nullable Map parameters, + @Nullable Supplier body, @NotNull Class javaBean) { + return request(Method.GET, endpoint, parameters, body, javaBean); + } + + /** + * Send a POST request to the API using the specified {@code end-point}, query + * {@code parameters} and request {@code body}. The response will then be read + * into the specified java {@code bean}. + * + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * request(Method.POST, endpoint, parameters, body, javaBean)
+	 * 
+ * + * @param - response java bean + * @param endpoint - (optional) end-point of the API + * @param parameters - (optional) query parameters of the request + * @param body - (optional) request body + * @param javaBean - class of the java bean to use + * + * @return Returns a {@link CompletableFuture} that will complete with the + * specified java {@code bean} or exception. + * + * @see #get(String, Map, Supplier, Class) + * @see #delete(String, Map, Supplier, Class) + * @see #update(String, Map, Supplier, Class) + * @see #put(String, Map, Supplier, Class) + * @see #request(Method, String, Map, Supplier, Class) + */ + @NotNull + protected CompletableFuture post(@Nullable String endpoint, @Nullable Map parameters, + @Nullable Supplier body, @NotNull Class javaBean) { + return request(Method.POST, endpoint, parameters, body, javaBean); + } + + /** + * Send a DELETE request to the API using the specified {@code end-point}, query + * {@code parameters} and request {@code body}. The response will then be read + * into the specified java {@code bean}. + * + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * request(Method.DELETE, endpoint, parameters, body, javaBean)
+	 * 
+ * + * @param - response java bean + * @param endpoint - (optional) end-point of the API + * @param parameters - (optional) query parameters of the request + * @param body - (optional) request body + * @param javaBean - class of the java bean to use + * + * @return Returns a {@link CompletableFuture} that will complete with the + * specified java {@code bean} or exception. + * + * @see #get(String, Map, Supplier, Class) + * @see #post(String, Map, Supplier, Class) + * @see #update(String, Map, Supplier, Class) + * @see #put(String, Map, Supplier, Class) + * @see #request(Method, String, Map, Supplier, Class) + */ + @NotNull + protected CompletableFuture delete(@Nullable String endpoint, @Nullable Map parameters, + @Nullable Supplier body, @NotNull Class javaBean) { + return request(Method.DELETE, endpoint, parameters, body, javaBean); + } + + /** + * Send an UPDATE request to the API using the specified {@code end-point}, + * query {@code parameters} and request {@code body}. The response will then be + * read into the specified java {@code bean}. + * + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * request(Method.UPDATE, endpoint, parameters, body, javaBean)
+	 * 
+ * + * @param - response java bean + * @param endpoint - (optional) end-point of the API + * @param parameters - (optional) query parameters of the request + * @param body - (optional) request body + * @param javaBean - class of the java bean to use + * + * @return Returns a {@link CompletableFuture} that will complete with the + * specified java {@code bean} or exception. + * + * @see #get(String, Map, Supplier, Class) + * @see #post(String, Map, Supplier, Class) + * @see #delete(String, Map, Supplier, Class) + * @see #put(String, Map, Supplier, Class) + * @see #request(Method, String, Map, Supplier, Class) + */ + @NotNull + protected CompletableFuture update(@Nullable String endpoint, @Nullable Map parameters, + @Nullable Supplier body, @NotNull Class javaBean) { + return request(Method.UPDATE, endpoint, parameters, body, javaBean); + } + + /** + * Send a PUT request to the API using the specified {@code end-point}, query + * {@code parameters} and request {@code body}. The response will then be read + * into the specified java {@code bean}. + * + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * request(Method.PUT, endpoint, parameters, body, javaBean)
+	 * 
+ * + * @param - response java bean + * @param endpoint - (optional) end-point of the API + * @param parameters - (optional) query parameters of the request + * @param body - (optional) request body + * @param javaBean - class of the java bean to use + * + * @return Returns a {@link CompletableFuture} that will complete with the + * specified java {@code bean} or exception. + * + * @see #get(String, Map, Supplier, Class) + * @see #post(String, Map, Supplier, Class) + * @see #update(String, Map, Supplier, Class) + * @see #delete(String, Map, Supplier, Class) + * @see #request(Method, String, Map, Supplier, Class) + */ + @NotNull + protected CompletableFuture put(@Nullable String endpoint, @Nullable Map parameters, + @Nullable Supplier body, @NotNull Class javaBean) { + return request(Method.PUT, endpoint, parameters, body, javaBean); + } + + /** + * Send a request to the API using the specified {@code method}, + * {@code end-point}, query {@code parameters} and request {@code body}. The + * response will then be read into the specified java {@code bean}. + * + * @param - response java bean + * @param method - HTTP method to use + * @param endpoint - (optional) end-point of the API + * @param parameters - (optional) query parameters of the request + * @param body - (optional) request body + * @param javaBean - class of the java bean to use + * + * @return Returns a {@link CompletableFuture} that will complete with the + * specified java {@code bean} or exception. + */ + @NotNull + protected CompletableFuture request(@NotNull Method method, @Nullable String endpoint, + @Nullable Map parameters, @Nullable Supplier body, + @NotNull Class javaBean) { + return submit(createRequest(method, endpoint, parameters, body)) + .thenApply(response -> readJSONResponse(response, javaBean)); + } + + /** + * Get the API token that should be included in all requests. + * + * @return Returns the {@link ApiKey} that will be included in all requests + */ + @Nullable + protected abstract ApiKey getApiKey(); + + /** + * Get the base URL of the API. + * + * @return Returns a string containing the URL pointing to the root of the API + */ + @NotNull + protected abstract String getBaseURL(); + + /** + * Get a map of query parameters to add to every request. + * + * @return Returns a {@link Map} containing query parameters to include in all + * requests + */ + @Nullable + protected abstract Map getDefaultQueryParameters(); + + /** + * Merge a query parameter with one that was specified in + * {@link #getDefaultQueryParameters()}. + * + * @param key - parameter key + * @param specified - specified parameter value + * @param defaultValue - default parameter value + * + * @return Returns the merge of {@code specified} and {@code defaultValue} + */ + protected String mergeQueryParameter(@NotNull String key, @NotNull String specified, + @Nullable String defaultValue) { + return specified + (defaultValue != null ? " " + defaultValue : ""); + } + + /** + * Create a new request using the specified method, end-point, query parameters, + * and request body. + * + * @param method - HTTP method to use + * @param endpoint - (optional) end-point of the API + * @param params - (optional) query parameters + * @param body - (optional) request body + * + * @return Returns the created {@link Request} + * + * @throws NullPointerException If {@code method} is {@code null} or + * {@link HttpUrl#parse(String)} returns + * {@code null} + */ + @NotNull + private Request createRequest(@NotNull Method method, @Nullable String endpoint, + @Nullable Map params, @Nullable Supplier body) { + Objects.requireNonNull(method); + + // Add end point if present + String tmp = getBaseURL(); + if (!(endpoint == null || endpoint.isBlank())) + tmp += '/' + endpoint; + + // Construct a new request builder + Builder builder = new Request.Builder(); + Objects.requireNonNull(HttpUrl.parse(tmp)).newBuilder(); + HttpUrl.Builder httpBuilder = Objects.requireNonNull(HttpUrl.parse(tmp)).newBuilder(); + + // Add request body if specified + if (body != null) { + // Construct and add body + RequestBody b = body.get(); + builder.method(method.name().toUpperCase(), b); + + // Add body content type + MediaType type = b.contentType(); + if (type != null) + builder.addHeader("Content-Type", type.toString()); + } else + builder.addHeader("Content-Type", "application/json"); + + // Merge specified query parameters with the default ones and add to builder + Map copyMap = params != null ? new HashMap<>(params) : new HashMap<>(); + Map defaultMap = getDefaultQueryParameters(); + if (defaultMap != null) + defaultMap.forEach((key, def) -> copyMap.merge(key, def, (v1, v2) -> mergeQueryParameter(key, v1, v2))); + copyMap.forEach(httpBuilder::addEncodedQueryParameter); + + // Add API key if present + ApiKey key = getApiKey(); + if (key != null) + switch (key.type()) { + case HEADER -> builder.addHeader(key.name(), key.token()); + case PARAMETER -> httpBuilder.addQueryParameter(key.name(), key.token()); + default -> throw new IllegalArgumentException("Unkown key type: " + key.type()); + } + + // Construct URL + builder.url(httpBuilder.build()); + + // Build the request + return builder.build(); + } + + /** + * Enqueue a {@link Request} and map it's {@link Callback} to a + * {@link CompletableFuture}. + * + * @param request - request to enqueue + * + * @return Returns a {@link CompletableFuture} that will complete normally when + * the {@link Callback#onResponse(Call, Response)} is called. Otherwise + * will complete exceptionally if the + * {@link Callback#onFailure(Call, IOException)} is called. + */ + @NotNull + private CompletableFuture submit(@NotNull Request request) { + Objects.requireNonNull(client); + logger.debug("{} {}", request.method().toUpperCase(), request.url().toString()); + + CompletableFuture callback = new CompletableFuture<>(); + client.newCall(request).enqueue(new FutureCallback(callback)); + return callback; + } + + /** + * Map a {@link Response} to a JavaBean. The specified response's content type + * must be {@code application/json}. + * + * @param JavaBean class + * @param response - response to get data from + * @param javaBean - JavaBean class to map JSON to + * + * @return Returns an instance of the {@code javaBean} that was constructed with + * the response's {@link okhttp3.ResponseBody#string() body} content + */ + @Nullable + private T readJSONResponse(@NotNull Response response, @NotNull Class javaBean) { + // Ensure content type is application/json + try (ResponseBody body = response.body()) { + if (body == null) + throw new NullPointerException("Empty body"); + + MediaType type = body.contentType(); + if (type != null && !type.subtype().equals("json")) + throw new CompletionException( + new IOException("Returned content type is not application/json: " + type)); + + String b = body.string(); + logger.debug(b); + + // Check for empty body + if (b.equals("[]") || b.equals("{}")) + return null; + + // Construct the JavaBean from the response body + ObjectMapper mapper = new ObjectMapper(); + try (JsonParser parser = mapper.createParser(b)) { + return mapper.readValue(parser, javaBean); + } + } catch (IOException e) { + throw new CompletionException(e); + } + } + + /** + * Get the executor used for asynchronous calls. + * + * @return Returns the {@link Executor} used for asynchronous calls. + */ + public Executor getExecutor() { + return client.dispatcher().executorService(); + } + + /** + * Get the name of this API. + * + * @return Returns the APIs name + */ + public String getName() { + return name; + } + + @Override + public void close() { + client.dispatcher().executorService().shutdown(); + } + + @Override + public int hashCode() { + return Objects.hash(client, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JsonApi other = (JsonApi) obj; + return Objects.equals(client, other.client) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "JsonApi [" + (name != null ? "name=" + name + ", " : "") + (client != null ? "client=" + client : "") + + "]"; + } + + /** + * Class that passes {@link Callback#onResponse(Call, Response)} and + * {@link Callback#onFailure(Call, IOException)} to the specified + * {@link CompletableFuture}. + */ + private static class FutureCallback implements Callback { + private final CompletableFuture callback; + + public FutureCallback(CompletableFuture callback) { + this.callback = Objects.requireNonNull(callback); + } + + @Override + public void onFailure(Call arg0, IOException arg1) { + callback.completeExceptionally(arg1); + } + + @Override + public void onResponse(Call arg0, Response arg1) throws IOException { + callback.complete(arg1); + } + } +} diff --git a/src/net/foxgenesis/http/Method.java b/src/net/foxgenesis/http/Method.java new file mode 100644 index 0000000..080fc73 --- /dev/null +++ b/src/net/foxgenesis/http/Method.java @@ -0,0 +1,5 @@ +package net.foxgenesis.http; + +public enum Method { + GET, PUT, POST, DELETE, UPDATE +} diff --git a/src/net/foxgenesis/property2/PropertyData.java b/src/net/foxgenesis/property2/PropertyData.java new file mode 100644 index 0000000..0415e65 --- /dev/null +++ b/src/net/foxgenesis/property2/PropertyData.java @@ -0,0 +1,29 @@ +package net.foxgenesis.property2; + +public interface PropertyData extends Comparable { + String getDisplayName(); + + String getFieldName(); + + String getDescription(); + + String getCategory(); + + String getKeywords(); + + String getSuffix(); + + String getPlaceholder(); + + T getDefaultValue(); + + T getMinValue(); + + T getMaxValue(); + + String getInputType(); + + default PropertyType getType() { + return PropertyType.valueOf(getInputType()); + } +} diff --git a/src/net/foxgenesis/property2/PropertyType.java b/src/net/foxgenesis/property2/PropertyType.java new file mode 100644 index 0000000..85c29e3 --- /dev/null +++ b/src/net/foxgenesis/property2/PropertyType.java @@ -0,0 +1,9 @@ +package net.foxgenesis.property2; + +public enum PropertyType { + TEXT, + CHECKBOX, + ENUM, + PASSWORD, + NUMBER +} diff --git a/src/net/foxgenesis/util/SystemInformation.java b/src/net/foxgenesis/util/SystemInformation.java new file mode 100644 index 0000000..aaa60d6 --- /dev/null +++ b/src/net/foxgenesis/util/SystemInformation.java @@ -0,0 +1,23 @@ +package net.foxgenesis.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class SystemInformation { + public static synchronized Path dumpSystemInformation(Path path) throws IOException { + if (path == null) + path = Path.of("sysinfo.txt"); + + if (Files.exists(path)) + return path; + + ProcessBuilder builder = new ProcessBuilder("msinfo32", "/report", path.toAbsolutePath().toString()); + builder.start().onExit().join(); + + while (!Files.exists(path)) + Thread.onSpinWait(); + + return path; + } +} diff --git a/src/net/foxgenesis/watame/WatameBot.java b/src/net/foxgenesis/watame/WatameBot.java index 080af86..8069a29 100644 --- a/src/net/foxgenesis/watame/WatameBot.java +++ b/src/net/foxgenesis/watame/WatameBot.java @@ -176,6 +176,36 @@ public static Plugin getPluginByClass(Class pluginClass) { return INSTANCE.pluginHandler.getPlugin(pluginClass); } + /** + * Register event listeners to the calling {@link Plugin}. + * + * @param listeners - event listeners to register + * + * @see #removeEventListeners(Object...) + */ + public static void addEventListeners(Object... listeners) { + Class callerClass = walker.getCallerClass(); + Module module = callerClass.getModule(); + Plugin plugin = INSTANCE.pluginHandler.getPluginForModule(module); + + INSTANCE.context.getEventRegister().registerListeners(plugin, listeners); + } + + /** + * Unregister event listeners from the calling {@link Plugin}. + * + * @param listeners - event listeners to unregister + * + * @see #addEventListeners(Object...) + */ + public static void removeEventListeners(Object... listeners) { + Class callerClass = walker.getCallerClass(); + Module module = callerClass.getModule(); + Plugin plugin = INSTANCE.pluginHandler.getPluginForModule(module); + + INSTANCE.context.getEventRegister().unregisterListeners(plugin, listeners); + } + /** * Get the database manager used to register custom databases. * diff --git a/src/net/foxgenesis/watame/command/IntegratedCommands.java b/src/net/foxgenesis/watame/command/IntegratedCommands.java index 64dec4a..90987b5 100644 --- a/src/net/foxgenesis/watame/command/IntegratedCommands.java +++ b/src/net/foxgenesis/watame/command/IntegratedCommands.java @@ -25,38 +25,25 @@ @PluginConfiguration(defaultFile = "/META-INF/integrated.ini", identifier = "integrated", outputFile = "integrated.ini", type = ConfigType.INI) public class IntegratedCommands extends Plugin implements CommandProvider { - private final boolean enableConfigCommand; - private final boolean enablePingCommand; - - public IntegratedCommands() { - super(); - - if (hasConfiguration("integrated")) { - Configuration config = getConfiguration("integrated"); - enableConfigCommand = config.getBoolean("IntegratedPlugin.enableOptionsCommand", false); - enablePingCommand = config.getBoolean("IntegratedPlugin.enablePingCommand", true); - } else { - enableConfigCommand = false; - enablePingCommand = true; - } - } - @Override public void preInit() {} @Override public void init(IEventStore builder) { - if (enableConfigCommand) - builder.registerListeners(this, new ConfigCommand()); + if (hasConfiguration("integrated")) { + Configuration config = getConfiguration("integrated"); - if (enablePingCommand) - builder.registerListeners(this, new PingCommand()); + if (config.getBoolean("IntegratedPlugin.enableOptionsCommand", false)) + builder.registerListeners(this, new ConfigCommand()); + if (config.getBoolean("IntegratedPlugin.enablePingCommand", true)) + builder.registerListeners(this, new PingCommand()); + if (config.getBoolean("IntegratedPlugin.enableUptimeCommand", true)) + builder.registerListeners(this, new UptimeCommand()); + } } @Override - public void postInit() { - - } + public void postInit() {} @Override public void onReady() {} @@ -66,7 +53,20 @@ public void close() {} @Override public Collection getCommands() { - return List.of(Commands.slash("ping", "Ping the bot to test the connection"), getOptionsCommand()); + List commands = new ArrayList<>(); + + if (hasConfiguration("integrated")) { + Configuration config = getConfiguration("integrated"); + + if (config.getBoolean("IntegratedPlugin.enableOptionsCommand", false)) + commands.add(getOptionsCommand()); + if (config.getBoolean("IntegratedPlugin.enablePingCommand", true)) + commands.add(Commands.slash("ping", "Ping the bot to test the connection")); + if (config.getBoolean("IntegratedPlugin.enableUptimeCommand", true)) + commands.add(Commands.slash("uptime", "Get the uptime of the application")); + } + + return commands; } diff --git a/src/net/foxgenesis/watame/command/UptimeCommand.java b/src/net/foxgenesis/watame/command/UptimeCommand.java new file mode 100644 index 0000000..58b9168 --- /dev/null +++ b/src/net/foxgenesis/watame/command/UptimeCommand.java @@ -0,0 +1,20 @@ +package net.foxgenesis.watame.command; + +import java.lang.management.ManagementFactory; + +import net.foxgenesis.watame.util.Response; + +import org.apache.commons.lang3.time.DurationFormatUtils; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; + +public class UptimeCommand extends ListenerAdapter { + @Override + public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { + if (event.getName().equals("uptime")) + event.replyEmbeds(Response.info(DurationFormatUtils + .formatDuration(ManagementFactory.getRuntimeMXBean().getUptime(), "DD:HH:MM:SS", true))) + .setEphemeral(true).queue(); + } +} diff --git a/src/net/foxgenesis/watame/plugin/EventStore.java b/src/net/foxgenesis/watame/plugin/EventStore.java index 3c44011..e93b6a3 100644 --- a/src/net/foxgenesis/watame/plugin/EventStore.java +++ b/src/net/foxgenesis/watame/plugin/EventStore.java @@ -25,7 +25,7 @@ public EventStore(JDABuilder builder) { } public void register(Plugin plugin) { - store.putIfAbsent(plugin, new HashSet<>()); + store.putIfAbsent(plugin, Collections.synchronizedSet(new HashSet<>())); } public void unregister(Plugin plugin) { @@ -53,15 +53,14 @@ public void registerListeners(Plugin plugin, Object... listener) { if (!store.containsKey(plugin)) throw new IllegalArgumentException("Provided plugin is not registered!"); Set listeners = store.get(plugin); - synchronized (listeners) { - logger.debug("Adding {} listeners from {}", listener.length, plugin.getInfo().getDisplayName()); - Collections.addAll(listeners, listener); - builder.addEventListeners(listener); - - if (jda != null) { - logger.debug("Adding {} listeners from {} to JDA", listener.length, plugin.getInfo().getDisplayName()); - jda.addEventListener(listener); - } + + logger.debug("Adding {} listeners from {}", listener.length, plugin.getInfo().getDisplayName()); + Collections.addAll(listeners, listener); + + builder.addEventListeners(listener); + if (jda != null) { + logger.debug("Adding {} listeners from {} to JDA", listener.length, plugin.getInfo().getDisplayName()); + jda.addEventListener(listener); } } @@ -70,18 +69,15 @@ public void unregisterListeners(Plugin plugin, Object... listener) { if (!store.containsKey(plugin)) throw new IllegalArgumentException("Provided plugin is not registered!"); Set listeners = store.get(plugin); - synchronized (listeners) { - logger.debug("Removing {} listeners from {}", listener.length, plugin.getInfo().getDisplayName()); - for (Object l : listener) - listeners.remove(l); - builder.removeEventListeners(listener); - - if (jda != null) { - logger.debug("Removing {} listeners from {} in JDA", listener.length, - plugin.getInfo().getDisplayName()); - jda.removeEventListener(listener); - } - + + logger.debug("Removing {} listeners from {}", listener.length, plugin.getInfo().getDisplayName()); + for (Object l : listener) + listeners.remove(l); + + builder.removeEventListeners(listener); + if (jda != null) { + logger.debug("Removing {} listeners from {} in JDA", listener.length, plugin.getInfo().getDisplayName()); + jda.removeEventListener(listener); } } diff --git a/src/net/foxgenesis/watame/plugin/PluginHandler.java b/src/net/foxgenesis/watame/plugin/PluginHandler.java index f08d539..8e1fc8d 100644 --- a/src/net/foxgenesis/watame/plugin/PluginHandler.java +++ b/src/net/foxgenesis/watame/plugin/PluginHandler.java @@ -151,6 +151,13 @@ public void loadPlugins() { return providers.values(); } + /** + * Construct all plugins from the provided list of providers. + * + * @param providers - collection of plugin providers + * + * @return Returns a collection of all constructed plugins + */ private Collection<@NotNull T> construct(Collection> providers) { // Copy list List> list = new ArrayList<>(providers); diff --git a/src/net/foxgenesis/watame/util/AttachmentData.java b/src/net/foxgenesis/watame/util/AttachmentData.java new file mode 100644 index 0000000..0e4716f --- /dev/null +++ b/src/net/foxgenesis/watame/util/AttachmentData.java @@ -0,0 +1,144 @@ +package net.foxgenesis.watame.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; + +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Message.Attachment; + +public class AttachmentData { + private static final Set IMAGE_EXTENSIONS = new HashSet<>( + Arrays.asList("jpg", "jpeg", "png", "gif", "webp", "tiff", "svg", "apng")); + private static final Set VIDEO_EXTENSIONS = new HashSet<>( + Arrays.asList("webm", "flv", "vob", "avi", "mov", "wmv", "amv", "mp4", "mpg", "mpeg", "gifv")); + + public final Message message; + + private final Attachment attachment; + private final URL url; + + private final String fileName; + private final String extension; + + private final boolean isVideo; + private final boolean isImage; + + @SuppressWarnings("resource") + public AttachmentData(Message message, Attachment attachment) { + this(message, Objects.requireNonNull(attachment), null); + } + + public AttachmentData(Message message, URL url) { + this(message, null, Objects.requireNonNull(url)); + } + + private AttachmentData(Message message, Attachment attachment, URL url) { + this.message = Objects.requireNonNull(message); + + if (attachment == null && url == null) + throw new IllegalArgumentException("No attachment or URL provided!"); + + this.url = url; + this.attachment = attachment; + + fileName = calculateFileName(); + extension = calculateFileExtension(); + isVideo = this.attachment != null ? this.attachment.isVideo() : VIDEO_EXTENSIONS.contains(getFileExtension()); + isImage = isVideo ? false + : this.attachment != null ? this.attachment.isImage() : IMAGE_EXTENSIONS.contains(getFileExtension()); + } + + public CompletableFuture getData() { + return getData(null); + } + + public CompletableFuture getData(Executor executor) { + return openAsyncConnection().thenApplyAsync(in -> { + try (in) { + return in.readAllBytes(); + } catch (IOException e) { + throw new CompletionException(e); + } + }, executor == null ? ForkJoinPool.commonPool() : executor); + } + + public String getFileExtension() { + return extension; + } + + public String getFileName() { + return fileName; + } + + public boolean isVideo() { + return isVideo; + } + + public boolean isImage() { + return isImage; + } + + public Message getMessage() { + return message; + } + + private String calculateFileName() { + if (attachment != null) + return attachment.getFileName(); + + String p = url.getFile(); + + if (p.isBlank()) + return ""; + + int l = p.lastIndexOf('/'); + return l == -1 ? "" : p.substring(l); + } + + private String calculateFileExtension() { + if (attachment != null) + return attachment.getFileExtension(); + + String p = url.getFile(); + + if (p.isBlank()) + return ""; + + int l = p.lastIndexOf('.'); + return l == -1 ? "" : p.substring(l + 1); + } + + @SuppressWarnings("resource") + public CompletableFuture openAsyncConnection() { + if (attachment != null) + return attachment.getProxy().download(); + + try { + return CompletableFuture.completedFuture(url.openStream()); + } catch (IOException e) { + return CompletableFuture.failedFuture(e); + } + } + + public InputStream openConnection() throws IOException { + if (attachment != null) + return attachment.getProxy().download().join(); + return url.openStream(); + } + + @Override + public String toString() { + return "AttachmentData [" + (message != null ? "message=" + message + ", " : "") + + (attachment != null ? "attachment=" + attachment + ", " : "") + + (url != null ? "url=" + url : "") + "]"; + } +} \ No newline at end of file diff --git a/src/net/foxgenesis/watame/util/DiscordUtils.java b/src/net/foxgenesis/watame/util/DiscordUtils.java index 0c55c56..442353a 100644 --- a/src/net/foxgenesis/watame/util/DiscordUtils.java +++ b/src/net/foxgenesis/watame/util/DiscordUtils.java @@ -1,40 +1,213 @@ package net.foxgenesis.watame.util; +import java.util.ArrayList; +import java.util.List; + +import net.foxgenesis.util.StringUtils; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; + +/** + * Utility class containing common methods for Discord. + */ public final class DiscordUtils { /** - * {@value} + * Default avatar image in base 64. + * + * @see #DEFAULT_AVATAR_URL */ public static final String DEFAULT_AVATAR = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAclBMVEVyidr///9uhtlvh9lshNlpgtj4+f37/P7z9fx0i9t5j9zs7/p2jdvl6fh9kt33+P3Q1/KHmt/a4PXq7fmtuumBlt7Ayu6VpeLj5/eKneCbquTc4fWksubU2/PH0PC1wOugr+a7xeyyvurEze+QouKqtudB7vlHAAAKAklEQVR4nO2d63ayOhCGMRMEBJHz+Szc/y1u0H4VrYEgQV1rz/OvXaXJS5LJZDIJkoQgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgyFqAEEoILHpg0d9/DoBBHCV+WHXVgbvKYGZd1ZrDk73Qr1UKfSMczLg7p6fdhUDjrCuY1vUJNQ0y2zTgK0UC0ezQs/TdD4oaBT5vRb3T7oZbeKGvEbJpdZfSd007C1Llt5Z6XocS5RWo2Y0V7Uci905d2V80MIEeqiC91VDN60SjS+pHqBR3ljvSuDs5dWJ8h0ZCzcbRb1WL6sT/8/rhYizpL+SPRQFqhE2ujEXquXfg7gebAbJ51kcdLK9sbSSvV0apLBPTbpPMK8v6Qtl4WdLGB1mWR5NKb6nMsFbvGtKttc9qJLIRjOQpRWzApcaXSaMXAH5VFql+2u/3yoXr3w30vzrpztkLzUHn1XwCaAdPH2vcqeUHNQL4wahbqU1fF7iok4yDn9T3A2uC3nq2pqENModGT9K7zrovzc/MHkD80etW9Mbo3/WgzoyTxlL/yJhBSesq9geVEiGJc/e8m5kfsDlEytJb9Y51bxN6eUZclZb+p/qcnJwgaw99l+015nca80R6s0Qg7fnWlY613797qrVeESl/6r0I5WiVodHbH6nLxzOkWttvbUbQvOOt8KDtX7ocl/lxpbwf3LyMe8/d9KLxb9Psjc1IbOv2fq3WoLKR5boYeRf2etpJMrHLsTd3Oh/e5cmR8GYk1azXZxfjKVEMil77Mtj5+HdH+z0SSXYrszhQLYkeaycKp3f+qjuL075DIpDfMt1MspuXLScPx870rVH3P75DIfV+SlOsNgwWz3uLNTZtOXqJ1fYSQfopTw2yYnN9F4116fz+kG5vUGl3LUq3lvstrxJZvxJPmzciSD92Rd10/D2wv9nqfOtGJN1pqirboyfbNiJIznwltqXYtg1J9bbBx8INt2xE0IpPC9zt6g0F3vlrHyOyt+yn5aflDWTbKST+x+3MgMW/XbAUqD4t7kq8la0B4wvszEC9VRsSW/gq8DVOW/k14M0X/h428msAjvNlvwdH3kZh+2lhvyj+Jo1Iz58WdqOkWygkH15VjNkkmkGST8saoW4RkpLz+YLfRyG+m4L58XXTmKN4z416XzLdX1Ez4d2UfFUn3e3OohWC/QUrwzEpdyoLJ8T7orliQBUdViTWpyU9UotVCPFm2y+vkovtpiT7sk7ad1PBMbfg04L+4olsQ/C/bK4YKERO+iT8Kofmii5yRxi6T8t5hsCB+DUhqHsEBr+J/2UOzZWINwd5HgiXFq4sTz154RFTnMJmSbknN7LOgZW6/FOoenSK4GxFC5NWhDluoC2YK9Tcs4lMqUz8jnMfXLcyc0irlSEunSUahS2DweBvjTQz/qWDEqpVPK/GCuEnZQ2Amt6CkKUuKqgIMXeZhT1OdwXqz/pC+/Iulwtou6DDiJrzKfcwPD+OfTDq6SdO3aNBJD7/MkbUQJR599SKv6l1oE22olL+zalc4CKKGogypx13nkWiiTFV3+JZfjOJeceioIFIfL7i1OrpGyUhO/OGsWFNG06LehIzI45zEafIGfnmhOnyKeXzNoBDynrknr2YTSjO/QpmaRCzpkWd1QTU4xsYSi1kIMp8o+LIzMOmLEvFNBRw4Fyt5SIGIhh8pQXM10lYO6vs5Y/MOWMcDQEDkYR8w55dXbAZj7Cz75kv5QG9FaCQNnxjYmIpw5huXHYX43Wj1E6AqZE5V78TI0J+vrycGERw4CtUiKmhfJFSdUrh838x4ZEA4VO4s9a3IZh86/sPKXTWO9/Q8iUDK8t7qTXRSzVOhVG8WiHNOKemiRN08nNrHE0oZJnfR/T1ATdacvqI7JcJ5vMnJpqd11PsV1/rFfIGEhv2jM9K+GO/FF4DvtutN6bAu1hLmS1CWQ4Ks3YA3CcBzmvHIZicbv7uxMqJJD5rJLusDDxScQekViebLtg4LFgKa5ZTtO+eN+KS4J6zdhtxav36AMOsTa3YU/PpI9z2eydgulhy/OBphwFtYnmpPN2qhiXJ1qunC7IgjeZZWEki3dQ/eJZvAMaS7djVB6FovaA0pftzWwSppqPJbvv4CJEW7SHs1qYOkWXb241xVx5o2dzSS70/hw7ksHBD3VuncPHOYWFL/xoFCNj1/BNK6f9eAgJEWxLyvrByFxEOS/No3Do8DJdEUWK0Jd9M43jxcAJ/OKCeLD+UGqwLZLySouBaQdNlTWDx77FEVu11XXnOXzjUuDJhgdi8Ls0de1VdmsnYP/LagX5rXVT4C3OhHlnp1ED7lTv4Y6J1SScLnLZP4a5UmHxhqtA9+jrHdM4l+QLUdUFhUk0YOP197auzB8t+pcKpeIlbvzSVvEDeTHSllQonE9qsuHxHJ9Y9e2rOCjdUuAs0e/vs6MCfjhWtWyDOKOzdXvk1r4cby5SN6be4rcJdbRBSpVsdNjk5oUzMmW6yscLetSdE6/ItzKpuJaRfgc2Ng5UK532aol/eUa3ivmCPl+ic9EtN2s7FbFZeHgXafFDfCelw1V5Yp+Ju+9rnTQzDHXDV3BLs1K3RNzQixxJY9yTol+eSX53FdFY9SPwhUkCk+QXxefVOPvjzC1nFMoeEPUIMP1tvWZ3MNy43TNJ4vlc4zyOuy1qRGZQfceou13P2HYscvDU3C0aeRi7JmACHet5EH4UcnCExj+OShsY1ngRENjNLPS2WeVJTz5Sv2ZuEmBmH6XJtMal7lO+0hZX8u4OzFyklgXPkDmUoqtubzsOPvOHi0Ion7K0LuyCDJFxTwd7qbPorkhI/Ka18LndbUY+OVVexdHsS4oYr/nVsxR0GhoQvbrZPg1Cj/17sJT4YJ10ZWE7kPgaaTnovrai9qvWB/ouY9q1vVJxXhKYiz3MDzE68/9DTetx3LpdAawc/bsOkyjqvKcuy8bwuq5KwtU3j/k5oOCTniHPGsQRfNURM7uD38e/ouNwPfBWj9Vx/GGzm4zZHzOsZKYGAaeKh8Mnl/ojTmq0Swpl2qSZb3EhLTZ7xwcqK5UTmSWZVcm2r61ua+U5krbNvQGedIsXt5K0uGQISFzNrjWitmwhzp8jcYJurW34gUjJ5pae+3oLDZIhWP4cbX18OxEjYK4i9gKvU+gUbc7jrdWhs2YA/NSBG2zAcgFrEiUDmrmzkxdp7LvUe7s1viyfO2MqNrn8Q/4m1UYP4rV9n6d1OOa6P+7tjkakoE0DuJn5F2UelL9P3fz4AqGwmde7qqrofhAq8dvN6x7WyV1X9aNWJIX/sAxBDU4IfZk1dWDkjheslaJNb59rLWp/Ii77nsgk/31eRhRq54Rgq/eZvPyEIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiDI/5b/APgEpXh5GhuhAAAAAElFTkSuQmCC"; /** - * {@value} + * Default avatar image URL. + * + * @see #DEFAULT_AVATAR */ public static final String DEFAULT_AVATAR_URL = "https://cdn.discordapp.com/embed/avatars/index.png"; + /** - * {@value} + * Get all attachments from a {@link Message}. + * + * @param message - message to check + * + * @return Returns a {@link List} of {@link AttachmentData} */ - public static final String PROFILE_LINK = "discord://-/users/%18d"; + public static List getAttachments(Message message) { + List attachments = new ArrayList<>(); + message.getAttachments().forEach(a -> attachments.add(new AttachmentData(message, a))); + StringUtils.findURLs(message.getContentRaw()).forEach(u -> attachments.add(new AttachmentData(message, u))); + return attachments; + } /** - * {@value} + * Get a link to a {@link User}'s profile via the discord protocol. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "discord://-/users/%18d".formatted(member.getIdLong())
+	 * 
+ * + * @param member - member to get profile of + * + * @return Returns a URL using the discord protocol pointing to the specified + * member's profile */ - public static final String MENTION_USER = "<@%18d>"; + public static String getProfileProtocolLink(User member) { + return getProfileProtocolLink(member.getIdLong()); + } /** - * {@value} + * Get a link to a member's profile via the discord protocol. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "discord://-/users/%18d".formatted(memberID)
+	 * 
+ * + * @param memberID - member id + * + * @return Returns a URL using the discord protocol pointing to the specified + * member's profile */ - public static final String MENTION_CHANNEL = "<#%18d>"; + public static String getProfileProtocolLink(long memberID) { + return "discord://-/users/%18d".formatted(memberID); + } /** - * {@value} + * Get a link to a {@link Guild} via the discord protocol. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "discord://-/channels/%18d".formatted(guild.getIdLong())
+	 * 
+ * + * @param guild - guild to get link for + * + * @return Returns a URl using the discord protocol pointing to the specified + * guild */ - public static final String MENTION_ROLE = "<@&%18d>"; + public static String getGuildProtocolLink(Guild guild) { + return "discord://-/channels/%18d".formatted(guild.getIdLong()); + } + + /** + * Mention a channel via its id. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "<#%18d>".formatted(channelID)
+	 * 
+ * + * @param channelID - channel id + * + * @return Returns a mention to the specified channel + */ + public static String mentionChannel(long channelID) { + return "<#%18d>".formatted(channelID); + } + + /** + * Mention a user via it's id. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "<@%18d>".formatted(userID)
+	 * 
+ * + * @param userID - user id + * + * @return Returns a mention to the specified user + */ + public static String mentionUser(long userID) { + return "<@%18d>".formatted(userID); + } + + /** + * Mention a role via its id. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "<@&%18d>".formatted(roleID)
+	 * 
+ * + * @param roleID - role id + * + * @return Returns a mention to the specified role + */ + public static String mentionRole(long roleID) { + return "<@&%18d>".formatted(roleID); + } + + /** + * Mention a slash command via its name and id. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "</%s:%18d>".formatted(name, commandID)
+	 * 
+ * + * @param name - command name + * @param commandID - command id + * + * @return Returns a mention to the specified slash command + */ + public static String mentionSlashCommand(String name, long commandID) { + return "".formatted(name, commandID); + } + /** - * {@value} + * Mention a slash command via its name, sub command and id. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "</%s:%18d>".formatted(command + " " + subCommand, commandID)
+	 * 
+ * + * @param command - command name + * @param subCommand - sub command name + * @param commandID - command id + * + * @return Returns a mention to the specified slash command */ - public static final String MENTION_SLASH_COMMAND = ""; + public static String mentionSlashCommand(String command, String subCommand, long commandID) { + return mentionSlashCommand(command + " " + subCommand, commandID); + } /** - * {@value} + * Mention a slash command via its name, group, sub command and id. + *

+ * This method is effectively equivalent to: + *

+ * + *
+	 * "</%s:%18d>".formatted(command + " " + group + " " + subCommand, commandID)
+	 * 
+ * + * @param command - command name + * @param group - command group + * @param subCommand - sub command + * @param commandID - command id + * + * @return Returns a mention to the specified slash command */ - public static final String MENTION_SLASH_COMMAND_WITH_GROUPS = ""; + public static String mentionSlashCommand(String command, String group, String subCommand, long commandID) { + return mentionSlashCommand(command + " " + group, subCommand, commandID); + } } diff --git a/src/net/foxgenesis/watame/util/TimeFormat.java b/src/net/foxgenesis/watame/util/TimeFormat.java new file mode 100644 index 0000000..f5fbf7a --- /dev/null +++ b/src/net/foxgenesis/watame/util/TimeFormat.java @@ -0,0 +1,267 @@ +package net.foxgenesis.watame.util; + +import java.util.Objects; + +/** + * Enumeration for creating Discord timestamps, which can be useful for + * specifying a date/time across multiple users time zones. + *

+ * Discord timestamps are created by specifying the Unix {@code time} in + * seconds and the display {@code style}.

<t : time : + * style>
There are multiple styles to choose from and will + * display differently based on the user's clock style. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Format table for the Unix timestamp of 1543392060
StyleDiscord TimestampOutput (12-hour clock)Output (24-hour clock)
Default{@code }November 28, 2018 9:01 AM28 November 2018 09:01
Short Time{@code }9:01 AM09:01
Long Time{@code }9:01:00 AM09:01:00
Short Date{@code }11/28/201828/11/2018
Long Date{@code }November 28, 201828 November 2018
Short Date/Time{@code }November 28, 2018 9:01 AM28 November 2018 09:01
Long Date/Time{@code }Wednesday, November 28, 2018 9:01 AMWednesday, 28 November 2018 09:01
Relative{@code }3 years ago3 years ago
+ * + * @author Ashley + */ +public enum TimeFormat { + /** + * The default display style. + * + * + * + * + * + * + * + * + * + * + *
Output for the Unix time of 1543392060
Output (12-hour clock)Output (24-hour clock)
November 28, 2018 9:01 AM28 November 2018 09:01
+ * + * @see #SHORT_TIME + * @see #LONG_TIME + * @see #SHORT_DATE + * @see #LONG_DATE + * @see #SHORT_DATE_TIME + * @see #LONG_DATE_TIME + * @see #RELATIVE + */ + DEFAULT(Character.MIN_VALUE), + /** + * Time is displayed in shorthand form. + * + * + * + * + * + * + * + * + * + * + *
Output for the Unix time of 1543392060
Output (12-hour clock)Output (24-hour clock)
9:01 AM09:01
+ * + * @see #LONG_TIME + */ + SHORT_TIME('t'), + /** + * Time is displayed in hours, minutes and seconds. + * + * + * + * + * + * + * + * + * + * + *
Output for the Unix time of 1543392060
Output (12-hour clock)Output (24-hour clock)
9:01:00 AM09:01:00
+ * + * @see #SHORT_TIME + */ + LONG_TIME('T'), + /** + * The date is displayed in shorthand form. + * + * + * + * + * + * + * + * + * + * + *
Output for the Unix time of 1543392060
Output (12-hour clock)Output (24-hour clock)
11/28/201828/11/2018
+ * + * @see #LONG_DATE + */ + SHORT_DATE('d'), + /** + * The date is displayed in full. + * + * + * + * + * + * + * + * + * + * + *
Output for the Unix time of 1543392060
Output (12-hour clock)Output (24-hour clock)
November 28, 201828 November 2018
+ * + * @see #SHORT_DATE + */ + LONG_DATE('D'), + /** + * Time is displayed with short date and time. + * + * + * + * + * + * + * + * + * + * + *
Output for the Unix time of 1543392060
Output (12-hour clock)Output (24-hour clock)
November 28, 2018 9:01 AM28 November 2018 09:01
+ * + * @see #LONG_DATE_TIME + */ + SHORT_DATE_TIME('f'), + /** + * Time is displayed with full date and time. + * + * + * + * + * + * + * + * + * + * + *
Output for the Unix time of 1543392060
Output (12-hour clock)Output (24-hour clock)
Wednesday, November 28, 2018 9:01 AMWednesday, 28 November 2018 09:01
+ * + * @see #SHORT_DATE_TIME + */ + LONG_DATE_TIME('F'), + /** + * Time is displayed relative to the timestamp. + * + * + * + * + * + * + * + * + * + * + *
Output for the Unix time of 1543392060
Output (12-hour clock)Output (24-hour clock)
3 years ago3 years ago
+ */ + RELATIVE('r'); + + /** + * Style character + */ + private final char styleChar; + + /** + * Create a new {@link TimeFormat} with the specified {@code style} character. + * The default style is represented with the character code of + * {@link Character#MIN_VALUE}. + * + * @param type - character that represents this style + */ + TimeFormat(char type) { + styleChar = Objects.requireNonNull(type); + } + + /** + * Create a new Discord timestamp with the specified Unix {@code time} in seconds. + * + * @param seconds - seconds since epoch + * + * @return Returns the created Discord timestamp + */ + public String format(long seconds) { + return ""; + } + + /** + * Get the character used to represent this style for formatting. + * + * @return Returns the character that specifies this style + */ + public char getStyleCharacter() { + return styleChar; + } + + /** + * Get the {@link TimeFormat} expressed by the specified {@code style} + * character. + * + * @param style - style character + * + * @return Returns the {@link TimeFormat} represented by the specified character + * or {@link #DEFAULT} if none was found + */ + public static TimeFormat fromCharacter(char style) { + for (TimeFormat format : values()) + if (format.styleChar == style) + return format; + return DEFAULT; + } +} From 1770fb117673438d2b45b9c0d0b54c81e9cf33cd Mon Sep 17 00:00:00 2001 From: Ashley Date: Tue, 21 Nov 2023 13:29:44 -0600 Subject: [PATCH 3/4] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 131d648..11ab23c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ assets/resources/database.properties /logs/ /config/ /lib/ +sysinfo.txt From 0dd46771eb29cceb304feb31e33d0fd0fff793b4 Mon Sep 17 00:00:00 2001 From: Ashley Date: Tue, 21 Nov 2023 13:48:26 -0600 Subject: [PATCH 4/4] Update PluginPropertyMapping.java --- .../foxgenesis/watame/property/PluginPropertyMapping.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/net/foxgenesis/watame/property/PluginPropertyMapping.java b/src/net/foxgenesis/watame/property/PluginPropertyMapping.java index 0753dd1..8238e40 100644 --- a/src/net/foxgenesis/watame/property/PluginPropertyMapping.java +++ b/src/net/foxgenesis/watame/property/PluginPropertyMapping.java @@ -44,6 +44,14 @@ public Role getAsRole() { return getGuild().getRoleById(getAsLong()); } + public Role[] getAsRoleArray() { + long[] arr = getAsLongArray(); + Role[] out = new Role[arr.length]; + for (int i = 0; i < arr.length; i++) + out[i] = getGuild().getRoleById(arr[i]); + return out; + } + @Nullable public GuildMessageChannel getAsMessageChannel() { return getGuild().getChannelById(GuildMessageChannel.class, getAsLong());