diff --git a/Datura/src/main/java/fns/datura/Datura.java b/Datura/src/main/java/fns/datura/Datura.java index 5fa5c92..862c130 100644 --- a/Datura/src/main/java/fns/datura/Datura.java +++ b/Datura/src/main/java/fns/datura/Datura.java @@ -32,7 +32,7 @@ import fns.datura.sql.MySQL; import fns.patchwork.base.Registration; import fns.patchwork.command.CommandHandler; -import fns.patchwork.service.SubscriptionProvider; +import fns.patchwork.provider.SubscriptionProvider; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; @@ -43,11 +43,10 @@ public class Datura extends JavaPlugin // Punishment private final Halter halter = new Halter(); private final Locker locker = new Locker(); - private Cager cager; - // Features private final CommandSpy commandSpy = new CommandSpy(); private final Fuckoff fuckoff = new Fuckoff(); + private Cager cager; @Override public void onEnable() @@ -55,12 +54,20 @@ public void onEnable() cager = new Cager(this); Registration.getServiceTaskRegistry() - .registerService(SubscriptionProvider.syncService(this, locker)); + .registerService(SubscriptionProvider.syncService(this, locker)); Registration.getServiceTaskRegistry() .registerService(SubscriptionProvider.syncService(this, cager)); Registration.getServiceTaskRegistry() .registerService(SubscriptionProvider.syncService(this, fuckoff)); + getSQL().createTable("bans", + "uuid VARCHAR(36) PRIMARY KEY", + "name VARCHAR(16)", + "issuer VARCHAR(16)", + "reason VARCHAR(255)", + "issued LONG", + "duration LONG"); + Bukkit.getPluginManager() .registerEvents(halter, this); Bukkit.getPluginManager() @@ -92,12 +99,12 @@ public Cager getCager() return cager; } - public CommandSpy getCommandSpy() + public CommandSpy getCommandSpy() { return commandSpy; } - public Fuckoff getFuckoff() + public Fuckoff getFuckoff() { return fuckoff; } diff --git a/Datura/src/main/java/fns/datura/punishment/SimpleBanEntry.java b/Datura/src/main/java/fns/datura/punishment/SimpleBanEntry.java new file mode 100644 index 0000000..ebdd367 --- /dev/null +++ b/Datura/src/main/java/fns/datura/punishment/SimpleBanEntry.java @@ -0,0 +1,98 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.datura.punishment; + +import com.google.errorprone.annotations.Immutable; +import fns.patchwork.bans.BanEntry; +import fns.patchwork.kyori.PlainTextWrapper; +import java.net.Inet6Address; +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@Immutable +public class SimpleBanEntry implements BanEntry +{ + private final String username; + private final UUID uuid; + private final String ipv6; + private final String reason; + private final Instant issued; + private final Instant expires; + private final String issuer; + + public SimpleBanEntry(final Player target, + final CommandSender issuer, + final String reason, + final Instant issued, + final Duration duration) { + this.username = PlainTextWrapper.toPlainText(target.name()); + this.uuid = target.getUniqueId(); + if (target.getAddress() != null && target.getAddress().getAddress() instanceof Inet6Address address) + this.ipv6 = address.getHostAddress(); + else + this.ipv6 = "N/A"; + this.issued = issued; + this.expires = issued.plus(duration); + this.issuer = PlainTextWrapper.toPlainText(issuer.name()); + this.reason = reason; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public UUID getUuid() { + return this.uuid; + } + + @Override + public String getIpv6() { + return this.ipv6; + } + + @Override + public String getReason() { + return this.reason; + } + + @Override + public Instant getIssued() { + return this.issued; + } + + @Override + public Instant getExpires() { + return this.expires; + } + + @Override + public String getIssuer() { + return this.issuer; + } +} diff --git a/Datura/src/main/java/fns/datura/sql/MySQL.java b/Datura/src/main/java/fns/datura/sql/MySQL.java index 00770cd..2fd4816 100644 --- a/Datura/src/main/java/fns/datura/sql/MySQL.java +++ b/Datura/src/main/java/fns/datura/sql/MySQL.java @@ -26,11 +26,11 @@ import fns.patchwork.base.Patchwork; import fns.patchwork.base.Shortcuts; import fns.patchwork.sql.SQL; +import fns.patchwork.sql.SQLResult; import fns.patchwork.utils.container.Identity; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -74,7 +74,7 @@ public void addCredentials(final String username, final String password) .append(password); } - public CompletableFuture getRow(final String table, final String column, final Identity identity) + public CompletableFuture getRow(final String table, final String column, final Identity identity) { return executeQuery("SELECT * FROM ? WHERE ? = ?", table, column, identity.getId()); } @@ -83,99 +83,104 @@ public CompletableFuture getRow(final String table, final String colu public CompletableFuture prepareStatement(final String query, final Object... args) { return getConnection() - .thenApplyAsync(connection -> - { - try - { - final PreparedStatement statement = connection.prepareStatement(query); - for (int i = 0; i < args.length; i++) - { - statement.setObject(i + 1, args[i]); - } - return statement; - } catch (SQLException ex) - { - throw new CompletionException("Failed to prepare statement: " - + query + "\n", ex); - } - }, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()); + .thenApplyAsync(connection -> + { + try + { + final PreparedStatement statement = connection.prepareStatement(query); + for (int i = 0; i < args.length; i++) + { + statement.setObject(i + 1, args[i]); + } + return statement; + } + catch (SQLException ex) + { + throw new CompletionException("Failed to prepare statement: " + + query + "\n", ex); + } + }, Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()); } private CompletableFuture getConnection() { return CompletableFuture.supplyAsync(() -> - { - try - { - return DriverManager.getConnection(url.toString()); - } catch (SQLException ex) - { - throw new CompletionException("Failed to connect to the database: " - + url.toString() + "\n", ex); - } - }, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()); + { + try + { + return DriverManager.getConnection(url.toString()); + } + catch (SQLException ex) + { + throw new CompletionException("Failed to connect to the database: " + + url.toString() + "\n", ex); + } + }, Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()); } @Override - public CompletableFuture executeQuery(final String query, final Object... args) + public CompletableFuture executeQuery(final String query, final Object... args) { return prepareStatement(query, args) - .thenApplyAsync(statement -> - { - try - { - return statement.executeQuery(); - } catch (SQLException ex) - { - throw new CompletionException( - "Failed to retrieve a result set from query: " + .thenApplyAsync(statement -> + { + try + { + return new SQLResult(statement.executeQuery()); + } + catch (SQLException ex) + { + throw new CompletionException( + "Failed to retrieve a result set from query: " + query + "\n", ex); - } - }, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()); + } + }, Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()); } @Override public CompletableFuture executeUpdate(final String query, final Object... args) { return prepareStatement(query, args) - .thenApplyAsync(statement -> - { - try - { - return statement.executeUpdate(); - } catch (SQLException ex) - { - throw new CompletionException("Failed to execute update: " - + query + "\n", ex); - } - }, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()); + .thenApplyAsync(statement -> + { + try + { + return statement.executeUpdate(); + } + catch (SQLException ex) + { + throw new CompletionException("Failed to execute update: " + + query + "\n", ex); + } + }, Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()); } @Override public CompletableFuture execute(final String query, final Object... args) { return prepareStatement(query, args) - .thenApplyAsync(statement -> - { - try - { - return statement.execute(); - } catch (SQLException ex) - { - throw new CompletionException("Failed to execute statement: " - + query + "\n", ex); - } - }, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()); + .thenApplyAsync(statement -> + { + try + { + return statement.execute(); + } + catch (SQLException ex) + { + throw new CompletionException("Failed to execute statement: " + + query + "\n", ex); + } + }, Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()); } @Override @@ -201,42 +206,27 @@ public CompletableFuture getColumn(final String table, final String colum final Identity identity, final Class type) { return executeQuery("SELECT ? FROM ? WHERE ? = ?", column, table, key, identity.getId()) - .thenApplyAsync(resultSet -> - { - try - { - if (resultSet.next()) - { - return resultSet.getObject(column, type); - } - } catch (SQLException ex) - { - throw new CompletionException( - "Failed to retrieve column: " + column + " from table: " + table + " " + - "where primary key: " + key + " is equal to: " + identity.getId() + "\n", - ex); - } - return null; - }, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()); + .thenApplyAsync(resultSet -> (resultSet.hasNext()) ? resultSet.autoCast(1, column, type) : null, + Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()); } public CompletableFuture updateColumn(final String table, final String column, final Object value, final String key, final Identity identity) { return executeUpdate("UPDATE ? SET ? = ? WHERE ? = ?", table, column, value, key, identity.getId()) - .thenApplyAsync(result -> result > 0, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()); + .thenApplyAsync(result -> result > 0, Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()); } public CompletableFuture deleteRow(final String table, final String key, final Identity identity) { return executeUpdate("DELETE FROM ? WHERE ? = ?", table, key, identity.getId()) - .thenApplyAsync(result -> result > 0, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()); + .thenApplyAsync(result -> result > 0, Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()); } public CompletableFuture insertRow(final String table, final Object... values) diff --git a/Datura/src/main/java/fns/datura/user/SimpleUserData.java b/Datura/src/main/java/fns/datura/user/SimpleUserData.java index 0a19c6a..5462a15 100644 --- a/Datura/src/main/java/fns/datura/user/SimpleUserData.java +++ b/Datura/src/main/java/fns/datura/user/SimpleUserData.java @@ -34,10 +34,8 @@ import fns.patchwork.user.User; import fns.patchwork.user.UserData; import fns.patchwork.utils.logging.FNS4J; -import java.sql.SQLException; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -69,14 +67,14 @@ public SimpleUserData(final Player player) } private SimpleUserData( - final UUID uuid, - final String username, - final User user, - final Group group, - final long playtime, - final boolean canInteract, - final long balance, - final boolean transactionsFrozen) + final UUID uuid, + final String username, + final User user, + final Group group, + final long playtime, + final boolean canInteract, + final long balance, + final boolean transactionsFrozen) { this.uuid = uuid; this.username = username; @@ -93,56 +91,53 @@ public static SimpleUserData fromSQL(final SQL sql, final String uuid) { return sql.executeQuery("SELECT * FROM users WHERE UUID = ?", uuid) .thenApplyAsync(result -> - { - try - { - if (result.next()) - { - final String g = result.getString("group"); - - final UUID u = UUID.fromString(uuid); - final String username = result.getString("username"); - - final Player player = Bukkit.getPlayer(u); - - if (player == null) - throw new IllegalStateException("Player should be online but they are not!"); - - final User user = new FreedomUser(player); - final Group group = Registration - .getGroupRegistry() - .getGroup(g); - - final long playtime = result.getLong("playtime"); - final boolean canInteract = result.getBoolean("canInteract"); - final long balance = result.getLong("balance"); - final boolean transactionsFrozen = result.getBoolean("transactionsFrozen"); - - return new SimpleUserData(u, username, user, group, playtime, - canInteract, balance, transactionsFrozen); - } - } catch (SQLException ex) - { - final String sb = "An error occurred while trying to retrieve user data for" + - " UUID " + - uuid + - " from the database." + - "\nCaused by: " + - ExceptionUtils.getRootCauseMessage(ex) + - "\nStack trace: " + - ExceptionUtils.getStackTrace(ex); - - FNS4J.getLogger("Datura") - .error(sb); - } - - final Player player = Bukkit.getPlayer(UUID.fromString(uuid)); - if (player == null) throw new IllegalStateException("Player should be online but they are not!"); - - return new SimpleUserData(player); - }, Shortcuts.provideModule(Patchwork.class) - .getExecutor() - .getAsync()) + { + + if (result.hasNext()) + { + final String g = result.getString("group"); + + final UUID u = UUID.fromString(uuid); + final String username = result.getString("username"); + + final Player player = Bukkit.getPlayer(u); + + if (player == null) + throw new IllegalStateException( + "Player should be online but they are not!"); + + final User user = new FreedomUser(player); + final Group group = Registration + .getGroupRegistry() + .getGroup(g); + + final long playtime = result.getLong("playtime"); + final boolean canInteract = result.getBoolean("canInteract"); + final long balance = result.getLong("balance"); + final boolean transactionsFrozen = result.getBoolean("transactionsFrozen"); + + return new SimpleUserData(u, username, user, group, playtime, + canInteract, balance, transactionsFrozen); + } + else + { + final String sb = "An error occurred while trying to retrieve user data for" + + " UUID " + + uuid + + " from the database."; + + FNS4J.getLogger("Datura") + .error(sb); + } + + final Player player = Bukkit.getPlayer(UUID.fromString(uuid)); + if (player == null) + throw new IllegalStateException("Player should be online but they are not!"); + + return new SimpleUserData(player); + }, Shortcuts.provideModule(Patchwork.class) + .getExecutor() + .getAsync()) .join(); } diff --git a/Fossil/src/main/java/fns/fossil/Fossil.java b/Fossil/src/main/java/fns/fossil/Fossil.java index a07711f..9d8102a 100644 --- a/Fossil/src/main/java/fns/fossil/Fossil.java +++ b/Fossil/src/main/java/fns/fossil/Fossil.java @@ -24,15 +24,17 @@ package fns.fossil; import fns.fossil.cmd.CakeCommand; +import fns.fossil.reactions.ReactionSystem; import fns.fossil.trail.Trailer; import fns.patchwork.base.Registration; import fns.patchwork.command.CommandHandler; -import fns.patchwork.service.SubscriptionProvider; +import fns.patchwork.provider.SubscriptionProvider; import org.bukkit.plugin.java.JavaPlugin; public class Fossil extends JavaPlugin { private final Trailer trailer = new Trailer(); + @Override public void onEnable() { diff --git a/Fossil/src/main/java/fns/fossil/reactions/CopyCatReaction.java b/Fossil/src/main/java/fns/fossil/reactions/CopyCatReaction.java index 41921bf..4c8b669 100644 --- a/Fossil/src/main/java/fns/fossil/reactions/CopyCatReaction.java +++ b/Fossil/src/main/java/fns/fossil/reactions/CopyCatReaction.java @@ -37,11 +37,17 @@ public final class CopyCatReaction extends Reaction { private final long reward; + private final BossBar bossBar; public CopyCatReaction(final long reward) { super(ReactionType.COPYCAT); this.reward = reward; + this.bossBar = BossBarDisplay.builder() + .setName(getRandomCharacterString()) + .setProgress(0.0F) + .setOverlay(BossBar.Overlay.NOTCHED_10) + .build(); } @Override @@ -53,16 +59,16 @@ public long getReward() @Override public void display(final Audience audience) { - final BossBar bossBar = BossBarDisplay.builder() - .setName(getRandomCharacterString()) - .setProgress(0.0F) - .build(); + audience.showBossBar(bossBar); } @Override public void onReact(final EconomicEntity entity) { - // + entity.getEconomicData() + .addToBalance(getReward()); + + this.cancel(); } public String getRandomCharacterString() @@ -79,4 +85,16 @@ public String getRandomCharacterString() return sb.toString(); } + + @Override + public void runTimer() + { + if (bossBar.progress() >= 1.0F) + { + this.cancel(); + return; + } + + bossBar.progress(bossBar.progress() + 0.1F); + } } diff --git a/Fossil/src/main/java/fns/fossil/reactions/ReactionSystem.java b/Fossil/src/main/java/fns/fossil/reactions/ReactionSystem.java new file mode 100644 index 0000000..726feaa --- /dev/null +++ b/Fossil/src/main/java/fns/fossil/reactions/ReactionSystem.java @@ -0,0 +1,59 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.fossil.reactions; + +import fns.fossil.Fossil; +import fns.patchwork.base.Registration; +import fns.patchwork.base.Shortcuts; +import fns.patchwork.provider.SubscriptionProvider; +import fns.patchwork.service.Task; +import fns.patchwork.service.TaskSubscription; +import java.time.Duration; + +public class ReactionSystem +{ + public static void startCopyCat() + { + final Fossil fossil = Shortcuts.provideModule(Fossil.class); + final TaskSubscription subscription = + SubscriptionProvider.runSyncTask(fossil, new CopyCatReaction(25L)); + + Registration.getServiceTaskRegistry().registerTask(subscription); + Registration.getServiceTaskRegistry().startTask(CopyCatReaction.class); + } + + private static final class SystemTask extends Task + { + private SystemTask() + { + super("sys-task", 0L, Duration.ofMinutes(15L)); + } + + @Override + public void run() + { + ReactionSystem.startCopyCat(); + } + } +} diff --git a/Patchwork/src/main/java/fns/patchwork/bans/BanEntry.java b/Patchwork/src/main/java/fns/patchwork/bans/BanEntry.java new file mode 100644 index 0000000..aa3cc76 --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/bans/BanEntry.java @@ -0,0 +1,43 @@ +package fns.patchwork.bans; + +import java.time.Instant; +import java.util.UUID; + +public interface BanEntry +{ + /** + * @return The username of the banned player. + */ + String getUsername(); + + /** + * @return The {@link UUID} of the banned player. + */ + UUID getUuid(); + + /** + * @return Either the IPv6 address of the banned player, if applicable, + * otherwise returns {@code "N/A"}. + */ + String getIpv6(); + + /** + * @return The reason for the ban. + */ + String getReason(); + + /** + * @return The {@link Instant} the ban was issued. + */ + Instant getIssued(); + + /** + * @return The {@link Instant} the ban expires. + */ + Instant getExpires(); + + /** + * @return The username of the individual who issued the ban. Can be {@code "CONSOLE"}. + */ + String getIssuer(); +} \ No newline at end of file diff --git a/Patchwork/src/main/java/fns/patchwork/base/Patchwork.java b/Patchwork/src/main/java/fns/patchwork/base/Patchwork.java index b60d596..6d97be6 100644 --- a/Patchwork/src/main/java/fns/patchwork/base/Patchwork.java +++ b/Patchwork/src/main/java/fns/patchwork/base/Patchwork.java @@ -25,10 +25,14 @@ import fns.patchwork.display.adminchat.AdminChatDisplay; import fns.patchwork.event.EventBus; -import fns.patchwork.service.FreedomExecutor; -import fns.patchwork.service.SubscriptionProvider; +import fns.patchwork.provider.ExecutorProvider; +import fns.patchwork.provider.SubscriptionProvider; +import fns.patchwork.registry.ServiceTaskRegistry; +import fns.patchwork.service.Service; +import fns.patchwork.utils.logging.FNS4J; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; /** * The base class for Patchwork. @@ -40,51 +44,65 @@ public class Patchwork extends JavaPlugin */ private EventBus eventBus; /** - * The {@link FreedomExecutor} for this plugin. + * The {@link ExecutorProvider} for this plugin. */ - private FreedomExecutor executor; + private ExecutorProvider executor; /** * The {@link AdminChatDisplay} for this plugin. */ private AdminChatDisplay acdisplay; - @Override - public void onDisable() - { - Bukkit.getScheduler() - .runTaskLater(this, () -> Registration - .getServiceTaskRegistry() - .stopAllServices(), 1L); + private static final ServiceRunner runner = new ServiceRunner(); - Registration.getServiceTaskRegistry() - .unregisterService(EventBus.class); - } @Override public void onEnable() { eventBus = new EventBus(this); - executor = new FreedomExecutor(this); + executor = new ExecutorProvider(this); acdisplay = new AdminChatDisplay(this); + Registration.getServiceTaskRegistry() + .registerService(SubscriptionProvider.asyncService(this, eventBus)); Registration.getServiceTaskRegistry() - .registerService(SubscriptionProvider.asyncService(this, eventBus)); + .registerService(SubscriptionProvider.asyncService(this, runner)); + // Will execute post-world getExecutor().getSync() - .execute(() -> Registration - .getServiceTaskRegistry() - .startAllServices()); + .execute(this::postWorld); Registration.getModuleRegistry().addModule(this); + + FNS4J.PATCHWORK.info("Successfully enabled Patchwork. API is ready to go."); + } + + @Override + public void onDisable() + { + Bukkit.getScheduler() + .runTaskLater(this, () -> Registration + .getServiceTaskRegistry() + .stopAllServices(), 1L); + + Registration.getServiceTaskRegistry() + .unregisterService(EventBus.class); + + FNS4J.PATCHWORK.info("Successfully disabled Patchwork. API is no longer available."); + } + + private void postWorld() + { + Registration.getServiceTaskRegistry() + .startAllServices(); } /** - * Gets the {@link FreedomExecutor} for this plugin. + * Gets the {@link ExecutorProvider} for this plugin. * - * @return the {@link FreedomExecutor} + * @return the {@link ExecutorProvider} */ - public FreedomExecutor getExecutor() + public ExecutorProvider getExecutor() { return executor; } @@ -95,6 +113,7 @@ public FreedomExecutor getExecutor() * * @return the {@link EventBus} */ + @ApiStatus.Experimental public EventBus getEventBus() { return eventBus; @@ -110,4 +129,34 @@ public AdminChatDisplay getAdminChatDisplay() { return acdisplay; } + + @ApiStatus.Internal + private static final class ServiceRunner extends Service + { + public ServiceRunner() + { + super("srv-runner"); + } + + @Override + public void tick() + { + final ServiceTaskRegistry r = Registration.getServiceTaskRegistry(); + r.getServices().forEach(s -> + { + if (!s.isActive()) + { + r.unregisterService(s.getService().getClass()); + } + }); + + r.getTasks().forEach(t -> + { + if (!t.isActive()) + { + r.unregisterTask(t.getTask().getClass()); + } + }); + } + } } diff --git a/Patchwork/src/main/java/fns/patchwork/base/Registration.java b/Patchwork/src/main/java/fns/patchwork/base/Registration.java index db491fa..8818cf3 100644 --- a/Patchwork/src/main/java/fns/patchwork/base/Registration.java +++ b/Patchwork/src/main/java/fns/patchwork/base/Registration.java @@ -27,6 +27,7 @@ import fns.patchwork.registry.EventRegistry; import fns.patchwork.registry.GroupRegistry; import fns.patchwork.registry.ModuleRegistry; +import fns.patchwork.registry.SQLRegistry; import fns.patchwork.registry.ServiceTaskRegistry; import fns.patchwork.registry.UserRegistry; @@ -62,6 +63,10 @@ public class Registration * The {@link ConfigRegistry} */ private static final ConfigRegistry configRegistry = new ConfigRegistry(); + /** + * The SQL Registry + */ + private static final SQLRegistry sqlRegistry = new SQLRegistry(); private Registration() { @@ -115,4 +120,12 @@ public static ConfigRegistry getConfigRegistry() { return configRegistry; } + + /** + * @return The {@link SQLRegistry} + */ + public static SQLRegistry getSQLRegistry() + { + return sqlRegistry; + } } \ No newline at end of file diff --git a/Patchwork/src/main/java/fns/patchwork/base/Shortcuts.java b/Patchwork/src/main/java/fns/patchwork/base/Shortcuts.java index 9482202..3dcd19e 100644 --- a/Patchwork/src/main/java/fns/patchwork/base/Shortcuts.java +++ b/Patchwork/src/main/java/fns/patchwork/base/Shortcuts.java @@ -23,7 +23,10 @@ package fns.patchwork.base; +import fns.patchwork.provider.ExecutorProvider; +import fns.patchwork.sql.SQL; import fns.patchwork.user.User; +import java.util.Optional; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; @@ -37,13 +40,23 @@ private Shortcuts() public static T provideModule(final Class pluginClass) { return Registration.getModuleRegistry() - .getProvider(pluginClass) - .getModule(); + .getProvider(pluginClass) + .getModule(); } public static User getUser(final Player player) { return Registration.getUserRegistry() - .getUser(player); + .getUser(player); + } + + public static ExecutorProvider getExecutors() + { + return provideModule(Patchwork.class).getExecutor(); + } + + public static Optional getSQL() + { + return Registration.getSQLRegistry().getSQL(); } } diff --git a/Patchwork/src/main/java/fns/patchwork/block/detector/NukerDetection.java b/Patchwork/src/main/java/fns/patchwork/block/detector/NukerDetection.java new file mode 100644 index 0000000..dc4fbb2 --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/block/detector/NukerDetection.java @@ -0,0 +1,47 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.patchwork.block.detector; + +import fns.patchwork.block.logger.TimedBlockLogger; +import java.util.Set; +import org.bukkit.entity.Player; + +public interface NukerDetection +{ + Set getTimedBlockLoggers(); + + boolean isNuking(); + + void addTimedBlockLogger(TimedBlockLogger timedBlockLogger); + + void removeTimedBlockLogger(TimedBlockLogger timedBlockLogger); + + default void ejectPlayer(final Player player) { + if (isNuking()) { + getTimedBlockLoggers().forEach(l -> { + + }); + } + } +} diff --git a/Patchwork/src/main/java/fns/patchwork/block/logger/BlockLogger.java b/Patchwork/src/main/java/fns/patchwork/block/logger/BlockLogger.java new file mode 100644 index 0000000..3071dbe --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/block/logger/BlockLogger.java @@ -0,0 +1,51 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.patchwork.block.logger; + +import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; + +public interface BlockLogger +{ + UUID getUUID(); + + int getEditedBlockCount(); + + void incrementBlockCount(); + + void decrementBlockCount(); + + boolean greaterThan(final int p0); + + default boolean isPlayer() { + return Bukkit.getPlayer(this.getUUID()) != null; + } + + default boolean isTNT() { + final Entity entity = Bukkit.getEntity(this.getUUID()); + return entity != null && entity.getType() == EntityType.PRIMED_TNT; + } +} diff --git a/Patchwork/src/main/java/fns/patchwork/block/logger/TimedBlockLogger.java b/Patchwork/src/main/java/fns/patchwork/block/logger/TimedBlockLogger.java new file mode 100644 index 0000000..f373c01 --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/block/logger/TimedBlockLogger.java @@ -0,0 +1,28 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.patchwork.block.logger; + +public interface TimedBlockLogger extends BlockLogger +{ +} diff --git a/Patchwork/src/main/java/fns/patchwork/command/BukkitDelegate.java b/Patchwork/src/main/java/fns/patchwork/command/BukkitDelegate.java index 01162c6..46efcf9 100644 --- a/Patchwork/src/main/java/fns/patchwork/command/BukkitDelegate.java +++ b/Patchwork/src/main/java/fns/patchwork/command/BukkitDelegate.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Set; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; @@ -156,50 +157,19 @@ private void processSubCommands(final @NotNull String @NotNull [] args, if (argTypes.length > args.length) return; - final Object[] objects = new Object[argTypes.length + 1]; - - for (int i = 0; i < argTypes.length; i++) - { - final Class argType = argTypes[i]; - final String arg = args[i]; + final Player p = (sender instanceof Player player) ? player : null; - if (argType.equals(String.class)) - { - if (i == argTypes.length - 1) - { - final String[] reasonArgs = Arrays.copyOfRange(args, i, args.length - 1); - final String reason = String.join(" ", reasonArgs); - objects[i] = reason; - } - else - { - continue; - } - } + final Object[] objects = new Object[argTypes.length + 1]; - if (argType.equals(Location.class)) - { - final String[] locationArgs = Arrays.copyOfRange(args, i, i + 3); - final String location = String.join(" ", locationArgs); - objects[i] = location; - } + parseArguments(args, provider, argTypes, objects); - final Object obj = provider.fromString(arg, argType); - if (obj == null) - { - FNS4J.getLogger("Datura") - .error("Failed to parse argument " + arg + " for type " + argType.getName()); - return; - } - objects[i] = obj; - } try { if (noConsole) { command.getSubcommands() .get(node) - .invoke(command, (Player) sender, objects); + .invoke(command, p, objects); } else { @@ -215,11 +185,55 @@ private void processSubCommands(final @NotNull String @NotNull [] args, } } + private void parseArguments(@NotNull String @NotNull [] args, + ContextProvider provider, + Class[] argTypes, + Object[] objects) + { + for (int i = 0; i < argTypes.length; i++) + { + final Class argType = argTypes[i]; + String arg = args[i]; + + boolean wasResolved = false; + + if (argType.equals(String.class) && (i == argTypes.length - 1)) + { + final String[] reasonArgs = Arrays.copyOfRange(args, i, args.length - 1); + final String reason = String.join(" ", reasonArgs); + objects[i] = reason; + wasResolved = true; + } + + if (argType.equals(Location.class)) + { + final String[] locationArgs = Arrays.copyOfRange(args, i, i + 3); + arg = String.join(" ", locationArgs); + } + + if (!wasResolved) + { + final Optional obj = provider.fromString(arg, argType); + if (obj.isEmpty()) + { + FNS4J.getLogger("Datura") + .error("Failed to parse argument " + arg + " for type " + argType.getName()); + continue; + } + objects[i] = obj.get(); + } + } + } + @Override - public List tabComplete(final CommandSender sender, final String alias, final String[] args) + public @NotNull List tabComplete(final @NotNull CommandSender sender, final @NotNull String alias, + final String[] args) { - final Set completions = command.getCompletions(); final List results = new ArrayList<>(); + final Set completions = command.getCompletions(); + + if (completions == null || completions.isEmpty()) + return results; if (args.length == 0) { diff --git a/Patchwork/src/main/java/fns/patchwork/config/Configuration.java b/Patchwork/src/main/java/fns/patchwork/config/Configuration.java index b7b4780..19ef8c2 100644 --- a/Patchwork/src/main/java/fns/patchwork/config/Configuration.java +++ b/Patchwork/src/main/java/fns/patchwork/config/Configuration.java @@ -25,6 +25,7 @@ import fns.patchwork.provider.Context; import fns.patchwork.provider.ContextProvider; +import java.util.Collection; import org.jetbrains.annotations.Unmodifiable; import java.io.File; @@ -87,7 +88,7 @@ public interface Configuration * @param clazz The class of the type. * @return The List object. */ - @Unmodifiable List getList(String path, Class clazz); + @Unmodifiable Collection getCollection(String path, Class clazz); /** * Gets a List object from the associated path. The List that is returned will be the String values which are stored diff --git a/Patchwork/src/main/java/fns/patchwork/config/GenericConfiguration.java b/Patchwork/src/main/java/fns/patchwork/config/GenericConfig.java similarity index 59% rename from Patchwork/src/main/java/fns/patchwork/config/GenericConfiguration.java rename to Patchwork/src/main/java/fns/patchwork/config/GenericConfig.java index 410e791..507eb8f 100644 --- a/Patchwork/src/main/java/fns/patchwork/config/GenericConfiguration.java +++ b/Patchwork/src/main/java/fns/patchwork/config/GenericConfig.java @@ -26,7 +26,9 @@ import com.electronwill.nightconfig.core.Config; import com.electronwill.nightconfig.core.ConfigFormat; import com.electronwill.nightconfig.core.UnmodifiableConfig; +import fns.patchwork.provider.ContextProvider; import fns.patchwork.utils.FileUtils; +import fns.patchwork.utils.logging.FNS4J; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; @@ -34,26 +36,28 @@ import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; -public final class GenericConfiguration implements Configuration +public final class GenericConfig implements Configuration { + private static final ContextProvider PROVIDER = new ContextProvider(); private final File configFile; private final String fileName; private final Config config; private final ConfigType configType; - public GenericConfiguration(@NotNull final ConfigType configType, - @Nullable final JavaPlugin plugin, - @NotNull final File dataFolder, - @NotNull final String fileName, - final boolean isConcurrent) throws IOException + public GenericConfig(@NotNull final ConfigType configType, + @Nullable final JavaPlugin plugin, + @NotNull final File dataFolder, + @NotNull final String fileName, + final boolean isConcurrent) throws IOException { if (!fileName.endsWith(configType.getExtension())) throw new IllegalArgumentException("File name must end with " + configType.getExtension() + "!"); @@ -78,20 +82,20 @@ public GenericConfiguration(@NotNull final ConfigType configType, this.load(); } - public GenericConfiguration(final ConfigType type, final File dataFolder, final String fileName) + public GenericConfig(final ConfigType type, final File dataFolder, final String fileName) throws IOException { this(type, null, dataFolder, fileName, false); } - public GenericConfiguration(final ConfigType type, final JavaPlugin plugin, final String fileName) + public GenericConfig(final ConfigType type, final JavaPlugin plugin, final String fileName) throws IOException { this(type, plugin, plugin.getDataFolder(), fileName, false); } - public GenericConfiguration(final ConfigType type, final File dataFolder, final String fileName, - final boolean isConcurrent) + public GenericConfig(final ConfigType type, final File dataFolder, final String fileName, + final boolean isConcurrent) throws IOException { this(type, null, dataFolder, fileName, isConcurrent); @@ -114,8 +118,10 @@ public void save() throws IOException } @Override - public void load() throws IOException { - try (final FileReader reader = new FileReader(this.configFile)) { + public void load() throws IOException + { + try (final FileReader reader = new FileReader(this.configFile)) + { this.config.clear(); final UnmodifiableConfig parsed = this.configType.getParser().parse(reader).unmodifiable(); @@ -145,7 +151,7 @@ public String getString(final String path) } @Override - public boolean getBoolean(String path) + public boolean getBoolean(final String path) { if (!(this.getConfig().get(path) instanceof Boolean)) throw new IllegalArgumentException(String.format("Value at path %s is not a boolean!", path)); @@ -153,22 +159,70 @@ public boolean getBoolean(String path) return this.getConfig().get(path); } + + /* + * I am pretty sure that this works, but not really. + * This is sort of a shot in the dark because Night Config did specify that they support collections + * in TOML and JSON files, but there is no specific get method for objects that are not primitives. + * Additionally, not all primitives are natively supported. + */ @Override - @ApiStatus.Internal - public @Unmodifiable List getList(String path, Class clazz) + public @Unmodifiable Collection getCollection(String path, Class clazz) { - // TODO: Figure out how to parse lists with Night Config. - - return new ArrayList<>(); + if (!(this.getConfig().get(path) instanceof Collection)) + throw new IllegalArgumentException(String.format("Value at path %s is not a collection!", path)); + + final Collection collection = this.getConfig().get(path); + final Collection collected = new ArrayList<>(); + + collection.stream() + .map(obj -> + { + final Optional optional; + + if (obj instanceof String string) + optional = PROVIDER.fromString(string, clazz); + else if (clazz.isInstance(obj)) + optional = Optional.of(clazz.cast(obj)); + else + optional = Optional.empty(); + + return optional; + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toCollection(() -> collected)); + + return collected; } + /* + * I am pretty sure that this works, but not really. + * This is sort of a shot in the dark because Night Config did specify that they support collections + * in TOML and JSON files, but there is no specific get method for objects that are not primitives. + * Additionally, not all primitives are natively supported. + */ @Override - @ApiStatus.Internal public @Unmodifiable List getStringList(String path) { - // TODO: Figure out how to parse lists with Night Config. + if (!(this.getConfig().get(path) instanceof Collection c)) + throw new IllegalArgumentException(String.format("Value at path %s is not a collection!", path)); + + final Collection collection = this.getConfig().get(path); + final List list = new ArrayList<>(); - return new ArrayList<>(); + if (c.isEmpty() || !(c.toArray()[0] instanceof String)) + { + FNS4J.PATCHWORK.warn(String.format("Collection at path %s is empty or does not contain strings!", path)); + FNS4J.PATCHWORK.warn("Returning empty list!"); + return list; + } + + collection.stream() + .map(String.class::cast) + .collect(Collectors.toCollection(() -> list)); + + return list; } @Override @@ -195,20 +249,22 @@ public double getDouble(String path) @Override public Optional get(String path, Class clazz) { - // I love ternary statements, sorry Allink :) - return clazz.isInstance(this.getConfig().get(path)) ? - Optional.of(clazz.cast(this.getConfig().get(path))) : - Optional.empty(); + return this.getConfig() + .getOptional(path) + .filter(clazz::isInstance) + .map(clazz::cast); } @Override public T getOrDefault(String path, Class clazz, T fallback) { - return this.get(path, clazz).orElse(fallback); + return this.get(path, clazz) + .orElse(fallback); } @Override - public void set(final String path, final T value) { + public void set(final String path, final T value) + { this.config.set(path, value); } diff --git a/Patchwork/src/main/java/fns/patchwork/config/WrappedBukkitConfiguration.java b/Patchwork/src/main/java/fns/patchwork/config/WrappedBukkitConfiguration.java index f50e146..1baf944 100644 --- a/Patchwork/src/main/java/fns/patchwork/config/WrappedBukkitConfiguration.java +++ b/Patchwork/src/main/java/fns/patchwork/config/WrappedBukkitConfiguration.java @@ -94,7 +94,7 @@ public boolean getBoolean(String path) } @Override - public List getList(String path, Class clazz) + public List getCollection(String path, Class clazz) { return this.contextProvider.getList(this.getStringList(path), clazz); } @@ -132,7 +132,7 @@ public void set(String path, T value) @Override public Optional get(String path, Class clazz) { - return Optional.ofNullable(this.contextProvider.fromString(path, clazz)); + return this.contextProvider.fromString(path, clazz); } @Override diff --git a/Patchwork/src/main/java/fns/patchwork/kyori/MiniMessageWrapper.java b/Patchwork/src/main/java/fns/patchwork/kyori/MiniMessageWrapper.java index c814ab9..fcb292e 100644 --- a/Patchwork/src/main/java/fns/patchwork/kyori/MiniMessageWrapper.java +++ b/Patchwork/src/main/java/fns/patchwork/kyori/MiniMessageWrapper.java @@ -37,14 +37,14 @@ public class MiniMessageWrapper private static final MiniMessage unsafe = MiniMessage.miniMessage(); private static final MiniMessage safe = MiniMessage.builder() .tags(TagResolver.resolver( - StandardTags.color(), - StandardTags.rainbow(), - StandardTags.gradient(), - StandardTags.newline(), - StandardTags.decorations(TextDecoration.ITALIC), - StandardTags.decorations(TextDecoration.BOLD), - StandardTags.decorations(TextDecoration.STRIKETHROUGH), - StandardTags.decorations(TextDecoration.UNDERLINED) + StandardTags.color(), + StandardTags.rainbow(), + StandardTags.gradient(), + StandardTags.newline(), + StandardTags.decorations(TextDecoration.ITALIC), + StandardTags.decorations(TextDecoration.BOLD), + StandardTags.decorations(TextDecoration.STRIKETHROUGH), + StandardTags.decorations(TextDecoration.UNDERLINED) )) .build(); diff --git a/Patchwork/src/main/java/fns/patchwork/provider/ContextProvider.java b/Patchwork/src/main/java/fns/patchwork/provider/ContextProvider.java index 9bb86f3..476a3a6 100644 --- a/Patchwork/src/main/java/fns/patchwork/provider/ContextProvider.java +++ b/Patchwork/src/main/java/fns/patchwork/provider/ContextProvider.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; @@ -60,7 +61,7 @@ */ public class ContextProvider { - public T fromString(final String string, final Class clazz) + public Optional fromString(final String string, final Class clazz) { return Stream.of(toBoolean(string, clazz), toLong(string, clazz), @@ -74,9 +75,9 @@ public T fromString(final String string, final Class clazz) toLocation(string, clazz), toComponent(string, clazz)) .filter(Objects::nonNull) - .findFirst() + .filter(clazz::isInstance) .map(clazz::cast) - .orElse(null); + .findFirst(); } private @Nullable Boolean toBoolean(final String string, final Class clazz) @@ -227,10 +228,9 @@ private OfflinePlayer toOfflinePlayer(final String string, final Class clazz) public @NotNull List<@Nullable T> getList(final List resolvable, final Class clazz) { final List resolved = new ArrayList<>(); - for (final String entry : resolvable) - { - resolved.add(this.fromString(entry, clazz)); - } + + resolvable.forEach(entry -> this.fromString(entry, clazz).ifPresent(resolved::add)); + return resolved; } } diff --git a/Patchwork/src/main/java/fns/patchwork/service/FreedomExecutor.java b/Patchwork/src/main/java/fns/patchwork/provider/ExecutorProvider.java similarity index 97% rename from Patchwork/src/main/java/fns/patchwork/service/FreedomExecutor.java rename to Patchwork/src/main/java/fns/patchwork/provider/ExecutorProvider.java index 3a5eb6c..cbdd2c9 100644 --- a/Patchwork/src/main/java/fns/patchwork/service/FreedomExecutor.java +++ b/Patchwork/src/main/java/fns/patchwork/provider/ExecutorProvider.java @@ -21,7 +21,7 @@ * SOFTWARE. */ -package fns.patchwork.service; +package fns.patchwork.provider; import fns.patchwork.base.Patchwork; import java.util.concurrent.CompletableFuture; @@ -34,7 +34,7 @@ * This class is here for both convenience purposes, and also for the sake of providing easy access to executors for * {@link CompletableFuture} invocations. */ -public class FreedomExecutor +public class ExecutorProvider { /** * An executor which runs tasks synchronously. @@ -46,9 +46,9 @@ public class FreedomExecutor private final Executor asyncExecutor; /** - * Creates a new {@link FreedomExecutor} instance. + * Creates a new {@link ExecutorProvider} instance. */ - public FreedomExecutor(final Patchwork patchwork) + public ExecutorProvider(final Patchwork patchwork) { syncExecutor = r -> Bukkit.getScheduler() .runTask(patchwork, r); diff --git a/Patchwork/src/main/java/fns/patchwork/service/SubscriptionProvider.java b/Patchwork/src/main/java/fns/patchwork/provider/SubscriptionProvider.java similarity index 87% rename from Patchwork/src/main/java/fns/patchwork/service/SubscriptionProvider.java rename to Patchwork/src/main/java/fns/patchwork/provider/SubscriptionProvider.java index 5d410b7..aeced8b 100644 --- a/Patchwork/src/main/java/fns/patchwork/service/SubscriptionProvider.java +++ b/Patchwork/src/main/java/fns/patchwork/provider/SubscriptionProvider.java @@ -21,8 +21,12 @@ * SOFTWARE. */ -package fns.patchwork.service; +package fns.patchwork.provider; +import fns.patchwork.service.Service; +import fns.patchwork.service.ServiceSubscription; +import fns.patchwork.service.Task; +import fns.patchwork.service.TaskSubscription; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -50,8 +54,8 @@ private SubscriptionProvider() * @return The new {@link ServiceSubscription} object. */ @NotNull - @Contract(value = "_, _ -> new", pure = false) - public static final ServiceSubscription syncService(@NotNull final JavaPlugin plugin, + @Contract(value = "_, _ -> new") + public static ServiceSubscription syncService(@NotNull final JavaPlugin plugin, @NotNull final S service) { return new ServiceSubscription<>(plugin, service); @@ -69,7 +73,7 @@ public static final ServiceSubscription syncService(@NotN */ @NotNull @Contract(value = "_,_,_ -> new", pure = false) - public static final ServiceSubscription syncService(@NotNull final JavaPlugin plugin, + public static ServiceSubscription syncService(@NotNull final JavaPlugin plugin, final long interval, @NotNull final S service) { @@ -87,7 +91,7 @@ public static final ServiceSubscription syncService(@NotN */ @NotNull @Contract(value = "_, _ -> new", pure = false) - public static final ServiceSubscription asyncService(@NotNull final JavaPlugin plugin, + public static ServiceSubscription asyncService(@NotNull final JavaPlugin plugin, @NotNull final S service) { return new ServiceSubscription<>(plugin, service, true); @@ -105,7 +109,7 @@ public static final ServiceSubscription asyncService(@Not */ @NotNull @Contract(value = "_,_,_ -> new", pure = false) - public static final ServiceSubscription asyncService(@NotNull final JavaPlugin plugin, + public static ServiceSubscription asyncService(@NotNull final JavaPlugin plugin, final long interval, @NotNull final S service) { @@ -123,7 +127,7 @@ public static final ServiceSubscription asyncService(@Not */ @NotNull @Contract(value = "_, _ -> new", pure = false) - public static final TaskSubscription runSyncTask(@NotNull final JavaPlugin plugin, + public static TaskSubscription runSyncTask(@NotNull final JavaPlugin plugin, @NotNull final T task) { return new TaskSubscription<>(plugin, task, false); @@ -140,7 +144,7 @@ public static final TaskSubscription runSyncTask(@NotNull fi */ @NotNull @Contract(value = "_, _ -> new", pure = false) - public static final TaskSubscription runAsyncTask(@NotNull final JavaPlugin plugin, + public static TaskSubscription runAsyncTask(@NotNull final JavaPlugin plugin, @NotNull final T task) { return new TaskSubscription<>(plugin, task, true); diff --git a/Patchwork/src/main/java/fns/patchwork/registry/BanRegistry.java b/Patchwork/src/main/java/fns/patchwork/registry/BanRegistry.java new file mode 100644 index 0000000..9bc6bf7 --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/registry/BanRegistry.java @@ -0,0 +1,57 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.patchwork.registry; + +import fns.patchwork.bans.BanEntry; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +public class BanRegistry +{ + private final Set loadedBans = new HashSet<>(); + + public Optional getBan(final UUID uuid) + { + return loadedBans.stream() + .filter(b -> b.getUuid().equals(uuid)) + .findFirst(); + } + + public void addBan(final BanEntry entry) + { + this.loadedBans.add(entry); + } + + public void removeBan(final BanEntry entry) + { + this.loadedBans.remove(entry); + } + + public void clearLocalStorage() + { + this.loadedBans.clear(); + } +} diff --git a/Patchwork/src/main/java/fns/patchwork/registry/SQLRegistry.java b/Patchwork/src/main/java/fns/patchwork/registry/SQLRegistry.java new file mode 100644 index 0000000..9f7eac3 --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/registry/SQLRegistry.java @@ -0,0 +1,44 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.patchwork.registry; + +import fns.patchwork.sql.SQL; +import java.util.Optional; + +public class SQLRegistry +{ + private SQL sql; + + public SQLRegistry() { + this.sql = null; + } + + public Optional getSQL() { + return (sql == null) ? Optional.empty() : Optional.of(sql); + } + + public void setSQL(final SQL sql) { + this.sql = sql; + } +} diff --git a/Patchwork/src/main/java/fns/patchwork/registry/ServiceTaskRegistry.java b/Patchwork/src/main/java/fns/patchwork/registry/ServiceTaskRegistry.java index c346c0c..a343a7a 100644 --- a/Patchwork/src/main/java/fns/patchwork/registry/ServiceTaskRegistry.java +++ b/Patchwork/src/main/java/fns/patchwork/registry/ServiceTaskRegistry.java @@ -23,13 +23,16 @@ package fns.patchwork.registry; +import fns.patchwork.base.Registration; +import fns.patchwork.provider.SubscriptionProvider; import fns.patchwork.service.Service; import fns.patchwork.service.ServiceSubscription; -import fns.patchwork.service.SubscriptionProvider; import fns.patchwork.service.Task; import fns.patchwork.service.TaskSubscription; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; /** @@ -195,8 +198,18 @@ public void registerTask(final TaskSubscription task) */ public void startService(final Class clazz) { - this.getService(clazz) - .start(); + Objects.requireNonNull(this.getService(clazz)) + .start(); + } + + @ApiStatus.Internal + public List> getServices() { + return services; + } + + @ApiStatus.Internal + public List> getTasks() { + return tasks; } /** @@ -214,13 +227,12 @@ public void startService(final Class clazz) * @see ServiceSubscription */ @Nullable + @SuppressWarnings("unchecked") public ServiceSubscription getService(final Class clazz) { for (final ServiceSubscription service : this.services) { - if (service.getService() - .getClass() - .equals(clazz)) + if (clazz.isInstance(service.getService())) { return (ServiceSubscription) service; } @@ -239,8 +251,8 @@ public ServiceSubscription getService(final Class claz */ public void stopService(final Class clazz) { - this.getService(clazz) - .stop(); + Objects.requireNonNull(this.getService(clazz)) + .stop(); } /** @@ -254,8 +266,8 @@ public void stopService(final Class clazz) */ public void startTask(final Class clazz) { - this.getTask(clazz) - .start(); + Objects.requireNonNull(this.getTask(clazz)) + .start(); } /** @@ -272,13 +284,11 @@ public void startTask(final Class clazz) * @see #registerTask(TaskSubscription) * @see TaskSubscription */ - public TaskSubscription getTask(final Class clazz) + public @Nullable TaskSubscription getTask(final Class clazz) { for (final TaskSubscription task : this.tasks) { - if (task.getTask() - .getClass() - .equals(clazz)) + if (clazz.isInstance(task.getTask())) { return (TaskSubscription) task; } @@ -297,8 +307,8 @@ public TaskSubscription getTask(final Class clazz) */ public void stopTask(final Class clazz) { - this.getTask(clazz) - .stop(); + Objects.requireNonNull(this.getTask(clazz)) + .stop(); } /** diff --git a/Patchwork/src/main/java/fns/patchwork/service/BukkitTimer.java b/Patchwork/src/main/java/fns/patchwork/service/BukkitTimer.java new file mode 100644 index 0000000..2c6e0b8 --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/service/BukkitTimer.java @@ -0,0 +1,42 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.patchwork.service; + +import java.time.Duration; +import java.time.Instant; +import java.util.Comparator; + +public class BukkitTimer extends TimedTask +{ + protected BukkitTimer(String name, Duration interval, Duration timeout) + { + super(name, interval, timeout); + } + + @Override + protected void runTimer() + { + + } +} diff --git a/Patchwork/src/main/java/fns/patchwork/service/Service.java b/Patchwork/src/main/java/fns/patchwork/service/Service.java index b541c89..dc3a779 100644 --- a/Patchwork/src/main/java/fns/patchwork/service/Service.java +++ b/Patchwork/src/main/java/fns/patchwork/service/Service.java @@ -23,6 +23,8 @@ package fns.patchwork.service; +import fns.patchwork.provider.SubscriptionProvider; + /** * Represents a ticking service. Services may be asynchronous or synchronous, however there are some restrictions: *
    diff --git a/Patchwork/src/main/java/fns/patchwork/service/ServiceSubscription.java b/Patchwork/src/main/java/fns/patchwork/service/ServiceSubscription.java index ef178d5..0406bbb 100644 --- a/Patchwork/src/main/java/fns/patchwork/service/ServiceSubscription.java +++ b/Patchwork/src/main/java/fns/patchwork/service/ServiceSubscription.java @@ -27,6 +27,7 @@ import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** @@ -61,11 +62,6 @@ public final class ServiceSubscription */ private final int serviceId; - /** - * Whether the service is currently running. - */ - private boolean isActive = false; - /** * Creates a new subscription for the given service. By default, this method will mark this service as a synchronous * service. This will also initialize the default interval to a single tick. @@ -79,7 +75,8 @@ public final class ServiceSubscription * @param plugin The plugin which owns the service. * @param service The service to subscribe to. */ - ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service) + @ApiStatus.Internal + public ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service) { this(plugin, service, 1L, false); } @@ -94,7 +91,8 @@ public final class ServiceSubscription * @param service The service to subscribe to. * @param async Whether the service should be scheduled asynchronously. */ - ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service, final boolean async) + @ApiStatus.Internal + public ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service, final boolean async) { this(plugin, service, 1L, async); } @@ -111,7 +109,8 @@ public final class ServiceSubscription * @param service The service to subscribe to. * @param interval The interval at which the service should be scheduled. */ - ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service, final long interval) + @ApiStatus.Internal + public ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service, final long interval) { this(plugin, service, interval, false); } @@ -126,8 +125,9 @@ public final class ServiceSubscription * @param interval The interval at which the service should be scheduled. * @param async Whether the service should be scheduled asynchronously. */ - ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service, - final long interval, final boolean async) + @ApiStatus.Internal + public ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service, + final long interval, final boolean async) { this.service = service; this.async = async; @@ -142,7 +142,8 @@ public final class ServiceSubscription .runTaskTimerAsynchronously(plugin, r, 0, interval); tempId[0] = task.getTaskId(); }; - } else + } + else { this.executor = r -> { @@ -160,7 +161,6 @@ public final class ServiceSubscription */ public void start() { - this.isActive = true; this.executor.execute(service::tick); } @@ -169,7 +169,6 @@ public void start() */ public void stop() { - this.isActive = false; Bukkit.getScheduler() .cancelTask(this.getServiceId()); } @@ -206,6 +205,9 @@ public boolean isAsync() */ public boolean isActive() { - return isActive; + return Bukkit.getScheduler() + .isQueued(this.getServiceId()) || + Bukkit.getScheduler() + .isCurrentlyRunning(this.getServiceId()); } } diff --git a/Patchwork/src/main/java/fns/patchwork/service/TaskSubscription.java b/Patchwork/src/main/java/fns/patchwork/service/TaskSubscription.java index 81aac2e..11388e2 100644 --- a/Patchwork/src/main/java/fns/patchwork/service/TaskSubscription.java +++ b/Patchwork/src/main/java/fns/patchwork/service/TaskSubscription.java @@ -29,6 +29,7 @@ import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.ApiStatus; /** * Represents a subscription to a task. Task subscriptions offer a nice wrapper for managing tasks, which are inevitably @@ -57,12 +58,6 @@ public final class TaskSubscription */ private final Executor executor; - /** - * True if the task is active, false otherwise. By default, this is set to false, and will be marked as true when - * the task is started. - */ - private boolean isActive = false; - /** * Creates a new task subscription. * @@ -70,7 +65,8 @@ public final class TaskSubscription * @param task The task that is being subscribed to. * @param async True if the task is async, false otherwise. */ - TaskSubscription(final JavaPlugin plugin, final T task, final boolean async) + @ApiStatus.Internal + public TaskSubscription(final JavaPlugin plugin, final T task, final boolean async) { this.task = task; this.async = async; @@ -184,7 +180,6 @@ private Pair getSync(final JavaPlugin plugin, final long dela */ public void start() { - this.isActive = true; executor.execute(task); } @@ -193,7 +188,6 @@ public void start() */ public void stop() { - this.isActive = false; Bukkit.getScheduler() .cancelTask(this.getTaskId()); } @@ -235,6 +229,8 @@ public Executor getExecutor() */ public boolean isActive() { - return isActive; + return !this.getTask().isCancelled() || + !Bukkit.getScheduler().isQueued(this.getTaskId()) && + !Bukkit.getScheduler().isCurrentlyRunning(this.getTaskId()); } } diff --git a/Patchwork/src/main/java/fns/patchwork/service/TimedTask.java b/Patchwork/src/main/java/fns/patchwork/service/TimedTask.java new file mode 100644 index 0000000..810111b --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/service/TimedTask.java @@ -0,0 +1,55 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.patchwork.service; + +import java.time.Duration; + +public abstract class TimedTask extends Task +{ + private final Duration timeout; + private long currentTimeSeconds = 0; + + protected TimedTask(final String name, final Duration interval, final Duration timeout) + { + super(name, 0, interval); + this.timeout = timeout; + } + + protected abstract void runTimer(); + + @Override + public void run() + { + if (this.currentTimeSeconds == 0) + this.currentTimeSeconds = System.currentTimeMillis() / 1000L; + + if (System.currentTimeMillis() / 1000L - this.currentTimeSeconds >= this.timeout.getSeconds()) + { + this.cancel(); + return; + } + + this.runTimer(); + } +} diff --git a/Patchwork/src/main/java/fns/patchwork/shop/Reaction.java b/Patchwork/src/main/java/fns/patchwork/shop/Reaction.java index 60480a6..08f3baa 100644 --- a/Patchwork/src/main/java/fns/patchwork/shop/Reaction.java +++ b/Patchwork/src/main/java/fns/patchwork/shop/Reaction.java @@ -23,13 +23,17 @@ package fns.patchwork.shop; +import fns.patchwork.service.Service; +import fns.patchwork.service.Task; +import fns.patchwork.service.TimedTask; import java.time.Duration; import net.kyori.adventure.text.Component; +import org.bukkit.event.Listener; /** * Represents a chat reaction that can be performed by a player. */ -public abstract class Reaction implements Reactable +public abstract class Reaction extends TimedTask implements Reactable { private final Duration reactionDuration; private final ReactionType reactionType; @@ -53,6 +57,9 @@ protected Reaction(final long seconds, final long reward, final ReactionType rea protected Reaction(final Duration duration, final long reward, final ReactionType reactionType) { + super("CopyCatReaction", + Duration.ofSeconds(1), + Duration.ofSeconds(10)); this.reward = reward; this.reactionDuration = duration; this.reactionType = reactionType; diff --git a/Patchwork/src/main/java/fns/patchwork/sql/SQL.java b/Patchwork/src/main/java/fns/patchwork/sql/SQL.java index d68409a..5b66a10 100644 --- a/Patchwork/src/main/java/fns/patchwork/sql/SQL.java +++ b/Patchwork/src/main/java/fns/patchwork/sql/SQL.java @@ -31,7 +31,7 @@ public interface SQL { CompletableFuture prepareStatement(final String query, final Object... args); - CompletableFuture executeQuery(final String query, final Object... args); + CompletableFuture executeQuery(final String query, final Object... args); CompletableFuture executeUpdate(final String query, final Object... args); diff --git a/Patchwork/src/main/java/fns/patchwork/sql/SQLResult.java b/Patchwork/src/main/java/fns/patchwork/sql/SQLResult.java new file mode 100644 index 0000000..b23dbd5 --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/sql/SQLResult.java @@ -0,0 +1,348 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.patchwork.sql; + +import fns.patchwork.utils.container.Pair; +import fns.patchwork.utils.logging.FNS4J; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public class SQLResult +{ + private final Map> resultMap = new HashMap<>(); + + /** + * This constructor will create a new SQLResult object from the specified ResultSet. + * This will iterate through all rows and columns of the ResultSet and store them in a Map. + * The Map will contain keys of integers representing the row number, and values of Maps + * containing the column names and their values. + * + * @param resultSet The ResultSet to create the SQLResult object from. + */ + public SQLResult(final ResultSet resultSet) + { + try + { + final ResultSetMetaData metaData = resultSet.getMetaData(); + final int columnCount = metaData.getColumnCount(); + + int rowIndex = 0; + + while (resultSet.next()) + { + rowIndex++; + final Map rowMap = new HashMap<>(); + + for (int columnIndex = 1; columnIndex <= columnCount; columnIndex++) + { + String columnName = metaData.getColumnName(columnIndex); + Object columnValue = resultSet.getObject(columnIndex); + rowMap.put(columnName, columnValue); + } + + resultMap.put(rowIndex, rowMap); + } + } + catch (SQLException ex) + { + FNS4J.getLogger("Tyr").error(ex.getMessage()); + } + } + + /** + * This method will return a map of all rows and their columns and values. + * + * @return A Map containing all rows and their columns and values. + */ + public Map> getResultMap() + { + return resultMap; + } + + /** + * This method will return a map of all columns and their values from the specified row. + * + * @param rowIndex The row index to get the column names from. + * @return A Map containing all column names and their values from the specified row. + */ + public Map getRow(final int rowIndex) + { + return resultMap.get(rowIndex); + } + + /** + * This method will return the value from the specified row and column. + * + * @param rowIndex The row index to get the column name from. + * @param columnName The column name to get the value from. + * @return The value from the specified row and column. + */ + public Object getValue(final int rowIndex, final String columnName) + { + return resultMap.get(rowIndex).get(columnName); + } + + /** + * This method will return the first value from the first row of the result set. + * + * @return A Pair containing the column name and the stored value. + */ + public Pair getFirst() + { + return new Pair<>(resultMap.get(1).entrySet().iterator().next().getKey(), + resultMap.get(1).entrySet().iterator().next().getValue()); + } + + /** + * This method will return the first value from the specified row of the result set. + * + * @param rowIndex The row index to get the column name from. + * @return A Pair containing the column name and the stored value. + */ + public Pair getFirst(final int rowIndex) + { + return new Pair<>(resultMap.get(rowIndex).entrySet().iterator().next().getKey(), + resultMap.get(rowIndex).entrySet().iterator().next().getValue()); + } + + /** + * This method will return the last value from the first row of the result set. + * + * @return A Pair containing the column name and the stored value. + */ + public Pair getLast() + { + return new Pair<>(resultMap.get(1).entrySet().iterator().next().getKey(), + resultMap.get(1).entrySet().iterator().next().getValue()); + } + + /** + * This method will return the last value from the specified row of the result set. + * + * @param rowIndex The row index to get the column name from. + * @return A Pair containing the column name and the stored value. + */ + public Pair getLast(final int rowIndex) + { + return new Pair<>(resultMap.get(rowIndex).entrySet().iterator().next().getKey(), + resultMap.get(rowIndex).entrySet().iterator().next().getValue()); + } + + /** + * This method will attempt to retrieve the value from the specified row and column, + * and cast it to the specified class. This will throw a {@link ClassCastException} if the + * returned value is not an instance of the provided class. + * + * @param rowIndex The row index to get the column name from. + * @param columnName The column name to get the value from. + * @param clazz The class to cast the value to. + * @param The expected type. + * @return The value from the specified row and column, cast to the specified class. + */ + public T autoCast(final int rowIndex, final String columnName, final Class clazz) + { + final Object value = resultMap.get(rowIndex).get(columnName); + + if (!clazz.isInstance(value)) + throw new ClassCastException("Cannot cast " + value.getClass().getName() + " to " + clazz.getName()); + + return clazz.cast(resultMap.get(rowIndex).get(columnName)); + } + + /** + * @param rowIndex The row index to get the column names from. + * @return A Set containing all column names from the specified row of the result set. + */ + public Set getColumnNames(final int rowIndex) + { + return resultMap.get(rowIndex).keySet(); + } + + /** + * @return A Set containing all column names from the first row of the result set. + */ + public Set getColumnNames() + { + return resultMap.get(1).keySet(); + } + + /** + * This method will apply the specified consumer to all rows of the result set. + * + * @param columnConsumer The consumer to apply to all rows of the result set. + */ + public void accept(final Consumer> columnConsumer) + { + this.resultMap.forEach((integer, map) -> columnConsumer.accept(map)); + } + + /** + * Checks to see if the result set contains the specified row number. + * Best used in a for loop, using {@link #rowCount()} as the upper bound. + * + * @param rowIndex The row index to check. + * @return True if the result set contains the specified row number, false otherwise. + */ + public boolean hasNext(final int rowIndex) + { + return this.resultMap.containsKey(rowIndex + 1); + } + + /** + * Checks to see if the result set has the first row. + * If row 1 doesn't exist, it's safe to say the result set is empty. + * + * @return True if the result set has row 1, false otherwise. + */ + public boolean hasNext() + { + return this.resultMap.containsKey(1); + } + + /** + * @return The number of rows in the result set. + */ + public int rowCount() + { + return this.resultMap.size(); + } + + /** + * @param rowIndex The row index from which to count columns. + * @return The number of columns in the specified row. + */ + public int columnCount(final int rowIndex) + { + return this.resultMap.get(rowIndex).size(); + } + + /** + * Retrieves a String value from the specified row and column. + * + * @param rowIndex The row index to get the column name from. + * @param columnName The column name to get the value from. + * @return The String value from the specified row and column. + * @see #autoCast(int, String, Class) + */ + public String getString(final int rowIndex, final String columnName) + { + return autoCast(rowIndex, columnName, String.class); + } + + /** + * This method will attempt to retrieve a String value from the specified column within the first row of the + * result set. + * + * @param columnName The column name to get the value from. + * @return The String value from the specified column within the first row of the result set. + * @see #getString(int, String) + */ + public String getString(final String columnName) + { + return getString(1, columnName); + } + + /** + * Retrieves an Integer value from the specified row and column. + * + * @param rowIndex The row index to get the column name from. + * @param columnName The column name to get the value from. + * @return The Integer value from the specified row and column. + * @see #autoCast(int, String, Class) + */ + public int getInteger(final int rowIndex, final String columnName) + { + return autoCast(rowIndex, columnName, Integer.class); + } + + /** + * This method will attempt to retrieve an Integer value from the specified column within the first row of the + * result set. + * + * @param columnName The column name to get the value from. + * @return The Integer value from the specified column within the first row of the result set. + * @see #getInteger(int, String) + */ + public int getInteger(final String columnName) + { + return getInteger(1, columnName); + } + + /** + * Retrieves a Long value from the specified row and column. + * + * @param rowIndex The row index to get the column name from. + * @param columnName The column name to get the value from. + * @return The Long value from the specified row and column. + * @see #autoCast(int, String, Class) + */ + public long getLong(final int rowIndex, final String columnName) + { + return autoCast(rowIndex, columnName, Long.class); + } + + /** + * This method will attempt to retrieve a Long value from the specified column within the first row of the + * result set. + * + * @param columnName The column name to get the value from. + * @return The Long value from the specified column within the first row of the result set. + * @see #getLong(int, String) + */ + public long getLong(final String columnName) + { + return getLong(1, columnName); + } + + /** + * Retrieves a Double value from the specified row and column. + * + * @param rowIndex The row index to get the column name from. + * @param columnName The column name to get the value from. + * @return The Double value from the specified row and column. + * @see #autoCast(int, String, Class) + */ + public boolean getBoolean(final int rowIndex, final String columnName) + { + return autoCast(rowIndex, columnName, Boolean.class); + } + + /** + * This method will attempt to retrieve a Boolean value from the specified column within the first row of the + * result set. + * + * @param columnName The column name to get the value from. + * @return The Boolean value from the specified column within the first row of the result set. + * @see #getBoolean(int, String) + */ + public boolean getBoolean(final String columnName) + { + return getBoolean(1, columnName); + } +} diff --git a/Tyr/build.gradle b/Tyr/build.gradle index 4bd615f..8a40b19 100644 --- a/Tyr/build.gradle +++ b/Tyr/build.gradle @@ -16,11 +16,10 @@ bukkit { } dependencies { - compileOnly project(":Patchwork") - compileOnly project(":Datura") + compileOnly project(path: ":Patchwork") + compileOnly project(path: ":Datura") - library 'com.hierynomus:sshj:0.28.0' - library 'org.bouncycastle:bcprov-jdk18on:1.76' + library 'com.j256.two-factor-auth:two-factor-auth:1.3' testImplementation platform('org.junit:junit-bom:5.9.1') testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/Tyr/src/main/java/fns/tyr/Tyr.java b/Tyr/src/main/java/fns/tyr/Tyr.java new file mode 100644 index 0000000..d1c5c16 --- /dev/null +++ b/Tyr/src/main/java/fns/tyr/Tyr.java @@ -0,0 +1,48 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.tyr; + +import fns.datura.Datura; +import fns.patchwork.base.Shortcuts; +import fns.patchwork.sql.SQL; +import fns.patchwork.utils.logging.FNS4J; + +public class Tyr +{ + public void onEnable() + { + final SQL sql = Shortcuts.provideModule(Datura.class).getSQL(); + sql.createTable("sessionData", + "user VARCHAR(16) NOT NULL PRIMARY KEY, secretKey VARCHAR(64) NOT NULL;") + .whenCompleteAsync((result, throwable) -> + { + if (throwable != null) + FNS4J.getLogger("Tyr") + .error(throwable.getMessage()); + }, Shortcuts.getExecutors() + .getAsync()); + + + } +} diff --git a/Tyr/src/main/java/fns/tyr/data/SQLEntry.java b/Tyr/src/main/java/fns/tyr/data/SQLEntry.java new file mode 100644 index 0000000..ffc6e4c --- /dev/null +++ b/Tyr/src/main/java/fns/tyr/data/SQLEntry.java @@ -0,0 +1,85 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.tyr.data; + +import fns.patchwork.base.Shortcuts; +import fns.patchwork.utils.logging.FNS4J; +import fns.tyr.oauth.Identity; + +public class SQLEntry +{ + private final Identity identity; + + public SQLEntry(final Identity identity) + { + this.identity = identity; + } + + public static SQLEntry load(final String username) + { + return Shortcuts.getSQL() + .map(c -> + c.executeQuery("SELECT * FROM sessionData WHERE user = ?;", username) + .thenApplyAsync(result -> + { + SQLEntry entry = null; + if (result.hasNext()) + { + final String user = result.getString("user"); + final String secretKey = result.getString("secretKey"); + + final Identity i = new Identity(user, secretKey); + + entry = new SQLEntry(i); + FNS4J.getLogger("Tyr") + .info("Loaded entry for " + username); + } + else + { + entry = new SQLEntry(Identity.of(username)); + FNS4J.getLogger("Tyr") + .info("Created a new entry for " + username); + } + return entry; + }, Shortcuts.getExecutors() + .getAsync()) + .join()) + .orElseThrow(() -> new IllegalStateException("SQL is not initialized!")); + } + + public void save() + { + Shortcuts.getSQL() + .orElseThrow(() -> new IllegalStateException("SQL is not available!")) + .executeUpdate("INSERT INTO sessionData (user, secretKey) VALUES (?, ?);", + this.identity.username(), + this.identity.secretKey()) + .whenCompleteAsync((result, throwable) -> + { + if (throwable != null) + FNS4J.getLogger("Tyr").error(throwable.getMessage()); + }, Shortcuts.getExecutors() + .getAsync()); + } +} diff --git a/Tyr/src/main/java/fns/tyr/oauth/Identity.java b/Tyr/src/main/java/fns/tyr/oauth/Identity.java new file mode 100644 index 0000000..93b7cf2 --- /dev/null +++ b/Tyr/src/main/java/fns/tyr/oauth/Identity.java @@ -0,0 +1,31 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.tyr.oauth; + +public record Identity(String username, String secretKey) +{ + public static Identity of(final String username) { + return new Identity(username, TOTP.createSecretKey()); + } +} diff --git a/Tyr/src/main/java/fns/tyr/oauth/OAuth2.java b/Tyr/src/main/java/fns/tyr/oauth/OAuth2.java new file mode 100644 index 0000000..998dd3f --- /dev/null +++ b/Tyr/src/main/java/fns/tyr/oauth/OAuth2.java @@ -0,0 +1,74 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.tyr.oauth; + +import fns.patchwork.base.Shortcuts; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +public class OAuth2 +{ + private final Set identitySet; + + public OAuth2() + { + this.identitySet = new HashSet<>(); + } + + public void addIdentity(Identity identity) + { + this.identitySet.add(identity); + } + + public void removeIdentity(Identity identity) + { + this.identitySet.remove(identity); + } + + public Optional getIdentity(final String username) + { + return this.identitySet.stream() + .filter(identity -> identity.username().equals(username)) + .findFirst(); + } + + public void loadAll() + { + Shortcuts.getSQL() + .ifPresent(sql -> sql.executeQuery("SELECT * FROM sessionData;") + .thenAcceptAsync(result -> + { + for (int i = 1; i < result.rowCount(); i++) + { + final String username = result.getString(i, + "user"); + final String secretKey = result.getString(i, + "secretKey"); + this.addIdentity( + new Identity(username, secretKey)); + } + })); + } +} diff --git a/Tyr/src/main/java/fns/tyr/oauth/TOTP.java b/Tyr/src/main/java/fns/tyr/oauth/TOTP.java new file mode 100644 index 0000000..7a7420b --- /dev/null +++ b/Tyr/src/main/java/fns/tyr/oauth/TOTP.java @@ -0,0 +1,63 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.tyr.oauth; + +import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil; +import fns.patchwork.utils.logging.FNS4J; +import java.security.GeneralSecurityException; + +/** + * User-friendly version of TimeBasedOneTimePasswordUtil. + */ +public final class TOTP +{ + private TOTP() + { + throw new AssertionError("This class cannot be instantiated."); + } + + public static String createSecretKey() + { + return TimeBasedOneTimePasswordUtil.generateBase32Secret(32); + } + + public static String createQRCode(final String username, final String secretKey) + { + return TimeBasedOneTimePasswordUtil.qrImageUrl(username, secretKey); + } + + public static boolean verify(final String secretKey, final int userCode) + { + try + { + int vCode = TimeBasedOneTimePasswordUtil.generateCurrentNumber(secretKey); + return vCode == userCode; + } + catch (GeneralSecurityException ex) + { + FNS4J.getLogger("Tyr").error("Failed to verify TOTP code: " + ex.getMessage()); + return false; + } + } +} diff --git a/Veritas/src/main/java/fns/veritas/Aggregate.java b/Veritas/src/main/java/fns/veritas/Aggregate.java index 51afc09..16d8607 100644 --- a/Veritas/src/main/java/fns/veritas/Aggregate.java +++ b/Veritas/src/main/java/fns/veritas/Aggregate.java @@ -28,11 +28,19 @@ import fns.veritas.bukkit.ServerListener; import fns.veritas.client.BotClient; import fns.veritas.client.BotConfig; +import java.io.IOException; import org.bukkit.Bukkit; public class Aggregate { private static final FNS4J logger = FNS4J.getLogger("Veritas"); + private static final String FAILED_PACKET = """ + Failed to process inbound chat packet. + An offending element was found transmitted through the stream. + The element has been dropped, and ignored. + Offending element: %s + Caused by: %s + Stack Trace: %s"""; private final BotClient bot; private final Veritas plugin; private final BukkitNative bukkitNativeListener; @@ -40,13 +48,39 @@ public class Aggregate public Aggregate(final Veritas plugin) { + BotClient bot1; this.plugin = plugin; - this.bot = new BotClient(new BotConfig(plugin)); + + try + { + bot1 = new BotClient(new BotConfig(plugin)); + } + catch (IOException ex) + { + getLogger().error("Failed to load bot config! Shutting down..."); + getLogger().error(ex); + this.bot = null; + this.serverListener = null; + this.bukkitNativeListener = null; + Bukkit.getPluginManager().disablePlugin(plugin); + return; + } + this.bukkitNativeListener = new BukkitNative(plugin); this.serverListener = new ServerListener(plugin); - Bukkit.getServer().getPluginManager().registerEvents(this.getBukkitNativeListener(), plugin); - this.getServerListener().minecraftChatBound().subscribe(); + Bukkit.getServer() + .getPluginManager() + .registerEvents(this.getBukkitNativeListener(), plugin); + this.getServerListener() + .minecraftChatBound() + .onErrorContinue((th, v) -> Aggregate.getLogger() + .error(FAILED_PACKET.formatted( + v.getClass().getName(), + th.getCause(), + th.getMessage()))) + .subscribe(); + this.bot = bot1; } public static FNS4J getLogger() @@ -69,6 +103,11 @@ public BotClient getBot() return bot; } + public BotConfig getBotConfig() + { + return bot.getConfig(); + } + public Veritas getPlugin() { return plugin; diff --git a/Veritas/src/main/java/fns/veritas/bukkit/ServerListener.java b/Veritas/src/main/java/fns/veritas/bukkit/ServerListener.java index 6d9e113..f0ac173 100644 --- a/Veritas/src/main/java/fns/veritas/bukkit/ServerListener.java +++ b/Veritas/src/main/java/fns/veritas/bukkit/ServerListener.java @@ -54,20 +54,20 @@ public ServerListener(final Veritas plugin) public Mono minecraftChatBound() { return bot.getClient() - .getEventDispatcher() - .on(MessageCreateEvent.class) - .filter(m -> m.getMessage() - .getChannelId() - .equals(bot.getChatChannelId())) - .filter(m -> m.getMember().orElse(null) != null) - .filter(m -> !m.getMessage() - .getAuthor() - .orElseThrow(IllegalAccessError::new) - .getId() - .equals(plugin.getAggregate().getBot().getClient().getSelfId())) - .doOnError(Aggregate.getLogger()::error) - .doOnNext(this::doMessageBodyDetails) - .then(); + .getEventDispatcher() + .on(MessageCreateEvent.class) + .filter(m -> m.getMessage() + .getChannelId() + .equals(bot.getConfig().getChatChannelId())) + .filter(m -> m.getMember().orElse(null) != null) + .filter(m -> !m.getMessage() + .getAuthor() + .orElseThrow(IllegalAccessError::new) + .getId() + .equals(bot.getClient().getSelfId())) + .doOnError(Aggregate.getLogger()::error) + .doOnNext(this::doMessageBodyDetails) + .then(); } private void doMessageBodyDetails(MessageCreateEvent m) @@ -81,7 +81,7 @@ private void doMessageBodyDetails(MessageCreateEvent m) .hoverEvent(HoverEvent.showText( Component.text("Click to join our Discord server!"))) .clickEvent(ClickEvent.openUrl( - plugin.getAggregate().getBot().getInviteLink()))) + plugin.getAggregate().getBotConfig().getInviteLink()))) .append(Component.text("] ", NamedTextColor.DARK_GRAY)); TextComponent user = Component.empty(); diff --git a/Veritas/src/main/java/fns/veritas/client/BotClient.java b/Veritas/src/main/java/fns/veritas/client/BotClient.java index 46bc391..a20a3ad 100644 --- a/Veritas/src/main/java/fns/veritas/client/BotClient.java +++ b/Veritas/src/main/java/fns/veritas/client/BotClient.java @@ -23,16 +23,20 @@ package fns.veritas.client; -import discord4j.common.util.Snowflake; import discord4j.core.DiscordClientBuilder; import discord4j.core.GatewayDiscordClient; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; import discord4j.core.object.entity.Guild; import discord4j.core.object.entity.Message; +import discord4j.core.object.entity.PartialMember; +import discord4j.core.object.entity.Role; +import discord4j.core.object.entity.User; +import discord4j.core.object.entity.channel.Channel; import discord4j.core.object.entity.channel.TextChannel; import discord4j.core.spec.MessageCreateSpec; import fns.veritas.cmd.base.BotCommandHandler; import java.util.List; +import java.util.Objects; import reactor.core.publisher.Mono; public class BotClient @@ -58,37 +62,11 @@ public BotClient(final BotConfig config) client.on(ChatInputInteractionEvent.class, handler::handle); } - - public String getBotId() - { - return client.getSelfId().asString(); - } - - public Mono getServerGuildId() - { - return client.getGuildById(config.getId()); - } - public GatewayDiscordClient getClient() { return client; } - public Snowflake getChatChannelId() - { - return config.getChatChannelId(); - } - - public Snowflake getLogChannelId() - { - return config.getLogChannelId(); - } - - public String getInviteLink() - { - return config.getInviteLink(); - } - public void messageChatChannel(String message, boolean system) { String channelID = config.getChatChannelId().asString(); @@ -119,7 +97,6 @@ private String sanitizeChatMessage(String message) if (message.contains("@")) { - // \u200B is Zero Width Space, invisible on Discord newMessage = message.replace("@", "@\u200B"); } @@ -144,6 +121,33 @@ private String sanitizeChatMessage(String message) return deformat(newMessage); } + public Mono isAdmin(final User user) + { + return getGuild().flatMap(guild -> guild.getMemberById(user.getId())) + .flatMapMany(PartialMember::getRoles) + .filter(role -> getConfig().getAdminRoleId().asLong() == role.getId().asLong()) + .filter(Objects::nonNull) + .next() + .hasElement(); + } + + public Mono getLogsChannel() { + return getGuild().flatMap(guild -> guild.getChannelById(getConfig().getLogChannelId())); + } + + public Mono getChatChannel() { + return getGuild().flatMap(guild -> guild.getChannelById(getConfig().getChatChannelId())); + } + + public Mono getGuild() { + return getClient().getGuildById(getConfig().getGuildId()); + } + + public BotConfig getConfig() + { + return config; + } + public String deformat(String input) { return input.replaceAll("([_\\\\`*>|])", "\\\\$1"); diff --git a/Veritas/src/main/java/fns/veritas/client/BotConfig.java b/Veritas/src/main/java/fns/veritas/client/BotConfig.java index 08407ad..c498c4a 100644 --- a/Veritas/src/main/java/fns/veritas/client/BotConfig.java +++ b/Veritas/src/main/java/fns/veritas/client/BotConfig.java @@ -24,7 +24,8 @@ package fns.veritas.client; import discord4j.common.util.Snowflake; -import fns.patchwork.config.WrappedBukkitConfiguration; +import fns.patchwork.config.ConfigType; +import fns.patchwork.config.GenericConfig; import fns.veritas.Aggregate; import fns.veritas.Veritas; import java.io.File; @@ -38,15 +39,22 @@ public class BotConfig { @NonNls - public static final String GUILD_ID = "guild_id"; + private static final String GUILD_ID = "bot_settings.guild_id"; @NonNls - private static final String BOT_TOKEN = "bot_token"; - private final WrappedBukkitConfiguration config; + private static final String BOT_TOKEN = "bot_settings.bot_token"; + @NonNls + private static final String MC_CHANNEL_ID = "bot_settings.mc_channel_id"; + @NonNls + private static final String LOG_CHANNEL_ID = "bot_settings.log_channel_id"; + @NonNls + private static final String INVITE_LINK = "bot_settings.invite_link"; + + + private final GenericConfig config; - public BotConfig(final Veritas plugin) + public BotConfig(final Veritas plugin) throws IOException { - this.config = new WrappedBukkitConfiguration(f0(plugin), - new File(plugin.getDataFolder(), "config.yml")); + this.config = new GenericConfig(ConfigType.TOML, plugin, "config.toml"); } public String getToken() @@ -54,29 +62,29 @@ public String getToken() return config.getString(BOT_TOKEN); } - public String getPrefix() + public Snowflake getGuildId() { - return config.getString("bot_prefix"); + return Snowflake.of(config.getLong(GUILD_ID)); } - public Snowflake getId() + public Snowflake getChatChannelId() { - return Snowflake.of(config.getString(GUILD_ID)); + return Snowflake.of(config.getLong(MC_CHANNEL_ID)); } - public Snowflake getChatChannelId() + public Snowflake getLogChannelId() { - return Snowflake.of(config.getString("channel_id")); + return Snowflake.of(config.getLong(LOG_CHANNEL_ID)); } - public Snowflake getLogChannelId() + public Snowflake getAdminRoleId() { - return Snowflake.of(config.getString("log_channel_id")); + return Snowflake.of(config.getLong("admin_settings.admin_role_id")); } public String getInviteLink() { - return config.getString("invite_link"); + return config.getString(INVITE_LINK); } private Function f0(final Veritas plugin) @@ -94,11 +102,10 @@ private Function f0(final Veritas plugin) catch (IOException | InvalidConfigurationException ex) { fc.addDefault(BOT_TOKEN, "token"); - fc.addDefault("bot_prefix", "!"); - fc.addDefault(GUILD_ID, GUILD_ID); - fc.addDefault("channel_id", "nil"); - fc.addDefault("log_channel_id", "nil"); - fc.addDefault("invite_link", "https://discord.gg/invite"); + fc.addDefault(GUILD_ID, 0); + fc.addDefault(MC_CHANNEL_ID, 0); + fc.addDefault(LOG_CHANNEL_ID, 0); + fc.addDefault(INVITE_LINK, "https://discord.gg/invite"); fc.options().copyDefaults(true); diff --git a/Veritas/src/main/java/fns/veritas/cmd/BanCommand.java b/Veritas/src/main/java/fns/veritas/cmd/BanCommand.java new file mode 100644 index 0000000..564a5fe --- /dev/null +++ b/Veritas/src/main/java/fns/veritas/cmd/BanCommand.java @@ -0,0 +1,98 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.veritas.cmd; + + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.command.ApplicationCommandInteractionOption; +import discord4j.core.object.command.ApplicationCommandInteractionOptionValue; +import discord4j.core.object.entity.User; +import fns.patchwork.base.Shortcuts; +import fns.veritas.Veritas; +import fns.veritas.cmd.base.BotCommand; +import java.time.Duration; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +public class BanCommand implements BotCommand +{ + + + @Override + public String getName() + { + return "ban"; + } + + @Override + public Mono handle(ChatInputInteractionEvent event) + { + final String playerName = event.getOption("player") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + final String reason = event.getOption("reason") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + final Duration duration = event.getOption("duration") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asLong) + .map(Duration::ofMinutes) + .orElse(Duration.ofMinutes(5)); + + final User user = event.getInteraction().getUser(); + return Shortcuts.provideModule(Veritas.class) + .getAggregate() + .getBot() + .isAdmin(user) + .doOnSuccess(b -> + { + if (Boolean.FALSE.equals(b)) + return; + + final Player player = Bukkit.getPlayer(playerName); + if (player == null) + { + event.reply() + .withEphemeral(true) + .withContent("Player not found") + .block(); + return; + } + + player.ban(reason, duration, user.getUsername()); + event.reply() + .withContent("Kicked " + playerName) + .withEphemeral(true) + .block(); + + event.getInteractionResponse() + .createFollowupMessage(user.getUsername() + ": Kicked " + playerName) + .then(); + }) + .then(); + } +} diff --git a/Veritas/src/main/java/fns/veritas/cmd/KickCommand.java b/Veritas/src/main/java/fns/veritas/cmd/KickCommand.java new file mode 100644 index 0000000..22e5e11 --- /dev/null +++ b/Veritas/src/main/java/fns/veritas/cmd/KickCommand.java @@ -0,0 +1,90 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.veritas.cmd; + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.command.ApplicationCommandInteractionOption; +import discord4j.core.object.command.ApplicationCommandInteractionOptionValue; +import discord4j.core.object.entity.User; +import fns.patchwork.base.Shortcuts; +import fns.patchwork.kyori.MiniMessageWrapper; +import fns.veritas.Veritas; +import fns.veritas.cmd.base.BotCommand; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +public class KickCommand implements BotCommand +{ + @Override + public String getName() + { + return "kick"; + } + + @Override + public Mono handle(ChatInputInteractionEvent event) + { + final String playerName = event.getOption("player") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + final String reason = event.getOption("reason") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + + final User user = event.getInteraction().getUser(); + return Shortcuts.provideModule(Veritas.class) + .getAggregate() + .getBot() + .isAdmin(user) + .doOnSuccess(b -> + { + if (Boolean.FALSE.equals(b)) + return; + + final Player player = Bukkit.getPlayer(playerName); + if (player == null) + { + event.reply() + .withEphemeral(true) + .withContent("Player not found") + .block(); + return; + } + + player.kick(MiniMessageWrapper.deserialize(true, reason)); + event.reply() + .withContent("Kicked " + playerName) + .withEphemeral(true) + .block(); + + event.getInteractionResponse() + .createFollowupMessage(user.getUsername() + ": Kicked " + playerName) + .then(); + }) + .then(); + } +} diff --git a/Veritas/src/main/java/fns/veritas/cmd/WhisperCommand.java b/Veritas/src/main/java/fns/veritas/cmd/WhisperCommand.java new file mode 100644 index 0000000..2717910 --- /dev/null +++ b/Veritas/src/main/java/fns/veritas/cmd/WhisperCommand.java @@ -0,0 +1,75 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package fns.veritas.cmd; + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.command.ApplicationCommandInteractionOption; +import discord4j.core.object.command.ApplicationCommandInteractionOptionValue; +import fns.patchwork.kyori.MiniMessageWrapper; +import fns.veritas.cmd.base.BotCommand; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +public class WhisperCommand implements BotCommand +{ + @Override + public String getName() + { + return "whisper"; + } + + @Override + public Mono handle(ChatInputInteractionEvent event) + { + final String player = event.getOption("player") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + + final String message = event.getOption("message") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + final Component c = MiniMessageWrapper.deserialize(true, + "[Whisper] " + + event.getInteraction().getUser().getUsername() + + ": " + + message); + + final Player actual = Bukkit.getPlayer(player); + if (actual == null) { + return event.reply("Player not found!") + .withEphemeral(true) + .then(); + } + + actual.sendMessage(c); + + return event.reply("Sent!") + .withEphemeral(true) + .then(); + } +} diff --git a/Veritas/src/main/resources/commands/ban.json b/Veritas/src/main/resources/commands/ban.json new file mode 100644 index 0000000..cddf194 --- /dev/null +++ b/Veritas/src/main/resources/commands/ban.json @@ -0,0 +1,24 @@ +{ + "name": "ban", + "description": "Bans a user from the server.", + "options": [ + { + "name": "player", + "description": "The player to ban.", + "type": 3, + "required": true + }, + { + "name": "reason", + "description": "The reason for the ban.", + "type": 3, + "required": true + }, + { + "name": "duration", + "description": "The duration of the ban, in minutes. Default is 5 minutes.", + "type": 4, + "required": false + } + ] +} \ No newline at end of file diff --git a/Veritas/src/main/resources/commands/kick.json b/Veritas/src/main/resources/commands/kick.json new file mode 100644 index 0000000..9129e21 --- /dev/null +++ b/Veritas/src/main/resources/commands/kick.json @@ -0,0 +1,18 @@ +{ + "name": "kick", + "description": "Kicks a user from the server", + "options": [ + { + "name": "player", + "type": 3, + "description": "The player to kick", + "required": true + }, + { + "name": "reason", + "type": 3, + "description": "The reason for kicking the player", + "required": true + } + ] +} \ No newline at end of file diff --git a/Veritas/src/main/resources/commands/whisper.json b/Veritas/src/main/resources/commands/whisper.json new file mode 100644 index 0000000..c6d26ac --- /dev/null +++ b/Veritas/src/main/resources/commands/whisper.json @@ -0,0 +1,18 @@ +{ + "name": "whisper", + "description": "Whisper to a user.", + "options": [ + { + "name": "player", + "type": 3, + "description": "The in-game user to whisper to.", + "required": true + }, + { + "name": "message", + "type": 3, + "description": "The message to send.", + "required": true + } + ] +} \ No newline at end of file diff --git a/Veritas/src/main/resources/config.toml b/Veritas/src/main/resources/config.toml new file mode 100644 index 0000000..5aa33e2 --- /dev/null +++ b/Veritas/src/main/resources/config.toml @@ -0,0 +1,10 @@ +[bot_settings] +bot_token = "xyz-123-REPLACE-ME" +invite_link = "https://discord.gg/invite" +guild_id = 0 +mc_channel_id = 0 +log_channel_id = 0 + +[admin_settings] +# This role will be able to use the /kick and /ban commands. +admin_role_id = 0 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2d97049..dd20d62 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ subprojects { apply plugin: 'java-library' apply plugin: 'net.minecrell.plugin-yml.bukkit' + repositories { jcenter() mavenCentral()