diff --git a/.env.example b/.env.example index 81b3a9a..cfd38d0 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,10 @@ MYSQL_PASSWORD= MYSQL_DATABASE= MYSQL_PORT=3306 -NETNEUTRALITY_SENTRY_DSN= \ No newline at end of file +NETNEUTRALITY_PO_TOKEN= +NETNEUTRALITY_VISITOR_DATA= + +NETNEUTRALITY_SENTRY_DSN= +GITLAB_HOST= +GITLAB_TOKEN= +GITLAB_PROJECT_ID= \ No newline at end of file diff --git a/.jpb/jpb-settings.xml b/.jpb/jpb-settings.xml deleted file mode 100644 index 935cf0d..0000000 --- a/.jpb/jpb-settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8a02ef4..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,29 +0,0 @@ -## 2.0.1 (2024-07-14) - -### Refactor - -- fix inspection errors for deprecated calls and concatenation - -## 2.0.0 (2024-07-14) - -### BREAKING CHANGE - -- removed TrackLoader amd LoadableSongs - -### Feat - -- **Settings**: add helpers to get version number of bot - -### Fix - -- fix relection package to new path -- fix wrongly named environment variables -- bump youtube-source to 1.4.0 - -### Refactor - -- Remove unused command -- add release to Sentry config -- remove convoluted TrackLoader and LoadableSongs abstraction -- **LogHandler**: use the exception itself instead of a string for buildSentryErrorMessage -- change package path to reflect group id of pom.xml diff --git a/Dockerfile b/Dockerfile index cc852a0..b855b52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17-jdk-alpine as build +FROM eclipse-temurin:17-jdk-alpine AS build COPY .mvn/ .mvn COPY mvnw pom.xml ./ diff --git a/bot.properties.example b/bot.properties.example index fe1be76..895405b 100644 --- a/bot.properties.example +++ b/bot.properties.example @@ -10,4 +10,10 @@ MYSQL_PASSWORD= MYSQL_DATABASE= MYSQL_PORT=3306 -NETNEUTRALITY_SENTRY_DSN= \ No newline at end of file +NETNEUTRALITY_PO_TOKEN= +NETNEUTRALITY_VISITOR_DATA= + +NETNEUTRALITY_SENTRY_DSN= +GITLAB_HOST= +GITLAB_TOKEN= +GITLAB_PROJECT_ID= \ No newline at end of file diff --git a/build-bot.sh b/build-bot.sh old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml index 19e1c1b..ec767f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: MYSQL_ROOT_PASSWORD: 'password' MARIADB_AUTO_UPGRADE: '1' ports: - - "3306" + - "3306:3306" networks: - internal @@ -27,6 +27,7 @@ services: - ./logs:/app/logs environment: MYSQL_HOST: 'db' + MYSQL_PORT: '3306' MYSQL_USER: 'java' MYSQL_PASSWORD: 'password' MYSQL_DATABASE: 'netneutrality' diff --git a/pom.xml b/pom.xml index 8a2db6d..5fbbc9e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ me.bobert netneutrality - 2.0.1 + 3.0.0 netneutrality-discord-bot Discord bot jar @@ -28,6 +28,16 @@ + + org.flywaydb + flyway-maven-plugin + 10.17.0 + + jdbc:mysql://localhost:3306/netneutrality + root + + + org.apache.maven.plugins maven-resources-plugin @@ -84,24 +94,23 @@ Lavalink Repository https://maven.lavalink.dev/releases - net.dv8tion JDA - 5.0.0 + 5.1.2 dev.arbjerg lavaplayer - 2.1.2 + 2.2.2 dev.lavalink.youtube v2 - 1.4.0 + 1.11.5 @@ -113,23 +122,43 @@ org.hibernate.orm hibernate-core - 6.1.7.Final + 6.5.2.Final org.hibernate.orm hibernate-hikaricp - 6.1.7.Final + 6.5.2.Final com.mysql mysql-connector-j - 8.3.0 + 9.0.0 + + + org.flywaydb + flyway-core + 10.17.0 + + + org.flywaydb + flyway-mysql + 10.17.0 io.sentry sentry - 7.6.0 + 7.12.1 + + + org.gitlab4j + gitlab4j-api + 6.0.0-rc.5 + + + net.fellbaum + jemoji + 1.7.0 @@ -163,12 +192,12 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.15.0 commons-codec commons-codec - 1.15 + 1.17.0 commons-io @@ -177,9 +206,9 @@ - junit - junit - 4.13.2 + org.junit.jupiter + junit-jupiter + 5.11.0 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..e5fec92 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "renovate/renovate-config" + ] +} \ No newline at end of file diff --git a/sql/netneutrality_jpa.sql b/sql/netneutrality_jpa.sql index 61976ad..3ccf4f8 100644 --- a/sql/netneutrality_jpa.sql +++ b/sql/netneutrality_jpa.sql @@ -11,9 +11,9 @@ SET time_zone = "+00:00"; -- -- Database: `netneutrality` -- -CREATE DATABASE IF NOT EXISTS netneutrality CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE IF NOT EXISTS `netneutrality` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE `netneutrality`; -USE netneutrality; -- -------------------------------------------------------- -- @@ -22,13 +22,13 @@ USE netneutrality; DROP TABLE IF EXISTS `blacklist`; CREATE TABLE IF NOT EXISTS `blacklist` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `guild_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `word` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `enabled` tinyint(1) NOT NULL, - `creator_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - PRIMARY KEY (`id`), - KEY `fk_blacklist_guilds` (`guild_id`) + `id` int(11) NOT NULL AUTO_INCREMENT, + `guild_id` varchar(255) NOT NULL, + `word` varchar(255) NOT NULL, + `enabled` tinyint(1) NOT NULL, + `creator_id` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_blacklist_guilds` (`guild_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -39,11 +39,11 @@ CREATE TABLE IF NOT EXISTS `blacklist` ( DROP TABLE IF EXISTS `guilds`; CREATE TABLE IF NOT EXISTS `guilds` ( - `guild_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `logging` tinyint(1) NOT NULL DEFAULT 0, - `debug` tinyint(1) NOT NULL DEFAULT 1, - `log_channel` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - PRIMARY KEY (`guild_id`) + `guild_id` varchar(255) NOT NULL, + `logging` tinyint(1) NOT NULL DEFAULT 0, + `debug` tinyint(1) NOT NULL DEFAULT 1, + `log_channel` varchar(255) DEFAULT NULL, + PRIMARY KEY (`guild_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -54,12 +54,12 @@ CREATE TABLE IF NOT EXISTS `guilds` ( DROP TABLE IF EXISTS `guild_settings`; CREATE TABLE IF NOT EXISTS `guild_settings` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `guild_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `value` text COLLATE utf8mb4_unicode_ci NOT NULL, - PRIMARY KEY (`id`), - KEY `fk_guildsetting_guilds` (`guild_id`) + `id` int(11) NOT NULL AUTO_INCREMENT, + `guild_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `value` text NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_guildsetting_guilds` (`guild_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -70,10 +70,10 @@ CREATE TABLE IF NOT EXISTS `guild_settings` ( DROP TABLE IF EXISTS `logging`; CREATE TABLE IF NOT EXISTS `logging` ( - `guild_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `channel_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `enabled` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`guild_id`,`channel_id`) + `guild_id` varchar(255) NOT NULL, + `channel_id` varchar(255) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`guild_id`,`channel_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -84,32 +84,33 @@ CREATE TABLE IF NOT EXISTS `logging` ( DROP TABLE IF EXISTS `log_message`; CREATE TABLE IF NOT EXISTS `log_message` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `guild_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `channel_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `message_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `date_sent` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), - `message` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `edited_message` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `message_id` (`message_id`), - KEY `fk_log_guilds` (`guild_id`) + `id` int(11) NOT NULL AUTO_INCREMENT, + `guild_id` varchar(255) NOT NULL, + `channel_id` varchar(255) NOT NULL, + `message_id` varchar(255) NOT NULL, + `user_id` varchar(255) NOT NULL, + `date_sent` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `message` varchar(2000) DEFAULT NULL, + `edited_message` varchar(2000) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `message_id` (`message_id`), + KEY `fk_log_guilds` (`guild_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- -- --- Table structure for table `permissions` +-- Table structure for table `tasks` -- -DROP TABLE IF EXISTS `permissions`; -CREATE TABLE IF NOT EXISTS `permissions` ( - `guild_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `role_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `type` int(11) DEFAULT NULL, - `command` int(11) DEFAULT NULL, - PRIMARY KEY (`guild_id`,`role_id`) +DROP TABLE IF EXISTS `tasks`; +CREATE TABLE IF NOT EXISTS `tasks` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `task_id` varchar(255) NOT NULL, + `task_class` varchar(255) NOT NULL, + `payload` text DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `tasks_unique_taskid` (`task_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -120,13 +121,13 @@ CREATE TABLE IF NOT EXISTS `permissions` ( DROP TABLE IF EXISTS `web_users`; CREATE TABLE IF NOT EXISTS `web_users` ( - `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `email` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, - `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `expires` timestamp NOT NULL DEFAULT current_timestamp(), - `refresh_token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - PRIMARY KEY (`id`) + `id` varchar(255) NOT NULL, + `email` mediumtext NOT NULL, + `token` varchar(255) NOT NULL, + `expires` timestamp NOT NULL DEFAULT current_timestamp(), + `refresh_token` varchar(255) NOT NULL, + `remember_token` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- @@ -137,25 +138,25 @@ CREATE TABLE IF NOT EXISTS `web_users` ( -- Constraints for table `blacklist` -- ALTER TABLE `blacklist` - ADD CONSTRAINT `fk_blacklist_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE; + ADD CONSTRAINT `fk_blacklist_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE; -- -- Constraints for table `guild_settings` -- ALTER TABLE `guild_settings` - ADD CONSTRAINT `fk_guildsetting_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE; + ADD CONSTRAINT `fk_guildsetting_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE; -- -- Constraints for table `logging` -- ALTER TABLE `logging` - ADD CONSTRAINT `fk_logging_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE; + ADD CONSTRAINT `fk_logging_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE; -- -- Constraints for table `log_message` -- ALTER TABLE `log_message` - ADD CONSTRAINT `fk_log_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE; + ADD CONSTRAINT `fk_log_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE; COMMIT; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; diff --git a/src/main/java/me/bobert/netneutrality/NetNeutrality.java b/src/main/java/me/bobert/netneutrality/NetNeutrality.java index 3f37905..980fbdb 100644 --- a/src/main/java/me/bobert/netneutrality/NetNeutrality.java +++ b/src/main/java/me/bobert/netneutrality/NetNeutrality.java @@ -7,39 +7,36 @@ package me.bobert.netneutrality; */ import io.sentry.Sentry; +import me.bobert.netneutrality.core.LogHandler; +import me.bobert.netneutrality.core.command.CommandManager; +import me.bobert.netneutrality.core.db.SessionManager; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.core.thread.ThreadManager; +import me.bobert.netneutrality.hooks.EventsHandler; +import me.bobert.netneutrality.http.server.WebApi; +import me.bobert.netneutrality.music.MusicManager; +import me.bobert.netneutrality.thread.ShutdownThread; +import me.bobert.netneutrality.utils.Settings; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.Activity; -import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.exceptions.InvalidTokenException; import net.dv8tion.jda.api.requests.GatewayIntent; -import me.bobert.netneutrality.command.CommandManager; -import me.bobert.netneutrality.core.LogHandler; -import me.bobert.netneutrality.db.DBController; -import me.bobert.netneutrality.hooks.SlashCommandHooks; -import me.bobert.netneutrality.hooks.guilds.ServerJoinHandler; -import me.bobert.netneutrality.hooks.message.EditHandler; -import me.bobert.netneutrality.hooks.message.MessageReceivedHandler; -import me.bobert.netneutrality.http.server.WebApi; -import me.bobert.netneutrality.music.MusicManager; -import me.bobert.netneutrality.thread.BaseThreadFactory; -import me.bobert.netneutrality.thread.ShutdownThread; -import me.bobert.netneutrality.thread.ThreadManager; -import me.bobert.netneutrality.utils.Settings; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; public class NetNeutrality { - + private static final Logger logger = LogManager.getLogger(NetNeutrality.class); + public static final String VERSION = Settings.getVersion(); + private static NetNeutrality instance; private static JDA jda; - public final static Logger logger = LogManager.getLogger(NetNeutrality.class); - public static final String VERSION = Settings.getInstance().getMavenVersion(); public static void main(String[] args) { - String env = Settings.getInstance().get("NETNEUTRALITY_BOT_ENV", "dev"); + System.out.println("Starting NetNeutrality v" + VERSION); + String env = Settings.getInstance().get("NETNEUTRALITY_BOT_ENV", "dev"); String sentryDsn = Settings.getInstance().get("NETNEUTRALITY_SENTRY_DSN"); if (sentryDsn != null) { logger.info("Found Sentry DSN in config, setting up the SDK.."); @@ -57,23 +54,41 @@ public class NetNeutrality { Configurator.setLevel("net.dv8tion.jda", Level.DEBUG); } - DBController.init(); - initJDA(); + instance = new NetNeutrality(); + } + + public NetNeutrality() { + startup(); + } + + private void startup() { + String token = Settings.getInstance().get("NETNEUTRALITY_BOT_TOKEN"); + if (token == null) { + throw new RuntimeException("No bot token provided, exiting."); + } + + // DatabaseMigration.migrate(); + SessionManager.getSessionFactory(); CommandManager.init(); MusicManager.initPlayerManager(); - jda.addEventListener(new MessageReceivedHandler(), new EditHandler(), new ServerJoinHandler()); - jda.addEventListener(new SlashCommandHooks()); + TaskManager.getInstance().init(); + initJDA(); + CommandManager.registerSlashCommands(jda); + jda.addEventListener(new EventsHandler()); new WebApi(Integer.parseInt(Settings.getInstance().get("NETNEUTRALITY_API_PORT", "9000"))); jda.getPresence().setActivity(Activity.watching("you")); - Runtime.getRuntime().addShutdownHook(new BaseThreadFactory("shutdown").newThread(new ShutdownThread())); - + Runtime.getRuntime().addShutdownHook(new ShutdownThread(this)); } - public static void initJDA() { + public static NetNeutrality getInstance() { + return instance; + } + + public void initJDA() { String token = Settings.getInstance().get("NETNEUTRALITY_BOT_TOKEN"); if (token == null) { - DBController.shutdown(); + SessionManager.shutdown(); throw new RuntimeException("No bot token provided, exiting."); } @@ -90,30 +105,22 @@ public class NetNeutrality { } } - public static void shutdown() { + public void shutdown() { System.out.println("[Net Neutrality] Shutting down..."); MusicManager.disconnectPlayers(); ThreadManager.shutdownExecutor(); disconnect(); - DBController.shutdown(); + SessionManager.shutdown(); System.out.println("[Net Neutrality] Shutdown complete"); } - public static JDA getJDA() { + public JDA getJDA() { return jda; } - public static User getSelf() { - return jda.getSelfUser(); - - } - - private static void disconnect() { + private void disconnect() { if (jda != null) { jda.shutdown(); } - } - - } diff --git a/src/main/java/me/bobert/netneutrality/command/CommandManager.java b/src/main/java/me/bobert/netneutrality/command/CommandManager.java deleted file mode 100644 index c2b7e51..0000000 --- a/src/main/java/me/bobert/netneutrality/command/CommandManager.java +++ /dev/null @@ -1,91 +0,0 @@ -package me.bobert.netneutrality.command; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.NetNeutrality; -import me.bobert.netneutrality.core.LogHandler; -import net.dv8tion.jda.api.interactions.commands.build.CommandData; -import net.dv8tion.jda.api.interactions.commands.build.Commands; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; -import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.reflections.Reflections; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Set; - -public class CommandManager { - - private final static Logger logger = LogManager.getLogger(CommandManager.class); - public static final HashMap commands = new HashMap<>(); - - public static void init() { - initCommands(); - registerSlashCommands(); - } - - public static boolean isValidCommand(String commandString) { - return commands.containsKey(commandString); - } - - public static AbstractCommand getCommand(String commandString) { - return commands.get(commandString); - } - - private static void initCommands() { - logger.info("Loading commands.."); - - Reflections reflections = new Reflections("me.bobert.netneutrality.command"); - Set> classes = reflections.getSubTypesOf(AbstractCommand.class); - - for (Class s : classes) { - try { - if (Modifier.isAbstract(s.getModifiers())) { - continue; - } - - AbstractCommand command = s.getConstructor().newInstance(); - - if (!command.isEnabled()) - continue; - - if (!commands.containsKey(command.getCommandName())) { - commands.put(command.getCommandName(), command); - } - } catch (InstantiationException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException | NoSuchMethodException | SecurityException e) { - LogHandler.log(e); - } - } - - logger.info("Loaded {} commands!", commands.size()); - } - - private static void registerSlashCommands() { - CommandListUpdateAction commandListUpdateAction = NetNeutrality.getJDA().updateCommands(); - List slashCommands = new ArrayList<>(); - - for (AbstractCommand command : commands.values()) { - if (!command.isEnabled() || !command.isSlashCommand()) { - continue; - } - - SlashCommandData data = Commands.slash(command.getCommandName(), command.getDescription()); - data.setDefaultPermissions(command.getSlashCommandPermission()); - data.setGuildOnly(command.isGuildOnly()); - command.registerSlashCommandOptions(data); - - slashCommands.add(data); - } - - commandListUpdateAction.addCommands(slashCommands).queue(); - } -} diff --git a/src/main/java/me/bobert/netneutrality/command/CommandType.java b/src/main/java/me/bobert/netneutrality/command/CommandType.java deleted file mode 100644 index 340993e..0000000 --- a/src/main/java/me/bobert/netneutrality/command/CommandType.java +++ /dev/null @@ -1,26 +0,0 @@ -package me.bobert.netneutrality.command; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -public enum CommandType { - MUSIC("Music", 1), ADMINISTRATION("Administration", 0), UTILITY("Utility", 5), CRYPTO("Crypto", 6), UNLISTED("", -1); - - private final String description; - private final int id; - - CommandType(String description, int id) { - this.description = description; - this.id = id; - } - - public String getDescription() { - return description; - } - - public int getId() { - return id; - } -} diff --git a/src/main/java/me/bobert/netneutrality/command/ICommand.java b/src/main/java/me/bobert/netneutrality/command/ICommand.java deleted file mode 100644 index 6d290b3..0000000 --- a/src/main/java/me/bobert/netneutrality/command/ICommand.java +++ /dev/null @@ -1,44 +0,0 @@ -package me.bobert.netneutrality.command; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; -import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; - -public interface ICommand { - - boolean needAdmin(); - - boolean isEnabled(); - - boolean isGuildOnly(); - - boolean isSlashCommand(); - - String getCommandName(); - - String getDescription(); - - DefaultMemberPermissions getSlashCommandPermission(); - - void registerSlashCommandOptions(SlashCommandData data); - - CommandType getType(); - - boolean canBeExecutedBy(Guild guild, Member member); - - boolean canBeExecutedFrom(Guild guild, MessageChannel channel); - - @Deprecated - CommandExecutionResult execute(MessageCommandContext context); - - CommandExecutionResult execute(SlashCommandContext context); -} diff --git a/src/main/java/me/bobert/netneutrality/command/admin/BlacklistCommand.java b/src/main/java/me/bobert/netneutrality/command/admin/BlacklistCommand.java index 9267025..1040340 100644 --- a/src/main/java/me/bobert/netneutrality/command/admin/BlacklistCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/admin/BlacklistCommand.java @@ -5,29 +5,20 @@ package me.bobert.netneutrality.command.admin; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.command.CommandType; -import me.bobert.netneutrality.core.LogHandler; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; -import me.bobert.netneutrality.db.DBController; -import me.bobert.netneutrality.db.controllers.DBBlacklist; +import me.bobert.netneutrality.core.command.AbstractAdminCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.db.entities.OBlacklist; -import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import me.bobert.netneutrality.db.service.BlacklistService; +import me.bobert.netneutrality.db.service.GuildService; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; -import java.util.ArrayList; +import java.util.List; -public class BlacklistCommand extends AbstractCommand { - - @Override - public boolean needAdmin() { - return true; - } +public class BlacklistCommand extends AbstractAdminCommand { @Override public boolean isEnabled() { @@ -50,62 +41,44 @@ public class BlacklistCommand extends AbstractCommand { } @Override - public DefaultMemberPermissions getSlashCommandPermission() { - return DefaultMemberPermissions.DISABLED; - } - - @Override - public void registerSlashCommandOptions(SlashCommandData data) { - data.addSubcommands( + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addSubcommands( new SubcommandData("get", "Get all blacklisted words for this guild"), new SubcommandData("add", "Add a word to the blacklist") - .addOption(OptionType.STRING, "word", "Word to add"), + .addOption(OptionType.STRING, "word", "Word to add", true), new SubcommandData("remove", "Remove a word from the blacklist") - .addOption(OptionType.STRING, "word", "Word to remove") + .addOption(OptionType.STRING, "word", "Word to remove", true) ); } @Override - public CommandType getType() { - return CommandType.ADMINISTRATION; - } - - @Override - public CommandExecutionResult execute(MessageCommandContext context) { - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - String sub = context.getEvent().getFullCommandName().split(" ")[1]; - DBBlacklist controller = DBController.get(DBBlacklist.class); - String word = context.getEvent().getOption("word", "", OptionMapping::getAsString); - context.ackAndWait(true); + public CommandExecutionResult execute(CommandContext context) { + String sub = context.getSubcommand(); + String word = context.getOption("word", "", CommandOption::getAsString); + BlacklistService service = GuildService.get(context.getGuild()).getBlacklistService(); switch (sub) { - case "get" -> { + case "get" -> context.async("Retrieving list, please wait...", asyncMessage -> { + List blacklists = service.getBlacklistForGuild(); - context.getHook().editOriginal("Retrieving list, please wait...").queue(t -> { - ArrayList blacklists = new ArrayList<>(); - try { - blacklists = controller.getBlacklistForGuild(context.getGuild()); - } catch (Exception e) { - LogHandler.log(e); - } + StringBuilder sb = new StringBuilder(); + sb.append("List of blacklisted words for this server: \n"); + for (OBlacklist bl : blacklists) { + sb.append(bl.getWord()).append(" -> ").append(bl.getEnabled() ? "enabled" : "disabled").append("\n"); + } - StringBuilder sb = new StringBuilder(); - for (OBlacklist bl : blacklists) { - sb.append(bl.getWord()).append(" -> ").append(bl.getEnabled() ? "enabled" : "disabled").append("\n"); - } + if (blacklists.isEmpty()) { + sb.append("No word found"); + } - t.editMessage("List of blacklisted words for this server [" + context.getGuild().getName() + "] :\n" + (blacklists.isEmpty() ? "No word found" : sb.toString())).queue(); - - }); + asyncMessage.edit(sb.toString()); + }); + case "add" -> { + return service.add(context.getGuildUser(), word); + } + case "remove" -> { + return service.delete(word); } - case "add" -> controller.add(word, context); - case "remove" -> controller.delete(word, context); - case "disable" -> controller.disable(word, context); - case "enable" -> controller.enable(word, context); } return CommandExecutionResult.none(); diff --git a/src/main/java/me/bobert/netneutrality/command/admin/ReactionCommand.java b/src/main/java/me/bobert/netneutrality/command/admin/ReactionCommand.java new file mode 100644 index 0000000..42f2cd7 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/command/admin/ReactionCommand.java @@ -0,0 +1,166 @@ +package me.bobert.netneutrality.command.admin; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.AbstractAdminCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class ReactionCommand extends AbstractAdminCommand { + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean isGuildOnly() { + return true; + } + + @Override + public String getCommandName() { + return "reaction"; + } + + @Override + public String getDescription() { + return "Manages emoji reactions on this server"; + } + + @Override + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addSubcommandGroups( + new SubcommandGroupData("emoji", "Give roles to users using emojis").addSubcommands( + new SubcommandData("list", "Lists all registered tasks for this guild"), + new SubcommandData("setup", "Add a role to a message").addOptions( + new OptionData(OptionType.STRING, "messageid", "The ID of the message", true) + .setRequiredLength(19, 19), + new OptionData(OptionType.STRING, "emoji", "Emoji associated with the role", true), + new OptionData(OptionType.ROLE, "role", "Role to add to the emoji", true) + ), + new SubcommandData("add", "Add a role to an existing message").addOptions( + new OptionData(OptionType.STRING, "messageid", "The ID of the message", true) + .setRequiredLength(19, 19), + new OptionData(OptionType.STRING, "emoji", "Emoji associated with the role", true), + new OptionData(OptionType.ROLE, "role", "Role to add to the emoji", true) + ), + new SubcommandData("remove", "Remove a role from a message").addOptions( + new OptionData(OptionType.STRING, "messageid", "The ID of the message", true) + .setRequiredLength(19, 19), + new OptionData(OptionType.STRING, "emoji", "Emoji to remove from the message", true) + ), + new SubcommandData("delete", "Remove all roles from a message").addOptions( + new OptionData(OptionType.STRING, "messageid", "The ID of the message", true) + .setRequiredLength(19, 19) + ) + ) + ); + } + + @Override + public CommandExecutionResult execute(CommandContext context) { + context.ephemeral(); + + String guildId = context.getGuild().getId(); + String messageId = context.getOption("messageid", CommandOption::getAsString); + String group = context.getSubcommandGroup(); + String sub = context.getSubcommand(); + + return switch (group) { + case "emoji" -> executeEmoji(context, guildId, messageId, sub); + default -> CommandExecutionResult.none(); + }; + } + + private CommandExecutionResult executeEmoji(CommandContext context, String guildId, String messageId, String sub) { + String id = String.format("%s:%s", guildId, messageId); + EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id); + + switch (sub) { + case "list" -> { + List tasks = TaskManager.getInstance().findTasksByPartialId(EmojiReactionRoleTask.class, guildId); + if (tasks.isEmpty()) { + return CommandExecutionResult.info("No tasks found for this guild"); + } + + StringBuilder builder = new StringBuilder(); + builder.append("I have found the following tasks for this guild:"); + + for (EmojiReactionRoleTask item : tasks) { + Map mapping = item.getEmojiMapping().entrySet().stream() + .map(entry -> { + Role role = context.getGuild().getRoleById(entry.getValue()); + return role != null ? Map.entry(entry.getKey(), role.getAsMention()) : null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + builder.append("\n- Task ID: ").append(item.getId()).append("\n") + .append(" Message: ") + .append(String.format(Message.JUMP_URL, item.getGuildId(), item.getChannelId(), item.getMessageId())) + .append("\n") + .append(" Emoji Mappings:\n"); + + if (mapping.isEmpty()) { + builder.append(" No emoji mappings found\n"); + } else { + mapping.forEach((emoji, roleMention) -> + builder.append(" - ").append(emoji).append(" → ").append(roleMention).append("\n") + ); + } + } + + return CommandExecutionResult.success(builder.toString()); + } + case "add" -> { + if (task == null) { + return CommandExecutionResult.error("The message ID provided isn't registered with the bot"); + } + + String emoji = context.getOption("emoji", CommandOption::getAsString); + String role = context.getOption("role", CommandOption::getAsString); + task.addEmoji(emoji, role); + return CommandExecutionResult.success("Added emoji to message successfully!"); + } + case "remove" -> { + String emoji = context.getOption("emoji", CommandOption::getAsString); + + if (task == null) { + return CommandExecutionResult.error("The message ID provided isn't registered with the bot"); + } + + task.removeEmoji(emoji); + return CommandExecutionResult.success("Removed emoji from message successfully!"); + } + case "delete" -> { + if (task == null) { + return CommandExecutionResult.error("The message ID provided isn't registered with the bot"); + } + + task.delete(); + return CommandExecutionResult.success("Unregistered the message ID successfully!"); + } + } + + return CommandExecutionResult.none(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/command/basic/HelpCommand.java b/src/main/java/me/bobert/netneutrality/command/basic/HelpCommand.java index 840ab45..5e3280f 100644 --- a/src/main/java/me/bobert/netneutrality/command/basic/HelpCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/basic/HelpCommand.java @@ -5,15 +5,18 @@ package me.bobert.netneutrality.command.basic; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.command.CommandType; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.*; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.BuiltInTask; +import me.bobert.netneutrality.tasks.help.BotInfo; +import me.bobert.netneutrality.tasks.help.CommandHelp; +import me.bobert.netneutrality.tasks.help.ReportBug; import me.bobert.netneutrality.utils.MessageFunction; -import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; -import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.buttons.Button; import java.util.List; @@ -45,32 +48,33 @@ public class HelpCommand extends AbstractCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { - + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addOption(OptionType.STRING, "command", "The name of the command you want help with", false); } @Override - public CommandType getType() { - return CommandType.UTILITY; + public CommandCategory getCategory() { + return CommandCategory.UTILITY; } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - if (context.getArgs().length == 0) { - List help = MessageFunction.buildHelpMessage(context.getGuildUser()); - - context.reply(new MessageCreateBuilder().addContent(context.getDiscordUser().getAsMention()).addContent(" help has been sent your way!").build()); - context.replyPrivate(help); + public CommandExecutionResult execute(CommandContext context) { + String command = context.getOption("command", null, CommandOption::getAsString); + if (command == null) { + Button commands = BuiltInTask.createButton(CommandHelp.class); + Button info = BuiltInTask.createButton(BotInfo.class); + Button reportbug = BuiltInTask.createButton(ReportBug.class); + ActionRow row = ActionRow.of(commands, info, reportbug); + context.replyEphemeral("Thanks for using the Net Neutrality Bot! What do you need help with?", List.of(row)); + return CommandExecutionResult.none(); } + CommandContainer container = CommandManager.getCommandContainer(command); + if (container.getCommand() == null) { + return CommandExecutionResult.warn("Command does not exist"); + } + + context.replyEphemeral(MessageFunction.buildCommandHelpMessage(container)); return CommandExecutionResult.none(); } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - List help = MessageFunction.buildHelpMessage(context.getGuildUser()); - - context.sendMessageInPrivate(help); - return CommandExecutionResult.success("Help has been sent in DM!"); - } } diff --git a/src/main/java/me/bobert/netneutrality/command/basic/PingCommand.java b/src/main/java/me/bobert/netneutrality/command/basic/PingCommand.java index e287444..2dc8f22 100644 --- a/src/main/java/me/bobert/netneutrality/command/basic/PingCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/basic/PingCommand.java @@ -5,12 +5,11 @@ package me.bobert.netneutrality.command.basic; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.command.CommandType; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; +import me.bobert.netneutrality.core.command.AbstractCommand; +import me.bobert.netneutrality.core.command.CommandCategory; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; public class PingCommand extends AbstractCommand { @@ -40,25 +39,17 @@ public class PingCommand extends AbstractCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandType getType() { - return CommandType.UTILITY; + public CommandCategory getCategory() { + return CommandCategory.UTILITY; } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - if (context.getArgs().length == 0) - context.reply(String.format("Pong! [%d ms]", context.getJDA().getGatewayPing())); - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { return CommandExecutionResult.success(String.format("Pong! [%d ms]", context.getJDA().getGatewayPing())); } diff --git a/src/main/java/me/bobert/netneutrality/command/bot/ConfigureCommand.java b/src/main/java/me/bobert/netneutrality/command/bot/ConfigureCommand.java index a74d50d..e9c9652 100644 --- a/src/main/java/me/bobert/netneutrality/command/bot/ConfigureCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/bot/ConfigureCommand.java @@ -5,34 +5,25 @@ package me.bobert.netneutrality.command.bot; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.command.CommandType; -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractAdminCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.db.GSettings; +import me.bobert.netneutrality.db.service.GuildService; import me.bobert.netneutrality.music.MusicManager; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; -import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; -import java.util.List; import java.util.Map; -public class ConfigureCommand extends AbstractCommand { - - @Override - public boolean needAdmin() { - return true; - } +public class ConfigureCommand extends AbstractAdminCommand { @Override public boolean isEnabled() { @@ -55,16 +46,11 @@ public class ConfigureCommand extends AbstractCommand { } @Override - public DefaultMemberPermissions getSlashCommandPermission() { - return DefaultMemberPermissions.DISABLED; - } - - @Override - public void registerSlashCommandOptions(SlashCommandData data) { - data.addSubcommands( + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addSubcommands( new SubcommandData("music-volume", "Volume for the music player") - .addOptions(new OptionData(OptionType.INTEGER, "volume", "Integer between 0 and 100", true) - .setRequiredRange(0, 100)), + .addOptions(new OptionData(OptionType.INTEGER, "volume", "Playback volume for every session", true) + .setRequiredRange(0, 100)), new SubcommandData("error-logging", "Send errors to #error and/or #log channel") .addOption(OptionType.BOOLEAN, "debug", "Enable or disable the fonctionnality", true), new SubcommandData("music-role", "Role needed to use music commands") @@ -75,175 +61,40 @@ public class ConfigureCommand extends AbstractCommand { } @Override - public CommandType getType() { - return CommandType.ADMINISTRATION; - } - - @Override - public CommandExecutionResult execute(MessageCommandContext context) { - String[] args = context.getArgs(); - GuildManager guildManager = GuildManager.getManager(context.getGuild()); - - if (args.length > 0) { - - // Role for music commands - if (context.getArgument(0).equals("music-role")) { - if (args.length == 1) { - String roleDb = guildManager.getGuildConfigManager().getSetting(GSettings.MUSIC_ROLE); - Role role = null; - if (!roleDb.isEmpty()) { - role = context.getGuild().getRoleById(roleDb); - } - - context.reply("Current role needed to use music commands (default is everyone): %s", (role == null ? "Everyone" : role.getAsMention())); - context.reply("Reset role using 'conf music-role reset' or set it using 'conf musicrole @'"); - } - - if (args.length == 2) { - if (context.getArgument(1).equals("reset")) { - if (!guildManager.getGuildConfigManager().getSetting(GSettings.MUSIC_ROLE).isEmpty()) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_ROLE, null); - } - - context.reply("Successful reset of the role"); - } - - List roles = context.getMessage().getMentions().getRoles(); - if (roles.size() == 1) { - Role role = roles.get(0); - if (role.getGuild().getId().equals(context.getGuild().getId())) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_ROLE, role.getId()); - context.reply("Successfully set %s as the role needed for music command", role.getAsMention()); - } - } - } - } - - // Role for music commands - if (context.getArgument(0).equals("music-channel")) { - if (args.length == 1) { - String channelDb = guildManager.getGuildConfigManager().getSetting(GSettings.MUSIC_CHANNEL); - TextChannel channel = null; - if (!channelDb.isEmpty()) { - channel = context.getGuild().getTextChannelById(channelDb); - } - - context.reply("Current channel where music commands can be executed (default is everywhere): %s", (channel == null ? "Everywhere" : channel.getAsMention())); - context.reply("Reset channel using 'conf music-channel reset' or set it using 'conf music-channel #'"); - } - - if (args.length == 2) { - if (context.getArgument(1).equals("reset")) { - if (!guildManager.getGuildConfigManager().getSetting(GSettings.MUSIC_CHANNEL).isEmpty()) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_CHANNEL, null); - } - - context.reply("Successful reset of the channel"); - } - - List channels = context.getMessage().getMentions().getChannels(TextChannel.class); - if (channels.size() == 1) { - TextChannel channel = channels.get(0); - if (channel.getGuild().getId().equals(context.getGuild().getId())) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_CHANNEL, channel.getId()); - context.reply("Successfully set %s as the channel required to execute music command", channel.getAsMention()); - } - } - } - } - - // Default volume for music player - if (context.getArgument(0).equals("music-volume")) { - if (args.length == 1) { - int volume = Integer.parseInt(guildManager.getGuildConfigManager().getSetting(GSettings.MUSIC_VOLUME)); - - context.reply("Default volume for music player: %d", volume); - context.reply("Set using 'conf music-volume '"); - } - - if (args.length == 2) { - try { - int volume = Integer.parseInt(args[1]); - - if (volume > 0 && volume <= 100) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_VOLUME, String.valueOf(volume)); - MusicManager.get(context.getGuild()).setVolume(volume); - - context.reply("Default volume for music player is now %d", volume); - } else - throw new NumberFormatException(); - - } catch (NumberFormatException e) { - context.reply("Not a value between 0 and 100"); - } - } - } - - // Debug - if (context.getArgument(0).equals("debug")) { - if (args.length == 1) { - boolean debug = guildManager.isDebugEnabled(); - - context.reply("Error logging is currently %s for this guild.", (debug ? "enabled" : "disabled")); - context.reply("Use 'conf debug 0/1' to toggle between states (0 = disabled, 1 = enabled)."); - } - - if (args.length == 2) { - String str = args[1]; - boolean debug = guildManager.isDebugEnabled(); - if (str.equals("0") || str.equalsIgnoreCase("disable")) { - debug = false; - } - - if (str.equals("1") || str.equalsIgnoreCase("enable")) { - debug = true; - } - - guildManager.setDebug(debug); - context.reply("Error logging is now %s for this guild", (debug ? "enabled" : "disabled")); - - } - } - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - String sub = context.getEvent().getFullCommandName().split(" ")[1]; - GuildManager guildManager = GuildManager.getManager(context.getGuild()); - context.ackAndWait(true); + public CommandExecutionResult execute(CommandContext context) { + String sub = context.getSubcommand(); + GuildService guildService = GuildService.get(context.getGuild().getId()); + context.ephemeral(); if ("music-role".equals(sub)) { - Role role = context.getEvent().getOption("role", OptionMapping::getAsRole); + Role role = context.getOption("role", CommandOption::getAsRole); assert role != null; if (role.getPosition() == -1) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_ROLE, null); + guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, null); return CommandExecutionResult.success("Everyone can now use music commands"); } if (role.getGuild().getId().equals(context.getGuild().getId())) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_ROLE, role.getId()); + guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, role.getId()); return CommandExecutionResult.success(String.format("Successfully set %s as the role needed for music command", role.getAsMention())); } } if ("music-channel".equals(sub)) { - GuildChannelUnion channel = context.getEvent().getOption("channel", OptionMapping::getAsChannel); + GuildChannelUnion channel = context.getOption("channel", CommandOption::getAsChannel); if (channel == null) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_CHANNEL, null); + guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_CHANNEL, null); return CommandExecutionResult.success("No channel restrictions for music commands"); } if (channel instanceof TextChannel) { if (channel.getGuild().getId().equals(context.getGuild().getId())) { - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_CHANNEL, channel.getId()); + guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_CHANNEL, channel.getId()); return CommandExecutionResult.success(String.format("Successfully set %s as the channel required to execute music command", channel.getAsMention())); } @@ -252,22 +103,22 @@ public class ConfigureCommand extends AbstractCommand { } if ("music-volume".equals(sub)) { - int volume = context.getEvent().getOption("volume", 30, OptionMapping::getAsInt); - guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_VOLUME, String.valueOf(volume)); + int volume = context.getOption("volume", 30, CommandOption::getAsInt); + guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_VOLUME, String.valueOf(volume)); MusicManager.get(context.getGuild()).setVolume(volume); return CommandExecutionResult.success(String.format("Default volume for music player is now %d", volume)); } - if ("debug".equals(sub)) { - boolean debug = context.getEvent().getOption("debug", true, OptionMapping::getAsBoolean); - guildManager.setDebug(debug); + if ("error-logging".equals(sub)) { + boolean debug = context.getOption("debug", true, CommandOption::getAsBoolean); + guildService.updateDebugMode(debug); - return CommandExecutionResult.success(String.format("Error logging is now %s for this guild", (debug ? "enabled" : "disabled"))); + return CommandExecutionResult.success("Error logging is now %s for this guild", (debug ? "enabled" : "disabled")); } if ("show-all".equals(sub)) { - Map settings = guildManager.getGuildConfigManager().getSettings(); + Map settings = guildService.getGuildSettingService().getSettings(); MessageCreateBuilder builder = new MessageCreateBuilder(); for (GSettings o : GSettings.values()) { @@ -284,6 +135,6 @@ public class ConfigureCommand extends AbstractCommand { context.replyEphemeral(builder.build()); } - return CommandExecutionResult.none(); + return CommandExecutionResult.error("Unknown subcommand: " + sub); } } diff --git a/src/main/java/me/bobert/netneutrality/command/crypto/CryptoPriceCommand.java b/src/main/java/me/bobert/netneutrality/command/crypto/CryptoPriceCommand.java index 597492c..f44fd99 100644 --- a/src/main/java/me/bobert/netneutrality/command/crypto/CryptoPriceCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/crypto/CryptoPriceCommand.java @@ -5,26 +5,28 @@ package me.bobert.netneutrality.command.crypto; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.command.CommandType; import me.bobert.netneutrality.core.LogHandler; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractCommand; +import me.bobert.netneutrality.core.command.CommandCategory; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.http.WebModule; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; -import org.json.JSONException; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; import org.json.JSONObject; import java.text.NumberFormat; import java.util.Currency; import java.util.Locale; +import java.util.concurrent.TimeUnit; public class CryptoPriceCommand extends AbstractCommand { - private final String baseURL = "https://min-api.cryptocompare.com/data/price"; + private final String exchange = "Coinbase"; + private final String baseURL = "https://min-api.cryptocompare.com/data/price?fsym=%s&tsyms=%s"; + private final String exchangeApi = String.format("https://min-api.cryptocompare.com/data/v4/all/exchanges?e=%s", exchange); @Override public boolean needAdmin() { @@ -52,68 +54,27 @@ public class CryptoPriceCommand extends AbstractCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { - data.addOption(OptionType.STRING, "symbol", "The crypto symbol to fetch price for", true); - data.addOption(OptionType.STRING, "currency", "In which currency should the price be displayed"); + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addOptions( + new OptionData(OptionType.STRING, "symbol", "The crypto symbol to fetch price for", true), + new OptionData(OptionType.STRING, "currency", "In which currency should the price be displayed") + .setRequiredLength(3, 3) + ); } @Override - public CommandType getType() { - return CommandType.CRYPTO; + public CommandCategory getCategory() { + return CommandCategory.UTILITY; } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - String[] args = context.getArgs(); - String currency = "USD"; - - if (args.length > 0) { - if (args.length == 2) { - try { - currency = Currency.getInstance(args[1].toUpperCase()).getCurrencyCode(); - } catch (IllegalArgumentException exception) { - - return CommandExecutionResult.warn("Please enter a valid currency"); - } - } - - String crypto = args[0].toUpperCase(); - String finalCurrency = currency; - context.getChannel().sendMessage("Retreiving price, please wait...").queue(t -> { - String price; - Locale usa = new Locale("en", "US"); - NumberFormat dollarFormat = NumberFormat.getCurrencyInstance(usa); - dollarFormat.setMaximumFractionDigits(24); - - try { - WebModule http = new WebModule(String.format("%s?fsym=%s&tsyms=%s", baseURL, crypto, finalCurrency.toUpperCase())).request(); - JSONObject json = http.getAsObject(); - if (json.isEmpty() || (json.has("Response") && json.getString("Response").equalsIgnoreCase("Error"))) { - t.editMessage("Crypto doesn't exist.. yet").queue(); - return; - } - - price = dollarFormat.format(json.getDouble(finalCurrency)); - t.editMessage(String.format("The current price of %s in %s is %s", crypto, finalCurrency, price)).queue(); - - } catch (JSONException | IllegalArgumentException | ArithmeticException e) { - t.editMessage(String.format("Error getting price of %s!", crypto)).queue(); - LogHandler.log(e); - } - }); - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { Locale usa = new Locale("en", "US"); NumberFormat dollarFormat = NumberFormat.getCurrencyInstance(usa); dollarFormat.setMaximumFractionDigits(24); - String symbol = context.getEvent().getOption("symbol", OptionMapping::getAsString); - String currency = context.getEvent().getOption("currency", "USD", OptionMapping::getAsString); + String symbol = context.getOption("symbol", CommandOption::getAsString).toUpperCase(); + String currency = context.getOption("currency", "USD", CommandOption::getAsString).toUpperCase(); try { currency = Currency.getInstance(currency).getCurrencyCode(); @@ -122,30 +83,50 @@ public class CryptoPriceCommand extends AbstractCommand { } String finalCurrency = currency.toUpperCase(); - context.getEvent().deferReply().queue(interactionHook -> { + context.async(asyncMessage -> { try { + if (!validateSymbol(symbol)) { + asyncMessage.edit("Crypto doesn't exist.. yet"); + return; + } - WebModule http = new WebModule(String.format("%s?fsym=%s&tsyms=%s", baseURL, symbol, finalCurrency)).request(); + WebModule http = new WebModule.Builder(String.format(baseURL, symbol, finalCurrency)).cache(30, TimeUnit.MINUTES).build().request(); JSONObject json = http.getAsObject(); if (json.isEmpty() || (json.has("Response") && json.getString("Response").equalsIgnoreCase("Error"))) { - interactionHook.editOriginal("Crypto doesn't exist.. yet").queue(); + asyncMessage.edit("Crypto doesn't exist.. yet"); return; } double value = json.getDouble(finalCurrency); String price = dollarFormat.format(value); - interactionHook.editOriginal(String.format("The current price of %s in %s is %s", symbol, finalCurrency, price)).queue(); + asyncMessage.edit(String.format("The current price of %s in %s is %s", symbol, finalCurrency, price)); - } catch (JSONException | IllegalArgumentException | ArithmeticException e) { - interactionHook.editOriginal(String.format("Error getting price of %s!", symbol)).queue(); + } catch (Exception e) { + asyncMessage.edit(String.format("Error getting price of %s!", symbol)); LogHandler.log(e); } - - }); return CommandExecutionResult.none(); } + + private boolean validateSymbol(String symbol) { + try { + WebModule http = new WebModule.Builder(exchangeApi).get().cache(1, TimeUnit.DAYS).build().request(); + JSONObject json = http.getAsObject(); + + JSONObject pairs = json.getJSONObject("Data") + .getJSONObject("exchanges") + .getJSONObject(exchange) + .getJSONObject("pairs"); + + return pairs.has(symbol); + + } catch (Exception e) { + LogHandler.log(e); + return false; + } + } } diff --git a/src/main/java/me/bobert/netneutrality/command/git/ReportIssueCommand.java b/src/main/java/me/bobert/netneutrality/command/git/ReportIssueCommand.java new file mode 100644 index 0000000..94a6670 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/command/git/ReportIssueCommand.java @@ -0,0 +1,77 @@ +package me.bobert.netneutrality.command.git; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.command.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractCommand; +import me.bobert.netneutrality.core.command.CommandCategory; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.http.WebModule; +import me.bobert.netneutrality.tasks.git.ReportIssueTask; +import me.bobert.netneutrality.utils.Settings; + +import java.util.concurrent.TimeUnit; + +public class ReportIssueCommand extends AbstractCommand { + @Override + public boolean needAdmin() { + return false; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean isGuildOnly() { + return false; + } + + @Override + public boolean isSlashCommandOnly() { + return true; + } + + @Override + public String getCommandName() { + return "report-bug"; + } + + @Override + public String getDescription() { + return "Used to report a bug with the bot."; + } + + @Override + public void registerCommandOptions(CommandOptionsBuilder builder) { + + } + + @Override + public CommandCategory getCategory() { + return CommandCategory.UTILITY; + } + + @Override + public CommandExecutionResult execute(CommandContext context) { + String gitlabHost = Settings.getInstance().get("GITLAB_HOST"); + String gitlabToken = Settings.getInstance().get("GITLAB_TOKEN"); + String projectId = Settings.getInstance().get("GITLAB_PROJECT_ID"); + + if (gitlabHost == null || gitlabToken == null || projectId == null) { + return CommandExecutionResult.error("Command is not configured, failed to create issue."); + } + + if (new WebModule.Builder(gitlabHost).ping().timeout(2, TimeUnit.SECONDS).build().request().isSuccess()) { + return CommandExecutionResult.task(new ReportIssueTask(context)); + } + + return CommandExecutionResult.error("Unable to create bug reports at this time, please try again later."); + } +} diff --git a/src/main/java/me/bobert/netneutrality/command/music/BitconnectCommand.java b/src/main/java/me/bobert/netneutrality/command/music/BitconnectCommand.java index 5e5947b..b0f3065 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/BitconnectCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/BitconnectCommand.java @@ -6,13 +6,12 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.command.CommandType; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandCategory; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class BitconnectCommand extends AbstractMusicCommand { @@ -32,30 +31,22 @@ public class BitconnectCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandType getType() { - return CommandType.UNLISTED; + public CommandCategory getCategory() { + return CommandCategory.UNLISTED; } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - player.play("https://www.youtube.com/watch?v=e5nyQmaq4k4", context, false, true); - + public CommandExecutionResult execute(CommandContext context) { + MusicManager manager = MusicManager.get(context.getGuild()); + context.ephemeral(); context.deleteMessage(); - return CommandExecutionResult.none(); - } - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - context.ackAndWait(); - - player.play("https://www.youtube.com/watch?v=e5nyQmaq4k4", context, false, true); + manager.play("https://www.youtube.com/watch?v=e5nyQmaq4k4", context, true); return CommandExecutionResult.success("Ready to invest?"); } } diff --git a/src/main/java/me/bobert/netneutrality/command/music/ClearQueueCommand.java b/src/main/java/me/bobert/netneutrality/command/music/ClearQueueCommand.java index 30f06a6..eb739a8 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/ClearQueueCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/ClearQueueCommand.java @@ -6,14 +6,12 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; -import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MusicUtils; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class ClearQueueCommand extends AbstractMusicCommand { @@ -38,62 +36,20 @@ public class ClearQueueCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); + public CommandExecutionResult execute(CommandContext context) { + MusicManager manager = MusicManager.get(context.getGuild()); + context.ack(); - if (!MusicUtils.checkIfConnected(player, context)) - return CommandExecutionResult.none(); - - if (context.getArgs().length == 0) { - context.reply("Select the track you want to delete from the queue (track# or all)"); - context.reply(MessageFunction.buildViewQueueMessage(player, true)); - } - - if (context.getArgs().length > 0) { - - int queueSize = player.getQueueSize(); - if (context.getArgument(0).equals("all")) { - - if (queueSize > 0) { - player.getScheduler().getQueueManager().clearQueue(); - context.reply(String.format("Removed %d track%s from the queue", queueSize--, (queueSize > 1 ? "s" : ""))); - } else { - context.reply("Queue is empty!"); - } - } else { - - try { - int num = Integer.parseInt(context.getArgument(0)); - - if (num > 0) { - return player.getScheduler().getQueueManager().removeFromQueue(num); - } else { - context.reply("Number out of range"); - } - - } catch (NumberFormatException e) { - context.reply("Please enter a number"); - } - } + if (MusicUtils.checkIfConnected(context)) { + return manager.getQueueManager().clearQueue(); } return CommandExecutionResult.none(); } - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - MusicManager manager = MusicManager.get(context.getGuild()); - context.ackAndWait(); - - if (!MusicUtils.checkIfConnected(manager, context)) - return CommandExecutionResult.none(); - - return manager.getScheduler().getQueueManager().clearQueue(); - } - } diff --git a/src/main/java/me/bobert/netneutrality/command/music/CurrentlyPlayingCommand.java b/src/main/java/me/bobert/netneutrality/command/music/CurrentlyPlayingCommand.java index 9c432c0..42cc979 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/CurrentlyPlayingCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/CurrentlyPlayingCommand.java @@ -6,14 +6,13 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MusicUtils; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class CurrentlyPlayingCommand extends AbstractMusicCommand { @@ -33,29 +32,20 @@ public class CurrentlyPlayingCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { - // no arguments + public void registerCommandOptions(CommandOptionsBuilder builder) { + } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - - if (MusicUtils.checkIfConnected(player, context) && player.isCurrentlyPlaying()) { - context.reply(MessageFunction.buildCurrentlyPlayingMessage(player)); - } else { - context.reply("Bot isn't playing any tracks right now"); - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { MusicManager manager = MusicManager.get(context.getGuild()); - context.ackAndWait(); + context.ack(); + + if (MusicUtils.checkIfConnected(context)) { + if (!manager.isCurrentlyPlaying()) { + return CommandExecutionResult.info("Bot isn't playing any tracks right now"); + } - if (MusicUtils.checkIfConnected(manager, context) && manager.isCurrentlyPlaying()) { context.reply(MessageFunction.buildCurrentlyPlayingMessage(manager)); } diff --git a/src/main/java/me/bobert/netneutrality/command/music/JoinCommand.java b/src/main/java/me/bobert/netneutrality/command/music/JoinCommand.java index 85c4d78..459e4e2 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/JoinCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/JoinCommand.java @@ -6,14 +6,11 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; -import me.bobert.netneutrality.exceptions.CommandExecutionException; -import me.bobert.netneutrality.music.MusicManager; -import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.utils.MusicUtils; public class JoinCommand extends AbstractMusicCommand { @@ -33,55 +30,13 @@ public class JoinCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - - if (context.getGuildUser().getVoiceState() == null) { - throw new CommandExecutionException("Voice state is null, this should not happen", context, null); - } - - if (!context.getGuildUser().getVoiceState().inAudioChannel()) { - return CommandExecutionResult.warn("You are not in a voice channel"); - } - - if (player.isConnected()) { - return CommandExecutionResult.warn("Player is already in a voice channel"); - } - - VoiceChannel vChannel = (VoiceChannel) context.getGuildUser().getVoiceState().getChannel(); - if (vChannel != null) { - player.connect(vChannel); - context.reply("Joining current voice channel"); - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - context.ackAndWait(); - - if (context.getGuildUser().getVoiceState() == null) { - throw new CommandExecutionException("Voice state is null, this should not happen", context, null); - } - - if (!context.getGuildUser().getVoiceState().inAudioChannel()) { - return CommandExecutionResult.warn("You are not in a voice channel"); - } - - if (player.isConnected()) { - return CommandExecutionResult.warn("Player is already in a voice channel"); - } - - VoiceChannel vChannel = (VoiceChannel) context.getGuildUser().getVoiceState().getChannel(); - if (vChannel != null) { - player.connect(vChannel); + public CommandExecutionResult execute(CommandContext context) { + if (MusicUtils.connect(context)) { context.reply("Joining current voice channel"); } diff --git a/src/main/java/me/bobert/netneutrality/command/music/LeaveCommand.java b/src/main/java/me/bobert/netneutrality/command/music/LeaveCommand.java index 806d5a7..6ab67af 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/LeaveCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/LeaveCommand.java @@ -6,13 +6,12 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MusicUtils; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class LeaveCommand extends AbstractMusicCommand { @@ -32,30 +31,17 @@ public class LeaveCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); + public CommandExecutionResult execute(CommandContext context) { + MusicManager manager = MusicManager.get(context.getGuild()); - if (MusicUtils.checkIfConnected(player, context)) { - player.leave(); - context.reply("Leaving voice channel and clearing queue"); - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - context.ackAndWait(); - - if (MusicUtils.checkIfConnected(player, context)) { - player.leave(); + if (MusicUtils.checkIfConnected(context)) { context.reply("Leaving voice channel and clearing queue"); + manager.disconnect(); } return CommandExecutionResult.none(); diff --git a/src/main/java/me/bobert/netneutrality/command/music/PlayCommand.java b/src/main/java/me/bobert/netneutrality/command/music/PlayCommand.java index 5f3da37..b6bd2de 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/PlayCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/PlayCommand.java @@ -6,14 +6,13 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class PlayCommand extends AbstractMusicCommand { @@ -33,28 +32,17 @@ public class PlayCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { - data.addOption(OptionType.STRING, "query", "URL or search query", true); + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addOption(OptionType.STRING, "query", "URL or search query", true); } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - - if (context.getArgs().length > 0) { - player.play(context.getMessageText(), context, false, false); - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { MusicManager manager = MusicManager.get(context.getGuild()); - String query = context.getEvent().getOption("query", OptionMapping::getAsString); - context.ackAndWait(); + String query = context.getOption("query", CommandOption::getAsString); + context.ack(); - manager.play(query, context, false, false); + manager.play(query, context); return CommandExecutionResult.none(); } } diff --git a/src/main/java/me/bobert/netneutrality/command/music/QueueCommand.java b/src/main/java/me/bobert/netneutrality/command/music/QueueCommand.java index 1d7075f..09a65ca 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/QueueCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/QueueCommand.java @@ -5,19 +5,17 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MusicUtils; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; public class QueueCommand extends AbstractMusicCommand { @@ -27,6 +25,11 @@ public class QueueCommand extends AbstractMusicCommand { return true; } + @Override + public boolean needAdmin() { + return true; + } + @Override public String getCommandName() { return "queue"; @@ -38,13 +41,8 @@ public class QueueCommand extends AbstractMusicCommand { } @Override - public DefaultMemberPermissions getSlashCommandPermission() { - return DefaultMemberPermissions.DISABLED; - } - - @Override - public void registerSlashCommandOptions(SlashCommandData data) { - data.addSubcommands( + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addSubcommands( new SubcommandData("clear-all", "Clear all queued tracks"), new SubcommandData("delete-track", "Remove a single track from the queue") .addOptions(new OptionData(OptionType.INTEGER, "position", "Position of track", true) @@ -59,101 +57,45 @@ public class QueueCommand extends AbstractMusicCommand { } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); + public CommandExecutionResult execute(CommandContext context) { + MusicManager manager = MusicManager.get(context.getGuild()); + String sub = context.getSubcommand(); + context.ack(); - if (!MusicUtils.checkIfConnected(player, context)) - return CommandExecutionResult.none(); + if (MusicUtils.checkIfConnected(context)) { - if (context.getArgs().length == 0) { - context.reply("Select the track you want to delete from the queue (track# or all)"); - context.reply(MessageFunction.buildViewQueueMessage(player, true)); - } - - if (context.getArgs().length > 0) { - - int queueSize = player.getQueueSize(); - if (context.getArgument(0).equals("all")) { - - if (queueSize > 0) { - player.getScheduler().getQueueManager().clearQueue(); - context.reply(String.format("Removed %d track%s from the queue", queueSize--, (queueSize > 1 ? "s" : ""))); - } else { - context.reply("Queue is empty!"); + switch (sub) { + case "clear-all" -> { + return manager.getQueueManager().clearQueue(); } - } else { - try { - int num = Integer.parseInt(context.getArgument(0)); + case "delete-track" -> { + int position = context.getOption("position", 0, CommandOption::getAsInt); + return manager.getQueueManager().removeFromQueue(position); + } - if (num > 0) { - return player.getScheduler().getQueueManager().removeFromQueue(num); - } else { - context.reply("Number out of range"); + case "show-queue" -> context.reply(MessageFunction.buildViewQueueMessage(manager)); + + case "shuffle" -> { + return manager.getQueueManager().shuffle(); + } + + case "from-user" -> { + Member user = context.getOption("user", CommandOption::getAsMember); + if (user == null) { + return CommandExecutionResult.warn("The user isn't from this guild"); } - } catch (NumberFormatException e) { - context.reply("Please enter a number"); - } - } - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - MusicManager manager = MusicManager.get(context.getGuild()); - context.ackAndWait(); - String sub = context.getEvent().getFullCommandName().split(" ")[1]; - - if (!MusicUtils.checkIfConnected(manager, context)) { - return CommandExecutionResult.none(); - } - - switch (sub) { - case "clear-all" -> { - return manager.getScheduler().getQueueManager().clearQueue(); - } - - case "delete-track" -> { - int position = context.getEvent().getOption("position", 0, OptionMapping::getAsInt); - return manager.getScheduler().getQueueManager().removeFromQueue(position); - } - - case "show-queue" -> context.reply(MessageFunction.buildViewQueueMessage(manager, true)); - - case "shuffle" -> { - return manager.getScheduler().getQueueManager().shuffle(); - } - - case "from-user" -> { - Member user = context.getEvent().getOption("user", OptionMapping::getAsMember); - if (user == null) { - context.reply("The user isn't from this guild"); - break; + context.reply(MessageFunction.buildUserTracksMessage(manager, user)); } - context.reply(MessageFunction.buildUserTracksMessage(manager, user)); - } + case "remove-from-user" -> { + Member member = context.getOption("user", CommandOption::getAsMember); + if (member == null) { + return CommandExecutionResult.warn("The user isn't from this guild"); + } - case "remove-from-user" -> { - Member member = context.getEvent().getOption("user", OptionMapping::getAsMember); - if (member == null) { - context.reply("The user isn't from this guild"); - break; - } - - int size = manager.getScheduler() - .getQueueManager() - .getQueueWithFilter(trackInfo -> member.equals(trackInfo.getRequester())) - .size(); - - if (size == 0) { - context.reply("No tracks added by user %s", member.getEffectiveName()); - } else { - manager.getScheduler().getQueueManager().removeTracksFromUser(member); - context.reply("Removed all tracks from user %s", member.getEffectiveName()); + return manager.getQueueManager().removeTracksFromUser(member); } } } diff --git a/src/main/java/me/bobert/netneutrality/command/music/ResumeCommand.java b/src/main/java/me/bobert/netneutrality/command/music/ResumeCommand.java new file mode 100644 index 0000000..da6e9a4 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/command/music/ResumeCommand.java @@ -0,0 +1,45 @@ +package me.bobert.netneutrality.command.music;/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.music.MusicManager; +import me.bobert.netneutrality.utils.MusicUtils; + +public class ResumeCommand extends AbstractMusicCommand { + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getCommandName() { + return "resume"; + } + + @Override + public String getDescription() { + return "Resume the music player at the next track if it was stopped using the command /stop"; + } + + @Override + public void registerCommandOptions(CommandOptionsBuilder builder) { + + } + + @Override + public CommandExecutionResult execute(CommandContext context) { + MusicManager manager = MusicManager.get(context.getGuild()); + + if (MusicUtils.checkIfConnected(context)) { + return manager.resume(); + } + + return CommandExecutionResult.none(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/command/music/ShuffleCommand.java b/src/main/java/me/bobert/netneutrality/command/music/ShuffleCommand.java index e4ea2c2..a63802c 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/ShuffleCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/ShuffleCommand.java @@ -6,12 +6,12 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MusicUtils; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class ShuffleCommand extends AbstractMusicCommand { @Override @@ -30,17 +30,17 @@ public class ShuffleCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandExecutionResult execute(SlashCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { MusicManager manager = MusicManager.get(context.getGuild()); - context.ackAndWait(); + context.ack(); - if (MusicUtils.checkIfConnected(manager, context)) { - return manager.getScheduler().getQueueManager().shuffle(); + if (MusicUtils.checkIfConnected(context)) { + return manager.getQueueManager().shuffle(); } return CommandExecutionResult.none(); diff --git a/src/main/java/me/bobert/netneutrality/command/music/SkipCommand.java b/src/main/java/me/bobert/netneutrality/command/music/SkipCommand.java index 7db97f4..9dc2373 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/SkipCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/SkipCommand.java @@ -6,17 +6,16 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MusicUtils; import me.bobert.netneutrality.utils.UserFunction; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class SkipCommand extends AbstractMusicCommand { @@ -36,47 +35,23 @@ public class SkipCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { - data.addOptions(new OptionData(OptionType.INTEGER, "skip", "Number of tracks to skip") + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addOptions(new OptionData(OptionType.INTEGER, "skip", "Number of tracks to skip") .setMinValue(1)); } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - - if (!MusicUtils.checkIfConnected(player, context)) - return CommandExecutionResult.none(); - - if (UserFunction.isAdmin(context.getGuildUser()) || MusicUtils.isUserDj(context.getGuildUser())) { - if (UserFunction.isAdmin(context.getGuildUser()) && context.getArgs().length > 0) { - try { - int skips = Integer.parseInt(context.getArgument(0)); - player.getScheduler().getQueueManager().skip(skips); - - context.reply("Skipping %s track%s", skips, (skips > 1 ? "s" : "")); - } catch (NumberFormatException e) { - context.reply("Please enter how many track(s) to skip"); - } - } else { - player.getScheduler().getQueueManager().skip(1); - context.reply("Skipping current track"); - } - } else { - player.voteSkip(context); - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { MusicManager manager = MusicManager.get(context.getGuild()); - int skips = context.getEvent().getOption("skip", 1, OptionMapping::getAsInt); - context.ackAndWait(); + context.ack(); - if (MusicUtils.checkIfConnected(manager, context)) { - return manager.getScheduler().getQueueManager().skip(skips); + if (MusicUtils.checkIfConnected(context)) { + int skips = context.getOption("skip", 1, CommandOption::getAsInt); + if (UserFunction.isAdmin(context) || MusicUtils.isUserDj(context)) { + return manager.getQueueManager().skip(skips); + } else { + return manager.voteSkip(context); + } } return CommandExecutionResult.none(); diff --git a/src/main/java/me/bobert/netneutrality/command/music/StopCommand.java b/src/main/java/me/bobert/netneutrality/command/music/StopCommand.java index 48e772d..bc0f727 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/StopCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/StopCommand.java @@ -6,14 +6,13 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MusicUtils; import me.bobert.netneutrality.utils.UserFunction; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class StopCommand extends AbstractMusicCommand { @@ -33,34 +32,20 @@ public class StopCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - - if (!MusicUtils.checkIfConnected(player, context)) - return CommandExecutionResult.none(); - - // Need a resume command before - if (UserFunction.isAdmin(context.getGuildUser())) { - player.stopTrack(); - context.reply("Stopping current track"); - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { MusicManager manager = MusicManager.get(context.getGuild()); - context.ackAndWait(); + context.ack(); - if (MusicUtils.checkIfConnected(manager, context)) { - manager.stopTrack(); - return CommandExecutionResult.success("Stopping music playback"); + if (MusicUtils.checkIfConnected(context)) { + if (UserFunction.isAdmin(context)) { + manager.stopTrack(); + context.reply("Stopping current track"); + } } return CommandExecutionResult.none(); diff --git a/src/main/java/me/bobert/netneutrality/command/music/ViewQueueCommand.java b/src/main/java/me/bobert/netneutrality/command/music/ViewQueueCommand.java index 84808dc..edf78ad 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/ViewQueueCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/ViewQueueCommand.java @@ -5,14 +5,13 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MusicUtils; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class ViewQueueCommand extends AbstractMusicCommand { @@ -32,39 +31,22 @@ public class ViewQueueCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); - - if (MusicUtils.checkIfConnected(player, context)) { - int queueSize = player.getQueueSize(); - if (queueSize > 0) { - context.reply(MessageFunction.buildViewQueueMessage(player, true)); - } else { - context.reply("Queue is empty!"); - } - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { MusicManager manager = MusicManager.get(context.getGuild()); - context.ackAndWait(); + context.ack(); - if (MusicUtils.checkIfConnected(manager, context)) { + if (MusicUtils.checkIfConnected(context)) { int queueSize = manager.getQueueSize(); - - if (queueSize > 0) { - context.reply(MessageFunction.buildViewQueueMessage(manager, true)); - } else { + if (queueSize == 0) { return CommandExecutionResult.warn("Queue is empty!"); } + + context.reply(MessageFunction.buildViewQueueMessage(manager)); } return CommandExecutionResult.none(); diff --git a/src/main/java/me/bobert/netneutrality/command/music/VolumeCommand.java b/src/main/java/me/bobert/netneutrality/command/music/VolumeCommand.java index 1a7178c..90740d7 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/VolumeCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/VolumeCommand.java @@ -6,17 +6,16 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MusicUtils; import me.bobert.netneutrality.utils.UserFunction; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class VolumeCommand extends AbstractMusicCommand { @@ -36,46 +35,23 @@ public class VolumeCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { - data.addOptions(new OptionData(OptionType.INTEGER, "volume", "Playback volume for this session", true) + public void registerCommandOptions(CommandOptionsBuilder builder) { + builder.addOptions(new OptionData(OptionType.INTEGER, "volume", "Playback volume for this session", true) .setRequiredRange(0, 100)); } @Override - public CommandExecutionResult execute(MessageCommandContext context) { - MusicManager player = MusicManager.get(context.getGuild()); + public CommandExecutionResult execute(CommandContext context) { + MusicManager manager = MusicManager.get(context.getGuild()); + int volume = context.getOption("volume", 30, CommandOption::getAsInt); - if (!MusicUtils.checkIfConnected(player, context)) - return CommandExecutionResult.none(); - - if (UserFunction.isAdmin(context.getGuildUser()) || MusicUtils.isUserDj(context.getGuildUser())) { - try { - int volume = Integer.parseInt(context.getArgument(0)); - - if (volume > 0 && volume <= 100) { - player.setVolume(volume); - context.reply("Volume set to %s", volume); - } else - throw new NumberFormatException(); - - } catch (NumberFormatException e) { - context.reply("Please enter a value from 0 to 100"); + if (MusicUtils.checkIfConnected(context)) { + if (UserFunction.isAdmin(context) || MusicUtils.isUserDj(context)) { + manager.setVolume(volume); + return CommandExecutionResult.success(String.format("Volume set to %s", volume)); } } return CommandExecutionResult.none(); } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - MusicManager manager = MusicManager.get(context.getGuild()); - int volume = context.getEvent().getOption("volume", 30, OptionMapping::getAsInt); - - if (MusicUtils.checkIfConnected(manager, context)) { - manager.setVolume(volume); - return CommandExecutionResult.success(String.format("Volume set to %s", volume)); - } - - return CommandExecutionResult.none(); - } } diff --git a/src/main/java/me/bobert/netneutrality/command/music/VoteSkipCommand.java b/src/main/java/me/bobert/netneutrality/command/music/VoteSkipCommand.java index ce84c07..cb08015 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/VoteSkipCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/VoteSkipCommand.java @@ -5,13 +5,12 @@ package me.bobert.netneutrality.command.music; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractMusicCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; +import me.bobert.netneutrality.core.command.AbstractMusicCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MusicUtils; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; public class VoteSkipCommand extends AbstractMusicCommand { @@ -31,26 +30,15 @@ public class VoteSkipCommand extends AbstractMusicCommand { } @Override - public void registerSlashCommandOptions(SlashCommandData data) { + public void registerCommandOptions(CommandOptionsBuilder builder) { } @Override - public CommandExecutionResult execute(MessageCommandContext context) { + public CommandExecutionResult execute(CommandContext context) { MusicManager manager = MusicManager.get(context.getGuild()); - if (MusicUtils.checkIfConnected(manager, context)) { - manager.voteSkip(context); - } - - return CommandExecutionResult.none(); - } - - @Override - public CommandExecutionResult execute(SlashCommandContext context) { - MusicManager manager = MusicManager.get(context.getGuild()); - - if (MusicUtils.checkIfConnected(manager, context)) { + if (MusicUtils.checkIfConnected(context)) { return manager.voteSkip(context); } diff --git a/src/main/java/me/bobert/netneutrality/command/react/CreateEmojiReaction.java b/src/main/java/me/bobert/netneutrality/command/react/CreateEmojiReaction.java new file mode 100644 index 0000000..c310c6d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/command/react/CreateEmojiReaction.java @@ -0,0 +1,44 @@ +package me.bobert.netneutrality.command.react; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.AbstractMessageMenuCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import me.bobert.netneutrality.tasks.reaction.EmojiRoleFirstStep; +import net.dv8tion.jda.api.entities.Message; + +public class CreateEmojiReaction extends AbstractMessageMenuCommand { + @Override + public CommandExecutionResult execute(Message target, CommandContext context) { + EmojiReactionRoleTask task = TaskManager.getInstance().findTaskByPartialId(EmojiReactionRoleTask.class, target.getId()).orElse(null); + + if (task != null) { + context.replyEphemeral("This message is already registered with the bot, please use the modify command instead"); + return CommandExecutionResult.none(); + } + + return CommandExecutionResult.task(new EmojiRoleFirstStep(target)); + } + + @Override + public boolean needAdmin() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getCommandName() { + return "Create emoji role mapping"; + } +} diff --git a/src/main/java/me/bobert/netneutrality/command/react/ModifyEmojiReaction.java b/src/main/java/me/bobert/netneutrality/command/react/ModifyEmojiReaction.java new file mode 100644 index 0000000..d1d2c9a --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/command/react/ModifyEmojiReaction.java @@ -0,0 +1,44 @@ +package me.bobert.netneutrality.command.react; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.AbstractMessageMenuCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import me.bobert.netneutrality.tasks.reaction.EmojiRoleModify; +import net.dv8tion.jda.api.entities.Message; + +public class ModifyEmojiReaction extends AbstractMessageMenuCommand { + + @Override + public CommandExecutionResult execute(Message target, CommandContext context) { + EmojiReactionRoleTask task = TaskManager.getInstance().findTaskByPartialId(EmojiReactionRoleTask.class, target.getId()).orElse(null); + if (task == null) { + context.replyEphemeral("This message is not registered with the bot, please use the create command instead"); + return CommandExecutionResult.none(); + } + + return CommandExecutionResult.task(new EmojiRoleModify(target, task)); + } + + @Override + public boolean needAdmin() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getCommandName() { + return "Modify emoji role mapping"; + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/AsyncContext.java b/src/main/java/me/bobert/netneutrality/context/AsyncContext.java new file mode 100644 index 0000000..a4ca65e --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/AsyncContext.java @@ -0,0 +1,130 @@ +package me.bobert.netneutrality.context; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.AbstractContext; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.utils.messages.MessageEditData; + +import java.util.List; +import java.util.function.Consumer; + +public class AsyncContext extends AbstractContext { + private final InteractionHook hook; + private final Message message; + + public AsyncContext(InteractionHook hook) { + super(hook.getJDA(), hook.getInteraction().getGuild(), hook.getInteraction().getMessageChannel(), hook.getInteraction().getMember(), hook.getInteraction().getUser()); + this.hook = hook; + this.message = null; + } + + public AsyncContext(Message message) { + super(message.getJDA(), message.isFromGuild() ? message.getGuild() : null, message.getChannel(), message.getMember(), message.getAuthor()); + this.hook = null; + this.message = message; + } + + public String getMessageId() { + if (hook != null) { + return hook.getId(); + } + + return message.getId(); + } + + @Override + public Event getEvent() { + return null; + } + + @Override + public String getId() { + if (hook != null) { + return hook.getInteraction().getId(); + } + + return String.format("async:%s", message.getId()); + } + + @Override + public ChannelType getType() { + return ChannelType.UNKNOWN; + } + + @Override + public InteractionHook getInteractionHook() { + return hook; + } + + @Override + public boolean isAcknowledged() { + if (hook != null) { + return hook.getInteraction().isAcknowledged(); + } + + return true; + } + + @Override + public boolean isFromGuild() { + return message != null ? message.isFromGuild() : hook.getInteraction().isFromGuild(); + } + + @Override + public void deleteMessage() { + if (hook != null) { + hook.deleteOriginal().queue(); + } + + if (message != null && getType() != ChannelType.PRIVATE) { + message.delete().queue(); + } + } + + @Override + public void reply(Modal modal) { + + } + + @Override + public void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer) { + if (hook != null) { + hook.sendMessage(messageCreateData).queue(); + } + + if (message != null) { + message.reply(messageCreateData).queue(); + } + } + + @Override + public void defer(boolean ephemeral, Consumer consumer) { + + } + + @Override + public void ack(boolean ephemeral) { + + } + + @Override + public void edit(MessageEditData messageEditData, boolean ephemeral, List components, Consumer consumer) { + if (hook != null) { + hook.editOriginal(messageEditData).queue(); + } + + if (message != null) { + message.editMessage(messageEditData).queue(); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/MusicChannelContext.java b/src/main/java/me/bobert/netneutrality/context/MusicChannelContext.java new file mode 100644 index 0000000..7dd80f3 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/MusicChannelContext.java @@ -0,0 +1,51 @@ +package me.bobert.netneutrality.context; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.AbstractContext; +import me.bobert.netneutrality.core.context.NoMessageContext; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; + +import java.util.List; +import java.util.function.Consumer; + +public class MusicChannelContext extends AbstractContext implements NoMessageContext { + + public MusicChannelContext(GuildMessageChannel channel, Member member) { + super(channel.getJDA(), channel.getGuild(), channel, member, member.getUser()); + } + + @Override + public Event getEvent() { + return null; + } + + @Override + public String getId() { + return String.format("music:%s", getChannel().getId()); + } + + @Override + public boolean isFromGuild() { + return true; + } + + @Override + public void deleteMessage() { + + } + + @Override + public void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer) { + if (getChannel().canTalk()) { + getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message))); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/command/MessageCommandContext.java b/src/main/java/me/bobert/netneutrality/context/command/MessageCommandContext.java new file mode 100644 index 0000000..7864318 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/command/MessageCommandContext.java @@ -0,0 +1,280 @@ +package me.bobert.netneutrality.context.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.MessageCommandOption; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.MessageContext; +import me.bobert.netneutrality.utils.Settings; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +public class MessageCommandContext extends CommandContext implements MessageContext { + private final MessageReceivedEvent event; + private final Message message; + + public MessageCommandContext(MessageReceivedEvent event) { + super(event.getJDA(), event.isFromGuild() ? event.getGuild() : null, event.getChannel(), event.getMember(), event.getAuthor()); + this.event = event; + this.message = event.getMessage(); + } + + @Override + public MessageReceivedEvent getEvent() { + return event; + } + + @Override + public String getId() { + return String.format("%s:%s:%s:%s", + getGuild() == null ? "PrivateMessage" : getGuild().getId(), + getChannel().getId(), + getDiscordUser().getId(), + message.getId()); + } + + @Override + public Message getMessage() { + return message; + } + + @Override + public List parseOptions(String[] args, List commandOptions) { + List options = new ArrayList<>(); + Mentions mentions = message.getMentions(); + int roleMentionIndex = 0; + int userMentionIndex = 0; + int memberMentionIndex = 0; + int channelMentionIndex = 0; + int index = 0; + + for (OptionData data : commandOptions) { + OptionType type = data.getType(); + Set requiredChannelTypes = data.getChannelTypes(); + List choices = data.getChoices(); + Object value = null; + + try { + switch (type) { + case STRING -> { + String str = args[index]; + + if ((str.contains("<@") || str.contains("<#")) && str.contains(">")) { + invalidateContext(data, "expected a string at position " + (index + 1) + " but a role/user/channel was provided"); + break; + } + + if (!choices.isEmpty()) { + List matched = choices.stream().filter(choice -> choice.getAsString().equalsIgnoreCase(str)).toList(); + if (matched.isEmpty()) { + invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices)); + } + } + + Integer min = data.getMinLength(); + Integer max = data.getMaxLength(); + + if (min != null && str.length() < min) { + invalidateContext(data, "minimum length for argument is " + min); + } + + if (max != null && str.length() > max) { + invalidateContext(data, "maximum length for argument is " + max); + } + + value = str; + } + case INTEGER -> { + long num = Long.parseLong(args[index]); + if (!choices.isEmpty()) { + List matched = choices.stream().filter(choice -> choice.getAsLong() == num).toList(); + if (matched.isEmpty()) { + invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices)); + } + } + value = num; + + Number max = data.getMaxValue(); + Number min = data.getMinValue(); + + if (min != null && num < min.longValue()) { + value = min.longValue(); + } else if (max != null && num > max.longValue()) { + value = max.longValue(); + } + + value = num; + } + case BOOLEAN -> { + String str = args[index]; + if (str.equalsIgnoreCase("true") || str.equalsIgnoreCase("false")) { + value = Boolean.parseBoolean(str); + break; + } + + invalidateContext(data, "expected true or false"); + } + case NUMBER -> { + double num = Long.parseLong(args[index]); + if (!choices.isEmpty()) { + List matched = choices.stream().filter(choice -> choice.getAsDouble() == num).toList(); + if (matched.isEmpty()) { + invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices)); + break; + } + } + + Number max = data.getMaxValue(); + Number min = data.getMinValue(); + value = num; + + if (min != null && num < min.doubleValue()) { + value = min.doubleValue(); + } else if (max != null && num > max.doubleValue()) { + value = max.doubleValue(); + } + } + case ROLE -> { + if (mentions.getRoles().size() > roleMentionIndex) { + Role role = mentions.getRoles().get(roleMentionIndex++); + if (role.getGuild().getId().equals(getGuild().getId())) { + value = role; + } + + break; + } + + invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid role."); + } + case USER -> { + if (mentions.getMembers().size() > memberMentionIndex) { + Member member = mentions.getMembers().get(memberMentionIndex++); + if (member.getGuild().getId().equals(getGuild().getId())) { + value = member; + break; + } + } + + if (mentions.getUsers().size() > userMentionIndex) { + value = mentions.getUsers().get(userMentionIndex++); + break; + } + + invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid user."); + } + case MENTIONABLE -> { + if (mentions.getRoles().size() > roleMentionIndex) { + Role role = mentions.getRoles().get(roleMentionIndex++); + if (role.getGuild().getId().equals(getGuild().getId())) { + value = role; + } + + break; + } + + if (mentions.getMembers().size() > memberMentionIndex) { + Member member = mentions.getMembers().get(memberMentionIndex++); + if (member.getGuild().getId().equals(getGuild().getId())) { + value = member; + } + + break; + } + + if (mentions.getUsers().size() > userMentionIndex) { + value = mentions.getUsers().get(userMentionIndex++); + break; + } + + invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid user or role."); + } + case CHANNEL -> { + if (mentions.getChannels().size() > channelMentionIndex) { + GuildChannel channel = mentions.getChannels().get(channelMentionIndex++); + + if (!requiredChannelTypes.contains(channel.getType())) { + invalidateContext(data, "not a valid channel type at index " + (index + 1) + + " , valid types are: " + validTypes(requiredChannelTypes)); + break; + } + + if (channel.getGuild().getId().equals(getGuild().getId())) { + value = channel; + break; + } + } + + invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid channel."); + } + } + } catch (IndexOutOfBoundsException e) { + if (data.isRequired()) { + // This should never happen because required options are validated before calling parseOptions() + throw new IllegalArgumentException("Required option " + data.getName() + " was not found at index " + index); + } + } catch (NumberFormatException ignored) { + invalidateContext(data, "the argument provided is not a number."); + } + + if (value != null) { + options.add(new MessageCommandOption(data, value)); + } + + index++; + } + + return options; + } + + private String validChoices(List choices) { + StringJoiner joiner = new StringJoiner(", "); + for (Command.Choice choice : choices) { + joiner.add(choice.getAsString()); + } + + return joiner.toString(); + } + + private String validTypes(Set types) { + StringJoiner joiner = new StringJoiner(", "); + for (ChannelType type : types) { + joiner.add(type.name()); + } + + return joiner.toString(); + } + + @Override + public String parseCommandName() { + String rawMessage = message.getContentRaw().trim(); + String prefix = Settings.getInstance().getPrefix(); + + if (rawMessage.startsWith(prefix)) { + return getFullCommand().split(" ", 2)[0]; + } + + return null; + } + + @Override + public String getFullCommand() { + String rawMessage = message.getContentRaw().trim(); + String prefix = Settings.getInstance().getPrefix(); + + return rawMessage.replaceFirst("^[" + prefix + "\\s]+", "").trim(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/command/MessageMenuContext.java b/src/main/java/me/bobert/netneutrality/context/command/MessageMenuContext.java new file mode 100644 index 0000000..89e2d6b --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/command/MessageMenuContext.java @@ -0,0 +1,61 @@ +package me.bobert.netneutrality.context.command; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.InteractionContext; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.List; + +public class MessageMenuContext extends CommandContext implements InteractionContext { + private final MessageContextInteractionEvent event; + + public MessageMenuContext(MessageContextInteractionEvent event) { + super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser()); + this.event = event; + } + + @Override + public Event getEvent() { + return event; + } + + @Override + public ChannelType getType() { + return event.getChannelType(); + } + + @Override + public void deleteMessage() { + + } + + @Override + public List parseOptions(String[] args, List commandOptions) { + return List.of(); // no options for context menu commands + } + + @Override + public String parseCommandName() { + return getFullCommand(); + } + + @Override + public String getFullCommand() { + return event.getName(); + } + + public Message getTarget() { + return event.getTarget(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/command/SlashCommandContext.java b/src/main/java/me/bobert/netneutrality/context/command/SlashCommandContext.java new file mode 100644 index 0000000..2d81b39 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/command/SlashCommandContext.java @@ -0,0 +1,66 @@ +package me.bobert.netneutrality.context.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.SlashCommandOption; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.InteractionContext; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.List; +import java.util.stream.Collectors; + +public class SlashCommandContext extends CommandContext implements InteractionContext { + private final SlashCommandInteractionEvent event; + + public SlashCommandContext(SlashCommandInteractionEvent event) { + super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser()); + this.event = event; + } + + @Override + public SlashCommandInteractionEvent getEvent() { + return event; + } + + @Override + public ChannelType getType() { + return event.getChannelType(); + } + + @Override + public void deleteMessage() { + // not implemented + } + + @Override + public List parseOptions(String[] args, List commandOptions) { + return event.getOptions().stream().map(SlashCommandOption::new).collect(Collectors.toUnmodifiableList()); + } + + @Override + public String parseCommandName() { + return event.getName(); + } + + @Override + public String getFullCommand() { + return event.getFullCommandName(); + } + + @Override + public String getSubcommandGroup() { + return event.getSubcommandGroup(); + } + + @Override + public String getSubcommand() { + return event.getSubcommandName(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/command/UserMenuContext.java b/src/main/java/me/bobert/netneutrality/context/command/UserMenuContext.java new file mode 100644 index 0000000..b3342ed --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/command/UserMenuContext.java @@ -0,0 +1,62 @@ +package me.bobert.netneutrality.context.command; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.InteractionContext; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.List; + +public class UserMenuContext extends CommandContext implements InteractionContext { + private final UserContextInteractionEvent event; + + public UserMenuContext(UserContextInteractionEvent event) { + super(event.getJDA(), event.getGuild(), (MessageChannel) event.getChannel(), event.getMember(), event.getUser()); + this.event = event; + } + + @Override + public Event getEvent() { + return event; + } + + @Override + public ChannelType getType() { + return event.getChannelType(); + } + + @Override + public void deleteMessage() { + + } + + @Override + public List parseOptions(String[] args, List commandOptions) { + return List.of(); // no options for context menu commands + } + + @Override + public String parseCommandName() { + return getFullCommand(); + } + + @Override + public String getFullCommand() { + return event.getName(); + } + + public User getTarget() { + return event.getTarget(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/tasks/ButtonTaskContext.java b/src/main/java/me/bobert/netneutrality/context/tasks/ButtonTaskContext.java new file mode 100644 index 0000000..30caaf9 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/tasks/ButtonTaskContext.java @@ -0,0 +1,36 @@ +package me.bobert.netneutrality.context.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.InteractionContext; +import me.bobert.netneutrality.core.context.TaskContext; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; + +public class ButtonTaskContext extends TaskContext implements InteractionContext { + private final ButtonInteractionEvent event; + + public ButtonTaskContext(ButtonInteractionEvent event) { + super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser()); + this.event = event; + } + + @Override + public ButtonInteractionEvent getEvent() { + return event; + } + + @Override + public ChannelType getType() { + return event.getChannelType(); + } + + @Override + public String getId() { + return event.getComponentId(); + } + +} diff --git a/src/main/java/me/bobert/netneutrality/context/tasks/EmojiTaskContext.java b/src/main/java/me/bobert/netneutrality/context/tasks/EmojiTaskContext.java new file mode 100644 index 0000000..3a93ede --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/tasks/EmojiTaskContext.java @@ -0,0 +1,53 @@ +package me.bobert.netneutrality.context.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.AsyncContext; +import me.bobert.netneutrality.core.context.NoMessageContext; +import me.bobert.netneutrality.core.context.TaskContext; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; + +import java.util.List; +import java.util.function.Consumer; + +public class EmojiTaskContext extends TaskContext implements NoMessageContext { + public static final String ID = "et:%s:%s"; + private final MessageReactionAddEvent event; + + public EmojiTaskContext(MessageReactionAddEvent event) { + super(event.getJDA(), event.isFromGuild() ? event.getGuild() : null, event.getChannel(), event.getMember(), event.getUser()); + this.event = event; + addContextData("emoji", event.getEmoji()); + addContextData("emoji-code", event.getEmoji().getAsReactionCode()); + } + + @Override + public MessageReactionAddEvent getEvent() { + return event; + } + + @Override + public String getId() { + return String.format(ID, getGuild().getId(), event.getMessageId()); + } + + @Override + public boolean isFromGuild() { + return event.isFromGuild(); + } + + @Override + public void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer) { + if (getChannel() != null && getChannel().canTalk()) { + getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message))); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/tasks/MessageTaskContext.java b/src/main/java/me/bobert/netneutrality/context/tasks/MessageTaskContext.java new file mode 100644 index 0000000..9009bec --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/tasks/MessageTaskContext.java @@ -0,0 +1,56 @@ +package me.bobert.netneutrality.context.tasks; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.MessageContext; +import me.bobert.netneutrality.core.context.TaskContext; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageReference; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; + +public class MessageTaskContext extends TaskContext implements MessageContext { + private final MessageReceivedEvent event; + private final Message message; + private final String id; + + public MessageTaskContext(MessageReceivedEvent event) { + super(event.getJDA(), event.isFromGuild() ? event.getGuild() : null, event.getChannel(), event.getMember(), event.getAuthor()); + this.event = event; + this.message = event.getMessage(); + + MessageReference reference = message.getMessageReference(); + this.id = reference == null ? event.getMessageId() : reference.getMessageId(); + } + + @Override + public Event getEvent() { + return event; + } + + @Override + public String getId() { + return id; + } + + @Override + public void replyEphemeral(MessageCreateData messageCreateData) { + if (getType() == ChannelType.PRIVATE) { + reply(messageCreateData); + return; + } + + sendMessageInPrivate(messageCreateData); + } + + @Override + public Message getMessage() { + return message; + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/tasks/ModalTaskContext.java b/src/main/java/me/bobert/netneutrality/context/tasks/ModalTaskContext.java new file mode 100644 index 0000000..348de30 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/context/tasks/ModalTaskContext.java @@ -0,0 +1,37 @@ +package me.bobert.netneutrality.context.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.InteractionContext; +import me.bobert.netneutrality.core.context.TaskContext; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; + +public class ModalTaskContext extends TaskContext implements InteractionContext { + private final ModalInteractionEvent event; + + public ModalTaskContext(ModalInteractionEvent event) { + super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser()); + this.event = event; + addContextData("values", event.getValues()); + } + + @Override + public ModalInteractionEvent getEvent() { + return event; + } + + @Override + public ChannelType getType() { + return event.getChannelType(); + } + + @Override + public String getId() { + return event.getModalId(); + } + +} diff --git a/src/main/java/me/bobert/netneutrality/core/BotState.java b/src/main/java/me/bobert/netneutrality/core/BotState.java index db78b80..fbd0793 100644 --- a/src/main/java/me/bobert/netneutrality/core/BotState.java +++ b/src/main/java/me/bobert/netneutrality/core/BotState.java @@ -5,9 +5,9 @@ package me.bobert.netneutrality.core; * https://discord.gg/qUa6zNFZsd */ +import me.bobert.netneutrality.db.service.GuildService; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.music.TrackScheduler; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import org.json.JSONArray; import org.json.JSONObject; @@ -21,25 +21,28 @@ public class BotState { //TODO: is it needed? private static void saveGuildsState() { - Map managers = GuildManager.getManagers(); + Map services = GuildService.getServices(); JSONObject main = new JSONObject(); JSONArray guildsArray = new JSONArray(); - for (GuildManager manager : managers.values()) { - MusicManager musicManager = MusicManager.get(manager.getGuild()); - TrackScheduler trackScheduler = musicManager.getScheduler(); + for (MusicManager manager : MusicManager.getManagers()) { + TrackScheduler trackScheduler = manager.getScheduler(); + } + + for (GuildService service : services.values()) { + JSONObject guild = new JSONObject(); JSONObject music = new JSONObject(); - guild.put("id", manager.getGuild().getId()); + guild.put("id", service.getEntity().getGuildId()); - MessageChannel currentChannel = trackScheduler.getTextChannel(); - if (currentChannel == null) { - music.put("current_channel", "null"); - } else { - music.put("current_channel", currentChannel.getId()); - } +// MessageChannel currentChannel = trackScheduler.getTextChannel(); +// if (currentChannel == null) { +// music.put("current_channel", "null"); +// } else { +// music.put("current_channel", currentChannel.getId()); +// } guild.put("music", music); diff --git a/src/main/java/me/bobert/netneutrality/core/CommandProcessor.java b/src/main/java/me/bobert/netneutrality/core/CommandProcessor.java deleted file mode 100644 index e76021a..0000000 --- a/src/main/java/me/bobert/netneutrality/core/CommandProcessor.java +++ /dev/null @@ -1,112 +0,0 @@ -package me.bobert.netneutrality.core; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import io.sentry.Sentry; -import me.bobert.netneutrality.command.AbstractCommand; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.command.CommandManager; -import me.bobert.netneutrality.core.context.CommandContext; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; -import me.bobert.netneutrality.exceptions.CommandExecutionException; -import me.bobert.netneutrality.utils.UserFunction; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import static me.bobert.netneutrality.command.CommandExecutionResult.Result.ERROR; -import static me.bobert.netneutrality.command.CommandExecutionResult.Result.NOACTION; - -public class CommandProcessor { - - private final static Logger logger = LogManager.getLogger(CommandProcessor.class); - private final String commandName; - private final CommandContext context; - - public CommandProcessor(String commandName, CommandContext context) { - this.commandName = commandName; - this.context = context; - } - - public void dispatch() { - - if (!CommandManager.isValidCommand(commandName)) { - return; - } - - AbstractCommand command = CommandManager.getCommand(commandName); - logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild().getId()); - - if (context instanceof SlashCommandContext slashCommandContext) - logger.debug("SlashCommandInteractionId: {}", slashCommandContext.getEvent().getInteraction().getId()); - - configureSentry(command); - - try { - - if (context instanceof MessageCommandContext && command.needAdmin() && !UserFunction.isAdmin(context.getGuildUser())) { - context.reply("You are not powerful enough, scrub"); - return; - } - - if (command.canBeExecutedBy(context.getGuild(), context.getGuildUser())) { - if (command.canBeExecutedFrom(context.getGuild(), context.getChannel())) { - CommandExecutionResult commandExecutionResult = null; - - if (context instanceof SlashCommandContext slashCommandContext) { - commandExecutionResult = command.execute(slashCommandContext); - } - - if (context instanceof MessageCommandContext messageCommandContext) { - commandExecutionResult = command.execute(messageCommandContext); - } - - if (commandExecutionResult == null) { - throw new RuntimeException("Command type is not implemented."); - } - - CommandExecutionResult.Result result = commandExecutionResult.getResult(); - String message = commandExecutionResult.getMessage(); - if (result == NOACTION) { - return; - } - - if (result == ERROR) { - Exception exception = commandExecutionResult.getException(); - - logger.error("Error from command {}: {}", command.getCommandName(), message, exception); - } - - context.reply(message); - } - } - - //TODO: make better - } catch (CommandExecutionException exception) { - LogHandler.log(exception); - } catch (Exception exception) { - LogHandler.log(new CommandExecutionException("Error during command execution", context, exception)); - } - } - - private void configureSentry(AbstractCommand command) { - - Sentry.configureScope(scope -> { - scope.setContexts("command", command.getCommandName()); - scope.setContexts("isAdmin", UserFunction.isAdmin(context.getGuildUser())); - scope.setContexts("canBeExecuted", command.canBeExecutedBy(context.getGuild(), context.getGuildUser())); - scope.setTag("channel", context.getChannel().getId()); - - if (context instanceof SlashCommandContext slashCommandContext) - scope.setTag("interactionId", slashCommandContext.getEvent().getInteraction().getId()); - - if (context instanceof MessageCommandContext messageCommandContext) - scope.setTag("message", messageCommandContext.getMessage().getId()); - - scope.setTag("guild", context.getGuild().getId()); - }); - } -} diff --git a/src/main/java/me/bobert/netneutrality/core/GuildConfigManager.java b/src/main/java/me/bobert/netneutrality/core/GuildConfigManager.java deleted file mode 100644 index 6e30312..0000000 --- a/src/main/java/me/bobert/netneutrality/core/GuildConfigManager.java +++ /dev/null @@ -1,97 +0,0 @@ -package me.bobert.netneutrality.core; - -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.db.GSettings; -import me.bobert.netneutrality.db.entities.OGuild; -import me.bobert.netneutrality.db.entities.OGuildSetting; -import me.bobert.netneutrality.utils.JpaUtils; -import net.dv8tion.jda.api.entities.Guild; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.hibernate.Session; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class GuildConfigManager { - - private static final Logger logger = LogManager.getLogger(GuildConfigManager.class); - private final Guild guild; - private final OGuild guildEntity; - - private final Map settingsCache = new HashMap<>(); - - public GuildConfigManager(Guild guild, OGuild guildEntity) { - this.guild = guild; - this.guildEntity = guildEntity; - - reloadSettingsCache(); - } - - public Map getSettings() { - return settingsCache; - } - - public String getSetting(GSettings setting) { - String value = settingsCache.get(setting.getName()); - - if (value == null) { - value = setting.getDefaultValue(); - } - - return value; - } - - - public void setSettingValue(GSettings setting, String value) { - Session session = SessionManager.getSessionFactory().openSession(); - JpaUtils.tryLoad(session, guildEntity, guildEntity.getGuildId()); - - OGuildSetting oGuildSetting = guildEntity.getSettingByName(setting.getName()); - - session.beginTransaction(); - if (value == null || setting.getDefaultValue().equals(value)) { - if (oGuildSetting != null) { - session.remove(oGuildSetting); - } - } else { - if (oGuildSetting == null) { - oGuildSetting = new OGuildSetting(); - oGuildSetting.setGuild(guildEntity); - oGuildSetting.setName(setting.getName()); - oGuildSetting.setValue(value); - - session.persist(oGuildSetting); - } else { - oGuildSetting.setValue(value); - - session.merge(oGuildSetting); - } - } - - session.getTransaction().commit(); - session.close(); - - reloadSettingsCache(); - } - - public void reloadSettingsCache() { - logger.debug("Reloading settings cache for guild: {}", guild.getId()); - settingsCache.clear(); - - JpaUtils.getWithSession(guildEntity, session -> { - Set settingSet = guildEntity.getGuildSettings(); - - for (OGuildSetting setting : settingSet) { - settingsCache.put(setting.getName(), setting.getValue()); - } - }); - } - -} diff --git a/src/main/java/me/bobert/netneutrality/core/GuildManager.java b/src/main/java/me/bobert/netneutrality/core/GuildManager.java deleted file mode 100644 index 8ed2587..0000000 --- a/src/main/java/me/bobert/netneutrality/core/GuildManager.java +++ /dev/null @@ -1,168 +0,0 @@ -package me.bobert.netneutrality.core; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.NetNeutrality; -import me.bobert.netneutrality.db.DBController; -import me.bobert.netneutrality.db.controllers.DBMessage; -import me.bobert.netneutrality.db.entities.OBlacklist; -import me.bobert.netneutrality.db.entities.OGuild; -import me.bobert.netneutrality.thread.ThreadManager; -import me.bobert.netneutrality.utils.JpaUtils; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.hibernate.Session; -import org.hibernate.SessionFactory; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -public class GuildManager { - - private static final Logger logger = LogManager.getLogger(GuildManager.class); - private static final Map managers = new ConcurrentHashMap<>(); - private static final SessionFactory sessionFactory = SessionManager.getSessionFactory(); - private final Set blacklistCache = new HashSet<>(); - private final GuildConfigManager guildConfigManager; - private final Guild guild; - private OGuild guildEntity; - - private GuildManager(Guild guild) { - this.guild = guild; - this.guildEntity = loadGuildFromDb(guild.getId()); - this.guildConfigManager = new GuildConfigManager(guild, this.guildEntity); - - managers.put(guild.getId(), this); - reloadBlacklistCache(); - } - - private GuildManager(String guildId) { - Guild temp = NetNeutrality.getJDA().getGuildById(guildId); - if (temp == null) - throw new IllegalArgumentException("Guild does not exist in current cache"); - - this.guild = temp; - this.guildEntity = loadGuildFromDb(guild.getId()); - this.guildConfigManager = new GuildConfigManager(guild, this.guildEntity); - - managers.put(guild.getId(), this); - reloadBlacklistCache(); - } - - public static GuildManager getManager(Guild guild) { - if (managers.containsKey(guild.getId())) { - return managers.get(guild.getId()); - } else { - return new GuildManager(guild); - } - } - - public static GuildManager getManager(String guildId) { - if (managers.containsKey(guildId)) { - return managers.get(guildId); - } else { - return new GuildManager(guildId); - } - } - - // Getter - public static Map getManagers() { - return managers; - } - - public Guild getGuild() { - return guild; - } - - public OGuild getGuildEntity() { - return guildEntity; - } - - public Set getBlacklistCache() { - return blacklistCache; - } - - public GuildConfigManager getGuildConfigManager() { - return guildConfigManager; - } - - // Methods - public boolean isLoggingEnabled() { - return guildEntity.getLogging(); - } - - public boolean isDebugEnabled() { - return guildEntity.getDebug(); - } - - public void setDebug(boolean toggle) { - if (toggle == isDebugEnabled()) - return; - - JpaUtils.updateWithTransaction(guildEntity, () -> guildEntity.setDebug(toggle)); - } - - public void reloadBlacklistCache() { - logger.debug("Reloading blacklist cache for guild: {}", guild.getId()); - blacklistCache.clear(); - - JpaUtils.getWithSession(guildEntity, session -> { - Set oBlacklistSet = guildEntity.getBlacklists(); - - for (OBlacklist bl : oBlacklistSet) { - if (bl.getEnabled()) { - blacklistCache.add(bl.getWord()); - } - } - }); - } - - private OGuild loadGuildFromDb(String guildId) { - Session session = sessionFactory.openSession(); - OGuild entity = session.get(OGuild.class, guildId); - - if (entity == null) { - logger.debug("Guild[{}] doesn't exist in DB, creating entity..", guildId); - session.beginTransaction(); - entity = new OGuild(); - - entity.setGuildId(guildId); - - session.persist(entity); - session.getTransaction().commit(); - - guild.getTextChannels() - .stream() - .filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log")) - .forEach(textChannel -> textChannel.sendMessage("Error logging enabled on channel " + textChannel.getAsMention()).queue()); - } - - session.close(); - return entity; - } - - public void reloadEntity() { - this.guildEntity = loadGuildFromDb(guild.getId()); - } - - public void removeFromDB() { - Session session = sessionFactory.openSession(); - session.beginTransaction(); - - session.remove(guildEntity); - session.getTransaction().commit(); - session.close(); - - managers.remove(guild.getId()); - } - -} diff --git a/src/main/java/me/bobert/netneutrality/core/LogHandler.java b/src/main/java/me/bobert/netneutrality/core/LogHandler.java index 9e9a09b..790036a 100644 --- a/src/main/java/me/bobert/netneutrality/core/LogHandler.java +++ b/src/main/java/me/bobert/netneutrality/core/LogHandler.java @@ -7,9 +7,10 @@ package me.bobert.netneutrality.core; import io.sentry.Sentry; import me.bobert.netneutrality.core.context.CommandContext; -import me.bobert.netneutrality.core.context.SlashCommandContext; -import me.bobert.netneutrality.exceptions.CommandException; -import me.bobert.netneutrality.exceptions.CommandExecutionException; +import me.bobert.netneutrality.core.context.Context; +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.db.service.GuildService; +import me.bobert.netneutrality.exceptions.ContextException; import me.bobert.netneutrality.utils.MessageFunction; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; @@ -18,7 +19,7 @@ import org.apache.logging.log4j.Logger; public class LogHandler { - private final static Logger logger = LogManager.getLogger(LogHandler.class); + private static final Logger logger = LogManager.getLogger(LogHandler.class); /** * Log an exception accordingly @@ -28,12 +29,10 @@ public class LogHandler { public static void log(Exception e) { Sentry.captureException(e); - if (e instanceof CommandExecutionException exception) { - CommandContext context = exception.getCommandContext(); - - logInternally(exception, context); - tryLogToDebugChannel(exception, context); - tryGracefulSlashContextError(exception, context); + if (e instanceof ContextException exception) { + logInternally(exception); + tryLogToDebugChannel(exception); + tryGracefulContextError(exception); return; } @@ -51,35 +50,48 @@ public class LogHandler { } public static void sendMessageToDebugChannel(Guild guild, String message) { - if (GuildManager.getManager(guild).isDebugEnabled()) { - guild.getTextChannels() - .stream() - .filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log")) - .filter(TextChannel::canTalk) - .forEach(textChannel -> textChannel.sendMessage(message).queue()); + guild.getTextChannels() + .stream() + .filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log")) + .filter(TextChannel::canTalk) + .forEach(textChannel -> textChannel.sendMessage(message).queue()); + + } + + private static void logInternally(ContextException exception) { + Context context = exception.getContext(); + String guildId = context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId(); + + logger.error("Exception thrown from context (guild: {}, user: {}): {}", guildId, + context.getDiscordUser().getId(), + exception.getMessage() + ); + + if (context instanceof CommandContext commandContext) { + logger.error("Caused by command: {}", commandContext.getCommand().getCommandName(), exception); + } + + if (context instanceof TaskContext taskContext) { + logger.error("Caused by task with ID: {}", taskContext.getId(), exception); } } - private static void logInternally(CommandException exception, CommandContext context) { - logger.error("Exception thrown from command [{}] in guild [{}]: {}", context.getCommand().getCommandName(), context.getGuild().getId(), exception.getMessage(), exception); - } + private static void tryLogToDebugChannel(ContextException exception) { + Context context = exception.getContext(); - private static void tryLogToDebugChannel(CommandException exception, CommandContext context) { if (context.getGuild() != null) { - if (GuildManager.getManager(context.getGuild()).isDebugEnabled()) { + if (GuildService.get(context.getGuild()).isDebugEnabled()) { context.getGuild() .getTextChannels() .stream() .filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log")) .filter(TextChannel::canTalk) - .forEach(textChannel -> textChannel.sendMessageEmbeds(MessageFunction.buildDebugMessage(context, exception)).queue()); + .forEach(textChannel -> textChannel.sendMessageEmbeds(MessageFunction.buildDebugMessage(exception)).queue()); } } } - private static void tryGracefulSlashContextError(CommandException exception, CommandContext context) { - if (context instanceof SlashCommandContext slashCommandContext) { - slashCommandContext.reply(MessageFunction.buildSentryErrorMessage(exception)); - } + private static void tryGracefulContextError(ContextException exception) { + exception.getContext().reply(MessageFunction.buildSentryErrorMessage(exception)); } } diff --git a/src/main/java/me/bobert/netneutrality/core/command/AbstractAdminCommand.java b/src/main/java/me/bobert/netneutrality/core/command/AbstractAdminCommand.java new file mode 100644 index 0000000..b98b3e1 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/AbstractAdminCommand.java @@ -0,0 +1,19 @@ +package me.bobert.netneutrality.core.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +public abstract class AbstractAdminCommand extends AbstractCommand { + + @Override + public boolean needAdmin() { + return true; + } + + @Override + public CommandCategory getCategory() { + return CommandCategory.ADMINISTRATION; + } +} diff --git a/src/main/java/me/bobert/netneutrality/command/AbstractCommand.java b/src/main/java/me/bobert/netneutrality/core/command/AbstractCommand.java similarity index 52% rename from src/main/java/me/bobert/netneutrality/command/AbstractCommand.java rename to src/main/java/me/bobert/netneutrality/core/command/AbstractCommand.java index f02d43b..439d57a 100644 --- a/src/main/java/me/bobert/netneutrality/command/AbstractCommand.java +++ b/src/main/java/me/bobert/netneutrality/core/command/AbstractCommand.java @@ -1,4 +1,4 @@ -package me.bobert.netneutrality.command; +package me.bobert.netneutrality.core.command; /* * Copyright (c) anthorob @@ -6,38 +6,43 @@ package me.bobert.netneutrality.command; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.core.context.MessageCommandContext; -import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; -public abstract class AbstractCommand implements ICommand { +public abstract class AbstractCommand implements Command { @Override public boolean isSlashCommand() { return true; } + @Override + public boolean isSlashCommandOnly() { + return false; + } + @Override public DefaultMemberPermissions getSlashCommandPermission() { + if (needAdmin()) { + return DefaultMemberPermissions.DISABLED; + } + return DefaultMemberPermissions.ENABLED; } @Override - public boolean canBeExecutedBy(Guild guild, Member member) { + public Type getType() { + return Type.SLASH; + } + + @Override + public boolean canBeExecutedBy(Member member) { return true; } @Override - public boolean canBeExecutedFrom(Guild guild, MessageChannel channel) { + public boolean canBeExecutedFrom(MessageChannel channel) { return true; } - - @Deprecated - @Override - public CommandExecutionResult execute(MessageCommandContext context) { - // Prefer slash commands - return CommandExecutionResult.none(); - } } diff --git a/src/main/java/me/bobert/netneutrality/core/command/AbstractMessageMenuCommand.java b/src/main/java/me/bobert/netneutrality/core/command/AbstractMessageMenuCommand.java new file mode 100644 index 0000000..1dfc4cd --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/AbstractMessageMenuCommand.java @@ -0,0 +1,56 @@ +package me.bobert.netneutrality.core.command; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.command.MessageMenuContext; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; +import net.dv8tion.jda.api.entities.Message; + +public abstract class AbstractMessageMenuCommand extends AbstractCommand { + @Override + public String getDescription() { + return null; + } + + @Override + public boolean isGuildOnly() { + return true; + } + + @Override + public boolean isSlashCommandOnly() { + return true; + } + + @Override + public void registerCommandOptions(CommandOptionsBuilder builder) { + // no options for context commands + } + + @Override + public CommandCategory getCategory() { + return CommandCategory.UNLISTED; + } + + @Override + public Type getType() { + return Type.MESSAGE; + } + + @Override + public CommandExecutionResult execute(CommandContext context) { + if (context instanceof MessageMenuContext mc) { + Message message = mc.getTarget(); + return execute(message, context); + } + + throw new IllegalArgumentException("Wrong context type for this command"); + } + + public abstract CommandExecutionResult execute(Message target, CommandContext context); +} diff --git a/src/main/java/me/bobert/netneutrality/command/AbstractMusicCommand.java b/src/main/java/me/bobert/netneutrality/core/command/AbstractMusicCommand.java similarity index 50% rename from src/main/java/me/bobert/netneutrality/command/AbstractMusicCommand.java rename to src/main/java/me/bobert/netneutrality/core/command/AbstractMusicCommand.java index c59f7a3..a4e9c8d 100644 --- a/src/main/java/me/bobert/netneutrality/command/AbstractMusicCommand.java +++ b/src/main/java/me/bobert/netneutrality/core/command/AbstractMusicCommand.java @@ -1,17 +1,18 @@ -package me.bobert.netneutrality.command; +package me.bobert.netneutrality.core.command; /* * Copyright (c) anthorob * * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.core.GuildManager; import me.bobert.netneutrality.db.GSettings; +import me.bobert.netneutrality.db.service.GuildService; import me.bobert.netneutrality.utils.UserFunction; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; public abstract class AbstractMusicCommand extends AbstractCommand { @@ -27,13 +28,19 @@ public abstract class AbstractMusicCommand extends AbstractCommand { } @Override - public CommandType getType() { - return CommandType.MUSIC; + public CommandCategory getCategory() { + return CommandCategory.MUSIC; } @Override - public boolean canBeExecutedBy(Guild guild, Member member) { - String roleDb = GuildManager.getManager(guild).getGuildConfigManager().getSetting(GSettings.MUSIC_ROLE); + public boolean canBeExecutedBy(Member member) { + if (member == null) { + return true; + } + + Guild guild = member.getGuild(); + + String roleDb = GuildService.get(guild.getId()).getGuildSettingService().getSetting(GSettings.MUSIC_ROLE); if (roleDb != null && !roleDb.isEmpty()) { if (UserFunction.isAdmin(member)) { @@ -43,7 +50,7 @@ public abstract class AbstractMusicCommand extends AbstractCommand { Role neededRole = guild.getRoleById(roleDb); if (neededRole == null) { // Role doesn't exist on guild anymore, remove from db - GuildManager.getManager(guild).getGuildConfigManager().setSettingValue(GSettings.MUSIC_ROLE, null); + GuildService.get(guild.getId()).getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, null); return true; } @@ -54,14 +61,15 @@ public abstract class AbstractMusicCommand extends AbstractCommand { } @Override - public boolean canBeExecutedFrom(Guild guild, MessageChannel channel) { - if (channel.getType() != ChannelType.TEXT) - return false; + public boolean canBeExecutedFrom(MessageChannel channel) { + if (channel instanceof GuildMessageChannel guildMessageChannel) { + if (channel.getType() != ChannelType.TEXT) + return false; - String channelDb = GuildManager.getManager(guild).getGuildConfigManager().getSetting(GSettings.MUSIC_CHANNEL); - - if (channelDb != null && !channelDb.isEmpty()) { - return channelDb.equals(channel.getId()); + String channelDb = GuildService.get(guildMessageChannel.getGuild()).getGuildSettingService().getSetting(GSettings.MUSIC_CHANNEL); + if (channelDb != null && !channelDb.isEmpty()) { + return channelDb.equals(channel.getId()); + } } return true; diff --git a/src/main/java/me/bobert/netneutrality/core/command/AbstractUserMenuCommand.java b/src/main/java/me/bobert/netneutrality/core/command/AbstractUserMenuCommand.java new file mode 100644 index 0000000..c9b2625 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/AbstractUserMenuCommand.java @@ -0,0 +1,46 @@ +package me.bobert.netneutrality.core.command; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.command.UserMenuContext; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; +import net.dv8tion.jda.api.entities.User; + +public abstract class AbstractUserMenuCommand extends AbstractCommand { + @Override + public String getDescription() { + return null; + } + + @Override + public boolean isSlashCommandOnly() { + return false; + } + + @Override + public void registerCommandOptions(CommandOptionsBuilder builder) { + // no options for context commands + } + + @Override + public Type getType() { + return Type.USER; + } + + @Override + public CommandExecutionResult execute(CommandContext context) { + if (context instanceof UserMenuContext uc) { + User user = uc.getTarget(); + return execute(user, context); + } + + throw new IllegalArgumentException("Wrong context type for this command"); + } + + public abstract CommandExecutionResult execute(User target, CommandContext context); +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/Command.java b/src/main/java/me/bobert/netneutrality/core/command/Command.java new file mode 100644 index 0000000..b6f3f02 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/Command.java @@ -0,0 +1,46 @@ +package me.bobert.netneutrality.core.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; +import me.bobert.netneutrality.core.context.CommandContext; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; + +public interface Command { + enum Type { + SLASH, USER, MESSAGE + } + + boolean needAdmin(); + + boolean isEnabled(); + + boolean isGuildOnly(); + + boolean isSlashCommand(); + + boolean isSlashCommandOnly(); + + String getCommandName(); + + String getDescription(); + + void registerCommandOptions(CommandOptionsBuilder builder); + + DefaultMemberPermissions getSlashCommandPermission(); + + CommandCategory getCategory(); + + Type getType(); + + boolean canBeExecutedBy(Member member); + + boolean canBeExecutedFrom(MessageChannel channel); + + CommandExecutionResult execute(CommandContext context); +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandCategory.java b/src/main/java/me/bobert/netneutrality/core/command/CommandCategory.java new file mode 100644 index 0000000..bc5e909 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandCategory.java @@ -0,0 +1,25 @@ +package me.bobert.netneutrality.core.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +public enum CommandCategory { + MUSIC("Music"), ADMINISTRATION("Administration"), UTILITY("Utility"), CRYPTO("Crypto"), UNLISTED(""); + + private final String description; + + CommandCategory(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return description; + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandContainer.java b/src/main/java/me/bobert/netneutrality/core/command/CommandContainer.java new file mode 100644 index 0000000..37e6ed7 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandContainer.java @@ -0,0 +1,39 @@ +package me.bobert.netneutrality.core.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.options.CommandOptions; +import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; + +public class CommandContainer { + public static final CommandContainer NULL_COMMAND = new CommandContainer(null); + private final Command command; + private CommandOptions commandOptions; + + public CommandContainer(Command command) { + this.command = command; + } + + public Command getCommand() { + return command; + } + + public CommandOptions getCommandOptions() { + return commandOptions; + } + + public void init() { + initOptions(); + } + + private void initOptions() { + if (command != null) { + CommandOptionsBuilder commandOptionsBuilder = new CommandOptionsBuilder(command.getType(), command); + command.registerCommandOptions(commandOptionsBuilder); + commandOptions = commandOptionsBuilder.build(); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/command/CommandExecutionResult.java b/src/main/java/me/bobert/netneutrality/core/command/CommandExecutionResult.java similarity index 69% rename from src/main/java/me/bobert/netneutrality/command/CommandExecutionResult.java rename to src/main/java/me/bobert/netneutrality/core/command/CommandExecutionResult.java index 45a2f4a..bc64784 100644 --- a/src/main/java/me/bobert/netneutrality/command/CommandExecutionResult.java +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandExecutionResult.java @@ -1,26 +1,34 @@ -package me.bobert.netneutrality.command; +package me.bobert.netneutrality.core.command; /* * Copyright (c) anthorob * * https://discord.gg/qUa6zNFZsd */ +import me.bobert.netneutrality.core.tasks.Task; + public class CommandExecutionResult { public enum Result { - SUCCESS, INFO, WARN, ERROR, NOACTION + SUCCESS, INFO, WARN, ERROR, NOACTION, TASK } private final Result result; private final String message; + private final Task task; private final Exception exception; - private CommandExecutionResult(Result result, String message, Exception exception) { + private CommandExecutionResult(Result result, String message, Task task, Exception exception) { this.result = result; this.message = message; + this.task = task; this.exception = exception; } + private CommandExecutionResult(Result result, String message, Exception exception) { + this(result, message, null, exception); + } + private CommandExecutionResult(Result result, String message) { this(result, message, null); } @@ -33,6 +41,10 @@ public class CommandExecutionResult { return message; } + public Task getTask() { + return task; + } + public Exception getException() { return exception; } @@ -41,6 +53,10 @@ public class CommandExecutionResult { return new CommandExecutionResult(Result.SUCCESS, message); } + public static CommandExecutionResult success(String format, Object... args) { + return success(String.format(format, args)); + } + public static CommandExecutionResult info(String message) { return new CommandExecutionResult(Result.INFO, message); } @@ -61,4 +77,7 @@ public class CommandExecutionResult { return new CommandExecutionResult(Result.NOACTION, null); } + public static CommandExecutionResult task(Task task) { + return new CommandExecutionResult(Result.TASK, null, task, null); + } } diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandManager.java b/src/main/java/me/bobert/netneutrality/core/command/CommandManager.java new file mode 100644 index 0000000..2d1723d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandManager.java @@ -0,0 +1,99 @@ +package me.bobert.netneutrality.core.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; +import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.reflections.Reflections; + +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class CommandManager { + + private static final Logger logger = LogManager.getLogger(CommandManager.class); + public static final Map commands = new ConcurrentHashMap<>(); + + public static void init() { + logger.info("Loading commands.."); + + initCommands(); + + logger.info("Loaded {} commands!", commands.size()); + } + + public static Command getCommand(String commandString) { + return getCommandContainer(commandString).getCommand(); + } + + public static CommandContainer getCommandContainer(String commandString) { + return commands.getOrDefault(commandString, CommandContainer.NULL_COMMAND); + } + + public static void initCommands() { + Set> classes = new Reflections("me.bobert.netneutrality.command").getSubTypesOf(Command.class); + for (Class s : classes) { + try { + if (Modifier.isAbstract(s.getModifiers())) { + continue; + } + + Command command = s.getConstructor().newInstance(); + if (!command.isEnabled()) { + continue; + } + + if (command.getCommandName() == null) { + throw new IllegalArgumentException("Command name is null"); + } + + if (command.getDescription() == null && command.getType() == Command.Type.SLASH) { + throw new IllegalArgumentException("Command description is null"); + } + + CommandContainer container = new CommandContainer(command); + container.init(); + + if (!commands.containsKey(command.getCommandName())) { + commands.put(command.getCommandName(), container); + } + } catch (Exception e) { + logger.error("Failed to load command {}: {}", s.getSimpleName(), e.getMessage()); + } + } + } + + public static void registerSlashCommands(JDA jda) { + CommandListUpdateAction commandListUpdateAction = jda.updateCommands(); + List slashCommands = new ArrayList<>(); + + for (CommandContainer container : commands.values()) { + Command command = container.getCommand(); + if (!command.isEnabled() || !command.isSlashCommand()) { + continue; + } + logger.debug("Registering slash command: {}", command.getCommandName()); + + try { + SlashCommandData data = container.getCommandOptions().getSlashCommandData(); + data.setDefaultPermissions(command.getSlashCommandPermission()); + data.setGuildOnly(command.isGuildOnly()); + + slashCommands.add(data); + } catch (Exception e) { + logger.error("Error registering slash command: {}", command.getCommandName(), e); + } + + } + + commandListUpdateAction.addCommands(slashCommands).queue(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java b/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java new file mode 100644 index 0000000..341c60f --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java @@ -0,0 +1,262 @@ +package me.bobert.netneutrality.core.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import io.sentry.Sentry; +import me.bobert.netneutrality.context.command.SlashCommandContext; +import me.bobert.netneutrality.core.LogHandler; +import me.bobert.netneutrality.core.command.options.CommandOptions; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.InteractionContext; +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.exceptions.CommandExecutionException; +import me.bobert.netneutrality.exceptions.GracefulContextError; +import me.bobert.netneutrality.utils.UserFunction; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static java.util.function.Predicate.not; +import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.*; + +public class CommandProcessor { + + private static final Logger logger = LogManager.getLogger(CommandProcessor.class); + private final CommandContext context; + + public CommandProcessor(CommandContext context) { + this.context = context; + } + + public void dispatch() { + if (!CommandManager.commands.containsKey(context.parseCommandName())) { + return; + } + + CommandContainer container = CommandManager.getCommandContainer(context.parseCommandName()); + Command command = container.getCommand(); + if (command == null) { + throw new IllegalArgumentException("Context has no command"); + } + + context.setCommand(command); + if (command.isGuildOnly() && !context.isFromGuild()) { + context.replyEphemeral("This command is only available in a guild"); + context.deleteMessage(); + return; + } + + if (command.isSlashCommandOnly() && !(context instanceof InteractionContext)) { + context.replyEphemeral("This command is only available as a slash command"); + return; + } + + try { + logger.info("Context ID: {}", context.getId()); + logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId()); + configureSentry(command); + + if (command.getType() == Command.Type.SLASH && !canExecute(command)) { + context.reply("You are not powerful enough, scrub"); + return; + } + + if (command.getType() == Command.Type.SLASH && !checkUsage(container.getCommandOptions())) { + return; + } + + if (!context.isValid()) { + StringBuilder reply = new StringBuilder("Invalid arguments provided for this command:\n"); + for (Map.Entry invalid : context.getInvalidOptions()) { + reply.append("- ").append(invalid.getKey().getName()).append(": ").append(invalid.getValue()).append("\n"); + } + + context.reply(reply.toString()); + return; + } + + CommandExecutionResult commandExecutionResult = command.execute(context); + if (commandExecutionResult == null) { + throw new RuntimeException("CommandExecutionResult is null"); + } + + CommandExecutionResult.Result result = commandExecutionResult.getResult(); + if (result == NOACTION) { + return; + } + + if (result == TASK) { + TaskManager.getInstance().insert(context, commandExecutionResult.getTask()); + } + + String message = commandExecutionResult.getMessage(); + if (result == ERROR) { + logger.error("Context[{}]: Error from command: {}", context.getId(), message, commandExecutionResult.getException()); + } + + if (result == WARN) { + logger.warn("Context[{}]: Warning from command: {}", context.getId(), message); + } + + if (message != null) { + context.reply(message); + } + } catch (GracefulContextError exception) { + logger.error(exception.getMessage()); + context.replyEphemeral(exception.getMessage()); + } catch (CommandExecutionException exception) { + LogHandler.log(exception); + } catch (Exception exception) { + LogHandler.log(new CommandExecutionException("Error during command execution", context, exception)); + } + } + + private boolean checkUsage(CommandOptions commandOptions) { + List groups = commandOptions.getSubcommandGroups(); + List subcommands = commandOptions.getSubcommands(); + List options = commandOptions.getOptions(); + String[] possibleCommands = Arrays.stream(context.getFullCommand().split(" ")).skip(1).toArray(String[]::new); + if (possibleCommands.length == 0) { + if (!groups.isEmpty()) { + // missing subcommand group + context.reply(invalidUsage("missing subcommand group")); + return false; + } + + if (!subcommands.isEmpty()) { + // missing subcommand + context.reply(invalidUsage("missing subcommand")); + return false; + } + + return checkOptions(possibleCommands, 0, options); + } + + SubcommandGroupData group = groups.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null); + if (group == null) { + SubcommandData subcommand = subcommands.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null); + if (subcommand == null) { + if (!groups.isEmpty()) { + // no group found but one is expected + context.reply(invalidUsage("unknown subcommand group")); + return false; + } + + if (!subcommands.isEmpty()) { + // no subcommand found but one is expected + context.reply(invalidUsage("unknown subcommand")); + return false; + } + + // no group found, no subcommand found, command with options only + return checkOptions(possibleCommands, 0, options); + } + + // no group found, subcommand found + context.setSubcommand(subcommand); + return checkOptions(possibleCommands, 1, subcommand.getOptions()); + } + + if (possibleCommands.length < 2) { + // missing subcommand for group + context.reply(invalidUsage("missing subcommand for command group")); + return false; + } + + SubcommandData subcommand = group.getSubcommands().stream().filter(data -> data.getName().equals(possibleCommands[1])).findFirst().orElse(null); + if (subcommand == null) { + // group found, no subcommand found + context.reply(invalidUsage("unknown subcommand in command group")); + return false; + } + + // group found, subcommand found + context.setSubcommandGroup(group); + context.setSubcommand(subcommand); + return checkOptions(possibleCommands, 2, subcommand.getOptions()); + } + + private boolean checkOptions(String[] args, int skip, List options) { + if (context instanceof InteractionContext) { + context.setCurrentCommandOptions(args, options); + return true; // slash commands are validated by discord + } + + List arguments = new ArrayList<>(Arrays.stream(args).skip(skip).toList()); + int required = options.stream().filter(OptionData::isRequired).toList().size(); + int optional = options.stream().filter(not(OptionData::isRequired)).toList().size(); + + if (arguments.size() < required) { + context.reply(invalidUsage("missing one or more required arguments")); + return false; + } + + // check if last option is a string, if it is then combine all remaining strings into one + // ["@user", "you", "are", "banned"] becomes ["@user", "you are banned"] + if (!options.isEmpty() && arguments.size() > 1) { + OptionData last = options.get(options.size() - 1); + if (last.getType() == OptionType.STRING) { + List lastArgs = arguments.stream().skip(options.size() - 1).toList(); + arguments.removeAll(lastArgs); + arguments.add(String.join(" ", lastArgs)); + } + } + + if (arguments.size() > required + optional) { + context.reply(invalidUsage("too many arguments")); + return false; + } + + context.setCurrentCommandOptions(arguments.toArray(String[]::new), options); + return true; + } + + private boolean canExecute(Command command) { + if (command.canBeExecutedFrom(context.getChannel())) { + return true; + } + + if (context instanceof SlashCommandContext) { + return true; // no further checks since slash perms are handled by guild settings on discord + } + + if (command.canBeExecutedBy(context.getGuildUser())) { + return true; + } + + return command.needAdmin() && UserFunction.isAdmin(context.getGuildUser()); + } + + private String invalidUsage(String message) { + return String.format("Invalid usage for command: %s\n" + + "Use **/help %s** for information on how to use this command", message, context.getCommand().getCommandName()); + } + + private void configureSentry(Command command) { + Sentry.configureScope(scope -> { + scope.setTag("command", command.getCommandName()); + scope.setTag("user", context.getGuildUser().getId()); + scope.setTag("channel", context.getChannel().getId()); + + if (context.getGuild() != null) { + scope.setTag("guild", context.getGuild().getId()); + scope.setTag("isAdmin", String.valueOf(UserFunction.isAdmin(context.getGuildUser()))); + scope.setTag("canBeExecutedBy", String.valueOf(command.canBeExecutedBy(context.getGuildUser()))); + } + + scope.setTag("contextId", context.getId()); + }); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/CommandOption.java b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOption.java new file mode 100644 index 0000000..0a17ae2 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOption.java @@ -0,0 +1,164 @@ +package me.bobert.netneutrality.core.command.options; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +/** + * This is a wrapper around {@link OptionData} that can support both slash and message commands + */ +public interface CommandOption { + OptionType getType(); + + /** + * The name of this option. + * + * @return The option name + */ + String getName(); + + /** + * The file uploaded for this option. + *
This is represented as an {@link Message.Attachment#isEphemeral() ephemeral} attachment which will only be hosted for up to 2 weeks. + * If you want a permanent reference, you must download it. + * + * @throws IllegalStateException + * If this option {@link #getType() type} is not {@link OptionType#ATTACHMENT} + * + * @return {@link net.dv8tion.jda.api.entities.Message.Attachment Attachment} + */ + Message.Attachment getAsAttachment(); + + /** + * The String representation of this option value. + *
This will automatically convert the value to a string if the type is not {@link OptionType#STRING OptionType.STRING}. + *
This will be the ID of any resolved entity such as {@link Role} or {@link Member}. + * + * @return The String representation of this option value + */ + String getAsString(); + + /** + * The boolean value. + * + * @throws IllegalStateException + * If this option is not of type {@link OptionType#BOOLEAN BOOLEAN} + * + * @return The boolean value + */ + boolean getAsBoolean(); + + /** + * The long value for this option. + *
This will be the ID of any resolved entity such as {@link Role} or {@link Member}. + * + * @throws IllegalStateException + * If this option {@link #getType() type} cannot be converted to a long + * @throws NumberFormatException + * If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid long value + * + * @return The long value + */ + long getAsLong(); + + /** + * The int value for this option. + *
This will be the ID of any resolved entity such as {@link Role} or {@link Member}. + * + *

It is highly recommended to assert int values by using {@link OptionData#setRequiredRange(long, long)} + * + * @throws IllegalStateException + * If this option {@link #getType() type} cannot be converted to a long + * @throws NumberFormatException + * If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid long value + * @throws ArithmeticException + * If the provided integer value cannot fit into a 32bit signed int + * + * @return The int value + */ + int getAsInt(); + + /** + * The double value for this option. + * + * @throws IllegalStateException + * If this option {@link #getType() type} cannot be converted to a double + * @throws NumberFormatException + * If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid double value + * + * @return The double value + */ + double getAsDouble(); + + /** + * The resolved {@link IMentionable} instance for this option value. + * + * @throws IllegalStateException + * If the mentioned entity is not resolvable + * + * @return The resolved {@link IMentionable} + */ + IMentionable getAsMentionable(); + + /** + * The resolved {@link Member} for this option value. + *
Note that {@link OptionType#USER OptionType.USER} can also accept users that are not members of a guild, in which case this will be null! + * + * @throws IllegalStateException + * If this option is not of type {@link OptionType#USER USER} or {@link OptionType#MENTIONABLE MENTIONABLE} + * + * @return The resolved {@link Member}, or null + */ + Member getAsMember(); + + /** + * The resolved {@link User} for this option value. + * + * @throws IllegalStateException + * If this option is not of type {@link OptionType#USER USER} or + * {@link OptionType#MENTIONABLE MENTIONABLE} without a resolved user + * + * @return The resolved {@link User} + */ + User getAsUser(); + + /** + * The resolved {@link Role} for this option value. + * + * @throws IllegalStateException + * If this option is not of type {@link OptionType#ROLE ROLE} or + * {@link OptionType#MENTIONABLE MENTIONABLE} without a resolved role + * + * @return The resolved {@link Role} + */ + Role getAsRole(); + + /** + * The {@link ChannelType} for the resolved channel. + * + * @throws IllegalStateException + * If this option is not of type {@link OptionType#CHANNEL CHANNEL} + * + * @return The {@link ChannelType} + */ + ChannelType getChannelType(); + + /** + * The resolved {@link net.dv8tion.jda.api.entities.channel.middleman.GuildChannel} for this option value. + *
Note that {@link OptionType#CHANNEL OptionType.CHANNEL} can accept channels of any type! + * + * @throws IllegalStateException + * If this option is not of type {@link OptionType#CHANNEL CHANNEL} + * or could not be resolved for unexpected reasons + * + * @return The resolved {@link net.dv8tion.jda.api.entities.channel.middleman.GuildChannel} + */ + GuildChannelUnion getAsChannel(); +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptions.java b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptions.java new file mode 100644 index 0000000..70d55cd --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptions.java @@ -0,0 +1,170 @@ +package me.bobert.netneutrality.core.command.options; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.Command; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +import net.dv8tion.jda.internal.interactions.CommandDataImpl; + +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +public class CommandOptions { + private final Command command; + private final CommandDataImpl commandData; + private final List options; + private final List subcommands; + private final List subcommandGroups; + + public CommandOptions(Command command, CommandDataImpl commandData) { + this.command = command; + this.commandData = commandData; + this.options = commandData.getOptions(); + this.subcommands = commandData.getSubcommands(); + this.subcommandGroups = commandData.getSubcommandGroups(); + } + + public Command getCommand() { + return command; + } + + public SlashCommandData getSlashCommandData() { + return commandData; + } + + public List getOptions() { + return options; + } + + public List getSubcommands() { + return subcommands; + } + + public List getSubcommandGroups() { + return subcommandGroups; + } + + public boolean isEmpty() { + return options.isEmpty() && subcommands.isEmpty() && subcommandGroups.isEmpty(); + } + + public String printUsage() { + StringBuilder usage = new StringBuilder(); + if (!options.isEmpty()) { + usage.append("- Usage: /").append(command.getCommandName()); + for (OptionData option : options) { + usage.append(" <").append(option.getName()).append(">"); + } + + usage.append(printOptions(options, null, false)); + } + + if (!subcommands.isEmpty()) { + for (SubcommandData subcommand : subcommands) { + usage.append("- Usage: /").append(command.getCommandName()).append(" ").append(subcommand.getName()); + + for (OptionData option : subcommand.getOptions()) { + usage.append(" <").append(option.getName()).append(">"); + } + + usage.append(" (").append(subcommand.getDescription()).append(")"); + usage.append(printOptions(subcommand.getOptions(), null, false)); + + usage.append("\n"); + } + } + + if (!subcommandGroups.isEmpty()) { + for (SubcommandGroupData subcommandGroup : subcommandGroups) { + usage.append("- Subcommand: /").append(command.getCommandName()) + .append(" ").append(subcommandGroup.getName()) + .append(" (").append(subcommandGroup.getDescription()).append(")") + .append("\n"); + + for (SubcommandData subcommand : subcommandGroup.getSubcommands()) { + usage.append(" - Usage: /").append(command.getCommandName()) + .append(" ").append(subcommandGroup.getName()) + .append(" ").append(subcommand.getName()); + + for (OptionData option : subcommand.getOptions()) { + usage.append(" <").append(option.getName()).append(">"); + } + + usage.append(printOptions(subcommand.getOptions(), subcommand.getDescription(), true)); + + usage.append("\n"); + } + } + } + + if (!isEmpty()) { + usage.append("\n\n-# *: required argument"); + } + + return usage.toString(); + } + + private String printOptions(List options, String description, boolean group) { + StringBuilder usage = new StringBuilder(); + + if (description != null) { + usage.append(" (").append(description).append(")"); + } + + for (OptionData option : options) { + usage.append("\n"); + if (group) usage.append(" "); + usage.append(" - "); + if (option.isRequired()) { + usage.append("*"); + } + usage.append(option.getName()).append(": ").append(option.getDescription()); + + if (option.getMinValue() != null) { + usage.append(" [Min value: ").append(option.getMinValue().longValue()).append("]"); + } + + if (option.getMaxValue() != null) { + usage.append(" [Max value: ").append(option.getMaxValue().longValue()).append("]"); + } + + if (option.getMinLength() != null) { + usage.append(" [Min length: ").append(option.getMinLength().longValue()).append("]"); + } + + if (option.getMaxLength() != null) { + usage.append(" [Max length: ").append(option.getMaxLength().longValue()).append("]"); + } + + List choices = option.getChoices(); + if (!choices.isEmpty()) { + StringJoiner joiner = new StringJoiner(", "); + for (net.dv8tion.jda.api.interactions.commands.Command.Choice choice : choices) { + joiner.add(choice.getAsString()); + } + + usage.append(" [Available choices: ").append(joiner).append("]"); + } + + Set requiredChannelTypes = option.getChannelTypes(); + if (!requiredChannelTypes.isEmpty()) { + StringJoiner joiner = new StringJoiner(", "); + for (ChannelType type : requiredChannelTypes) { + joiner.add(type.name()); + } + + usage.append(" [Required channel types: ").append(joiner).append("]"); + } + } + + return usage.toString(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptionsBuilder.java b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptionsBuilder.java new file mode 100644 index 0000000..20652d4 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptionsBuilder.java @@ -0,0 +1,185 @@ +package me.bobert.netneutrality.core.command.options; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.Command; +import net.dv8tion.jda.api.interactions.DiscordLocale; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.build.*; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationMap; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.interactions.CommandDataImpl; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public class CommandOptionsBuilder implements SlashCommandData { + private final Command command; + private CommandDataImpl commandData; + + public CommandOptionsBuilder(Command.Type type, Command command) { + this.command = command; + + if (type == Command.Type.SLASH) { + commandData = (CommandDataImpl) Commands.slash(command.getCommandName(), command.getDescription()); + } + + if (type == Command.Type.USER) { + commandData = (CommandDataImpl) Commands.user(command.getCommandName()); + } + + if (type == Command.Type.MESSAGE) { + commandData = (CommandDataImpl) Commands.message(command.getCommandName()); + } + } + + public CommandOptions build() { + return new CommandOptions(command, commandData); + } + + @Override + public @Nonnull SlashCommandData setLocalizationFunction(@Nonnull LocalizationFunction localizationFunction) { + return commandData.setLocalizationFunction(localizationFunction); + } + + @Override + public @Nonnull SlashCommandData setName(@Nonnull String name) { + return commandData.setName(name); + } + + @Override + public @Nonnull SlashCommandData setNameLocalization(@Nonnull DiscordLocale locale, @Nonnull String name) { + return commandData.setNameLocalization(locale, name); + } + + @Override + public @Nonnull SlashCommandData setNameLocalizations(@Nonnull Map map) { + return commandData.setNameLocalizations(map); + } + + @Override + public @Nonnull SlashCommandData setDefaultPermissions(@Nonnull DefaultMemberPermissions permission) { + return commandData.setDefaultPermissions(permission); + } + + @Override + public @Nonnull SlashCommandData setGuildOnly(boolean guildOnly) { + return commandData.setGuildOnly(guildOnly); + } + + @Override + public @Nonnull SlashCommandData setNSFW(boolean nsfw) { + return commandData.setNSFW(nsfw); + } + + @Override + public @Nonnull String getName() { + return commandData.getName(); + } + + @Override + public @Nonnull LocalizationMap getNameLocalizations() { + return commandData.getNameLocalizations(); + } + + @Override + public @Nonnull net.dv8tion.jda.api.interactions.commands.Command.Type getType() { + return commandData.getType(); + } + + @Override + public @Nonnull DefaultMemberPermissions getDefaultPermissions() { + return commandData.getDefaultPermissions(); + } + + @Override + public boolean isGuildOnly() { + return commandData.isGuildOnly(); + } + + @Override + public boolean isNSFW() { + return commandData.isNSFW(); + } + + @Override + public @Nonnull SlashCommandData setDescription(@Nonnull String description) { + return commandData.setDescription(description); + } + + @Override + public @Nonnull SlashCommandData setDescriptionLocalization(@Nonnull DiscordLocale locale, @Nonnull String description) { + return commandData.setDescriptionLocalization(locale, description); + } + + @Override + public @Nonnull SlashCommandData setDescriptionLocalizations(@Nonnull Map map) { + return commandData.setDescriptionLocalizations(map); + } + + @Override + public @Nonnull String getDescription() { + return commandData.getDescription(); + } + + @Override + public @Nonnull LocalizationMap getDescriptionLocalizations() { + return commandData.getDescriptionLocalizations(); + } + + @Override + public boolean removeOptions(@Nonnull Predicate condition) { + return commandData.removeOptions(condition); + } + + @Override + public boolean removeSubcommands(@Nonnull Predicate condition) { + return commandData.removeSubcommands(condition); + } + + @Override + public boolean removeSubcommandGroups(@Nonnull Predicate condition) { + return commandData.removeSubcommandGroups(condition); + } + + @Override + public @Nonnull List getSubcommands() { + return commandData.getSubcommands(); + } + + @Override + public @Nonnull List getSubcommandGroups() { + return commandData.getSubcommandGroups(); + } + + @Override + public @Nonnull List getOptions() { + return commandData.getOptions(); + } + + @Override + public @Nonnull SlashCommandData addOptions(@Nonnull OptionData... options) { + return commandData.addOptions(options); + } + + @Override + public @Nonnull SlashCommandData addSubcommands(@Nonnull SubcommandData... subcommands) { + return commandData.addSubcommands(subcommands); + } + + @Override + public @Nonnull SlashCommandData addSubcommandGroups(@Nonnull SubcommandGroupData... groups) { + return commandData.addSubcommandGroups(groups); + } + + @Override + public @Nonnull DataObject toData() { + return commandData.toData(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/MessageCommandOption.java b/src/main/java/me/bobert/netneutrality/core/command/options/MessageCommandOption.java new file mode 100644 index 0000000..ab1f0b3 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/options/MessageCommandOption.java @@ -0,0 +1,157 @@ +package me.bobert.netneutrality.core.command.options; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +public class MessageCommandOption implements CommandOption { + private final OptionData data; + private final OptionType type; + private final Object value; + + public MessageCommandOption(OptionData data, Object value) { + this.data = data; + this.type = data.getType(); + this.value = value; + } + + @Override + public OptionType getType() { + return data.getType(); + } + + @Override + public String getName() { + return data.getName(); + } + + @Override + public Message.Attachment getAsAttachment() { + throw new UnsupportedOperationException("Not supported for message commands"); + } + + @Override + public String getAsString() { + return switch (type) { + case STRING -> (String) value; + case BOOLEAN -> String.valueOf((boolean) value); + case NUMBER -> String.valueOf((double) value); + case MENTIONABLE -> ((IMentionable) value).getId(); + case CHANNEL -> ((GuildChannelUnion) value).getId(); + case ROLE -> ((Role) value).getId(); + case USER -> ((User) value).getId(); + case INTEGER -> String.valueOf((long) value); + case ATTACHMENT -> ((Message.Attachment) value).getId(); + default -> throw new IllegalStateException("Cannot convert option of type " + type + " to long"); + }; + } + + @Override + public boolean getAsBoolean() { + if (type != OptionType.BOOLEAN) { + throw new IllegalStateException("Cannot convert option of type " + type + " to boolean"); + } + + return (boolean) value; + } + + @Override + public long getAsLong() { + return switch (type) { + case STRING -> Long.parseLong((String) value); + case MENTIONABLE -> ((IMentionable) value).getIdLong(); + case CHANNEL -> ((GuildChannelUnion) value).getIdLong(); + case ROLE -> ((Role) value).getIdLong(); + case USER -> ((User) value).getIdLong(); + case INTEGER -> (long) value; + case NUMBER -> ((Double) value).longValue(); + case ATTACHMENT -> ((Message.Attachment) value).getIdLong(); + default -> throw new IllegalStateException("Cannot convert option of type " + type + " to long"); + }; + } + + @Override + public int getAsInt() { + return Math.toIntExact(getAsLong()); + } + + @Override + public double getAsDouble() { + return switch (type) { + case STRING -> Double.parseDouble((String) value); + case NUMBER -> (double) value; + case INTEGER -> (int) value; + default -> throw new IllegalStateException("Cannot convert option of type " + type + " to double"); + }; + } + + @Override + public IMentionable getAsMentionable() { + if (value instanceof IMentionable) { + return (IMentionable) value; + } + + throw new IllegalStateException("Cannot resolve option of type " + type + " to IMentionable"); + } + + @Override + public Member getAsMember() { + if (value instanceof Member) { + return (Member) value; + } + + if (value instanceof User) { + return null; + } + + throw new IllegalStateException("Cannot resolve option of type " + type + " to Member"); + } + + @Override + public User getAsUser() { + if (value instanceof User) { + return (User) value; + } + + throw new IllegalStateException("Cannot resolve option of type " + type + " to User"); + } + + @Override + public Role getAsRole() { + if (value instanceof Role) { + return (Role) value; + } + + throw new IllegalStateException("Cannot resolve option of type " + type + " to Role"); + } + + @Override + public ChannelType getChannelType() { + return getAsChannel().getType(); + } + + @Override + public GuildChannelUnion getAsChannel() { + if (value instanceof GuildChannelUnion) { + return (GuildChannelUnion) value; + } + + throw new IllegalStateException("Cannot resolve option of type " + type + " to GuildChannelUnion"); + } + + @Override + public String toString() { + return "MessageCommandOption{" + + "name=" + data.getName() + + ", type=" + type + + ", value=" + value + + '}'; + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/SlashCommandOption.java b/src/main/java/me/bobert/netneutrality/core/command/options/SlashCommandOption.java new file mode 100644 index 0000000..90a8c0d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/options/SlashCommandOption.java @@ -0,0 +1,90 @@ +package me.bobert.netneutrality.core.command.options; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; + +public class SlashCommandOption implements CommandOption { + private final OptionMapping mapping; + + public SlashCommandOption(OptionMapping mapping) { + this.mapping = mapping; + } + + @Override + public OptionType getType() { + return mapping.getType(); + } + + @Override + public String getName() { + return mapping.getName(); + } + + @Override + public Message.Attachment getAsAttachment() { + return mapping.getAsAttachment(); + } + + @Override + public String getAsString() { + return mapping.getAsString(); + } + + @Override + public boolean getAsBoolean() { + return mapping.getAsBoolean(); + } + + @Override + public long getAsLong() { + return mapping.getAsLong(); + } + + @Override + public int getAsInt() { + return mapping.getAsInt(); + } + + @Override + public double getAsDouble() { + return mapping.getAsDouble(); + } + + @Override + public IMentionable getAsMentionable() { + return mapping.getAsMentionable(); + } + + @Override + public Member getAsMember() { + return mapping.getAsMember(); + } + + @Override + public User getAsUser() { + return mapping.getAsUser(); + } + + @Override + public Role getAsRole() { + return mapping.getAsRole(); + } + + @Override + public ChannelType getChannelType() { + return mapping.getChannelType(); + } + + @Override + public GuildChannelUnion getAsChannel() { + return mapping.getAsChannel(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/context/AbstractContext.java b/src/main/java/me/bobert/netneutrality/core/context/AbstractContext.java new file mode 100644 index 0000000..ca55682 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/context/AbstractContext.java @@ -0,0 +1,62 @@ +package me.bobert.netneutrality.core.context; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import io.sentry.Sentry; +import me.bobert.netneutrality.context.command.SlashCommandContext; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; + +public abstract class AbstractContext implements Context { + private final JDA jda; + private final Guild guild; + private final MessageChannel channel; + private final Member member; + private final User user; + + public AbstractContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) { + this.jda = jda; + this.guild = guild; + this.channel = channel; + this.member = member; + this.user = user; + + if (user != null) { + io.sentry.protocol.User sentryUser = new io.sentry.protocol.User(); + sentryUser.setId(user.getId()); + sentryUser.setUsername(user.getEffectiveName()); + Sentry.setUser(sentryUser); + } + } + + @Override + public JDA getJDA() { + return jda; + } + + @Override + public Guild getGuild() { + return guild; + } + + @Override + public MessageChannel getChannel() { + return channel; + } + + @Override + public Member getGuildUser() { + return member; + } + + @Override + public User getDiscordUser() { + return user; + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java b/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java index 74d7abb..a950e28 100644 --- a/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java +++ b/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java @@ -1,60 +1,138 @@ package me.bobert.netneutrality.core.context; - /* * Copyright (c) anthorob * * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.command.AbstractCommand; +import me.bobert.netneutrality.core.command.Command; +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.utils.Settings; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; -import net.dv8tion.jda.api.events.Event; -import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; -public interface CommandContext { +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; - AbstractCommand getCommand(); +public abstract class CommandContext extends AbstractContext { + private Command command; + private SubcommandGroupData subcommandGroup; + private SubcommandData subcommand; + private final Map invalidOptions = new HashMap<>(); + private final List options; - Event getEvent(); - - Guild getGuild(); - - MessageChannel getChannel(); - - Member getGuildUser(); - - User getDiscordUser(); - - JDA getJDA(); - - void deleteMessage(); - - void reply(MessageCreateData message); - - void reply(String message); - - void reply(MessageEmbed... embeds); - - default void reply(String format, Object... arguments) { - reply(String.format(format, arguments)); + public CommandContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) { + super(jda, guild, channel, member, user); + this.options = new ArrayList<>(); } - void replyToMessage(String message); - - default void replyToMessage(MessageCreateData message) { - reply(message); + public Command getCommand() { + return command; } - default void replyToMessage(String format, Object... arguments) { - replyToMessage(String.format(format, arguments)); + public void setCommand(Command command) { + this.command = command; } - default void replyToMessage(MessageEmbed... embeds) { - replyToMessage(MessageCreateData.fromEmbeds(embeds)); + public void setSubcommandGroup(SubcommandGroupData subcommandGroup) { + this.subcommandGroup = subcommandGroup; } + + public void setSubcommand(SubcommandData subcommand) { + this.subcommand = subcommand; + } + + public void setCurrentCommandOptions(String[] args, List options) { + this.options.clear(); + this.options.addAll(parseOptions(args, options)); + } + + protected void invalidateContext(OptionData option, String message) { + invalidOptions.put(option, message); + } + + public boolean isValid() { + return invalidOptions.isEmpty(); + } + + public Set> getInvalidOptions() { + return invalidOptions.entrySet(); + } + + public T getOption(String name, Supplier fallback, Function resolver) { + Objects.requireNonNull(resolver, "resolver cannot be null"); + CommandOption mapping = getOption(name); + if (mapping != null) + return resolver.apply(mapping); + return fallback == null ? null : fallback.get(); + } + + public T getOption(String name, T fallback, Function resolver) { + Objects.requireNonNull(resolver, "resolver cannot be null"); + CommandOption mapping = getOption(name); + if (mapping != null) + return resolver.apply(mapping); + return fallback; + } + + public T getOption(String name, Function resolver) { + return getOption(name, null, resolver); + } + + public CommandOption getOption(String name) { + List options = getOptionsByName(name); + return options.isEmpty() ? null : options.get(0); + } + + public List getOptionsByName(String name) { + Objects.requireNonNull(name, "name cannot be null"); + return options.stream().filter(opt -> opt.getName().equals(name)).collect(Collectors.toList()); + } + + public String getSubcommandGroup() { + if (subcommandGroup == null) { + return null; + } + + return subcommandGroup.getName(); + } + + public String getSubcommand() { + if (subcommand == null) { + return null; + } + + return subcommand.getName(); + } + + public boolean isMessageFromOwner() { + if (getGuildUser() == null) { + return false; + } + + return getGuildUser().isOwner(); + } + + public boolean isMessageFromCreator() { + String creatorId = Settings.getInstance().get("NETNEUTRALITY_BOT_OWNER"); + + return creatorId.equals(getDiscordUser().getId()); + } + + public abstract List parseOptions(String[] args, List commandOptions); + + public abstract String parseCommandName(); + + public abstract String getFullCommand(); + + + } diff --git a/src/main/java/me/bobert/netneutrality/core/context/Context.java b/src/main/java/me/bobert/netneutrality/core/context/Context.java new file mode 100644 index 0000000..bcf7ebd --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/context/Context.java @@ -0,0 +1,208 @@ +package me.bobert.netneutrality.core.context; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.AsyncContext; +import me.bobert.netneutrality.utils.ContextUtils; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.utils.messages.MessageEditData; + +import java.util.List; +import java.util.function.Consumer; + +public interface Context { + Event getEvent(); + + String getId(); + + ChannelType getType(); + + JDA getJDA(); + + InteractionHook getInteractionHook(); + + Guild getGuild(); + + MessageChannel getChannel(); + + Member getGuildUser(); + + User getDiscordUser(); + + boolean isAcknowledged(); + + boolean isFromGuild(); + + void deleteMessage(); + + void reply(Modal modal); + + void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer); + + void defer(boolean ephemeral, Consumer consumer); + + void ack(boolean ephemeral); + + default void ack() { + ack(false); + } + + default void ephemeral() { + ack(true); + } + + default void reply(MessageCreateData messageCreateData) { + reply(messageCreateData, false, List.of(), asyncMessage -> {}); + } + + default void reply(MessageCreateData messageCreateData, List components) { + reply(messageCreateData, false, components, asyncMessage -> {}); + } + + default void reply(MessageCreateData messageCreateData, Consumer consumer) { + reply(messageCreateData, false, List.of(), consumer); + } + + default void reply(String message) { + replyWithLimit(message); + } + + default void reply(String message, Consumer consumer) { + reply(MessageCreateData.fromContent(message), consumer); + } + + default void reply(MessageEmbed embed) { + reply(List.of(embed)); + } + + default void reply(List embeds) { + reply(MessageCreateData.fromEmbeds(embeds)); + } + + default void edit(MessageEditData messageEditData, boolean ephemeral, List components, Consumer consumer) { + if (isAcknowledged() && getInteractionHook() != null) { + getInteractionHook().editOriginal(messageEditData).setComponents(components).queue(message1 -> consumer.accept(new AsyncContext(message1))); + return; + } + + reply(MessageCreateData.fromEditData(messageEditData), ephemeral, components, consumer); + } + + default void edit(MessageEditData messageEditData, List components) { + edit(messageEditData, false, components, asyncMessage -> {}); + } + + default void edit(MessageEditData messageEditData) { + edit(messageEditData, false, List.of(), asyncMessage -> {}); + } + + default void edit(String message) { + edit(MessageEditData.fromContent(message)); + } + + default void edit(MessageEmbed embed) { + edit(List.of(embed), List.of()); + } + + default void edit(List embeds, List components) { + edit(MessageEditData.fromEmbeds(embeds), components); + } + + default void edit(MessageEmbed embed, List components) { + edit(List.of(embed), components); + } + + default void async(MessageCreateData messageCreateData, Consumer consumer) { + reply(messageCreateData, false, List.of(), consumer); + } + + default void async(String message, Consumer consumer) { + async(MessageCreateData.fromContent(message), consumer); + } + + default void async(Consumer consumer) { + defer(false, consumer); + } + + default void replyEphemeral(MessageCreateData messageCreateData, List components) { + reply(messageCreateData, true, components, asyncMessage -> {}); + } + + default void replyEphemeral(MessageCreateData messageCreateData) { + replyEphemeral(messageCreateData, List.of()); + } + + default void replyEphemeral(String message) { + replyEphemeralWithLimit(message); + } + + default void replyEphemeral(String message, List components) { + replyEphemeral(MessageCreateData.fromContent(message), components); + } + + default void replyEphemeral(List embeds) { + replyEphemeral(MessageCreateData.fromEmbeds(embeds)); + } + + default void sendMessageInPrivate(MessageCreateData messageCreateData, List components, Consumer consumer) { + User user = getDiscordUser(); + if (user != null) { + user.openPrivateChannel() + .flatMap(channel -> channel.sendMessage(messageCreateData).setComponents(components)) + .queue(message -> consumer.accept(new AsyncContext(message))); + } + } + + default void sendMessageInPrivate(String message, List components, Consumer consumer) { + sendMessageInPrivate(MessageCreateData.fromContent(message), components, consumer); + } + + default void sendMessageInPrivate(MessageCreateData messageCreateData, List components) { + sendMessageInPrivate(messageCreateData, components, asyncContext -> {}); + } + + default void sendMessageInPrivate(String message, Consumer consumer) { + sendMessageInPrivate(message, List.of(), consumer); + } + + default void sendMessageInPrivate(MessageCreateData messageCreateData) { + sendMessageInPrivate(messageCreateData, List.of()); + } + + default void sendMessageInPrivate(List embeds) { + sendMessageInPrivate(MessageCreateData.fromEmbeds(embeds)); + } + + default void sendMessageInPrivate(String message) { + sendMessageInPrivateWithLimit(message); + } + + private void replyWithLimit(String message) { + for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) { + reply(MessageCreateData.fromContent(chunk)); + } + } + + private void replyEphemeralWithLimit(String message) { + for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) { + replyEphemeral(MessageCreateData.fromContent(chunk)); + } + } + + private void sendMessageInPrivateWithLimit(String message) { + for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) { + sendMessageInPrivate(MessageCreateData.fromContent(chunk)); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/context/InteractionContext.java b/src/main/java/me/bobert/netneutrality/core/context/InteractionContext.java new file mode 100644 index 0000000..9008e75 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/context/InteractionContext.java @@ -0,0 +1,92 @@ +package me.bobert.netneutrality.core.context; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.AsyncContext; +import net.dv8tion.jda.api.interactions.Interaction; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.callbacks.IModalCallback; +import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; +import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; + +import java.util.List; +import java.util.function.Consumer; + +public interface InteractionContext extends Context { + + @Override + default String getId() { + return getInteraction().getId(); + } + + @Override + default InteractionHook getInteractionHook() { + if (!isAcknowledged()) { + throw new RuntimeException("Interaction isn't acknowledged yet"); + } + + return getReplyCallback().getHook(); + } + + @Override + default void ack(boolean ephemeral) { + defer(ephemeral, asyncMessage -> {}); + } + + @Override + default void defer(boolean ephemeral, Consumer consumer) { + if (isAcknowledged()) { + throw new RuntimeException("Interaction is already acknowledged, please use getInteractionHook() instead."); + } + + getReplyCallback().deferReply(ephemeral).queue(hook -> consumer.accept(new AsyncContext(hook))); + } + + @Override + default boolean isFromGuild() { + return getInteraction().isFromGuild(); + } + + @Override + default boolean isAcknowledged() { + return getInteraction().isAcknowledged(); + } + + @Override + default void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer) { + if (isAcknowledged()) { + getInteractionHook().sendMessage(messageCreateData).setEphemeral(ephemeral).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message))); + return; + } + + getReplyCallback().reply(messageCreateData).setEphemeral(ephemeral).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message))); + } + + @Override + default void reply(Modal modal) { + if (getInteraction() instanceof IModalCallback) { + ((IModalCallback) getInteraction()).replyModal(modal).queue(); + } + } + + private IReplyCallback getReplyCallback() { + try { + return (IReplyCallback) getEvent(); + } catch (ClassCastException e) { + throw new IllegalArgumentException(getEvent().getClass().getName() + " does not implement class 'IReplyCallback'", e); + } + } + + private Interaction getInteraction() { + try { + return (Interaction) getEvent(); + } catch (ClassCastException e) { + throw new IllegalArgumentException(getEvent().getClass().getName() + " does not implement class 'Interaction'", e); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/context/MessageCommandContext.java b/src/main/java/me/bobert/netneutrality/core/context/MessageCommandContext.java deleted file mode 100644 index 9ef63af..0000000 --- a/src/main/java/me/bobert/netneutrality/core/context/MessageCommandContext.java +++ /dev/null @@ -1,192 +0,0 @@ -package me.bobert.netneutrality.core.context; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import io.sentry.Breadcrumb; -import io.sentry.Sentry; -import me.bobert.netneutrality.command.AbstractCommand; -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.thread.ThreadManager; -import me.bobert.netneutrality.utils.Settings; -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.*; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.utils.messages.MessageCreateData; - -import java.util.Collection; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; - -public class MessageCommandContext implements CommandContext { - - private static final ExecutorService commandExecutor = ThreadManager.getExecutor(); - - private AbstractCommand command; - private final MessageReceivedEvent event; - private final Message message; - private final Guild guild; - private final MessageChannel channel; - private final Member member; - private final User user; - private final JDA jda; - - public MessageCommandContext(MessageReceivedEvent event, Message message, Member member, User user, Guild guild, MessageChannel channel, JDA jda) { - this.event = event; - this.message = message; - this.member = member; - this.guild = guild; - this.channel = channel; - this.user = user; - this.jda = jda; - - io.sentry.protocol.User sentryUser = new io.sentry.protocol.User(); - sentryUser.setId(member.getId()); - sentryUser.setUsername(member.getEffectiveName()); - Sentry.setUser(sentryUser); - Sentry.addBreadcrumb(Breadcrumb.info("New context for message id " + message.getId())); - } - - public static MessageCommandContext createContext(MessageReceivedEvent event) { - Message message = event.getMessage(); - Member member = event.getMember(); - User user = event.getAuthor(); - Guild guild = event.getGuild(); - MessageChannel channel = event.getChannel(); - JDA jda = event.getJDA(); - - return new MessageCommandContext(event, message, member, user, guild, channel, jda); - } - - @Override - public MessageReceivedEvent getEvent() { - return event; - } - - public String getArgument(int index) { - return getArgs()[index]; - } - - public String[] getArgs() { - String msg = message.getContentDisplay().trim(); - msg = msg.substring(Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$").length()).trim(); - String cmd = msg.split(" ", 2)[0]; - String[] args = msg.replaceFirst("(" + cmd + ")(\\s+)?", "").split("\s"); - - if (args.length == 1 && args[0].isEmpty()) - args = new String[0]; - - return args; - } - - public String getMessageText() { - String msg = message.getContentDisplay().trim(); - msg = msg.substring(Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$").length()).trim(); - String cmd = msg.split(" ", 2)[0]; - - return msg.replaceFirst("(" + cmd + ")(\s)?", ""); - } - - public Message getMessage() { - return message; - } - - public Guild getGuild() { - return guild; - } - - public MessageChannel getChannel() { - return channel; - } - - public Member getGuildUser() { - return member; - } - - public User getDiscordUser() { - return user; - } - - public JDA getJDA() { - return jda; - } - - public AbstractCommand getCommand() { - return command; - } - - public GuildManager getGuildManager() { - return GuildManager.getManager(guild); - } - - public void reply(String message) { - reply(MessageCreateData.fromContent(message)); - } - - public void reply(String format, Object... arguments) { - reply(String.format(format, arguments)); - } - - public void reply(MessageEmbed... embeds) { - reply(MessageCreateData.fromEmbeds(embeds)); - } - - public void reply(MessageCreateData msg) { - if (channel != null && channel.canTalk()) { - channel.sendMessage(msg).queue(); - } - } - - @Override - public void replyToMessage(String message) { - replyToMessage(MessageCreateData.fromContent(message)); - } - - @Override - public void replyToMessage(String format, Object... arguments) { - replyToMessage(String.format(format, arguments)); - } - - public void replyToMessage(MessageCreateData msg) { - if (channel != null && channel.canTalk() && message != null) { - message.reply(msg).queue(); - } - } - - public void replyPrivate(String message) { - replyPrivate(MessageCreateData.fromContent(message)); - } - - public void replyPrivate(MessageCreateData msg) { - user.openPrivateChannel().flatMap(channel -> channel.sendMessage(msg)).queue(); - } - - public void replyPrivate(MessageEmbed... embeds) { - replyPrivate(MessageCreateData.fromEmbeds(embeds)); - } - - public void replyPrivate(Collection embeds) { - user.openPrivateChannel().flatMap(channel -> channel.sendMessageEmbeds(embeds)).queue(); - } - - @Override - public void deleteMessage() { - if (message != null) { - message.delete().queueAfter(1, TimeUnit.SECONDS); - } - } - - public boolean isMessageFromOwner() { - return member.isOwner(); - } - - public boolean isMessageFromCreator() { - String creatorId = Settings.getInstance().get("NETNEUTRALITY_BOT_OWNER"); - - return creatorId.equals(member.getId()); - } - -} diff --git a/src/main/java/me/bobert/netneutrality/core/context/MessageContext.java b/src/main/java/me/bobert/netneutrality/core/context/MessageContext.java new file mode 100644 index 0000000..cf33c24 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/context/MessageContext.java @@ -0,0 +1,45 @@ +package me.bobert.netneutrality.core.context; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.AsyncContext; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public interface MessageContext extends NoMessageContext { + Message getMessage(); + + @Override + default boolean isFromGuild() { + return getMessage().isFromGuild(); + } + + @Override + default void deleteMessage() { + if (getMessage() != null && getType() != ChannelType.PRIVATE) { + getMessage().delete().queueAfter(1, TimeUnit.SECONDS); + } + } + + @Override + default void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer) { + if (ephemeral && getType() != ChannelType.PRIVATE) { + sendMessageInPrivate(messageCreateData, components); + return; + } + + if (getChannel() != null && getChannel().canTalk()) { + getMessage().reply(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message))); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/context/NoMessageContext.java b/src/main/java/me/bobert/netneutrality/core/context/NoMessageContext.java new file mode 100644 index 0000000..1aeea14 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/context/NoMessageContext.java @@ -0,0 +1,47 @@ +package me.bobert.netneutrality.core.context; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.AsyncContext; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.modals.Modal; + +import java.util.function.Consumer; + +public interface NoMessageContext extends Context { + + @Override + default InteractionHook getInteractionHook() { + return null; + } + + @Override + default boolean isAcknowledged() { + return true; + } + + @Override + default ChannelType getType() { + return getChannel().getType(); + } + + @Override + default void reply(Modal modal) { + // only available for interactions + } + + @Override + default void defer(boolean ephemeral, Consumer consumer) { + reply("Bot is thinking...", consumer); + } + + @Override + default void ack(boolean ephemeral) { + + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/context/SlashCommandContext.java b/src/main/java/me/bobert/netneutrality/core/context/SlashCommandContext.java deleted file mode 100644 index 35064c1..0000000 --- a/src/main/java/me/bobert/netneutrality/core/context/SlashCommandContext.java +++ /dev/null @@ -1,180 +0,0 @@ -package me.bobert.netneutrality.core.context; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.command.AbstractCommand; -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.User; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import net.dv8tion.jda.api.interactions.InteractionHook; -import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; -import net.dv8tion.jda.api.utils.messages.MessageCreateData; -import net.dv8tion.jda.api.utils.messages.MessageEditData; - -import java.util.Collection; - -public class SlashCommandContext implements CommandContext { - - private final SlashCommandInteractionEvent event; - private AbstractCommand command; - private final JDA jda; - private final Guild guild; - private final MessageChannel channel; - private final Member member; - private final User user; - - private SlashCommandContext(JDA jda, SlashCommandInteractionEvent event, Guild guild, MessageChannel channel, Member member, User user) { - this.jda = jda; - this.event = event; - this.guild = guild; - this.channel = channel; - this.member = member; - this.user = user; - } - - public static SlashCommandContext createContext(SlashCommandInteractionEvent event) { - Member member = event.getMember(); - User user = event.getUser(); - Guild guild = event.getGuild(); - MessageChannel channel = event.getChannel(); - JDA jda = event.getJDA(); - - return new SlashCommandContext(jda, event, guild, channel, member, user); - } - - @Override - public SlashCommandInteractionEvent getEvent() { - return event; - } - - @Override - public AbstractCommand getCommand() { - return command; - } - - @Override - public JDA getJDA() { - return jda; - } - - @Override - public Guild getGuild() { - return guild; - } - - @Override - public MessageChannel getChannel() { - return channel; - } - - @Override - public Member getGuildUser() { - return member; - } - - @Override - public User getDiscordUser() { - return user; - } - - @Override - public void deleteMessage() { - // not implemented - } - - public void sendMessageInPrivate(String msg) { - sendMessageInPrivate(new MessageCreateBuilder().setContent(msg).build()); - } - - public void sendMessageInPrivate(MessageCreateData msg) { - user.openPrivateChannel().flatMap(channel -> channel.sendMessage(msg)).queue(); - } - - public void sendMessageInPrivate(MessageEmbed embed, MessageEmbed... embeds) { - user.openPrivateChannel().flatMap(channel -> channel.sendMessageEmbeds(embed, embeds)).queue(); - } - - public void sendMessageInPrivate(Collection embeds) { - user.openPrivateChannel().flatMap(channel -> channel.sendMessageEmbeds(embeds)).queue(); - } - - public void reply(MessageCreateData message) { - if (event.isAcknowledged()) { - getHook().editOriginal(MessageEditData.fromCreateData(message)).queue(); - //getHook().sendMessage(message).setEphemeral(false).queue(); - return; - } - - event.reply(message).queue(); - } - - public void reply(MessageEmbed... embeds) { - reply(MessageCreateData.fromEmbeds(embeds)); - } - - @Override - public void reply(String message) { - reply(MessageCreateData.fromContent(message)); - } - - public void replyEphemeral(MessageCreateData message) { - if (event.isAcknowledged()) { - getHook().editOriginal(MessageEditData.fromCreateData(message)).queue(); - //getHook().sendMessage(message).setEphemeral(true).queue(); - return; - } - - event.reply(message).setEphemeral(true).queue(); - } - - public void replyEphemeral(MessageEmbed... embeds) { - replyEphemeral(MessageCreateData.fromEmbeds(embeds)); - } - - public void replyEphemeral(String format, Object... arguments) { - replyEphemeral(String.format(format, arguments)); - } - - public void replyEphemeral(String message) { - replyEphemeral(MessageCreateData.fromContent(message)); - } - - @Override - public void replyToMessage(String message) { - replyEphemeral(message); - } - - public InteractionHook getHook() { - if (!event.isAcknowledged()) - throw new RuntimeException("Interaction isn't acknowledged yet"); - - return event.getHook(); - } - - public void ackAndWait(boolean ephemeral) { - if (event.isAcknowledged()) - throw new RuntimeException("Interaction is already acknowledged, please use getHook() instead."); - - event.deferReply(ephemeral).queue(); - } - - public void ackAndWait() { - ackAndWait(false); - } - - public void ack() { - ackAndWait(); - } - - public void ephemeral() { - ackAndWait(true); - } - -} diff --git a/src/main/java/me/bobert/netneutrality/core/context/TaskContext.java b/src/main/java/me/bobert/netneutrality/core/context/TaskContext.java new file mode 100644 index 0000000..185ea57 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/context/TaskContext.java @@ -0,0 +1,39 @@ +package me.bobert.netneutrality.core.context; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class TaskContext extends AbstractContext { + private final Map contextData; + + public TaskContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) { + super(jda, guild, channel, member, user); + this.contextData = new ConcurrentHashMap<>(); + } + + @Override + public void deleteMessage() { + + } + + public abstract String getId(); + + public void addContextData(String key, Object value) { + contextData.put(key, value); + } + + public Object getContextData(String key) { + return contextData.get(key); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/db/DatabaseMigration.java b/src/main/java/me/bobert/netneutrality/core/db/DatabaseMigration.java new file mode 100644 index 0000000..fe846e5 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/db/DatabaseMigration.java @@ -0,0 +1,34 @@ +package me.bobert.netneutrality.core.db; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.utils.Settings; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.flywaydb.core.Flyway; + +public class DatabaseMigration { + private final static Logger logger = LogManager.getLogger(DatabaseMigration.class); + + public static void migrate() { + String dbName = Settings.getInstance().get("MYSQL_DATABASE", "netneutrality"); + String dbUser = Settings.getInstance().get("MYSQL_USER", "root"); + String dbPass = Settings.getInstance().get("MYSQL_PASSWORD", ""); + String dbHost = Settings.getInstance().get("MYSQL_HOST", "localhost"); + + try { + Flyway flyway = Flyway.configure() + .dataSource(String.format("jdbc:mysql://%s:3306/%s", dbHost, dbName), dbUser, dbPass) + .baselineOnMigrate(true) + .load(); + + flyway.migrate(); + } catch (Exception e) { + logger.error(e); + System.exit(1); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/SessionManager.java b/src/main/java/me/bobert/netneutrality/core/db/SessionManager.java similarity index 86% rename from src/main/java/me/bobert/netneutrality/core/SessionManager.java rename to src/main/java/me/bobert/netneutrality/core/db/SessionManager.java index fab89e5..110a13f 100644 --- a/src/main/java/me/bobert/netneutrality/core/SessionManager.java +++ b/src/main/java/me/bobert/netneutrality/core/db/SessionManager.java @@ -1,4 +1,4 @@ -package me.bobert.netneutrality.core; +package me.bobert.netneutrality.core.db; /* * Copyright (c) anthorob * @@ -20,7 +20,7 @@ import java.util.HashMap; import java.util.Map; public class SessionManager { - private final static Logger logger = LogManager.getLogger(SessionManager.class); + private static final Logger logger = LogManager.getLogger(SessionManager.class); private static StandardServiceRegistry registry; private static SessionFactory sessionFactory; @@ -35,9 +35,9 @@ public class SessionManager { String dbHost = Settings.getInstance().get("MYSQL_HOST", "localhost"); logger.info(String.format("Connecting to database '%s' at %s:3306 with user '%s'", dbName, dbHost, dbUser)); - settings.put(Environment.URL, String.format("jdbc:mysql://%s:3306/%s", dbHost, dbName)); - settings.put(Environment.USER, dbUser); - settings.put(Environment.PASS, dbPass); + settings.put(Environment.JAKARTA_JDBC_URL, String.format("jdbc:mysql://%s:3306/%s", dbHost, dbName)); + settings.put(Environment.JAKARTA_JDBC_USER, dbUser); + settings.put(Environment.JAKARTA_JDBC_PASSWORD, dbPass); registryBuilder.applySettings(settings); registry = registryBuilder.build(); diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/AbstractBuiltInTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractBuiltInTask.java new file mode 100644 index 0000000..f334bf4 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractBuiltInTask.java @@ -0,0 +1,40 @@ +package me.bobert.netneutrality.core.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.TaskContext; + +public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltInTask { + + @Override + public String getName() { + return getId(); + } + + @Override + public boolean isInsertable() { + return true; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public void replyToInteraction(CommandContext context) { + // Task is loaded at runtime, this will never be called + } + + @Override + public Lifecycle execute(TaskContext context) { + executeTask(context); + return Lifecycle.BUILTIN; + } + + public abstract void executeTask(TaskContext context); +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/AbstractPersistableTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractPersistableTask.java new file mode 100644 index 0000000..4b03f13 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractPersistableTask.java @@ -0,0 +1,58 @@ +package me.bobert.netneutrality.core.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; +import org.json.JSONObject; + +public abstract class AbstractPersistableTask extends AbstractTask implements PersistableTask { + + @Override + public boolean isInsertable() { + return true; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public void replyToInteraction(CommandContext context) { + // Task loaded from database, this will never be called + } + + @Override + public void insert(CommandContext context) { + if (TaskManager.getInstance().hasTask(getId())) { + throw new RuntimeException("Cannot insert a task that already exists"); + } + + TaskManager.getInstance().insert(context, this); + } + + @Override + public void save() { + if (!TaskManager.getInstance().hasTask(getId())) { + throw new RuntimeException("Cannot save a task that doesn't exist yet"); + } + + TaskManager.getInstance().getTaskService().persistTask(this); + } + + @Override + public void delete() { + TaskManager.getInstance().getTaskService().unpersistTask(this); + TaskManager.getInstance().removeTask(this); + } + + @Override + public String toPayload() { + JSONObject payload = new JSONObject(this); + + return payload.toString(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java new file mode 100644 index 0000000..ba5d5a1 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java @@ -0,0 +1,48 @@ +package me.bobert.netneutrality.core.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; + +import java.time.OffsetDateTime; +import java.util.Objects; + +public abstract class AbstractTask implements Task { + private final OffsetDateTime creationTime; + + public AbstractTask() { + creationTime = OffsetDateTime.now(); + } + + @Override + public OffsetDateTime getCreationTime() { + return creationTime; + } + + @Override + public void insert(CommandContext context) { + TaskManager.getInstance().insert(context, this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AbstractTask that = (AbstractTask) o; + return Objects.equals(getId(), that.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } + + @Override + public String toString() { + return String.format("Task{id=%s, lockId=%s, class=%s, name=%s, creationTime=%s}", getId(), getLockId(), getClass().getSimpleName(), getName(), getCreationTime()); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/BuiltInTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/BuiltInTask.java new file mode 100644 index 0000000..a64fa2d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/BuiltInTask.java @@ -0,0 +1,25 @@ +package me.bobert.netneutrality.core.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +/** + * Represents a task that has a known ID and is loaded at runtime. + * This can be any task and is not necessarily a button. + */ +public interface BuiltInTask extends Task { + + static Button createButton(Class clazz) { + BuiltInTask builtInTask = TaskManager.getInstance().findBuiltInTaskByClass(clazz); + + if (builtInTask == null) { + throw new IllegalArgumentException("Task class " + clazz.getName() + " was not registered by the task manager."); + } + + return builtInTask.createButton(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/GenericCustomIdTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/GenericCustomIdTask.java new file mode 100644 index 0000000..8ab28a2 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/GenericCustomIdTask.java @@ -0,0 +1,27 @@ +package me.bobert.netneutrality.core.tasks; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +public abstract class GenericCustomIdTask extends AbstractTask { + private final String customId; + + public GenericCustomIdTask(String id) { + this.customId = id; + } + + @Override + public String getId() { + return customId; + } + + @Override + public Button createButton() { + return null; + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/GenericRandomIdTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/GenericRandomIdTask.java new file mode 100644 index 0000000..23cde6e --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/GenericRandomIdTask.java @@ -0,0 +1,15 @@ +package me.bobert.netneutrality.core.tasks; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import java.util.UUID; + +public abstract class GenericRandomIdTask extends GenericCustomIdTask { + public GenericRandomIdTask() { + super(UUID.randomUUID().toString()); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/IStepTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/IStepTask.java new file mode 100644 index 0000000..fee1dbb --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/IStepTask.java @@ -0,0 +1,147 @@ +package me.bobert.netneutrality.core.tasks; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +public interface IStepTask { + + /** + * Defines the type of input expected for a step. + */ + enum StepType { + /** + * Expects a string input from the user. + */ + STRING, + + /** + * Expects a valid role ID for the current guild. + * The role must exist in the guild where the command was executed. + */ + ROLE, + + /** + * Expects a valid emoji. + */ + EMOJI + } + + class StepResult { + private enum ResultType { + SUCCESS, FAILURE + } + + private final ResultType type; + private final String message; + private final Task nextStep; + + /** + * Represents the result of a step execution. Each step can either succeed, fail, or transition to another step. + */ + private StepResult(ResultType type, String message, Task nextStep) { + this.type = type; + this.message = message; + this.nextStep = nextStep; + } + + /** + * Indicates that the step was completed successfully, with an optional follow-up step. + * + * @param message the success message + * @param nextStep the next step in the workflow, or null if this is the final step + * @return a successful result + */ + public static StepResult success(String message, Task nextStep) { + return new StepResult(ResultType.SUCCESS, message, nextStep); + } + + /** + * Indicates that the step was completed successfully. + * + * @param message the success message + * @return a successful result without a next step + */ + public static StepResult success(String message) { + return new StepResult(ResultType.SUCCESS, message, null); + } + + /** + * Indicates that the step has failed. + * + * @param message the failure message + * @return a failed result + */ + public static StepResult failure(String message) { + return new StepResult(ResultType.FAILURE, message, null); + } + + /** + * Moves directly to the next step without displaying a message. + * + * @param nextTask the next task in the workflow + * @return a successful result that advances to the next step + */ + public static StepResult next(Task nextTask) { + return new StepResult(ResultType.SUCCESS, null, nextTask); + } + + /** + * Gets the message associated with the step result. + * + * @return the result message, or null if there is no message + */ + public String getMessage() { + return message; + } + + /** + * Gets the next step in the task workflow. + * + * @return the next step, or null if this is the last step + */ + public Task getNextStep() { + return nextStep; + } + + /** + * Checks whether the step execution was successful. + * + * @return true if the step was successful, false otherwise + */ + public boolean isSuccess() { + return type == ResultType.SUCCESS; + } + } + + /** + * Gets the type of step input expected. + * + * @return the step type + */ + StepType getStepType(); + + /** + * Provides a general description of the step. This will be displayed before asking the question. + * + * @return the step description + */ + String getStepDescription(); + + /** + * Provides the specific question for this step. + * + * @return the step question + */ + String getStepQuestion(); + + /** + * Executes the step using the provided answer. + * + * @param answer the user's input, validated against the expected step type + * @return the result of executing this step + */ + StepResult executeStep(Object answer); +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java new file mode 100644 index 0000000..a5a1cc7 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java @@ -0,0 +1,23 @@ +package me.bobert.netneutrality.core.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; + +/** + * Represents a task that can be persisted and restored later. + * The persistable task provides functionality to serialize its state + * so that it can be stored in a database. + */ +public interface PersistableTask extends Task { + void insert(CommandContext context); + + void save(); + + void delete(); + + String toPayload(); +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/Task.java b/src/main/java/me/bobert/netneutrality/core/tasks/Task.java new file mode 100644 index 0000000..bc2a084 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/Task.java @@ -0,0 +1,79 @@ +package me.bobert.netneutrality.core.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +import java.time.OffsetDateTime; + +public interface Task { + enum Lifecycle { + BUILTIN, + PARENT, + KEEP, + REMOVE_SELF, + REMOVE_SELF_AND_CHILDREN + } + /** + * This method should generate a unique ID to differentiate it from other tasks. + *

+ * This determines what triggers a task to be executed. + * For example, a modal or button would use the component id. + * For an emoji reaction, the guild and message id would be combined. + *

+ * + * @return a unique ID for this task + */ + String getId(); + + /** + * Retrieves the lock ID that is used to synchronize tasks + * that require a locking mechanism. + * + * @return the lock ID associated with this task + */ + default String getLockId() { + return getId(); + } + + /** + * This is only used for debugging purpose. + * @return Name of the task + */ + String getName(); + + /** + *

If this returns false, the task will not get inserted and executed.

+ */ + boolean isInsertable(); + + /** + * When this returns true, the task will get removed by the cleanup thread. + * @return If the task is expired + */ + boolean isExpired(); + + OffsetDateTime getCreationTime(); + + Button createButton(); + + /** + *

This method is used to reply to the interaction that triggered this task.

+ *

If the task is implementing {@link WithChildren}, the child tasks must be instanciated in this method.

+ */ + void replyToInteraction(CommandContext context); + + /** + * Executes the task. + * @param context The context for this task. + */ + Lifecycle execute(TaskContext context); + + void insert(CommandContext context); +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java b/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java new file mode 100644 index 0000000..cd13aa0 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java @@ -0,0 +1,315 @@ +package me.bobert.netneutrality.core.tasks; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.LogHandler; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.InteractionContext; +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.trait.Deletable; +import me.bobert.netneutrality.core.tasks.trait.NotAcknowledgeable; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import me.bobert.netneutrality.core.tasks.trait.WithParent; +import me.bobert.netneutrality.core.thread.ThreadManager; +import me.bobert.netneutrality.db.service.TaskService; +import me.bobert.netneutrality.exceptions.GracefulContextError; +import me.bobert.netneutrality.exceptions.TaskExecutionException; +import me.bobert.netneutrality.thread.CleanupTaskThread; +import net.dv8tion.jda.api.interactions.InteractionHook; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.reflections.Reflections; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +public class TaskManager { + private static final Logger logger = LogManager.getLogger(TaskManager.class); + private static TaskManager instance; + private final TaskService taskService; + private final Map tasks; + private final List pendingTasks; + private final Map deletableTasksContext; + private final Map tasksLock; + + private TaskManager() { + this.tasks = new ConcurrentHashMap<>(); + this.pendingTasks = new CopyOnWriteArrayList<>(); + this.deletableTasksContext = new ConcurrentHashMap<>(); + this.tasksLock = new ConcurrentHashMap<>(); + this.taskService = new TaskService(); + } + + public static TaskManager getInstance() { + if (instance == null) { + instance = new TaskManager(); + } + + return instance; + } + + public void init() { + loadBuiltInTasks(); + taskService.loadTasks(); + + ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new CleanupTaskThread(), 1, 5, TimeUnit.MINUTES); + } + + protected TaskService getTaskService() { + return taskService; + } + + public Task getTask(String id) { + return tasks.get(id); + } + + public boolean hasTask(String id) { + return tasks.containsKey(id) || pendingTasks.stream().anyMatch(task -> task.getId().equals(id)); + } + + public synchronized Collection getTasks() { + return tasks.values(); + } + + public void insert(CommandContext context, Task task) { + String id = task.getId(); + + if (hasTask(id)) { + context.reply("A task with the same ID already exists!"); + return; + } + + task.replyToInteraction(context); + + if (task.isInsertable()) { + logger.info("Inserting task: {}", task); + + if (task instanceof PersistableTask persistableTask) { + taskService.persistTask(persistableTask); + } + + if (task instanceof IStepTask) { + pendingTasks.add(task); + } else { + tasks.put(id, task); + } + } + + if (task instanceof Deletable) { + deletableTasksContext.put(task.getId(), context); + } + + if (task instanceof WithChildren taskWithChildren) { + List childrens = taskWithChildren.getChildTasks(); + if (childrens == null || childrens.isEmpty()) { + logger.warn("Parent task '{}' doesn't create any childs", task.getName()); + return; + } + + for (Task child : childrens) { + insert(context, child); + } + } + } + + public void activateTask(Task task, String oldId) { + if (pendingTasks.remove(task)) { + tasks.put(task.getId(), task); + logger.info("Updating task {} with new id: {} (was: {})", + task.getName(), task.getId(), oldId); + } + } + + public void removeTask(Task task) { + String id = task.getId(); + + if (tasks.remove(id) != null) { + logger.debug("Removing task: {}", id); + + ReentrantLock lock = tasksLock.remove(task.getLockId()); + if (lock != null) { + logger.debug("Cleaning up lock for removed task: {}", task.getId()); + } + } + } + + public void handle(TaskContext context) { + String taskId = context.getId(); + Task task = getTask(taskId); + + if (task == null && context instanceof InteractionContext) { + context.replyEphemeral("This task is no longer available, someone might have already used it or it might have expired"); + } + + if (task != null) { + String lockId = task.getLockId(); + ReentrantLock lock = tasksLock.computeIfAbsent(lockId, k -> { + if (task instanceof PersistableTask || task instanceof BuiltInTask) { + return null; + } + + return new ReentrantLock(); + }); + + try { + if (lock != null) { + logger.debug("Aquiring lock for task: {}", taskId); + lock.lock(); + + if (!tasks.containsKey(taskId)) { + logger.debug("Task {} is no longer available after aquiring lock", taskId); + tasksLock.remove(lockId); + return; + } + } + + logger.debug("Executing task: {}", taskId); + Task.Lifecycle status = task.execute(context); + handleTaskLifecycle(task, status); + + deleteInteraction(task); + + // If interaction is still not acknowledged after execution, send a placeholder response. + if (!context.isAcknowledged() && !(task instanceof NotAcknowledgeable)) { + logger.warn("Interaction not acknowledged"); + context.replyEphemeral("Task executed successfully but interaction wasn't acknowledged, report this bug by using the command /report-bug"); + } + } catch (GracefulContextError exception) { + logger.error(exception.getMessage()); + context.replyEphemeral(exception.getMessage()); + } catch (TaskExecutionException exception) { + LogHandler.log(exception); + } catch (Exception exception) { + LogHandler.log(new TaskExecutionException("Error during task execution", context, exception)); + } finally { + if (lock != null) { + logger.debug("Releasing lock for task: {}", taskId); + lock.unlock(); + } + } + } + } + + public Optional findTaskByPartialId(Class clazz, String partialId) { + List tasks = findTasksByPartialId(clazz, partialId); + return Optional.ofNullable(tasks.isEmpty() ? null : tasks.get(0)); + } + + public List findTasksByPartialId(Class clazz, String partialId) { + return tasks.values().stream() + .filter(clazz::isInstance) + .map(clazz::cast) + .filter(task -> task.getId().contains(partialId)) + .collect(Collectors.toList()); + } + + public BuiltInTask findBuiltInTaskByClass(Class clazz) { + return tasks.values().stream() + .filter(clazz::isInstance) + .map(task -> (BuiltInTask) task) + .findFirst() + .orElse(null); + } + + public void insertPersistableTask(PersistableTask task) { + if (tasks.containsKey(task.getId())) { + logger.error("Trying to load persistable task but ID already exists: {}", task.getId()); + return; + } + + logger.info("Loaded persistable task: {}", task); + tasks.put(task.getId(), task); + } + + private void loadBuiltInTasks() { + logger.info("Loading built-in tasks..."); + + Set> classes = new Reflections("me.bobert.netneutrality.tasks").getSubTypesOf(BuiltInTask.class); + for (Class s : classes) { + try { + if (Modifier.isAbstract(s.getModifiers())) { + continue; + } + + BuiltInTask task = s.getConstructor().newInstance(); + + logger.info("Loaded task: {}", task); + tasks.put(task.getId(), task); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + logger.error("Failed to instantiate built-in task", e); + } + } + } + + private void deleteInteraction(Task task) { + if (task instanceof WithParent taskWithParent) { + Task parent = taskWithParent.getParentTask(); + + if (parent instanceof Deletable deletable && task.isInsertable()) { + deleteInteraction(parent.getId(), deletable); + return; + } + } + + if (task instanceof Deletable deletable && task.isInsertable()) { + deleteInteraction(task.getId(), deletable); + } + } + + private void deleteInteraction(String taskId, Deletable deletable) { + logger.debug("Deleting interaction: {}", deletable); + + CommandContext context = deletableTasksContext.get(taskId); + if (context != null) { + deletableTasksContext.remove(taskId); + + InteractionHook hook = context.getInteractionHook(); + if (hook != null) { + if (hook.isExpired()) { + context.replyEphemeral("This interaction has expired, can't remove it"); + } + + if (!hook.isExpired()) { + hook.deleteOriginal().queue(); + } + } + } + + deletable.onInteractionDelete(); + } + + private void handleTaskLifecycle(Task task, Task.Lifecycle status) { + if (status == null) { + throw new IllegalArgumentException("Task lifecycle status cannot be null"); + } + + switch (status) { + case REMOVE_SELF: + removeTask(task); + break; + + case REMOVE_SELF_AND_CHILDREN: + if (task instanceof WithChildren taskWithChildren) { + for (Task child : taskWithChildren.getChildTasks()) { + removeTask(child); + } + } + removeTask(task); + break; + + case KEEP: + default: + break; + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/ButtonTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ButtonTask.java new file mode 100644 index 0000000..f18d76e --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ButtonTask.java @@ -0,0 +1,53 @@ +package me.bobert.netneutrality.core.tasks.impl; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.GenericRandomIdTask; +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; + +import java.time.OffsetDateTime; +import java.util.List; + +public abstract class ButtonTask extends GenericRandomIdTask { + private final ButtonStyle buttonStyle; + private final String buttonLabel; + + public ButtonTask(ButtonStyle buttonStyle, String buttonLabel) { + this.buttonStyle = buttonStyle; + this.buttonLabel = buttonLabel; + } + + @Override + public Button createButton() { + if (buttonStyle == null || buttonLabel == null) { + return null; + } + + return Button.of(buttonStyle, getId(), buttonLabel); + } + + @Override + public boolean isExpired() { + return getCreationTime().plusMinutes(15).isBefore(OffsetDateTime.now()); + } + + public ActionRow createActionRow(List tasks) { + ActionRow row = ActionRow.of(tasks.stream().map(Task::createButton).toList()); + if (this instanceof WithChildren parent) { + parent.addChild(tasks); + } + + return row; + } + + public ActionRow createActionRow(Task... tasks) { + return createActionRow(List.of(tasks)); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java new file mode 100644 index 0000000..dae4604 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java @@ -0,0 +1,62 @@ +package me.bobert.netneutrality.core.tasks.impl; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.tasks.EmojiTaskContext; +import me.bobert.netneutrality.core.tasks.AbstractPersistableTask; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class EmojiTask extends AbstractPersistableTask { + private final Map emojiMapping = new ConcurrentHashMap<>(); + private final String guildId; + private final String channelId; + private final String messageId; + + public EmojiTask(String guildId, String channelId, String messageId) { + this.guildId = guildId; + this.channelId = channelId; + this.messageId = messageId; + } + + public String getGuildId() { + return guildId; + } + + public String getChannelId() { + return channelId; + } + + public String getMessageId() { + return messageId; + } + + public void addEmoji(String emoji, String snowflake) { + emojiMapping.put(emoji, snowflake); + save(); + } + + public void removeEmoji(String emoji) { + emojiMapping.remove(emoji); + save(); + } + + public Map getEmojiMapping() { + return emojiMapping; + } + + @Override + public String getId() { + return String.format(EmojiTaskContext.ID, getGuildId(), getMessageId()); + } + + @Override + public Button createButton() { + return null; + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/ModalTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ModalTask.java new file mode 100644 index 0000000..94db7a9 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ModalTask.java @@ -0,0 +1,30 @@ +package me.bobert.netneutrality.core.tasks.impl; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.GenericRandomIdTask; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; + +import java.time.OffsetDateTime; +import java.util.List; + +public abstract class ModalTask extends GenericRandomIdTask { + + @Override + public boolean isExpired() { + return getCreationTime().plusMinutes(5).isBefore(OffsetDateTime.now()); + } + + public ModalMapping getValue(String key, TaskContext context) { + List mappings = (List) context.getContextData("values"); + return mappings.stream() + .filter(o -> o instanceof ModalMapping) + .map(o -> (ModalMapping) o) + .filter(mapping -> mapping.getId().equals(key)) + .findFirst().orElse(null); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/StepTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/StepTask.java new file mode 100644 index 0000000..0c7f92a --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/StepTask.java @@ -0,0 +1,169 @@ +package me.bobert.netneutrality.core.tasks.impl; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.MessageContext; +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.AbstractTask; +import me.bobert.netneutrality.core.tasks.IStepTask; +import me.bobert.netneutrality.core.tasks.TaskManager; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.fellbaum.jemoji.Emoji; +import net.fellbaum.jemoji.EmojiManager; + +import java.time.OffsetDateTime; +import java.util.Optional; +import java.util.UUID; + +/** + * Represents a task that consists of multiple sequential steps. The task starts with an initial message (question) + * and expects a user response. Each step can lead to another step, forming a workflow. + */ +public abstract class StepTask extends AbstractTask implements IStepTask { + private final Object stepLock = new Object(); + private CommandContext commandContext; + private Guild guild; + private String id = UUID.randomUUID().toString(); + + @Override + public String getId() { + return id; + } + + @Override + public boolean isInsertable() { + return true; + } + + public Guild getGuild() { + return guild; + } + + @Override + public void replyToInteraction(CommandContext context) { + synchronized (stepLock) { + this.commandContext = context; + } + + if (context.isFromGuild()) { + guild = context.getGuild(); + } + + if (guild == null && getStepType() == StepType.ROLE) { + throw new IllegalArgumentException("Step task is of type role but it was inserted in a non-guild context"); + } + + context.replyEphemeral("Please follow the instructions sent in DM."); + context.sendMessageInPrivate(getStepDescription() + "\n\n" + getStepQuestion(), + asyncContext -> assignId(asyncContext.getMessageId())); + } + + @Override + public Lifecycle execute(TaskContext context) { + synchronized (stepLock) { + if (!context.getDiscordUser().equals(commandContext.getDiscordUser())) { + return Lifecycle.KEEP; + } + } + + if (context instanceof MessageContext messageContext) { + Message message = messageContext.getMessage(); + String input = message.getContentRaw(); + + + Object answer = validateInput(context, input); + if (answer == null) { + return Lifecycle.KEEP; + } + + StepResult result = executeStep(answer); + if (!result.isSuccess()) { + context.replyEphemeral(result.getMessage()); + return Lifecycle.KEEP; + } + + if (result.getMessage() != null) { + context.replyEphemeral(result.getMessage()); + } + + if (result.getNextStep() != null) { + result.getNextStep().insert(commandContext); + } + + return Lifecycle.REMOVE_SELF; + } + + throw new IllegalArgumentException("Step task is not of type MessageContext"); + } + + @Override + public boolean isExpired() { + // is it really needed to expire this type of task? + return getCreationTime().plusMinutes(60).isBefore(OffsetDateTime.now()); + } + + @Override + public Button createButton() { + return null; + } + + /** + * Validates the user input against the expected step type. + * + * @param context the task execution context + * @param input the user-provided input + * @return the parsed input if valid, or null if invalid + */ + private Object validateInput(TaskContext context, String input) { + switch (getStepType()) { + case ROLE -> { + try { + Role role = guild.getRoleById(input); + if (role == null) { + context.replyEphemeral("The role doesn't exist"); + return null; + } + + return role; + } catch (NumberFormatException e) { + context.replyEphemeral("Not a valid role ID"); + return null; + } + } + case EMOJI -> { + Optional parsed = EmojiManager.getEmoji(input); + + if (parsed.isPresent()) { + return parsed.get().getEmoji(); + } + + context.replyEphemeral("Not a valid emoji"); + return null; + } + case STRING -> { + return input; + } + default -> throw new IllegalStateException("Unknown step type: " + getStepType()); + } + } + + private void assignId(String newId) { + if (newId == null || newId.isBlank()) { + throw new IllegalArgumentException("Task ID cannot be null or empty."); + } + + String oldId = id; + synchronized (this) { + this.id = newId; + TaskManager.getInstance().activateTask(this, oldId); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskFactory.java b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskFactory.java new file mode 100644 index 0000000..80b89b1 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskFactory.java @@ -0,0 +1,81 @@ +package me.bobert.netneutrality.core.tasks.persistence; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.PersistableTask; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Map; + +public class TaskFactory { + public enum ParamType { + STRING(String.class), + INT(Integer.class), + MAP(Map.class); + + private final Class typeClass; + + ParamType(Class typeClass) { + this.typeClass = typeClass; + } + + public Class getTypeClass() { + return typeClass; + } + + public static ParamType fromString(String type) { + return switch (type.toLowerCase()) { + case "string" -> STRING; + case "int" -> INT; + case "map" -> MAP; + default -> throw new IllegalArgumentException("Unsupported type: " + type); + }; + } + } + + public static PersistableTask createTaskInstance(String className, JSONArray params, JSONArray types, JSONObject payload) throws Exception { + if (params.length() != types.length()) { + throw new RuntimeException("Wrong number of parameters and types found in task mappings"); + } + + Class clazz = Class.forName(className); + if (!PersistableTask.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("Class does not implement PersistableTask."); + } + + Class[] classTypes = new Class[types.length()]; + Object[] classParams = new Object[params.length()]; + + for (int i = 0; i < params.length(); i++) { + ParamType paramType = ParamType.fromString(types.getString(i)); + String paramName = params.getString(i); + + classTypes[i] = paramType.getTypeClass(); + classParams[i] = extractValue(paramType, paramName, payload); + } + + try { + return (PersistableTask) clazz.getConstructor(classTypes).newInstance(classParams); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodException("No matching constructor found with parameters: " + Arrays.toString(classTypes)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid arguments for constructor. Expected: " + Arrays.toString(classParams), e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Constructor threw an exception", e.getCause()); + } + } + + private static Object extractValue(ParamType type, String paramName, JSONObject payload) { + return switch (type) { + case STRING -> payload.getString(paramName); + case INT -> payload.getInt(paramName); + case MAP -> payload.getJSONObject(paramName).toMap(); + }; + } +} \ No newline at end of file diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskRegistry.java b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskRegistry.java new file mode 100644 index 0000000..e0dc7b3 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskRegistry.java @@ -0,0 +1,94 @@ +package me.bobert.netneutrality.core.tasks.persistence; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.PersistableTask; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class TaskRegistry { + @FunctionalInterface + public interface TaskCreator { + PersistableTask fromPayload(JSONObject payload); + } + + private static final Logger logger = LogManager.getLogger(TaskRegistry.class); + private static final Map taskCreators = new HashMap<>(); + private static final String FILE_NAME = "persistable-tasks.json"; + + // JSON keys as constants + private static final String KEY_TASKS = "tasks"; + private static final String KEY_CLASS = "class"; + private static final String KEY_PARAMS = "params"; + private static final String KEY_TYPES = "types"; + + public static TaskCreator getTaskCreator(String className) { + return taskCreators.get(className); + } + + public static void loadTaskConfig() { + logger.info("Reading task mappings from {}..", FILE_NAME); + + try (InputStream is = TaskFactory.class.getClassLoader().getResourceAsStream(FILE_NAME)) { + if (is == null) { + throw new FileNotFoundException("Could not load: " + FILE_NAME); + } + + String jsonString = IOUtils.toString(is, StandardCharsets.UTF_8); + JSONObject json = new JSONObject(jsonString); + JSONArray tasksArray = json.optJSONArray(KEY_TASKS); + + if (tasksArray == null) { + logger.warn("No tasks found in JSON: {}", FILE_NAME); + return; + } + + for (int i = 0; i < tasksArray.length(); i++) { + JSONObject taskJson = tasksArray.getJSONObject(i); + + String className = taskJson.optString(KEY_CLASS, "").trim(); + if (className.isEmpty()) { + logger.warn("Missing JSON key '{}' at index {}", KEY_CLASS, i); + continue; + } + + JSONArray params = taskJson.optJSONArray(KEY_PARAMS); + if (params == null) { + logger.warn("Missing JSON key '{}' for class: {}", KEY_PARAMS, className); + continue; + } + + JSONArray types = taskJson.optJSONArray(KEY_TYPES); + if (types == null) { + logger.warn("Missing JSON key '{}' for class: {}", KEY_TYPES, className); + continue; + } + + taskCreators.put(className, payload -> { + try { + return TaskFactory.createTaskInstance(className, params, types, payload); + } catch (Exception e) { + logger.error("Failed to create instance of '{}': {}", className, e.getMessage()); + return null; + } + }); + } + + logger.info("{} mappings registered", taskCreators.size()); + } catch (Exception e) { + logger.error(e); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/trait/Deletable.java b/src/main/java/me/bobert/netneutrality/core/tasks/trait/Deletable.java new file mode 100644 index 0000000..661a8cd --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/Deletable.java @@ -0,0 +1,19 @@ +package me.bobert.netneutrality.core.tasks.trait; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * Represents a task that will remove its interaction from the channel after execution + */ +public interface Deletable { + + /** + * This method is called after the interaction associated with a deletable task + * has been deleted. Tasks that implement the {@link Deletable} interface can provide + * their own cleanup logic in this method. + */ + void onInteractionDelete(); +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/trait/NotAcknowledgeable.java b/src/main/java/me/bobert/netneutrality/core/tasks/trait/NotAcknowledgeable.java new file mode 100644 index 0000000..e3c3b75 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/NotAcknowledgeable.java @@ -0,0 +1,12 @@ +package me.bobert.netneutrality.core.tasks.trait; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * Represents a task that will not acknowledge the interaction when executed. + */ +public interface NotAcknowledgeable { +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithChildren.java b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithChildren.java new file mode 100644 index 0000000..22cf018 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithChildren.java @@ -0,0 +1,29 @@ +package me.bobert.netneutrality.core.tasks.trait; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.Task; + +import java.util.List; + +public interface WithChildren extends Task { + + @Override + default Lifecycle execute(TaskContext context) { + return Lifecycle.PARENT; + } + + List getChildTasks(); + + default void addChild(List tasks) { + getChildTasks().addAll(tasks); + } + + default void addChild(Task... tasks) { + addChild(List.of(tasks)); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithParent.java b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithParent.java new file mode 100644 index 0000000..026dbaf --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithParent.java @@ -0,0 +1,34 @@ +package me.bobert.netneutrality.core.tasks.trait; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.Task; + +public interface WithParent extends Task { + + /** + * Retrieves the lock ID associated with the parent task. + * + * @return the lock ID of the parent task + */ + @Override + default String getLockId() { + return getParentTask().getLockId(); + } + + @Override + default void replyToInteraction(CommandContext context) { + // Child tasks shouldn't reply to the interaction, this is the job of the parent task + } + + /** + * Retrieves the parent task associated with this task. + * + * @return the parent task of type E + */ + E getParentTask(); +} diff --git a/src/main/java/me/bobert/netneutrality/thread/BaseThreadFactory.java b/src/main/java/me/bobert/netneutrality/core/thread/BaseThreadFactory.java similarity index 94% rename from src/main/java/me/bobert/netneutrality/thread/BaseThreadFactory.java rename to src/main/java/me/bobert/netneutrality/core/thread/BaseThreadFactory.java index 1c33201..d0b5c16 100644 --- a/src/main/java/me/bobert/netneutrality/thread/BaseThreadFactory.java +++ b/src/main/java/me/bobert/netneutrality/core/thread/BaseThreadFactory.java @@ -1,4 +1,4 @@ -package me.bobert.netneutrality.thread; +package me.bobert.netneutrality.core.thread; /* * Copyright (c) anthorob diff --git a/src/main/java/me/bobert/netneutrality/thread/ThreadManager.java b/src/main/java/me/bobert/netneutrality/core/thread/ThreadManager.java similarity index 55% rename from src/main/java/me/bobert/netneutrality/thread/ThreadManager.java rename to src/main/java/me/bobert/netneutrality/core/thread/ThreadManager.java index f2d88e7..a08fb9e 100644 --- a/src/main/java/me/bobert/netneutrality/thread/ThreadManager.java +++ b/src/main/java/me/bobert/netneutrality/core/thread/ThreadManager.java @@ -1,4 +1,4 @@ -package me.bobert.netneutrality.thread; +package me.bobert.netneutrality.core.thread; /* * Copyright (c) anthorob * @@ -10,40 +10,24 @@ import java.util.concurrent.*; public class ThreadManager { - public enum ExecutorType { - SINGLE, SCHEDULED, FIXED - } - private static final Map executors = new ConcurrentHashMap<>(); - public static ScheduledExecutorService getMusicExecutor() { + public static ScheduledExecutorService getScheduledExecutor() { return (ScheduledExecutorService) executors.computeIfAbsent( - "music-executor", + "scheduled-executor", s -> Executors.newScheduledThreadPool( Runtime.getRuntime().availableProcessors(), - new BaseThreadFactory("music-executor") + new BaseThreadFactory(s) ) ); } - public static ExecutorService getExecutor() { - return executors.computeIfAbsent("general-executor", s -> Executors.newCachedThreadPool(new BaseThreadFactory("general-executor"))); - } - public static ExecutorService getExecutor(String name) { - return executors.computeIfAbsent(name, s -> Executors.newSingleThreadExecutor(new BaseThreadFactory(name))); + return executors.computeIfAbsent(name, s -> Executors.newCachedThreadPool(new BaseThreadFactory(s))); } - public static ExecutorService getExecutor(String name, ExecutorType type) { - ExecutorService executor; - - switch (type) { - case FIXED -> executor = executors.computeIfAbsent(name, s -> Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new BaseThreadFactory(name))); - case SCHEDULED -> executor = executors.computeIfAbsent(name, s -> Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new BaseThreadFactory(name))); - default -> executor = executors.computeIfAbsent(name, s -> Executors.newSingleThreadExecutor(new BaseThreadFactory(name))); - } - - return executor; + public static ExecutorService getExecutor() { + return getExecutor("general-executor"); } public static void shutdownExecutor() { diff --git a/src/main/java/me/bobert/netneutrality/db/AbstractController.java b/src/main/java/me/bobert/netneutrality/db/AbstractController.java deleted file mode 100644 index 6fad802..0000000 --- a/src/main/java/me/bobert/netneutrality/db/AbstractController.java +++ /dev/null @@ -1,23 +0,0 @@ -package me.bobert.netneutrality.db; - -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.SessionManager; -import org.hibernate.SessionFactory; - -public abstract class AbstractController { - - protected SessionFactory sessionFactory; - - public AbstractController() { - sessionFactory = SessionManager.getSessionFactory(); - } - - protected SessionFactory getSessionFactory() { - return sessionFactory; - } -} diff --git a/src/main/java/me/bobert/netneutrality/db/DBController.java b/src/main/java/me/bobert/netneutrality/db/DBController.java deleted file mode 100644 index aab0723..0000000 --- a/src/main/java/me/bobert/netneutrality/db/DBController.java +++ /dev/null @@ -1,38 +0,0 @@ -package me.bobert.netneutrality.db; - -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.SessionManager; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.lang.reflect.InvocationTargetException; - -public class DBController { - - private final static Logger logger = LogManager.getLogger(DBController.class); - - public static T get(Class returnType) { - if (returnType == null) - throw new IllegalArgumentException("return type is null"); - - try { - return returnType.getConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - public static void shutdown() { - SessionManager.shutdown(); - } - - public static void init() { - SessionManager.getSessionFactory(); - } - -} diff --git a/src/main/java/me/bobert/netneutrality/db/GSettings.java b/src/main/java/me/bobert/netneutrality/db/GSettings.java index 5945ff5..64f094a 100644 --- a/src/main/java/me/bobert/netneutrality/db/GSettings.java +++ b/src/main/java/me/bobert/netneutrality/db/GSettings.java @@ -9,7 +9,8 @@ public enum GSettings { //TODO: description MUSIC_ROLE("MUSIC_ROLE", ""), MUSIC_CHANNEL("MUSIC_CHANNEL", ""), - MUSIC_VOLUME("MUSIC_VOLUME", "30"); + MUSIC_VOLUME("MUSIC_VOLUME", "30"), + MUSIC_MAXPLAYLISTSIZE("MUSIC_MAXPLAYLISTSIZE", "20"); private final String name; private final String defaultValue; diff --git a/src/main/java/me/bobert/netneutrality/db/Repository.java b/src/main/java/me/bobert/netneutrality/db/Repository.java new file mode 100644 index 0000000..09494a6 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/Repository.java @@ -0,0 +1,44 @@ +package me.bobert.netneutrality.db; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.db.SessionManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.exception.ConstraintViolationException; + +import java.util.List; + +public interface Repository { + Logger logger = LogManager.getLogger(Repository.class); + + List findAll(); + + O findById(String id); + + default void save(O entity) { + Transaction transaction = null; + + try(Session session = SessionManager.getSessionFactory().openSession()) { + transaction = session.beginTransaction(); + session.merge(entity); + transaction.commit(); + } catch (ConstraintViolationException e) { + logger.error(e); + throw e; + } catch (Exception e) { + logger.error(e); + + if (transaction != null) { + transaction.rollback(); + } + } + } + + void deleteBy(String id); +} diff --git a/src/main/java/me/bobert/netneutrality/db/controllers/DBBlacklist.java b/src/main/java/me/bobert/netneutrality/db/controllers/DBBlacklist.java deleted file mode 100644 index 19dea2a..0000000 --- a/src/main/java/me/bobert/netneutrality/db/controllers/DBBlacklist.java +++ /dev/null @@ -1,141 +0,0 @@ -package me.bobert.netneutrality.db.controllers; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.core.context.CommandContext; -import me.bobert.netneutrality.db.AbstractController; -import me.bobert.netneutrality.db.entities.OBlacklist; -import me.bobert.netneutrality.db.entities.OGuild; -import me.bobert.netneutrality.utils.JpaUtils; -import net.dv8tion.jda.api.entities.Guild; -import org.hibernate.Session; -import org.hibernate.query.Query; - -import java.util.ArrayList; - -public class DBBlacklist extends AbstractController { - - public void add(String word, CommandContext context) { - GuildManager guildManager = GuildManager.getManager(context.getGuild()); - - - OBlacklist bl = getWordFromDb(word, context.getGuild().getId()); - - if (bl == null) { - Session session = getSessionFactory().openSession(); - - session.beginTransaction(); - bl = new OBlacklist(); - bl.setGuild(guildManager.getGuildEntity()); - bl.setCreatorId(context.getGuildUser().getId()); - bl.setWord(word); - bl.setEnabled(true); - - session.persist(bl); - session.getTransaction().commit(); - session.close(); - - guildManager.reloadBlacklistCache(); - context.reply(String.format("Added the word [%s] to the blacklist.", word)); - } else { - context.reply("Word is already blacklisted"); - } - - - - } - - public void delete(String word, CommandContext context) { - GuildManager guildManager = GuildManager.getManager(context.getGuild()); - OBlacklist bl = getWordFromDb(word, context.getGuild().getId()); - - if (bl != null) { - Session session = getSessionFactory().openSession(); - session.beginTransaction(); - - session.remove(bl); - session.getTransaction().commit(); - session.close(); - - guildManager.reloadBlacklistCache(); - context.reply("Removed the word [%s] from the blacklist.", word); - } else { - context.reply("Can't delete word because it doesn't exist"); - } - } - - public void enable(String word, CommandContext context) { - GuildManager guildManager = GuildManager.getManager(context.getGuild()); - - - OBlacklist bl = getWordFromDb(word, context.getGuild().getId()); - if (bl != null && !bl.getEnabled()) { - Session session = getSessionFactory().openSession(); - session.beginTransaction(); - - bl.setEnabled(true); - - session.merge(bl); - session.getTransaction().commit(); - session.close(); - - guildManager.reloadBlacklistCache(); - context.reply("Enabled the word [%s] for the blacklist.", word); - } else { - context.reply("Either word doesn't exist or is already enabled"); - } - - - } - - public void disable(String word, CommandContext context) { - GuildManager guildManager = GuildManager.getManager(context.getGuild()); - - - OBlacklist bl = getWordFromDb(word, context.getGuild().getId()); - if (bl != null && bl.getEnabled()) { - Session session = getSessionFactory().openSession(); - session.beginTransaction(); - - bl.setEnabled(false); - - session.merge(bl); - session.getTransaction().commit(); - session.close(); - - guildManager.reloadBlacklistCache(); - context.reply("Disabled the word [%s] for the blacklist.", word); - } else { - context.reply("Either word doesn't exist or is already disabled"); - } - } - - public ArrayList getBlacklistForGuild(Guild guild) { - GuildManager guildManager = GuildManager.getManager(guild); - Session session = getSessionFactory().openSession(); - - OGuild guildEntity = guildManager.getGuildEntity(); - JpaUtils.tryLoad(session, guildEntity, guildEntity.getGuildId()); - - ArrayList blacklists = new ArrayList<>(guildEntity.getBlacklists()); - session.close(); - - return blacklists; - } - - private OBlacklist getWordFromDb(String word, String guildId) { - Session session = getSessionFactory().openSession(); - - Query query = session.createNamedQuery("OBlacklist.findByWordEqualsIgnoreCaseAndGuild_GuildIdEquals", OBlacklist.class); - query.setParameter("guildId", guildId); - query.setParameter("word", word); - OBlacklist blacklist = query.uniqueResult(); - - session.close(); - return blacklist; - } -} diff --git a/src/main/java/me/bobert/netneutrality/db/controllers/DBMessage.java b/src/main/java/me/bobert/netneutrality/db/controllers/DBMessage.java deleted file mode 100644 index 81b1b38..0000000 --- a/src/main/java/me/bobert/netneutrality/db/controllers/DBMessage.java +++ /dev/null @@ -1,97 +0,0 @@ -package me.bobert.netneutrality.db.controllers; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.db.AbstractController; -import me.bobert.netneutrality.db.entities.OLogMessage; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.hibernate.Session; -import org.hibernate.query.Query; - -import java.util.List; - -public class DBMessage extends AbstractController { - - private final static Logger logger = LogManager.getLogger(DBMessage.class); - - public void log(GuildManager guildManager, MessageChannel channel, Member author, Message message) { - Session session = getSessionFactory().openSession(); - session.beginTransaction(); - - OLogMessage logMessage = new OLogMessage(); - logMessage.setGuild(guildManager.getGuildEntity()); - logMessage.setMessageId(message.getId()); - logMessage.setMessage(message.getContentRaw().trim()); - logMessage.setChannelId(channel.getId()); - logMessage.setUserId(author.getId()); - logMessage.setDateSent(message.getTimeCreated().toInstant()); - - session.persist(logMessage); - session.getTransaction().commit(); - session.close(); - } - - public void logEdit(String messageId, String newMessage) { - Session session = getSessionFactory().openSession(); - Query query = session.createNamedQuery("OLogMessage.findByMessageIdEquals", OLogMessage.class); - query.setParameter("messageId", messageId); - - OLogMessage message = query.uniqueResult(); - if (message != null) { - session.beginTransaction(); - message.setEditedMessage(newMessage); - - session.merge(message); - session.getTransaction().commit(); - } - - session.close(); - } - - public OLogMessage getMessage(String messageId) { - Session session = getSessionFactory().openSession(); - Query query = session.createNamedQuery("OLogMessage.findByMessageIdEquals", OLogMessage.class); - query.setParameter("messageId", messageId); - - OLogMessage message = query.uniqueResult(); - session.close(); - - return message; - } - - public List getMessagesFromUser(String userId, String guildId, int max) { - Session session = getSessionFactory().openSession(); - Query query = session.createNamedQuery("OLogMessage.findByUserIdEquals", OLogMessage.class); - query.setParameter("guildId", guildId); - query.setParameter("userId", userId); - query.setMaxResults(max); - - List messages = query.getResultList(); - session.close(); - - return messages; - } - - public String getMessageString(String messageId) { - Session session = getSessionFactory().openSession(); - Query query = session.createNamedQuery("OLogMessage.findByMessageIdEquals", OLogMessage.class); - query.setParameter("messageId", messageId); - - OLogMessage message = query.uniqueResult(); - session.close(); - - if (message == null) - return null; - else - return message.getMessage(); - - } -} diff --git a/src/main/java/me/bobert/netneutrality/db/entities/OBlacklist.java b/src/main/java/me/bobert/netneutrality/db/entities/OBlacklist.java index 5db94c6..6a44f51 100644 --- a/src/main/java/me/bobert/netneutrality/db/entities/OBlacklist.java +++ b/src/main/java/me/bobert/netneutrality/db/entities/OBlacklist.java @@ -10,9 +10,6 @@ import jakarta.persistence.*; @Entity @Table(name = "blacklist") -@NamedQueries({ - @NamedQuery(name = "OBlacklist.findByWordEqualsIgnoreCaseAndGuild_GuildIdEquals", query = "select o from OBlacklist o where upper(o.word) = upper(:word) and o.guild.guildId = :guildId") -}) public class OBlacklist { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/me/bobert/netneutrality/db/entities/OLogMessage.java b/src/main/java/me/bobert/netneutrality/db/entities/OLogMessage.java index e8b8a82..4211dc7 100644 --- a/src/main/java/me/bobert/netneutrality/db/entities/OLogMessage.java +++ b/src/main/java/me/bobert/netneutrality/db/entities/OLogMessage.java @@ -12,10 +12,6 @@ import java.time.Instant; @Entity @Table(name = "log_message") -@NamedQueries({ - @NamedQuery(name = "OLogMessage.findByMessageIdEquals", query = "select o from OLogMessage o where o.messageId = :messageId"), - @NamedQuery(name = "OLogMessage.findByGuild_GuildIdEqualsAndUserIdEquals", query = "select o from OLogMessage o where o.guild.guildId = :guildId and o.userId = :userId") -}) public class OLogMessage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/me/bobert/netneutrality/db/entities/OTask.java b/src/main/java/me/bobert/netneutrality/db/entities/OTask.java new file mode 100644 index 0000000..035d3bf --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/entities/OTask.java @@ -0,0 +1,60 @@ +package me.bobert.netneutrality.db.entities; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import jakarta.persistence.*; + +@Entity +@Table(name = "tasks", uniqueConstraints = {@UniqueConstraint(columnNames = {"task_id"})}) +public class OTask { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Integer id; + + @Column(name = "task_id", nullable = false) + private String taskId; + + @Column(name = "task_class", nullable = false) + private String taskClass; + + @Lob + @Column(name = "payload") + private String payload; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getTaskClass() { + return taskClass; + } + + public void setTaskClass(String taskClass) { + this.taskClass = taskClass; + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + +} \ No newline at end of file diff --git a/src/main/java/me/bobert/netneutrality/db/entities/OWebUser.java b/src/main/java/me/bobert/netneutrality/db/entities/OWebUser.java new file mode 100644 index 0000000..808bf7b --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/entities/OWebUser.java @@ -0,0 +1,86 @@ +package me.bobert.netneutrality.db.entities; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import jakarta.persistence.*; +import org.hibernate.annotations.ColumnDefault; + +import java.time.Instant; + +@Entity +@Table(name = "web_users") +public class OWebUser { + @Id + @Column(name = "id", nullable = false) + private String id; + + @Lob + @Column(name = "email", nullable = false) + private String email; + + @Column(name = "token", nullable = false) + private String token; + + @ColumnDefault("current_timestamp()") + @Column(name = "expires", nullable = false) + private Instant expires; + + @Column(name = "refresh_token", nullable = false) + private String refreshToken; + + @Column(name = "remember_token", length = 100) + private String rememberToken; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Instant getExpires() { + return expires; + } + + public void setExpires(Instant expires) { + this.expires = expires; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public String getRememberToken() { + return rememberToken; + } + + public void setRememberToken(String rememberToken) { + this.rememberToken = rememberToken; + } + +} \ No newline at end of file diff --git a/src/main/java/me/bobert/netneutrality/db/repository/BlacklistRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/BlacklistRepository.java new file mode 100644 index 0000000..b6bd763 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/repository/BlacklistRepository.java @@ -0,0 +1,105 @@ +package me.bobert.netneutrality.db.repository; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.db.SessionManager; +import me.bobert.netneutrality.db.Repository; +import me.bobert.netneutrality.db.entities.OBlacklist; +import me.bobert.netneutrality.db.entities.OGuild; +import me.bobert.netneutrality.utils.JpaUtils; +import net.dv8tion.jda.api.entities.Member; +import org.hibernate.Session; +import org.hibernate.query.Query; + +import java.util.ArrayList; +import java.util.List; + +public class BlacklistRepository implements Repository { + + @Override + public List findAll() { + List blacklists = new ArrayList<>(); + + JpaUtils.runWithSession(session -> { + Query query = session.createQuery("FROM OBlacklist", OBlacklist.class); + + blacklists.addAll(query.list()); + }); + + return blacklists; + } + + @Override + public OBlacklist findById(String id) { + return null; + } + + @Override + public void deleteBy(String id) { + JpaUtils.runWithTransaction(session -> { + Query query = session.createQuery("SELECT o FROM OBlacklist o WHERE o.id = :blacklistId", OBlacklist.class); + query.setParameter("blacklistId", id); + + OBlacklist oTask = query.uniqueResult(); + if (oTask != null) { + session.remove(oTask); + } + }); + } + + public List findByGuildId(String guildId) { + List blacklists = new ArrayList<>(); + + JpaUtils.runWithSession(session -> { + Query query = session.createQuery("SELECT o FROM OBlacklist o WHERE o.guild.guildId = :guildId", OBlacklist.class); + query.setParameter("guildId", guildId); + + blacklists.addAll(query.list()); + }); + + return blacklists; + } + + public void add(OGuild oGuild, Member member, String word) { + JpaUtils.runWithTransaction(session -> { + OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word); + + if (bl == null) { + bl = new OBlacklist(); + bl.setGuild(oGuild); + bl.setCreatorId(member.getId()); + bl.setWord(word); + bl.setEnabled(true); + + session.persist(bl); + } else { + bl.setEnabled(true); + session.merge(bl); + } + }); + } + + public void delete(OGuild oGuild, String word) { + JpaUtils.runWithTransaction(session -> { + OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word); + if (bl != null) { + session.remove(bl); + } + }); + } + + private OBlacklist getWordFromDb(String guildId, String word) { + try(Session session = SessionManager.getSessionFactory().openSession()) { + Query query = session.createQuery("SELECT o FROM OBlacklist o WHERE upper(o.word) = upper(:word) AND o.guild.guildId = :guildId", OBlacklist.class); + query.setParameter("guildId", guildId); + query.setParameter("word", word); + OBlacklist blacklist = query.uniqueResult(); + + session.close(); + return blacklist; + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/db/repository/GuildRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/GuildRepository.java new file mode 100644 index 0000000..dadeb3b --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/repository/GuildRepository.java @@ -0,0 +1,53 @@ +package me.bobert.netneutrality.db.repository;/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.db.SessionManager; +import me.bobert.netneutrality.db.Repository; +import me.bobert.netneutrality.db.entities.OGuild; +import me.bobert.netneutrality.utils.JpaUtils; +import org.hibernate.Session; +import org.hibernate.query.Query; + +import java.util.ArrayList; +import java.util.List; + +public class GuildRepository implements Repository { + @Override + public List findAll() { + List guilds = new ArrayList<>(); + + JpaUtils.runWithSession(session -> { + Query query = session.createQuery("FROM OGuild", OGuild.class); + + guilds.addAll(query.list()); + }); + + return guilds; + } + + @Override + public OGuild findById(String id) { + try(Session session = SessionManager.getSessionFactory().openSession()) { + Query query = session.createQuery("SELECT o FROM OGuild o WHERE o.guildId = :guildId", OGuild.class); + query.setParameter("guildId", id); + + return query.uniqueResult(); + } + } + + @Override + public void deleteBy(String id) { + JpaUtils.runWithTransaction(session -> { + Query query = session.createQuery("SELECT o FROM OGuild o WHERE o.guildId = :guildId", OGuild.class); + query.setParameter("guildId", id); + + OGuild oGuild = query.uniqueResult(); + if (oGuild != null) { + session.remove(oGuild); + } + }); + } +} diff --git a/src/main/java/me/bobert/netneutrality/db/repository/GuildSettingRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/GuildSettingRepository.java new file mode 100644 index 0000000..66a5a02 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/repository/GuildSettingRepository.java @@ -0,0 +1,93 @@ +package me.bobert.netneutrality.db.repository;/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.db.SessionManager; +import me.bobert.netneutrality.db.GSettings; +import me.bobert.netneutrality.db.Repository; +import me.bobert.netneutrality.db.entities.OGuild; +import me.bobert.netneutrality.db.entities.OGuildSetting; +import me.bobert.netneutrality.utils.JpaUtils; +import org.hibernate.Session; +import org.hibernate.query.Query; + +import java.util.ArrayList; +import java.util.List; + +public class GuildSettingRepository implements Repository { + @Override + public List findAll() { + List settings = new ArrayList<>(); + + JpaUtils.runWithSession(session -> { + Query query = session.createQuery("FROM OGuildSetting", OGuildSetting.class); + + settings.addAll(query.list()); + }); + + return settings; + } + + @Override + public OGuildSetting findById(String id) { + try(Session session = SessionManager.getSessionFactory().openSession()) { + Query query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.id = :id", OGuildSetting.class); + query.setParameter("id", id); + + return query.uniqueResult(); + } + } + + @Override + public void deleteBy(String id) { + JpaUtils.runWithTransaction(session -> { + Query query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.id = :id", OGuildSetting.class); + query.setParameter("id", id); + + OGuildSetting setting = query.uniqueResult(); + if (setting != null) { + session.remove(setting); + } + }); + } + + public List findByGuild(String guildId) { + List settings = new ArrayList<>(); + + JpaUtils.runWithSession(session -> { + Query query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.guild.guildId = :guildId", OGuildSetting.class); + query.setParameter("guildId", guildId); + + settings.addAll(query.list()); + }); + + return settings; + } + + public void setSettingValue(OGuild oGuild, GSettings setting, String value) { + JpaUtils.getWithSession(oGuild, oGuild.getGuildId(), session -> { + OGuildSetting oGuildSetting = oGuild.getSettingByName(setting.getName()); + + if (value == null || setting.getDefaultValue().equals(value)) { + if (oGuildSetting != null) { + session.remove(oGuildSetting); + } + } else { + if (oGuildSetting == null) { + oGuildSetting = new OGuildSetting(); + oGuildSetting.setGuild(oGuild); + oGuildSetting.setName(setting.getName()); + oGuildSetting.setValue(value); + + session.persist(oGuildSetting); + } else { + oGuildSetting.setValue(value); + + session.merge(oGuildSetting); + } + } + }); + } +} diff --git a/src/main/java/me/bobert/netneutrality/db/repository/MessageRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/MessageRepository.java new file mode 100644 index 0000000..2294701 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/repository/MessageRepository.java @@ -0,0 +1,106 @@ +package me.bobert.netneutrality.db.repository; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.db.SessionManager; +import me.bobert.netneutrality.db.Repository; +import me.bobert.netneutrality.db.entities.OGuild; +import me.bobert.netneutrality.db.entities.OLogMessage; +import me.bobert.netneutrality.utils.JpaUtils; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import org.hibernate.Session; +import org.hibernate.query.Query; + +import java.util.ArrayList; +import java.util.List; + +public class MessageRepository implements Repository { + @Override + public List findAll() { + List messages = new ArrayList<>(); + + JpaUtils.runWithSession(session -> { + Query query = session.createQuery("FROM OLogMessage", OLogMessage.class); + + messages.addAll(query.list()); + }); + + return messages; + } + + @Override + public OLogMessage findById(String id) { + try(Session session = SessionManager.getSessionFactory().openSession()) { + Query query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class); + query.setParameter("messageId", id); + + return query.uniqueResult(); + } + } + + @Override + public void deleteBy(String id) { + JpaUtils.runWithTransaction(session -> { + Query query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class); + query.setParameter("messageId", id); + + OLogMessage oLogMessage = query.uniqueResult(); + if (oLogMessage != null) { + session.remove(oLogMessage); + } + }); + } + + //TODO: move to a service + public void log(OGuild oGuild, MessageChannel channel, Member author, Message message) { + JpaUtils.runWithTransaction(session -> { + OLogMessage logMessage = new OLogMessage(); + logMessage.setGuild(oGuild); + logMessage.setMessageId(message.getId()); + logMessage.setMessage(message.getContentRaw().trim()); + logMessage.setChannelId(channel.getId()); + logMessage.setUserId(author.getId()); + logMessage.setDateSent(message.getTimeCreated().toInstant()); + + session.persist(logMessage); + }); + } + + public void logEdit(String messageId, String newMessage) { + JpaUtils.runWithTransaction(session -> { + Query query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class); + query.setParameter("messageId", messageId); + + OLogMessage message = query.uniqueResult(); + if (message != null) { + message.setEditedMessage(newMessage); + + session.merge(message); + } + }); + } + + public List getMessagesFromUser(String userId, String guildId, int max) { + List messages = new ArrayList<>(); + + JpaUtils.runWithSession(session -> { + Query query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.guild.guildId = :guildId AND o.userId = :userId", OLogMessage.class); + query.setParameter("guildId", guildId); + query.setParameter("userId", userId); + query.setMaxResults(max); + + messages.addAll(query.list()); + }); + + return messages; + } + + public String getMessageString(String messageId) { + return findById(messageId).getMessage(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/db/repository/TaskRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/TaskRepository.java new file mode 100644 index 0000000..127fd0a --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/repository/TaskRepository.java @@ -0,0 +1,56 @@ +package me.bobert.netneutrality.db.repository; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + + +import me.bobert.netneutrality.core.db.SessionManager; +import me.bobert.netneutrality.db.Repository; +import me.bobert.netneutrality.db.entities.OTask; +import me.bobert.netneutrality.utils.JpaUtils; +import org.hibernate.Session; +import org.hibernate.query.Query; + +import java.util.ArrayList; +import java.util.List; + +public class TaskRepository implements Repository { + + @Override + public List findAll() { + List tasks = new ArrayList<>(); + + JpaUtils.runWithSession(session -> { + Query query = session.createQuery("FROM OTask", OTask.class); + + tasks.addAll(query.list()); + }); + + return tasks; + } + + @Override + public OTask findById(String id) { + try(Session session = SessionManager.getSessionFactory().openSession()) { + Query query = session.createQuery("SELECT o FROM OTask o WHERE o.taskId = :taskId", OTask.class); + query.setParameter("taskId", id); + + return query.uniqueResult(); + } + } + + @Override + public void deleteBy(String id) { + JpaUtils.runWithTransaction(session -> { + Query query = session.createQuery("SELECT o FROM OTask o WHERE o.taskId = :taskId", OTask.class); + query.setParameter("taskId", id); + + OTask oTask = query.uniqueResult(); + if (oTask != null) { + session.remove(oTask); + } + }); + } +} diff --git a/src/main/java/me/bobert/netneutrality/db/service/BlacklistService.java b/src/main/java/me/bobert/netneutrality/db/service/BlacklistService.java new file mode 100644 index 0000000..2a07ac4 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/service/BlacklistService.java @@ -0,0 +1,98 @@ +package me.bobert.netneutrality.db.service; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.db.entities.OBlacklist; +import me.bobert.netneutrality.db.entities.OGuild; +import me.bobert.netneutrality.db.repository.BlacklistRepository; +import me.bobert.netneutrality.utils.JpaUtils; +import net.dv8tion.jda.api.entities.Member; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class BlacklistService { + private static final Logger logger = LoggerFactory.getLogger(BlacklistService.class); + private static final Map> blacklistCache = new ConcurrentHashMap<>(); + private final BlacklistRepository blacklistRepository; + private final GuildService guildService; + + public BlacklistService(GuildService guildService) { + this.blacklistRepository = new BlacklistRepository(); + this.guildService = guildService; + init(); + } + + private void init() { + reloadBlacklistCache(); + } + + public void reloadBlacklistCache() { + OGuild oGuild = guildService.getEntity(); + logger.debug("Reloading blacklist cache for guild: {}", oGuild.getGuildId()); + + JpaUtils.getWithSession(oGuild, oGuild.getGuildId(), session -> { + Set blacklistWords = oGuild.getBlacklists().stream() + .filter(OBlacklist::getEnabled) + .map(OBlacklist::getWord) + .collect(Collectors.toSet()); + + blacklistCache.put(oGuild.getGuildId(), blacklistWords); + }); + } + + public CommandExecutionResult add(Member member, String word) { + try { + blacklistRepository.add(guildService.getEntity(), member, word); + reloadBlacklistCache(); + + return CommandExecutionResult.success("Added the word [%s] to the blacklist.", word); + } catch (Exception e) { + return CommandExecutionResult.warn("Failed to add word to blacklist: " + e.getMessage()); + } + } + + public CommandExecutionResult delete(String word) { + try { + blacklistRepository.delete(guildService.getEntity(), word); + reloadBlacklistCache(); + + return CommandExecutionResult.success("Removed the word [%s] to the blacklist.", word); + } catch (Exception e) { + return CommandExecutionResult.warn("Failed to remove word from blacklist: " + e.getMessage()); + } + } + + public List getBlacklistForGuild() { + try { + return blacklistRepository.findByGuildId(guildService.getEntity().getGuildId()); + } catch (Exception e) { + return List.of(); + } + } + + public void removeGuildBlacklist() { + logger.info("Clearing blacklist cache for guild: {}", guildService.getEntity().getGuildId()); + blacklistCache.remove(guildService.getEntity().getGuildId()); + } + + public static boolean isWordBlacklisted(String guildId, String word) { + Set blacklistWords = blacklistCache.get(guildId); + return blacklistWords != null && blacklistWords.contains(word); + } + + public static Set getBlacklist(String guildId) { + return blacklistCache.getOrDefault(guildId, Collections.emptySet()); + } +} diff --git a/src/main/java/me/bobert/netneutrality/db/service/GuildService.java b/src/main/java/me/bobert/netneutrality/db/service/GuildService.java new file mode 100644 index 0000000..cb60287 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/service/GuildService.java @@ -0,0 +1,85 @@ +package me.bobert.netneutrality.db.service; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.db.entities.OGuild; +import me.bobert.netneutrality.db.repository.GuildRepository; +import me.bobert.netneutrality.utils.JpaUtils; +import net.dv8tion.jda.api.entities.Guild; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class GuildService { + private static final Map services = new ConcurrentHashMap<>(); + private static final Logger logger = LoggerFactory.getLogger(GuildService.class); + private final GuildRepository guildRepository = new GuildRepository(); + private final BlacklistService blacklistService; + private final GuildSettingService guildSettingService; + private OGuild oGuild; + + private GuildService(String guildId) { + this.oGuild = findOrCreateGuild(guildId); + this.blacklistService = new BlacklistService(this); + this.guildSettingService = new GuildSettingService(this); + } + + public static GuildService get(String guildId) { + return services.computeIfAbsent(guildId, GuildService::new); + } + + public static GuildService get(Guild guild) { + return get(guild.getId()); + } + + public static Map getServices() { + return services; + } + + public OGuild getEntity() { + return oGuild; + } + + public BlacklistService getBlacklistService() { + return blacklistService; + } + + public GuildSettingService getGuildSettingService() { + return guildSettingService; + } + + public OGuild findOrCreateGuild(String guildId) { + OGuild guildEntity = guildRepository.findById(guildId); + if (guildEntity == null) { + logger.debug("Guild[{}] doesn't exist in DB, creating entity..", guildId); + + guildEntity = new OGuild(); + guildEntity.setGuildId(guildId); + guildRepository.save(guildEntity); + } + + return guildEntity; + } + + public boolean isDebugEnabled() { + return oGuild.getDebug(); + } + + public void updateDebugMode(boolean debug) { + JpaUtils.updateWithTransaction(oGuild, () -> oGuild.setDebug(debug)); + } + + public void deleteGuild() { + guildRepository.deleteBy(oGuild.getGuildId()); + } + + public void reloadEntity() { + this.oGuild = findOrCreateGuild(oGuild.getGuildId()); + } +} diff --git a/src/main/java/me/bobert/netneutrality/db/service/GuildSettingService.java b/src/main/java/me/bobert/netneutrality/db/service/GuildSettingService.java new file mode 100644 index 0000000..e71683f --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/service/GuildSettingService.java @@ -0,0 +1,60 @@ +package me.bobert.netneutrality.db.service; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.db.GSettings; +import me.bobert.netneutrality.db.entities.OGuildSetting; +import me.bobert.netneutrality.db.repository.GuildSettingRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class GuildSettingService { + private static final Logger logger = LoggerFactory.getLogger(GuildSettingService.class); + private final GuildSettingRepository guildSettingRepository = new GuildSettingRepository(); + private final GuildService guildService; + private final Map settingsCache = new ConcurrentHashMap<>(); + + public GuildSettingService(GuildService guildService) { + this.guildService = guildService; + reloadSettingsCache(); + } + + public Map getSettings() { + return settingsCache; + } + + public String getSetting(GSettings setting) { + String value = settingsCache.get(setting.getName()); + + if (value == null) { + value = setting.getDefaultValue(); + } + + return value; + } + + public void setSettingValue(GSettings setting, String value) { + guildSettingRepository.setSettingValue(guildService.getEntity(), setting, value); + + reloadSettingsCache(); + } + + public void reloadSettingsCache() { + logger.debug("Reloading settings cache for guild: {}", guildService.getEntity().getGuildId()); + settingsCache.clear(); + + + List settings = guildSettingRepository.findByGuild(guildService.getEntity().getGuildId()); + for (OGuildSetting setting : settings) { + settingsCache.put(setting.getName(), setting.getValue()); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/db/service/TaskService.java b/src/main/java/me/bobert/netneutrality/db/service/TaskService.java new file mode 100644 index 0000000..5685b51 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/db/service/TaskService.java @@ -0,0 +1,108 @@ +package me.bobert.netneutrality.db.service; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.PersistableTask; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.core.tasks.persistence.TaskRegistry; +import me.bobert.netneutrality.db.entities.OTask; +import me.bobert.netneutrality.db.repository.TaskRepository; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONObject; + +import java.util.List; +import java.util.Objects; + +public class TaskService { + private static final Logger logger = LogManager.getLogger(TaskService.class); + private final TaskRepository taskRepository; + + public TaskService() { + this.taskRepository = new TaskRepository(); + } + + public void loadTasks() { + TaskRegistry.loadTaskConfig(); + logger.info("Loading persistable tasks..."); + + List tasks = taskRepository.findAll(); + + for (OTask oTask : tasks) { + String taskId = oTask.getTaskId(); + String taskClass = oTask.getTaskClass(); + logger.debug("Trying to load task with ID: {} and class: {}", taskId, taskClass); + + String payload = oTask.getPayload(); + + try { + TaskRegistry.TaskCreator creator = TaskRegistry.getTaskCreator(taskClass); + if (creator == null) { + throw new IllegalArgumentException("No task creator registered for class: " + taskClass); + } + + JSONObject json = new JSONObject(payload); + PersistableTask task = creator.fromPayload(json); + if (task == null) { + continue; + } + + validateTask(task, oTask); + TaskManager.getInstance().insertPersistableTask(task); + } catch (Exception e) { + logger.error("Error loading task with ID {}: {}", taskId, e.getMessage()); + } + } + } + + private void validateTask(PersistableTask task, OTask oTask) throws IllegalArgumentException { + String taskId = oTask.getTaskId(); + String payload = oTask.getPayload(); + + if (!Objects.equals(taskId, task.getId())) { + throw new IllegalArgumentException("Task ID mismatch, expected: " + task.getId()); + } + + JSONObject json = new JSONObject(payload); + String payloadId = json.getString("id"); + if (!Objects.equals(payloadId, task.getId())) { + throw new IllegalArgumentException("Task payload ID mismatch, expected: " + task.getId()); + } + + String payloadLockId = json.getString("lockId"); + if (!Objects.equals(payloadLockId, task.getLockId())) { + throw new IllegalArgumentException("Task lock ID mismatch, expected: " + task.getLockId()); + } + } + + public void persistTask(PersistableTask task) { + String taskClass = task.getClass().getName(); + if (TaskRegistry.getTaskCreator(taskClass) == null) { + logger.error("No TaskCreator found for class: {}", taskClass); + return; + } + + TaskRepository taskRepository = new TaskRepository(); + OTask oTask = taskRepository.findById(task.getId()); + if (oTask == null) { + oTask = new OTask(); + oTask.setTaskId(task.getId()); + oTask.setTaskClass(task.getClass().getName()); + } + + oTask.setPayload(task.toPayload()); + + logger.info("Persisting task: {}", oTask.getTaskId()); + taskRepository.save(oTask); + } + + public void unpersistTask(PersistableTask task) { + logger.debug("Unpersisting task: {}", task.getId()); + TaskRepository taskRepository = new TaskRepository(); + + taskRepository.deleteBy(task.getId()); + } +} diff --git a/src/main/java/me/bobert/netneutrality/exceptions/CommandException.java b/src/main/java/me/bobert/netneutrality/exceptions/CommandException.java deleted file mode 100644 index c006425..0000000 --- a/src/main/java/me/bobert/netneutrality/exceptions/CommandException.java +++ /dev/null @@ -1,24 +0,0 @@ -package me.bobert.netneutrality.exceptions; - -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.context.CommandContext; - -public abstract class CommandException extends RuntimeException { - - public static final String DEFAULT_MESSAGE = "Unknown error during command execution."; - private final CommandContext context; - - public CommandException(String message, CommandContext context, Throwable cause) { - super(message == null ? DEFAULT_MESSAGE : message, cause); - this.context = context; - } - - public CommandContext getCommandContext() { - return context; - } -} diff --git a/src/main/java/me/bobert/netneutrality/exceptions/CommandExecutionException.java b/src/main/java/me/bobert/netneutrality/exceptions/CommandExecutionException.java index 0639059..cf190b1 100644 --- a/src/main/java/me/bobert/netneutrality/exceptions/CommandExecutionException.java +++ b/src/main/java/me/bobert/netneutrality/exceptions/CommandExecutionException.java @@ -1,5 +1,4 @@ package me.bobert.netneutrality.exceptions; - /* * Copyright (c) anthorob * @@ -8,17 +7,9 @@ package me.bobert.netneutrality.exceptions; import me.bobert.netneutrality.core.context.CommandContext; -/** - * Logs the error internally, without sending a message - */ -public class CommandExecutionException extends CommandException { +public class CommandExecutionException extends ContextException { - /** - * Logs the error internally, without sending a message - * @param context The command context - */ public CommandExecutionException(String message, CommandContext context, Throwable cause) { super(message, context, cause); } - } diff --git a/src/main/java/me/bobert/netneutrality/exceptions/ContextException.java b/src/main/java/me/bobert/netneutrality/exceptions/ContextException.java new file mode 100644 index 0000000..d8ac1f5 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/exceptions/ContextException.java @@ -0,0 +1,24 @@ +package me.bobert.netneutrality.exceptions; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.Context; + +public class ContextException extends RuntimeException { + public static final String DEFAULT_MESSAGE = "Unknown error during execution."; + private final Context context; + + public ContextException(String message, Context context, Throwable cause) { + super(message == null ? DEFAULT_MESSAGE : message, cause); + this.context = context; + } + + public Context getContext() { + return context; + } + + +} diff --git a/src/main/java/me/bobert/netneutrality/exceptions/GracefulContextError.java b/src/main/java/me/bobert/netneutrality/exceptions/GracefulContextError.java new file mode 100644 index 0000000..0434f72 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/exceptions/GracefulContextError.java @@ -0,0 +1,19 @@ +package me.bobert.netneutrality.exceptions; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.Context; + +/** + * When thrown, this will call {@link Context#replyEphemeral(String)} with the specified message + */ +public class GracefulContextError extends RuntimeException { + + public GracefulContextError(String message) { + super(message); + } +} diff --git a/src/main/java/me/bobert/netneutrality/exceptions/TaskExecutionException.java b/src/main/java/me/bobert/netneutrality/exceptions/TaskExecutionException.java new file mode 100644 index 0000000..2f08e5e --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/exceptions/TaskExecutionException.java @@ -0,0 +1,15 @@ +package me.bobert.netneutrality.exceptions; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; + +public class TaskExecutionException extends ContextException { + + public TaskExecutionException(String message, TaskContext context, Throwable cause) { + super(message, context, cause); + } +} diff --git a/src/main/java/me/bobert/netneutrality/hooks/EventsHandler.java b/src/main/java/me/bobert/netneutrality/hooks/EventsHandler.java new file mode 100644 index 0000000..3f5b930 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/hooks/EventsHandler.java @@ -0,0 +1,150 @@ +package me.bobert.netneutrality.hooks; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.context.command.MessageCommandContext; +import me.bobert.netneutrality.context.command.MessageMenuContext; +import me.bobert.netneutrality.context.command.SlashCommandContext; +import me.bobert.netneutrality.context.command.UserMenuContext; +import me.bobert.netneutrality.context.tasks.ButtonTaskContext; +import me.bobert.netneutrality.context.tasks.EmojiTaskContext; +import me.bobert.netneutrality.context.tasks.MessageTaskContext; +import me.bobert.netneutrality.context.tasks.ModalTaskContext; +import me.bobert.netneutrality.core.LogHandler; +import me.bobert.netneutrality.core.command.CommandProcessor; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.Context; +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.core.thread.ThreadManager; +import me.bobert.netneutrality.db.service.GuildService; +import me.bobert.netneutrality.utils.Settings; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.events.guild.GuildJoinEvent; +import net.dv8tion.jda.api.events.guild.GuildLeaveEvent; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; +import net.dv8tion.jda.api.hooks.EventListener; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; +import java.util.function.Function; + +public class EventsHandler implements EventListener { + private static final Map, Function> listeners = new ConcurrentHashMap<>(); + private static final Logger logger = LoggerFactory.getLogger(EventsHandler.class); + private final ExecutorService executorService = ThreadManager.getExecutor("events-handler"); + + static { + addListener(MessageReceivedEvent.class, event -> { + if (event.getAuthor().isBot()) return null; + if (event.isWebhookMessage()) return null; + + Message message = event.getMessage(); + String rawMessage = message.getContentDisplay().trim(); + String prefix = Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$"); + if (rawMessage.startsWith(prefix)) { + return new MessageCommandContext(event); + } + + return new MessageTaskContext(event); + }); + + addListener(UserContextInteractionEvent.class, UserMenuContext::new); + addListener(MessageContextInteractionEvent.class, MessageMenuContext::new); + addListener(SlashCommandInteractionEvent.class, SlashCommandContext::new); + addListener(ModalInteractionEvent.class, ModalTaskContext::new); + addListener(ButtonInteractionEvent.class, ButtonTaskContext::new); + addListener(MessageReactionAddEvent.class, EmojiTaskContext::new); + addGenericListener(GuildJoinEvent.class, event -> { + GuildService service = GuildService.get(event.getGuild()); + + if (service.isDebugEnabled()) { + LogHandler.sendMessageToDebugChannel(event.getGuild(), "Error logging enabled on this channel"); + } + }); + addGenericListener(GuildLeaveEvent.class, event -> GuildService.get(event.getGuild()).deleteGuild()); + } + + /** + * Register a listener for an event assocated with a {@link Context}. + * + * @param eventClass the class type of the event to listen for + * @param function the function to handle the event, which takes an event of the specified type and returns a {@link Context} + * @param the type of the event that extends the {@link Event} class + */ + @SuppressWarnings("unchecked") + public static void addListener(Class eventClass, Function function) { + logger.info("Registering listener for {}", eventClass.getSimpleName()); + listeners.put(eventClass, (Function) function); + } + + /** + * Adds a generic listener for a specified event type. The consumer will be invoked + * whenever an event of the specified type is detected. + * + * @param eventClass the class type of the event to listen for + * @param consumer the consumer to handle the event, which takes an event of the specified type and performs an action + * @param the type of the event that extends the {@link Event} class + */ + public static void addGenericListener(Class eventClass, Consumer consumer) { + addListener(eventClass, t -> { + consumer.accept(t); + return null; + }); + } + + @Override + public void onEvent(@NotNull GenericEvent event) { + executorService.submit(() -> handleEvent((Event) event)); + } + + @SuppressWarnings("unchecked") + private void handleEvent(Event event) { + Class eventClass = event.getClass(); + + while (eventClass != null) { + if (listeners.containsKey(eventClass)) { + Function function = (Function) listeners.get(eventClass); + Context context = function.apply(event); + + if (context instanceof CommandContext commandContext) { + handleCommand(commandContext); + } + + if (context instanceof TaskContext taskContext) { + handleTask(taskContext); + } + + break; + } + + eventClass = eventClass.getSuperclass(); + } + } + + private void handleCommand(CommandContext context) { + CommandProcessor processor = new CommandProcessor(context); + processor.dispatch(); + } + + private void handleTask(TaskContext context) { + TaskManager.getInstance().handle(context); + } +} diff --git a/src/main/java/me/bobert/netneutrality/hooks/SlashCommandHooks.java b/src/main/java/me/bobert/netneutrality/hooks/SlashCommandHooks.java deleted file mode 100644 index 1cf33e0..0000000 --- a/src/main/java/me/bobert/netneutrality/hooks/SlashCommandHooks.java +++ /dev/null @@ -1,25 +0,0 @@ -package me.bobert.netneutrality.hooks; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.CommandProcessor; -import me.bobert.netneutrality.core.context.SlashCommandContext; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.jetbrains.annotations.NotNull; - -public class SlashCommandHooks extends ListenerAdapter { - - @Override - public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) { - String commandName = event.getName(); - - SlashCommandContext commandContext = SlashCommandContext.createContext(event); - CommandProcessor processor = new CommandProcessor(commandName, commandContext); - processor.dispatch(); - - } -} diff --git a/src/main/java/me/bobert/netneutrality/hooks/guilds/ServerJoinHandler.java b/src/main/java/me/bobert/netneutrality/hooks/guilds/ServerJoinHandler.java deleted file mode 100644 index ebdb410..0000000 --- a/src/main/java/me/bobert/netneutrality/hooks/guilds/ServerJoinHandler.java +++ /dev/null @@ -1,26 +0,0 @@ -package me.bobert.netneutrality.hooks.guilds; - -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.GuildManager; -import net.dv8tion.jda.api.events.guild.GuildJoinEvent; -import net.dv8tion.jda.api.events.guild.GuildLeaveEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.jetbrains.annotations.NotNull; - -public class ServerJoinHandler extends ListenerAdapter { - - @Override - public void onGuildJoin(@NotNull GuildJoinEvent event) { - GuildManager.getManager(event.getGuild()); - } - - @Override - public void onGuildLeave(@NotNull GuildLeaveEvent event) { - GuildManager.getManager(event.getGuild()).removeFromDB(); - } -} diff --git a/src/main/java/me/bobert/netneutrality/hooks/message/EditHandler.java b/src/main/java/me/bobert/netneutrality/hooks/message/EditHandler.java deleted file mode 100644 index 3fab697..0000000 --- a/src/main/java/me/bobert/netneutrality/hooks/message/EditHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -package me.bobert.netneutrality.hooks.message; - -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.core.SessionManager; -import me.bobert.netneutrality.db.DBController; -import me.bobert.netneutrality.db.controllers.DBMessage; -import me.bobert.netneutrality.db.entities.OGuild; -import me.bobert.netneutrality.utils.JpaUtils; -import me.bobert.netneutrality.utils.MessageFunction; -import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; -import net.dv8tion.jda.api.events.message.MessageUpdateEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.hibernate.Session; - -import java.util.stream.Collectors; - -public class EditHandler extends ListenerAdapter { - - @Override - public void onMessageUpdate(MessageUpdateEvent event) { - - if (event.getAuthor().isBot()) return; - - GuildManager guildManager = GuildManager.getManager(event.getGuild()); - - if (guildManager.getGuildEntity().getLogging()) { - OGuild guildEntity = guildManager.getGuildEntity(); - - if (guildEntity.getLogChannel() != null && !guildEntity.getLogChannel().isEmpty()) { - Session session = SessionManager.getSessionFactory().openSession(); - JpaUtils.tryLoad(session, guildEntity, guildEntity.getGuildId()); - - int size = guildEntity.getLoggings() - .stream() - .filter(oLogging -> oLogging.getId().getChannelId().equals(event.getChannel().getId()) && oLogging.getEnabled()) - .collect(Collectors.toSet()) - .size(); - - if (size > 0) { - TextChannel channel = event.getGuild().getTextChannelById(guildEntity.getLogChannel()); - - if (channel != null) { - String originalMessage = DBController.get(DBMessage.class).getMessageString(event.getMessage().getId()); - DBController.get(DBMessage.class).logEdit(event.getMessage().getId(), event.getMessage().getContentDisplay()); - - channel.sendMessageEmbeds(MessageFunction.buildEditMessage(event.getMember(), event.getMessage(), originalMessage)).queue(); - } - } - } - } - } -} diff --git a/src/main/java/me/bobert/netneutrality/hooks/message/MessageReceivedHandler.java b/src/main/java/me/bobert/netneutrality/hooks/message/MessageReceivedHandler.java deleted file mode 100644 index 7defada..0000000 --- a/src/main/java/me/bobert/netneutrality/hooks/message/MessageReceivedHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -package me.bobert.netneutrality.hooks.message; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.core.CommandProcessor; -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.core.context.MessageCommandContext; -import me.bobert.netneutrality.utils.Settings; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.jetbrains.annotations.NotNull; - -public class MessageReceivedHandler extends ListenerAdapter { - - @Override - public void onMessageReceived(@NotNull MessageReceivedEvent event) { - if (event.getAuthor().isBot()) return; - if (event.isWebhookMessage()) return; - - if (!event.isFromGuild()) - return; - - Message message = event.getMessage(); - - MessageCommandContext context = MessageCommandContext.createContext(event); - - String rawMessage = message.getContentDisplay().trim(); - String prefix = Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$"); - - if (rawMessage.startsWith(prefix)) { - rawMessage = rawMessage.replaceFirst(String.format("^[%s\\s]{1,}", prefix), "").trim(); - // rawMessage = rawMessage.substring(prefix.length()).trim(); - String commandString = rawMessage.split(" ", 2)[0]; - - CommandProcessor processor = new CommandProcessor(commandString, context); - processor.dispatch(); - } - } -} diff --git a/src/main/java/me/bobert/netneutrality/hooks/message/ReactionHandler.java b/src/main/java/me/bobert/netneutrality/hooks/message/ReactionHandler.java deleted file mode 100644 index bcbae8c..0000000 --- a/src/main/java/me/bobert/netneutrality/hooks/message/ReactionHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package me.bobert.netneutrality.hooks.message; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.events.message.MessageUpdateEvent; -import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; -import net.dv8tion.jda.api.events.message.react.MessageReactionRemoveEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.jetbrains.annotations.NotNull; - -public class ReactionHandler extends ListenerAdapter { - - @Override - public void onMessageReactionAdd(@NotNull MessageReactionAddEvent event) { - Member member = event.getMember(); - } - - @Override - public void onMessageReactionRemove(@NotNull MessageReactionRemoveEvent event) { - - } - - @Override - public void onMessageUpdate(@NotNull MessageUpdateEvent event) { - - } -} diff --git a/src/main/java/me/bobert/netneutrality/hooks/voice/VoiceEventHandler.java b/src/main/java/me/bobert/netneutrality/hooks/voice/VoiceEventHandler.java deleted file mode 100644 index 00543ca..0000000 --- a/src/main/java/me/bobert/netneutrality/hooks/voice/VoiceEventHandler.java +++ /dev/null @@ -1,18 +0,0 @@ -package me.bobert.netneutrality.hooks.voice; -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.jetbrains.annotations.NotNull; - -public class VoiceEventHandler extends ListenerAdapter { - - @Override - public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) { - super.onGuildVoiceUpdate(event); - } -} diff --git a/src/main/java/me/bobert/netneutrality/http/WebModule.java b/src/main/java/me/bobert/netneutrality/http/WebModule.java index 03a2750..907d836 100644 --- a/src/main/java/me/bobert/netneutrality/http/WebModule.java +++ b/src/main/java/me/bobert/netneutrality/http/WebModule.java @@ -6,190 +6,352 @@ package me.bobert.netneutrality.http; * https://discord.gg/qUa6zNFZsd */ +import com.google.gson.Gson; import io.sentry.Breadcrumb; import io.sentry.Sentry; -import me.bobert.netneutrality.NetNeutrality; -import me.bobert.netneutrality.core.LogHandler; +import me.bobert.netneutrality.utils.Settings; import okhttp3.*; import org.json.JSONArray; import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; +import java.util.concurrent.TimeUnit; public class WebModule { + private enum Method { + PING, GET, POST + } - private final String url; - private final OkHttpClient client = new OkHttpClient(); - private Response response; - //private final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 OPR/82.0.4227.43"; - private static final String userAgent = String.format("Net Neutrality/%s (Discord bot, tony@bobert.me)", NetNeutrality.VERSION); + private static final String USER_AGENT = "Net Neutrality/%s (Discord bot, discord.gg/trAqhQ5Wsr)"; + private static final Logger log = LoggerFactory.getLogger(WebModule.class); + private static final Map cache = Collections.synchronizedMap( + new LinkedHashMap<>(100, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 50; // Limit cache to 50 entries + } + } + ); + private final Method method; + private final String url; + private final long cacheExpirationMillis; + private final OkHttpClient client; + private final Map headers; + private final Request request; + private CachedResponse cachedResponse; - public WebModule(String url) { - this.url = url; - } + private WebModule(Builder builder) { + this.method = builder.method; + this.url = builder.url; + this.client = builder.client; + this.headers = builder.headers; + this.cacheExpirationMillis = builder.timeUnit.toMillis(builder.timeout); + this.request = builder.request; + } - /** - * Send a request to the specified URL in the constructor. - *
Needs to be executed before any other method call - * - * @return an instance of the WebModule class with the Response of the Web Request - */ - public WebModule request() { - Sentry.addBreadcrumb(Breadcrumb.http(url, "get")); - Request request = new Request.Builder().url(this.url) - .addHeader("User-Agent", userAgent) - .build(); - try { - response = client.newCall(request).execute(); - Sentry.addBreadcrumb(Breadcrumb.http(url, "get", response.code())); - } catch (IOException e) { - Sentry.captureException(e); - } + /** + * Invalidates a cached response for the specified URL. + *

+ * This method removes any cached responses matching the given URL. + * + * @param url the URL of the cached response to invalidate + */ + public static void invalidateCache(String url) { + cache.keySet().removeIf(s -> s.contains(url)); + } - return this; - } + /** + * Sends the HTTP request based on the configuration and retrieves the response. + *

+ * If the request is a GET request and caching is enabled, the response will be cached. + * Cached responses are reused until they expire. + *

+ * POST requests do not support caching. + * + * @return the {@code WebModule} instance, allowing further interaction with the response + * @throws RuntimeException if the request fails + */ + public WebModule request() { + String cacheKey = createCacheKey(method, url, headers); - public WebModule request(Map headers) { - Sentry.addBreadcrumb(Breadcrumb.http(url, "get")); - Request.Builder builder = new Request.Builder().url(this.url) - .addHeader("User-Agent", userAgent); + if (method == Method.GET && cacheKey != null) { + CachedResponse cachedResponse = cache.get(cacheKey); - for (Map.Entry header : headers.entrySet()) { - builder.addHeader(header.getKey(), header.getValue()); - } + if (cachedResponse != null && !cachedResponse.isExpired()) { + log.debug("Retrieving cached response: {}", cacheKey); + this.cachedResponse = cachedResponse; + return this; + } + } - Request request = builder.build(); + long startTime = System.currentTimeMillis(); + try (Response response = client.newCall(request).execute()) { + long elapsedTime = System.currentTimeMillis() - startTime; + Sentry.addBreadcrumb(Breadcrumb.http(url, method.name(), response.code())); + String body = response.body() != null ? response.body().string() : null; - try { - response = client.newCall(request).execute(); - Sentry.addBreadcrumb(Breadcrumb.http(url, "get", response.code())); - } catch (IOException e) { - Sentry.captureException(e); - } + this.cachedResponse = new CachedResponse( + response.code(), + response.headers(), + elapsedTime, + body, + System.currentTimeMillis() + cacheExpirationMillis, + null + ); + } catch (IOException e) { + long elapsedTime = System.currentTimeMillis() - startTime; + log.error("Request failed: {}", e.getMessage(), e); + Sentry.captureException(e); - return this; - } - - public WebModule post(TreeMap params, Map headers) { - Sentry.addBreadcrumb(Breadcrumb.http(url, "post")); - RequestBody body = RequestBody.create(createJson(params), MediaType.parse("application/json; charset=utf-8")); - Request.Builder builder = new Request.Builder().url(this.url) - .addHeader("User-Agent", userAgent); + this.cachedResponse = new CachedResponse( + 0, // Status code 0 indicates a failure + new Headers.Builder().build(), + elapsedTime, + null, + System.currentTimeMillis() + cacheExpirationMillis, + e.getMessage() + ); + } - for (Map.Entry header : headers.entrySet()) { - builder.addHeader(header.getKey(), header.getValue()); - } - Request request = builder.post(body).build(); - try { - response = client.newCall(request).execute(); - Sentry.addBreadcrumb(Breadcrumb.http(url, "post", response.code())); - } catch (IOException e) { - Sentry.captureException(e); - } + if (method == Method.GET && cacheKey != null) { + cache.put(cacheKey, this.cachedResponse); + } - return this; - } + return this; + } - public static String createJson(TreeMap params) { - StringBuilder json = new StringBuilder(); + private String createCacheKey(Method method, String url, Map headers) { + if (method == Method.POST) { + return null; // no cache for post requests + } - for (Map.Entry param : params.entrySet()) { - String str = "\"" + param.getKey() + "\":\"" + param.getValue() + "\""; - json.append(str); - json.append(","); - } - json.insert(0, "{"); + StringBuilder keyBuilder = new StringBuilder(method.name()).append(":").append(url); + if (headers != null) { + headers.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> keyBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append(";")); + } + return keyBuilder.toString(); + } - return (json.length() > 1 ? json.substring(0, json.length() - 1) + "}" : ""); - } + public JSONObject getAsObject() { + return cachedResponse.getBody() + .map(JSONObject::new) + .orElseThrow(() -> new IllegalStateException("No body available for this response")); + } - public static String createJsonObject(List key, List value) { - if (key.size() != value.size()) - throw new IllegalArgumentException("Key list size not equal to value list size"); + public JSONArray getAsArray() { + return cachedResponse.getBody() + .map(JSONArray::new) + .orElseThrow(() -> new IllegalStateException("No body available for this response")); + } - StringBuilder json = new StringBuilder(); + public String getRawData() { + return cachedResponse.getBody().orElse(null); + } - for (int index = 0; index < key.size();) { - json.append(String.format("\"%s\":\"%s\"", key.get(index), value.get(index))); - json.append(","); - } - json.insert(0, "{"); + public boolean isSuccess() { + return cachedResponse != null && cachedResponse.getStatusCode() >= 200 && cachedResponse.getStatusCode() < 400; + } - return (json.length() > 1 ? json.substring(0, json.length() - 1) + "}" : ""); - } + public static class Builder { + private Request request; + private final String url; + private Method method = Method.GET; + private long timeout = 5; + private TimeUnit timeUnit = TimeUnit.MINUTES; + private OkHttpClient client; + private long connectTimeout; + private long readTimeout; + private long writeTimeout; + private final Map headers = new HashMap<>(); + private final Map params = new HashMap<>(); - public static String createJsonArray(List list) { - StringBuilder json = new StringBuilder(); + /** + * Creates a new {@code Builder} instance for the specified URL. + * + * @param url the URL for the HTTP request + */ + public Builder(String url) { + this.url = url; + } - for (String str : list) { - json.append("\"").append(str).append("\""); - json.append(","); - } - json.insert(0, "["); + public Builder ping() { + this.method = Method.PING; + return this; + } - return (json.length() > 1 ? json.substring(0, json.length() - 1) + "]" : ""); - } + /** + * Configures the request to use the HTTP GET method. + * + * @return the current {@code Builder} instance for chaining + */ + public Builder get() { + this.method = Method.GET; + return this; + } - public static String createJsonArray(TreeMap users) { - StringBuilder json = new StringBuilder(); + /** + * Configures the request to use the HTTP POST method and sets the request parameters. + * + * @param params a map of key-value pairs to include in the POST request body + * @return the current {@code Builder} instance for chaining + */ + public Builder post(Map params) { + this.params.putAll(params); + this.method = Method.POST; + return this; + } - for (Map.Entry param : users.entrySet()) { - String str = "{\"" + param.getKey() + "\":\"" + param.getValue() + "\"}"; - json.append(str); - json.append(","); - } - json.insert(0, "["); + /** + * Adds a custom header to the request. + * + * @param key the name of the header + * @param value the value of the header + * @return the current {@code Builder} instance for chaining + */ + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } - return (json.length() > 1 ? json.substring(0, json.length() - 1) + "]" : "[]"); - } + /** + * Configures caching for the request. + * + * @param timeout the cache expiration duration + * @param timeUnit the unit of time for the cache expiration + * @return the current {@code Builder} instance for chaining + */ + public Builder cache(long timeout, TimeUnit timeUnit) { + this.timeout = timeout; + this.timeUnit = timeUnit; + return this; + } - - /** - * @return the response as a JSONObject - */ - public JSONObject getAsObject() { - if (response == null) - throw new RuntimeException("Request has not been sent"); + public Builder timeout(long timeout, TimeUnit timeUnit) { + this.connectTimeout = timeUnit.toMillis(timeout); + this.readTimeout = timeUnit.toMillis(timeout); + this.writeTimeout = timeUnit.toMillis(timeout); + return this; + } - try { - ResponseBody body = response.body(); - return new JSONObject(body.string()); - } catch (IOException | NullPointerException e) { - LogHandler.log(e); - return new JSONObject(); - } - } - - /** - * Use if your response start with an array : [ ] - * - * @return the response as a JSONArray - */ - public JSONArray getAsArray() { - if (response == null) - throw new RuntimeException("Request has not been sent"); + /** + * Sets the connection timeout for the HTTP client. + * + * @param timeout the timeout duration + * @param timeUnit the unit of time for the timeout + * @return the current {@code Builder} instance for chaining + */ + public Builder connectTimeout(long timeout, TimeUnit timeUnit) { + this.connectTimeout = timeUnit.toMillis(timeout); + return this; + } - try { - ResponseBody body = response.body(); - return new JSONArray(body.string()); - } catch (IOException | NullPointerException e) { - LogHandler.log(e); - return new JSONArray(); - } - } - - /** - * @return the response as a String - */ - public String getRawData() throws IOException { - if (response == null) - throw new RuntimeException("Request has not been sent"); + /** + * Sets the read timeout for the HTTP client. + * + * @param timeout the timeout duration + * @param timeUnit the unit of time for the timeout + * @return the current {@code Builder} instance for chaining + */ + public Builder readTimeout(long timeout, TimeUnit timeUnit) { + this.readTimeout = timeUnit.toMillis(timeout); + return this; + } - return response.body().string(); - } + /** + * Sets the write timeout for the HTTP client. + * + * @param timeout the timeout duration + * @param timeUnit the unit of time for the timeout + * @return the current {@code Builder} instance for chaining + */ + public Builder writeTimeout(long timeout, TimeUnit timeUnit) { + this.writeTimeout = timeUnit.toMillis(timeout); + return this; + } + private Request createRequest() { + Request.Builder builder = new Request.Builder().url(url); + builder.addHeader("User-Agent", String.format(USER_AGENT, Settings.getVersion())); + headers.forEach(builder::addHeader); + + if (method == Method.PING) { + builder.head(); + } + + if (method == Method.GET) { + builder.get(); + } + + if (method == Method.POST && !params.isEmpty()) { + RequestBody body = RequestBody.create(new Gson().toJson(params), MediaType.parse("application/json; charset=utf-8")); + builder.post(body); + } + + return builder.build(); + } + + /** + * Builds and returns a configured {@link WebModule} instance. + * + * @return a new {@code WebModule} instance + */ + public WebModule build() { + this.client = new OkHttpClient.Builder() + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS) + .readTimeout(readTimeout, TimeUnit.MILLISECONDS) + .build(); + this.request = createRequest(); + + return new WebModule(this); + } + } + + private static class CachedResponse { + private final int statusCode; + private final Headers headers; + private final long elapsedTime; + private final String body; + private final long expirationTime; + private final String errorMessage; + + public CachedResponse(int statusCode, Headers headers, long elapsedTime, String body, long expirationTime, String errorMessage) { + this.statusCode = statusCode; + this.headers = headers; + this.elapsedTime = elapsedTime; + this.body = body; + this.expirationTime = expirationTime; + this.errorMessage = errorMessage; + } + + public boolean isExpired() { + return System.currentTimeMillis() > expirationTime; + } + + public int getStatusCode() { + return statusCode; + } + + public Headers getHeaders() { + return headers; + } + + public long getElapsedTime() { + return elapsedTime; + } + + public Optional getBody() { + return Optional.ofNullable(body); + } + + public Optional getErrorMessage() { + return Optional.ofNullable(errorMessage); + } + } } diff --git a/src/main/java/me/bobert/netneutrality/http/server/ActionHandler.java b/src/main/java/me/bobert/netneutrality/http/server/ActionHandler.java index a2b8f65..7637ed7 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/ActionHandler.java +++ b/src/main/java/me/bobert/netneutrality/http/server/ActionHandler.java @@ -6,10 +6,10 @@ package me.bobert.netneutrality.http.server; * https://discord.gg/qUa6zNFZsd */ +import com.google.gson.Gson; import com.sun.net.httpserver.HttpExchange; import me.bobert.netneutrality.NetNeutrality; -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.http.WebModule; +import me.bobert.netneutrality.db.service.GuildService; import net.dv8tion.jda.api.entities.Guild; import org.apache.logging.log4j.LogManager; import org.json.JSONObject; @@ -26,7 +26,7 @@ public class ActionHandler extends AbstractHandler { if (path.equals("/actions/shutdown")) { LogManager.getLogger(NetNeutrality.class).info("Request to shutdown received"); - NetNeutrality.shutdown(); + NetNeutrality.getInstance().shutdown(); sendStringResponse(exchange, "Shutting down.."); System.exit(0); @@ -35,10 +35,10 @@ public class ActionHandler extends AbstractHandler { if (path.equals("/actions/status")) { TreeMap json = new TreeMap<>(); - json.put("health", String.valueOf(NetNeutrality.getJDA().getStatus())); - json.put("ping", String.valueOf(NetNeutrality.getJDA().getGatewayPing())); + json.put("health", String.valueOf(NetNeutrality.getInstance().getJDA().getStatus())); + json.put("ping", String.valueOf(NetNeutrality.getInstance().getJDA().getGatewayPing())); - sendJsonResponse(exchange, WebModule.createJson(json)); + sendJsonResponse(exchange, new Gson().toJson(json)); return; } @@ -47,10 +47,10 @@ public class ActionHandler extends AbstractHandler { String[] args = exchange.getRequestURI().toString().split("/"); String guildId = args[args.length - 1]; - Guild guild = NetNeutrality.getJDA().getGuildById(guildId); + Guild guild = NetNeutrality.getInstance().getJDA().getGuildById(guildId); if (guild != null) { - GuildManager manager = GuildManager.getManager(guild); - Map settings = manager.getGuildConfigManager().getSettings(); + GuildService service = GuildService.get(guild); + Map settings = service.getGuildSettingService().getSettings(); JSONObject setting = new JSONObject(settings); json.put("status", "ok"); @@ -68,16 +68,11 @@ public class ActionHandler extends AbstractHandler { String[] args = exchange.getRequestURI().toString().split("/"); String guildId = args[args.length - 1]; - Guild guild = NetNeutrality.getJDA().getGuildById(guildId); boolean ok = true; //TODO: add bool - if (guild == null) - GuildManager.getManager(guildId).getGuildConfigManager().reloadSettingsCache(); - else - GuildManager.getManager(guild).getGuildConfigManager().reloadSettingsCache(); - + GuildService.get(guildId).getGuildSettingService().reloadSettingsCache(); json.put("status", String.valueOf(ok)); - sendJsonResponse(exchange, WebModule.createJson(json)); + sendJsonResponse(exchange, new Gson().toJson(json)); return; } diff --git a/src/main/java/me/bobert/netneutrality/http/server/GuildHandler.java b/src/main/java/me/bobert/netneutrality/http/server/GuildHandler.java index 26016b8..c082b06 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/GuildHandler.java +++ b/src/main/java/me/bobert/netneutrality/http/server/GuildHandler.java @@ -25,7 +25,7 @@ public class GuildHandler extends AbstractHandler { JSONArray array = new JSONArray(); - JDA jda = NetNeutrality.getJDA(); + JDA jda = NetNeutrality.getInstance().getJDA(); jda.getGuilds().forEach(guild -> { JSONObject obj = new JSONObject(); @@ -45,7 +45,7 @@ public class GuildHandler extends AbstractHandler { String guild = args[args.length - 1]; JSONArray array = new JSONArray(); - JDA jda = NetNeutrality.getJDA(); + JDA jda = NetNeutrality.getInstance().getJDA(); if (jda.getGuildById(guild) == null) { sendStringResponse(exchange, path); @@ -69,7 +69,7 @@ public class GuildHandler extends AbstractHandler { String[] args = exchange.getRequestURI().toString().split("/"); String guild = args[args.length - 1]; JSONArray array = new JSONArray(); - JDA jda = NetNeutrality.getJDA(); + JDA jda = NetNeutrality.getInstance().getJDA(); if (jda.getGuildById(guild) == null) { sendStringResponse(exchange, path); diff --git a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java index 216c7c1..c7a595d 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java +++ b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java @@ -7,7 +7,7 @@ package me.bobert.netneutrality.http.server; */ import com.sun.net.httpserver.HttpServer; -import me.bobert.netneutrality.thread.BaseThreadFactory; +import me.bobert.netneutrality.core.thread.BaseThreadFactory; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,7 +18,7 @@ import java.util.concurrent.Executors; public class WebApi { - private final static Logger logger = LogManager.getLogger(WebApi.class); + private static final Logger logger = LogManager.getLogger(WebApi.class); private HttpServer server; diff --git a/src/main/java/me/bobert/netneutrality/music/MusicManager.java b/src/main/java/me/bobert/netneutrality/music/MusicManager.java index 4b69cbc..3a44a69 100644 --- a/src/main/java/me/bobert/netneutrality/music/MusicManager.java +++ b/src/main/java/me/bobert/netneutrality/music/MusicManager.java @@ -13,46 +13,41 @@ import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager; import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import dev.lavalink.youtube.YoutubeAudioSourceManager; -import me.bobert.netneutrality.command.CommandExecutionResult; -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.core.LogHandler; +import me.bobert.netneutrality.core.command.CommandExecutionResult; import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.thread.ThreadManager; import me.bobert.netneutrality.db.GSettings; -import me.bobert.netneutrality.exceptions.CommandExecutionException; +import me.bobert.netneutrality.db.service.GuildService; import me.bobert.netneutrality.music.source.UdioAudioSourceManager; +import me.bobert.netneutrality.thread.MusicManagerCleanupThread; import me.bobert.netneutrality.thread.MusicPlayerAutoDisconnectThread; -import me.bobert.netneutrality.thread.ThreadManager; +import me.bobert.netneutrality.utils.MusicUtils; +import me.bobert.netneutrality.utils.Settings; import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; public class MusicManager { - private final static Logger logger = LogManager.getLogger(MusicManager.class); - private final static Map managers = new ConcurrentHashMap<>(); - private final static ScheduledExecutorService executor = ThreadManager.getMusicExecutor(); - - private final static AudioPlayerManager playerManager = new DefaultAudioPlayerManager(); + private static final Logger logger = LogManager.getLogger(MusicManager.class); + private static final Map managers = new ConcurrentHashMap<>(); + private static final AudioPlayerManager playerManager = new DefaultAudioPlayerManager(); private final Guild guild; private final AudioPlayer player; private final TrackScheduler scheduler; - private ScheduledFuture autoDisconnectThread; private final int baseVolume; private MusicManager(Guild guild) { this.guild = guild; this.player = playerManager.createPlayer(); this.scheduler = new TrackScheduler(this); - this.baseVolume = Integer.parseInt(GuildManager.getManager(guild).getGuildConfigManager().getSetting(GSettings.MUSIC_VOLUME)); + this.baseVolume = Integer.parseInt(GuildService.get(guild).getGuildSettingService().getSetting(GSettings.MUSIC_VOLUME)); guild.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(player)); player.addListener(scheduler); @@ -73,28 +68,70 @@ public class MusicManager { playerManager.registerSourceManager(SoundCloudAudioSourceManager.createDefault()); playerManager.registerSourceManager(new UdioAudioSourceManager()); playerManager.registerSourceManager(new HttpAudioSourceManager()); + + String po = Settings.getInstance().get("NETNEUTRALITY_PO_TOKEN"); + String visitor = Settings.getInstance().get("NETNEUTRALITY_VISITOR_DATA"); + dev.lavalink.youtube.clients.Web.setPoTokenAndVisitorData(po, visitor); + + ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new MusicPlayerAutoDisconnectThread(), 15, 15, TimeUnit.MINUTES); + ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new MusicManagerCleanupThread(), 1, 1, TimeUnit.HOURS); } - public boolean isNotConnectedTo(VoiceChannel vChannel) { - return vChannel == null || !vChannel.equals(guild.getAudioManager().getConnectedChannel()); + public static void disconnectPlayers() { + for (MusicManager mm : managers.values()) { + mm.getQueueManager().clearQueue(); + mm.player.destroy(); + } } - public synchronized void connect(VoiceChannel vChannel) { - if (vChannel != null && isNotConnectedTo(vChannel)) { - logger.debug("Guild {}: connecting to voice channel {}", vChannel.getGuild().getId(), vChannel.getId()); - guild.getAudioManager().openAudioConnection(vChannel); + public static Collection getManagers() { + return managers.values(); + } + + public TrackScheduler getScheduler() { + return scheduler; + } + + public QueueManager getQueueManager() { + return scheduler.getQueueManager(); + } + + public AudioPlayer getPlayer() { + return player; + } + + public Guild getGuild() { + return guild; + } + + public synchronized boolean isNotConnectedTo(AudioChannel vChannel) { + AudioChannel current = guild.getAudioManager().getConnectedChannel(); + return vChannel == null || current == null || !vChannel.getId().equals(current.getId()); + } + + public synchronized void connect(AudioChannel audioChannel) { + if (audioChannel != null) { + logger.debug("Guild {}: connecting to voice channel {}", audioChannel.getGuild().getId(), audioChannel.getId()); + guild.getAudioManager().openAudioConnection(audioChannel); guild.getAudioManager().setSelfDeafened(true); player.setVolume(baseVolume); - - if (autoDisconnectThread == null) - autoDisconnectThread = executor.scheduleWithFixedDelay(new MusicPlayerAutoDisconnectThread(this), 240, 240, TimeUnit.SECONDS); } else { throw new RuntimeException("Error connecting to voice channel"); } } - public boolean isConnected() { - return guild != null && guild.getAudioManager().getConnectedChannel() != null; + public synchronized void disconnect() { + if (guild.getAudioManager().getConnectedChannel() != null) { + logger.debug("Guild {}: disconnecting from voice channel {}", guild.getId(), guild.getAudioManager().getConnectedChannel().getId()); + player.stopTrack(); + guild.getAudioManager().closeAudioConnection(); + } + + scheduler.detachContext(); + } + + public synchronized boolean isConnected() { + return guild.getAudioManager().getConnectedChannel() != null; } public synchronized AudioTrack getCurrentlyPlaying() { @@ -105,81 +142,22 @@ public class MusicManager { return player.getPlayingTrack() != null; } - public synchronized Member getRequesterOfCurrentTrack() { - QueueManager.TrackInfo info = scheduler.getQueueManager().peekHeadOfQueue(); - - if (info == null) - return null; - - return info.getRequester(); + public synchronized void play(String query, CommandContext context) { + play(query, context, false); } - protected AudioPlayer getPlayer() { - return player; - } - - public Guild getGuild() { - return guild; - } - - public synchronized void leave() { - if (guild != null && guild.getAudioManager().getConnectedChannel() != null) { - logger.debug("Guild {}: leaving voice channel {}", guild.getId(), guild.getAudioManager().getConnectedChannel().getId()); - player.stopTrack(); - guild.getAudioManager().closeAudioConnection(); - } - - scheduler.detachTextChannel(); - - if (autoDisconnectThread != null) { - autoDisconnectThread.cancel(true); - autoDisconnectThread = null; - } - } - - public static void disconnectPlayers() { - for (MusicManager mm : managers.values()) { - mm.getScheduler().getQueueManager().clearQueue(); - mm.player.destroy(); - } - } - - public synchronized void play(String query, CommandContext context, boolean needToBeConnected, boolean silent) { + public synchronized void play(String query, CommandContext context, boolean silent) { logger.debug("Guild {}: request to play '{}' by user {}", guild.getId(), query, context.getGuildUser().getId()); - Member member = context.getGuildUser(); - MessageChannel channel = context.getChannel(); + if (MusicUtils.canConnect(context)) { + SourceFinder.AudioSourceType type = SourceFinder.getSource(query); + String identifier = SourceFinder.getIdentifier(query); + if (identifier == null) { + context.reply("No source found to play the given url"); + return; + } - if (member.getVoiceState() == null) { - logger.error("Guild {}: Voice state is null", guild.getId()); - context.replyToMessage("Voice state is null, something has gone terribly wrong."); - LogHandler.log(new CommandExecutionException("Voice state is null", context, null)); - - return; - } - - if (!member.getVoiceState().inAudioChannel()) { - context.replyToMessage("You are not in a voice channel"); - return; - } - - VoiceChannel vChannel = (VoiceChannel) member.getVoiceState().getChannel(); - if (needToBeConnected && isNotConnectedTo(vChannel)) { - context.replyToMessage("The player is not in your voice channel"); - return; - } - - if (isNotConnectedTo(vChannel)) { - connect(vChannel); - scheduler.attachTextChannel(channel); - } - - SourceFinder.AudioSourceType type = SourceFinder.getSource(query); - String identifier = SourceFinder.getIdentifier(query); - - if (identifier != null) { TrackLoadingHandler loadingHandler = new TrackLoadingHandler(context, this, identifier, type, silent); - playerManager.loadItemOrdered(this, identifier, loadingHandler); } } @@ -188,30 +166,32 @@ public class MusicManager { player.stopTrack(); } - public synchronized CommandExecutionResult voteSkip(CommandContext context) { - VoiceChannel voiceChannel = (VoiceChannel) guild.getAudioManager().getConnectedChannel(); + public synchronized CommandExecutionResult resume() { + return getQueueManager().resume(); + } - if (voiceChannel == null) { - return CommandExecutionResult.none(); + public synchronized CommandExecutionResult voteSkip(CommandContext context) { + AudioChannel audioChannel = guild.getAudioManager().getConnectedChannel(); + if (audioChannel == null) { + return CommandExecutionResult.warn("Bot is not connected to a voice channel"); } if (!isCurrentlyPlaying()) { - return CommandExecutionResult.none(); + return CommandExecutionResult.warn("No tracks are currently playing"); } - QueueManager.TrackInfo info = scheduler.getQueueManager().peekHeadOfQueue(); + TrackInfo info = getQueueManager().peekHeadOfQueue(); if (info == null) { - return CommandExecutionResult.none(); + return CommandExecutionResult.warn("Queue is empty"); } // Minus the bot itself - int connected = voiceChannel.getMembers().size() - 1; + int connected = audioChannel.getMembers().size() - 1; if (connected < 3) { - context.reply("Skipping current track since not enough people are connected to the voice channel."); - scheduler.getQueueManager().skip(); + getQueueManager().skip(); - return CommandExecutionResult.none(); + return CommandExecutionResult.info("Skipping current track since not enough people are connected to the voice channel."); } double needed = connected / 2.1; @@ -220,12 +200,13 @@ public class MusicManager { logger.debug("VoteSkip: guild={}, connected={}, needed={}, skips={}, user={}", context.getGuild().getId(), connected, needed, info.getSkips().size(), context.getGuildUser().getId()); if (info.getSkips().size() > needed) { - return scheduler.getQueueManager().skip(); - } else { - context.reply("Adding your vote to skip the current track. Current votes: %d/%d", info.getSkips().size(), (int) Math.ceil(needed)); + return getQueueManager().skip(); } - return CommandExecutionResult.none(); + return CommandExecutionResult.success(String.format("Adding your vote to skip the current track. Current votes: %d/%d", + info.getSkips().size(), + (int) Math.ceil(needed)) + ); } public synchronized void setVolume(int volume) { @@ -233,10 +214,6 @@ public class MusicManager { } public int getQueueSize() { - return scheduler.getQueueManager().getQueueSize(); - } - - public TrackScheduler getScheduler() { - return scheduler; + return getQueueManager().getQueueSize(); } } diff --git a/src/main/java/me/bobert/netneutrality/music/QueueManager.java b/src/main/java/me/bobert/netneutrality/music/QueueManager.java index 9676928..acdd29c 100644 --- a/src/main/java/me/bobert/netneutrality/music/QueueManager.java +++ b/src/main/java/me/bobert/netneutrality/music/QueueManager.java @@ -7,7 +7,7 @@ package me.bobert.netneutrality.music; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import me.bobert.netneutrality.command.CommandExecutionResult; +import me.bobert.netneutrality.core.command.CommandExecutionResult; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import org.apache.logging.log4j.LogManager; @@ -18,68 +18,7 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.function.Predicate; public class QueueManager { - - public static class TrackInfo { - - private final AudioTrack track; - private final Member member; - private final Set skips; - private final Logger logger = LogManager.getLogger(QueueManager.class); - - public TrackInfo(AudioTrack track, Member member) { - this.track = track; - this.member = member; - this.skips = new HashSet<>(); - } - - public AudioTrack getTrack() { - return track; - } - - public Set getSkips() { - return skips; - } - - public void addSkip(Member member) { - if (!hasSkip(member)) { - logger.debug("VoteSkip: user={}, skip doesn't exist, before={}, after={}", member.getId(), skips.size(), skips.size() + 1); - skips.add(member.getId()); - } - } - - public boolean hasSkip(Member member) { - return skips.contains(member.getId()); - } - - public Member getRequester() { - return member; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TrackInfo trackInfo = (TrackInfo) o; - return track.equals(trackInfo.track); - } - - @Override - public int hashCode() { - return Objects.hash(track); - } - - @Override - public String toString() { - return "TrackInfo{" + - "track=" + track.getInfo().title + - ", member=" + member.getEffectiveName() + - ", skips=" + skips + - '}'; - } - } - private final Logger logger = LogManager.getLogger(QueueManager.class); - private final TrackScheduler scheduler; private final AudioPlayer player; private final Guild guild; @@ -99,10 +38,6 @@ public class QueueManager { } } - public TrackScheduler getScheduler() { - return scheduler; - } - public Guild getGuild() { return guild; } @@ -138,7 +73,7 @@ public class QueueManager { } public CommandExecutionResult clearQueue() { - scheduler.getMusicManager().leave(); + scheduler.getMusicManager().disconnect(); if (queue.isEmpty()) { return CommandExecutionResult.warn("Queue is empty!"); @@ -155,27 +90,33 @@ public class QueueManager { } } - public void removeTracksFromUser(Member member) { + public CommandExecutionResult removeTracksFromUser(Member member) { + if (queue.stream().filter(trackInfo -> member.equals(trackInfo.getRequester())).toList().isEmpty()) { + return CommandExecutionResult.info(String.format("No tracks added by user %s", member.getEffectiveName())); + } + synchronized (queue) { int size = queue.stream().filter(trackInfo -> member.equals(trackInfo.getRequester())).toList().size(); - - if (size > 0) { - if (size == queue.size()) { - clearQueue(); - return; - } - - TrackInfo first = queue.peek(); - queue.removeIf(trackInfo -> member.equals(trackInfo.getRequester())); - - if (first != null && member.equals(first.getRequester())) { - skip(1); - } + if (size == 0) { + return CommandExecutionResult.info(String.format("No tracks added by user %s", member.getEffectiveName())); } + + if (size == queue.size()) { + return clearQueue(); + } + + TrackInfo first = queue.peek(); + queue.removeIf(trackInfo -> member.equals(trackInfo.getRequester())); + + if (first != null && member.equals(first.getRequester())) { + skip(1); + } + + return CommandExecutionResult.success(String.format("Removed all tracks from user %s", member.getEffectiveName())); } } - public ArrayList getAllQueuedTrack() { + public List getAllQueuedTracks() { synchronized (queue) { ArrayList tracks = new ArrayList<>(); @@ -184,8 +125,15 @@ public class QueueManager { } } + public void queue(List tracks, Member member) { + tracks.forEach(track -> queue(track, member)); + } + public void queue(AudioTrack track, Member member) { TrackInfo info = new TrackInfo(track, member); + if (queue.contains(info)) { + return; + } synchronized (queue) { if (queue.contains(info)) { @@ -207,6 +155,20 @@ public class QueueManager { } } + public CommandExecutionResult resume() { + synchronized (queue) { + TrackInfo next = queue.peek(); + + if (next != null) { + player.startTrack(next.getTrack().makeClone(), false); + return CommandExecutionResult.success("Starting playback of track " + next.getTrack().getInfo().title); + } + + player.stopTrack(); + return CommandExecutionResult.info("Queue is empty, leaving voice channel"); + } + } + public void startNextTrack() { synchronized (queue) { TrackInfo last = queue.poll(); @@ -225,13 +187,18 @@ public class QueueManager { } public CommandExecutionResult skip(int nbTrack) { - if (nbTrack == 0) { + if (nbTrack < 1) { nbTrack = 1; } + if (queue.isEmpty()) { + scheduler.getMusicManager().disconnect(); + return CommandExecutionResult.info("Queue is empty, leaving voice channel"); + } + synchronized (queue) { if (queue.isEmpty()) { - scheduler.getMusicManager().leave(); + scheduler.getMusicManager().disconnect(); return CommandExecutionResult.info("Queue is empty, leaving voice channel"); } @@ -245,7 +212,7 @@ public class QueueManager { } if (queue.isEmpty()) { - scheduler.getMusicManager().leave(); + scheduler.getMusicManager().disconnect(); return CommandExecutionResult.info("Queue is empty, leaving voice channel"); } @@ -256,27 +223,27 @@ public class QueueManager { } public CommandExecutionResult shuffle() { - if (queue.size() > 3) { - return CommandExecutionResult.none(); + if (queue.size() <= 3) { + return CommandExecutionResult.warn("The queue is too small to be shuffled"); } synchronized (queue) { - if (queue.size() > 3) { - logger.debug("Guild {}: shuffling queue", guild.getId()); - TrackInfo current = queue.poll(); - - ArrayList newQueue = new ArrayList<>(queue); - - Collections.shuffle(newQueue); - - queue.clear(); - queue.add(current); - queue.addAll(newQueue); - - return CommandExecutionResult.success("Queue is now shuffled."); + if (queue.size() <= 3) { + return CommandExecutionResult.warn("The queue is too small to be shuffled"); } - return CommandExecutionResult.none(); + logger.debug("Guild {}: shuffling queue", guild.getId()); + TrackInfo current = queue.poll(); + + ArrayList newQueue = new ArrayList<>(queue); + + Collections.shuffle(newQueue); + + queue.clear(); + queue.add(current); + queue.addAll(newQueue); + + return CommandExecutionResult.success("The queue is now shuffled."); } } diff --git a/src/main/java/me/bobert/netneutrality/music/TrackInfo.java b/src/main/java/me/bobert/netneutrality/music/TrackInfo.java new file mode 100644 index 0000000..b711421 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/music/TrackInfo.java @@ -0,0 +1,72 @@ +package me.bobert.netneutrality.music;/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import net.dv8tion.jda.api.entities.Member; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class TrackInfo { + private final AudioTrack track; + private final Member member; + private final Set skips; + private static final Logger logger = LogManager.getLogger(QueueManager.class); + + public TrackInfo(AudioTrack track, Member member) { + this.track = track; + this.member = member; + this.skips = new HashSet<>(); + } + + public AudioTrack getTrack() { + return track; + } + + public Set getSkips() { + return skips; + } + + public void addSkip(Member member) { + if (!hasSkip(member)) { + logger.debug("VoteSkip: user={}, skip doesn't exist, before={}, after={}", member.getId(), skips.size(), skips.size() + 1); + skips.add(member.getId()); + } + } + + public boolean hasSkip(Member member) { + return skips.contains(member.getId()); + } + + public Member getRequester() { + return member; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TrackInfo trackInfo = (TrackInfo) o; + return track.equals(trackInfo.track); + } + + @Override + public int hashCode() { + return Objects.hash(track); + } + + @Override + public String toString() { + return "TrackInfo{" + + "track=" + track.getInfo().title + + ", member=" + member.getEffectiveName() + + ", skips=" + skips + + '}'; + } +} diff --git a/src/main/java/me/bobert/netneutrality/music/TrackLoadingHandler.java b/src/main/java/me/bobert/netneutrality/music/TrackLoadingHandler.java index 007a040..2fe3ade 100644 --- a/src/main/java/me/bobert/netneutrality/music/TrackLoadingHandler.java +++ b/src/main/java/me/bobert/netneutrality/music/TrackLoadingHandler.java @@ -12,19 +12,22 @@ import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import me.bobert.netneutrality.core.LogHandler; import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.tasks.music.LoadPlaylistTask; import me.bobert.netneutrality.utils.MessageFunction; +import me.bobert.netneutrality.utils.MusicUtils; import net.dv8tion.jda.api.entities.Guild; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class TrackLoadingHandler implements AudioLoadResultHandler { - private final Logger logger = LogManager.getLogger(TrackLoadingHandler.class); + private static final Logger logger = LogManager.getLogger(TrackLoadingHandler.class); private final String identifier; private final SourceFinder.AudioSourceType type; private final boolean silent; private final Guild guild; - private final TrackScheduler scheduler; + private final QueueManager queueManager; private final CommandContext context; public TrackLoadingHandler(CommandContext context, MusicManager manager, String identifier, SourceFinder.AudioSourceType type, boolean silent) { @@ -32,19 +35,21 @@ public class TrackLoadingHandler implements AudioLoadResultHandler { this.type = type; this.silent = silent; this.guild = manager.getGuild(); - this.scheduler = manager.getScheduler(); + this.queueManager = manager.getQueueManager(); this.context = context; } @Override public void trackLoaded(AudioTrack audioTrack) { - context.deleteMessage(); - logger.debug("Guild {}: track loaded: {}", guild.getId(), audioTrack.getIdentifier()); - scheduler.getQueueManager().queue(audioTrack, context.getGuildUser()); - if (!silent) { - context.reply(MessageFunction.buildAddingTrackMessage(audioTrack, type)); + context.deleteMessage(); + if (MusicUtils.connect(context)) { + queueManager.queue(audioTrack, context.getGuildUser()); + + if (!silent) { + context.reply(MessageFunction.buildAddingTrackMessage(audioTrack, context.getGuildUser(), type)); + } } } @@ -52,41 +57,26 @@ public class TrackLoadingHandler implements AudioLoadResultHandler { public void playlistLoaded(AudioPlaylist playlist) { logger.debug("Guild {}: playlist loaded: {}", guild.getId(), identifier); - if (!playlist.getTracks().isEmpty()) { - if (playlist.isSearchResult()) { - - AudioTrack track = playlist.getTracks().get(0); - scheduler.getQueueManager().queue(track, context.getGuildUser()); - - if (!silent) { - context.reply(MessageFunction.buildAddingTrackMessage(track, type)); - } - - } else { - context.deleteMessage(); - if (!silent) { - context.reply(MessageFunction.buildAddingPlaylistMessage(playlist, identifier, type)); - } - - for (AudioTrack track : playlist.getTracks()) { - scheduler.getQueueManager().queue(track, context.getGuildUser()); - } - } - } + new LoadPlaylistTask(playlist, type).insert(context); } @Override public void noMatches() { + logger.debug("Guild {}: no matches for identifier {}", guild.getId(), identifier); + if (!silent) - context.replyToMessage("No matches found"); + context.reply("No matches found"); } @Override public void loadFailed(FriendlyException e) { - logger.debug("Guild {}: load failed for identifier {}", guild.getId(), identifier, e); - LogHandler.log(e); + logger.debug("Guild {}: load failed for identifier {}", guild.getId(), identifier); + + //TODO: better error messages + RuntimeException exception = new RuntimeException("Failed to load track: " + e.getMessage(), e); + LogHandler.log(exception); if (!silent) - context.replyToMessage(MessageFunction.buildSentryErrorMessage(new RuntimeException("Failed to load track: " + e.getMessage()))); + context.reply(MessageFunction.buildSentryErrorMessage(exception)); } } diff --git a/src/main/java/me/bobert/netneutrality/music/TrackScheduler.java b/src/main/java/me/bobert/netneutrality/music/TrackScheduler.java index 99de059..47d2b5f 100644 --- a/src/main/java/me/bobert/netneutrality/music/TrackScheduler.java +++ b/src/main/java/me/bobert/netneutrality/music/TrackScheduler.java @@ -10,22 +10,23 @@ import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; +import me.bobert.netneutrality.context.MusicChannelContext; import me.bobert.netneutrality.core.LogHandler; -import me.bobert.netneutrality.utils.MessageFunction; +import me.bobert.netneutrality.core.context.Context; +import me.bobert.netneutrality.exceptions.ContextException; import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.Member; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class TrackScheduler extends AudioEventAdapter { - private final static Logger logger = LogManager.getLogger(TrackScheduler.class); - + private static final Logger logger = LogManager.getLogger(TrackScheduler.class); private final QueueManager queueManager; private final MusicManager manager; private final AudioPlayer player; - private MessageChannel currentChannel; private final Guild guild; + private Context context; + private AudioTrack lastStuckTrack = null; public TrackScheduler(MusicManager manager) { this.manager = manager; @@ -50,28 +51,24 @@ public class TrackScheduler extends AudioEventAdapter { return guild; } - public MessageChannel getTextChannel() { - return currentChannel; - } - - public synchronized void attachTextChannel(MessageChannel channel) { - this.currentChannel = channel; - } - - public synchronized void detachTextChannel() { - this.currentChannel = null; - } - - public void sendMessageToChannel(String msg) { - if (currentChannel != null && currentChannel.canTalk()) { - currentChannel.sendMessage(msg).queue(); + public Member getDj() { + if (context != null) { + return context.getGuildUser(); } + + return null; } - public void sendMessageToChannel(MessageEmbed embed, MessageEmbed... embeds) { - if (currentChannel != null && currentChannel.canTalk()) { - currentChannel.sendMessageEmbeds(embed, embeds).queue(); - } + public synchronized void attachContext(MusicChannelContext context) { + this.context = context; + } + + public synchronized void detachContext() { + this.context = null; + } + + public Context getContext() { + return context; } @Override @@ -103,6 +100,7 @@ public class TrackScheduler extends AudioEventAdapter { } if (endReason == AudioTrackEndReason.STOPPED) { + // maybe skip removing head to make resuming easier? queueManager.removeHeadOfQueue(); } @@ -116,15 +114,55 @@ public class TrackScheduler extends AudioEventAdapter { @Override public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) { - logger.debug("Guild {}: exception while playing track [{}]: {}", guild.getId(), track.getIdentifier(), exception.getMessage()); - LogHandler.log(exception); - sendMessageToChannel(MessageFunction.buildSentryErrorMessage(new RuntimeException(String.format("Error while playing track [%s]: %s", track.getInfo().title, exception.getMessage())))); + logger.error("Guild {}: exception while playing track: {}", guild.getId(), track.getIdentifier()); + + LogHandler.log(new ContextException(String.format("Error while playing track: %s", track.getInfo().title), getContext(), exception)); } @Override public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) { - logger.debug("Guild {}: currently playing track is stuck: {}", guild.getId(), track.getIdentifier()); - sendMessageToChannel("Current track is stuck, skipping to next"); - queueManager.startNextTrack(); + logger.error("Guild {}: currently playing track is stuck: {}", guild.getId(), track.getIdentifier()); + + if (lastStuckTrack == null || !lastStuckTrack.getIdentifier().equals(track.getIdentifier())) { + lastStuckTrack = track; + + logger.debug("Trying to load stuck track again: {}", track.getIdentifier()); + context.async("Current track is stuck, trying to load it again", asyncContext -> { + try { + TrackInfo info = queueManager.peekHeadOfQueue(); + if (info != null && info.getTrack().getIdentifier().equals(track.getIdentifier())) { + AudioTrack clone = track.makeClone(); + long lastPosition = track.getPosition(); + + player.stopTrack(); + + if (queueManager.peekHeadOfQueue() != null && queueManager.peekHeadOfQueue().getTrack().getIdentifier().equals(track.getIdentifier())) { + queueManager.removeHeadOfQueue(); + } + + queueManager.queue(clone, info.getRequester()); + + AudioTrack playing = player.getPlayingTrack(); + if (playing != null && playing.getIdentifier().equals(track.getIdentifier())) { + playing.setPosition(lastPosition); + lastStuckTrack = null; + } else { + asyncContext.edit("Failed to seek track to last timestamp, the track will play from the beginning"); + } + } else { + asyncContext.edit("Failed to load last playing track, skipping to next.."); + queueManager.startNextTrack(); + } + } catch (Exception e) { + LogHandler.log(e); + asyncContext.edit("Failed to load last playing track, skipping to next.."); + queueManager.startNextTrack(); + } + }); + } else { + context.reply("Current track is stuck, skipping to next.."); + queueManager.startNextTrack(); + } + } } diff --git a/src/main/java/me/bobert/netneutrality/music/source/UdioAudioSourceManager.java b/src/main/java/me/bobert/netneutrality/music/source/UdioAudioSourceManager.java index eaeaa79..fb5c4d7 100644 --- a/src/main/java/me/bobert/netneutrality/music/source/UdioAudioSourceManager.java +++ b/src/main/java/me/bobert/netneutrality/music/source/UdioAudioSourceManager.java @@ -129,7 +129,7 @@ public class UdioAudioSourceManager implements AudioSourceManager { } protected JsonBrowser loadTrackInfoFromPageContent(String content) throws IOException { - String startTag = "self.__next_f.push([1,\"9:"; + String startTag = "self.__next_f.push([1,\"f:"; String endTag = "\\n\"])"; String json = DataFormatTools.extractBetween(content, startTag, endTag); diff --git a/src/main/java/me/bobert/netneutrality/tasks/emoji/EmojiReactionRoleTask.java b/src/main/java/me/bobert/netneutrality/tasks/emoji/EmojiReactionRoleTask.java new file mode 100644 index 0000000..a6853eb --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/emoji/EmojiReactionRoleTask.java @@ -0,0 +1,45 @@ +package me.bobert.netneutrality.tasks.emoji; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.impl.EmojiTask; +import me.bobert.netneutrality.utils.GuildUtils; +import net.dv8tion.jda.api.entities.Role; + +import java.util.Map; + +public class EmojiReactionRoleTask extends EmojiTask { + public EmojiReactionRoleTask(String guildId, String channelId, String messageId) { + super(guildId, channelId, messageId); + } + + // constructor for auto loading + public EmojiReactionRoleTask(String guildId, String channelId, String messageId, Map emojiMapping) { + super(guildId, channelId, messageId); + getEmojiMapping().putAll(emojiMapping); + } + + @Override + public String getName() { + return "emoji-reaction-role"; + } + + @Override + public Lifecycle execute(TaskContext context) { + String emoji = (String) context.getContextData("emoji-code"); + + if (getEmojiMapping().containsKey(emoji)) { + Role role = context.getGuild().getRoleById(getEmojiMapping().get(emoji)); + + if (role != null) { + GuildUtils.addRolesToMember(context, role); + } + } + + return Lifecycle.KEEP; + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/generic/AcceptButton.java b/src/main/java/me/bobert/netneutrality/tasks/generic/AcceptButton.java new file mode 100644 index 0000000..30624e9 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/generic/AcceptButton.java @@ -0,0 +1,67 @@ +package me.bobert.netneutrality.tasks.generic; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.trait.NotAcknowledgeable; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import me.bobert.netneutrality.core.tasks.trait.WithParent; +import me.bobert.netneutrality.core.tasks.impl.ButtonTask; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; + +import java.util.function.Function; + +/** + * A generic button task that represents a button to accept a choice. + * @param

the type of the parent task, which must implement WithChildren + */ +public class AcceptButton

extends ButtonTask implements WithParent

, NotAcknowledgeable { + private final P parentTask; + private final Function function; + + protected AcceptButton(P parentTask, ButtonStyle buttonStyle, String buttonLabel, Function function) { + super(buttonStyle, buttonLabel); + this.parentTask = parentTask; + this.function = function; + } + + public static

AcceptButton

createPrimaryButton(P parentTask, String buttonLabel, Function function) { + return new AcceptButton<>(parentTask, ButtonStyle.PRIMARY, buttonLabel, function); + } + + public static

AcceptButton

createSecondaryButton(P parentTask, String buttonLabel, Function function) { + return new AcceptButton<>(parentTask, ButtonStyle.SECONDARY, buttonLabel, function); + } + + public static

AcceptButton

createSuccessButton(P parentTask, String buttonLabel, Function function) { + return new AcceptButton<>(parentTask, ButtonStyle.SUCCESS, buttonLabel, function); + } + + public static

AcceptButton

createDangerButton(P parentTask, String buttonLabel, Function function) { + return new AcceptButton<>(parentTask, ButtonStyle.DANGER, buttonLabel, function); + } + + @Override + public P getParentTask() { + return parentTask; + } + + @Override + public String getName() { + return "accept-" + parentTask.getName(); + } + + @Override + public boolean isInsertable() { + return true; + } + + @Override + public Lifecycle execute(TaskContext context) { + return function.apply(context); + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/generic/CancelButton.java b/src/main/java/me/bobert/netneutrality/tasks/generic/CancelButton.java new file mode 100644 index 0000000..0d5e8ac --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/generic/CancelButton.java @@ -0,0 +1,51 @@ +package me.bobert.netneutrality.tasks.generic; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.trait.NotAcknowledgeable; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import me.bobert.netneutrality.core.tasks.trait.WithParent; +import me.bobert.netneutrality.core.tasks.impl.ButtonTask; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; + +import java.util.function.Function; + +public class CancelButton

extends ButtonTask implements WithParent

, NotAcknowledgeable { + private final P parentTask; + private final Function function; + + protected CancelButton(P parentTask, ButtonStyle buttonStyle, String buttonLabel, Function function) { + super(buttonStyle, buttonLabel); + this.parentTask = parentTask; + this.function = function; + } + + public static

CancelButton

create(P parentTask, String buttonLabel, Function function) { + return new CancelButton<>(parentTask, ButtonStyle.DANGER, buttonLabel, function); + } + + @Override + public P getParentTask() { + return parentTask; + } + + @Override + public String getName() { + return "cancel-" + parentTask.getName(); + } + + @Override + public boolean isInsertable() { + return true; + } + + @Override + public Lifecycle execute(TaskContext context) { + return function.apply(context); + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/git/ReportIssueTask.java b/src/main/java/me/bobert/netneutrality/tasks/git/ReportIssueTask.java new file mode 100644 index 0000000..8e9c0ad --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/git/ReportIssueTask.java @@ -0,0 +1,110 @@ +package me.bobert.netneutrality.tasks.git; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import io.sentry.Sentry; +import me.bobert.netneutrality.context.command.SlashCommandContext; +import me.bobert.netneutrality.core.LogHandler; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.impl.ModalTask; +import me.bobert.netneutrality.utils.Settings; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.gitlab4j.api.GitLabApi; +import org.gitlab4j.api.GitLabApiException; +import org.gitlab4j.api.models.Issue; + +public class ReportIssueTask extends ModalTask { + private static final Logger logger = LogManager.getLogger(ReportIssueTask.class); + private final boolean insertable; + + public ReportIssueTask(CommandContext context) { + this.insertable = context instanceof SlashCommandContext; + } + + @Override + public String getName() { + return "report-issue"; + } + + @Override + public boolean isInsertable() { + return insertable; + } + + @Override + public void replyToInteraction(CommandContext context) { + TextInput title = TextInput.create("title", "Title", TextInputStyle.SHORT) + .setPlaceholder("Brief description of the issue.") + .setMinLength(10) + .setMaxLength(100) + .build(); + + TextInput body = TextInput.create("description", "Description", TextInputStyle.PARAGRAPH) + .setPlaceholder("And I scream from the top of my lungs What's going on?") + .setMinLength(30) + .setMaxLength(1000) + .build(); + + Modal modal = Modal.create(getId(), "Report a bug") + .addComponents(ActionRow.of(title), ActionRow.of(body)) + .build(); + + context.reply(modal); + } + + @Override + public Lifecycle execute(TaskContext context) { + context.ephemeral(); + + String gitlabHost = Settings.getInstance().get("GITLAB_HOST"); + String gitlabToken = Settings.getInstance().get("GITLAB_TOKEN"); + String projectId = Settings.getInstance().get("GITLAB_PROJECT_ID"); + + if (gitlabHost.isBlank() || gitlabToken.isBlank() || projectId.isBlank()) { + logger.error("Gitlab is not configured in the bot settings, can't execute task"); + context.reply("Command is not configured, failed to create issue."); + return null; + } + + ModalMapping modalTitle = getValue("title", context); + ModalMapping modalDescription = getValue("description", context); + + if (modalTitle == null || modalDescription == null) { + context.reply("Failed to create issue."); + throw new IllegalArgumentException("Modal values are null"); + } + + User user = context.getDiscordUser(); + try (GitLabApi gitLabApi = new GitLabApi(gitlabHost, gitlabToken)) { + gitLabApi.setRequestTimeout(1000, 5000); + + String title = String.format("Bug report: %s", modalTitle.getAsString()); + StringBuilder description = new StringBuilder(); + description.append("Issue reported from user of bot\n\n"); + description.append("User: ").append(user.getName()).append(String.format("[%s]", user.getId())).append("\n\n"); + description.append("Last Sentry ID: ").append(Sentry.getLastEventId()).append("\n\n"); + description.append("Description from user: \n\n").append(modalDescription.getAsString()); + + Issue issue = gitLabApi.getIssuesApi().createIssue(projectId, title, description.toString()); + + context.reply(String.format("Issue was successfully created: %s", issue.getWebUrl())); + + } catch (GitLabApiException e) { + context.reply("Failed to create issue."); + LogHandler.log(e); + } + + return Lifecycle.REMOVE_SELF; + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/help/BotInfo.java b/src/main/java/me/bobert/netneutrality/tasks/help/BotInfo.java new file mode 100644 index 0000000..014b908 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/help/BotInfo.java @@ -0,0 +1,29 @@ +package me.bobert.netneutrality.tasks.help; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +public class BotInfo extends AbstractBuiltInTask { + + @Override + public String getId() { + return "help-info"; + } + + @Override + public Button createButton() { + return Button.primary(getId(), "Info"); + } + + @Override + public void executeTask(TaskContext context) { + context.sendMessageInPrivate("For information about the bot and to follow the development, " + + "you can join the discord here: https://discord.gg/udVJwTgV"); + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/help/CommandHelp.java b/src/main/java/me/bobert/netneutrality/tasks/help/CommandHelp.java new file mode 100644 index 0000000..37eeb5b --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/help/CommandHelp.java @@ -0,0 +1,31 @@ +package me.bobert.netneutrality.tasks.help; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask; +import me.bobert.netneutrality.utils.MessageFunction; +import me.bobert.netneutrality.utils.Settings; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +public class CommandHelp extends AbstractBuiltInTask { + + @Override + public String getId() { + return "help-commands"; + } + + @Override + public Button createButton() { + return Button.primary(getId(), "Commands"); + } + + @Override + public void executeTask(TaskContext context) { + context.replyEphemeral("The prefix for the bot is " + Settings.getInstance().getPrefix()); + context.replyEphemeral(MessageFunction.buildHelpMessage()); + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/help/ReportBug.java b/src/main/java/me/bobert/netneutrality/tasks/help/ReportBug.java new file mode 100644 index 0000000..7555581 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/help/ReportBug.java @@ -0,0 +1,29 @@ +package me.bobert.netneutrality.tasks.help; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +public class ReportBug extends AbstractBuiltInTask { + + @Override + public String getId() { + return "help-reportbug"; + } + + @Override + public Button createButton() { + return Button.danger(getId(), "Report bug"); + } + + @Override + public void executeTask(TaskContext context) { + context.sendMessageInPrivate("To report a bug with the bot, you can either use the /report-bug command " + + "or join the discord here: https://discord.gg/udVJwTgV"); + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/music/LoadPlaylistTask.java b/src/main/java/me/bobert/netneutrality/tasks/music/LoadPlaylistTask.java new file mode 100644 index 0000000..b31eb16 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/music/LoadPlaylistTask.java @@ -0,0 +1,134 @@ +package me.bobert.netneutrality.tasks.music; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.core.tasks.trait.Deletable; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import me.bobert.netneutrality.db.GSettings; +import me.bobert.netneutrality.db.service.GuildService; +import me.bobert.netneutrality.music.MusicManager; +import me.bobert.netneutrality.music.SourceFinder; +import me.bobert.netneutrality.core.tasks.impl.ButtonTask; +import me.bobert.netneutrality.tasks.generic.AcceptButton; +import me.bobert.netneutrality.tasks.generic.CancelButton; +import me.bobert.netneutrality.utils.MessageFunction; +import me.bobert.netneutrality.utils.MusicUtils; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.interactions.components.ActionRow; + +import java.util.ArrayList; +import java.util.List; + +public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deletable { + private final List childrens = new ArrayList<>(); + private final AudioPlaylist playlist; + private final SourceFinder.AudioSourceType sourceType; + + public LoadPlaylistTask(AudioPlaylist playlist, SourceFinder.AudioSourceType sourceType) { + super(null, null); + this.playlist = playlist; + this.sourceType = sourceType; + } + + @Override + public String getName() { + return "load-playlist"; + } + + @Override + public boolean isInsertable() { + return false; + } + + @Override + public void replyToInteraction(CommandContext context) { + if (playlist.getTracks().isEmpty()) { + context.reply("No tracks found"); + return; + } + + int searchResultsLimit = 5; // Max number of buttons per row + int maxPlaylistSizeBeforeAsking = Integer.parseInt(GuildService.get(context.getGuild()) + .getGuildSettingService() + .getSetting(GSettings.MUSIC_MAXPLAYLISTSIZE)); + + // Search results + if (playlist.isSearchResult()) { + List searchResults = playlist.getTracks().stream().limit(searchResultsLimit).toList(); + List loadTasks = new ArrayList<>(); + + for (int i = 0; i < searchResults.size(); i++) { + AudioTrack track = searchResults.get(i); + loadTasks.add(new LoadTrackFromPlaylistTask(this, String.valueOf(i + 1), track)); + } + + ActionRow firstRow = createActionRow(loadTasks); + ActionRow secondRow = commonButtons(searchResults, "Add all tracks", "Not for me honey"); + + context.edit(MessageFunction.buildSearchResultsMessage(searchResults, context.getGuildUser()), List.of(firstRow, secondRow)); + + return; + } + + List tracks = playlist.getTracks(); + if (tracks.size() > maxPlaylistSizeBeforeAsking) { + ActionRow firstRow = commonButtons(playlist.getTracks(), "I'm ready", "Please don't"); + + context.edit(MessageFunction.buildPlaylistLoadMessage(playlist, context.getGuildUser()), List.of(firstRow)); + return; + } + + // Load playlist normally + MusicManager manager = MusicManager.get(context.getGuild()); + if (MusicUtils.connect(context)) { + manager.getQueueManager().queue(tracks, context.getGuildUser()); + + context.edit(MessageFunction.buildAddingPlaylistMessage(tracks, playlist.getName(), context.getGuildUser(), sourceType)); + } + } + + private ActionRow commonButtons(List tracks, String addAllText, String cancelText) { + Task accept = AcceptButton.createSuccessButton(this, addAllText, context -> { + MusicManager manager = MusicManager.get(context.getGuild()); + Member member = context.getGuildUser(); + + if (MusicUtils.connect(context)) { + manager.getQueueManager().queue(tracks, member); + context.reply(MessageFunction.buildAddingPlaylistMessage(tracks, playlist.getName(), member, SourceFinder.AudioSourceType.Youtube)); + + return Lifecycle.REMOVE_SELF_AND_CHILDREN; + } + + return Lifecycle.KEEP; + }); + + Task cancel = CancelButton.create(this, cancelText, context -> Lifecycle.REMOVE_SELF_AND_CHILDREN); + addChild(accept, cancel); + + return ActionRow.of( + accept.createButton().withEmoji(Emoji.fromFormatted("U+1F60E")), + cancel.createButton().withEmoji(Emoji.fromFormatted("U+1F92E")) + ); + } + + @Override + public List getChildTasks() { + return childrens; + } + + @Override + public void onInteractionDelete() { + for (Task child : getChildTasks()) { + TaskManager.getInstance().removeTask(child); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/music/LoadTrackFromPlaylistTask.java b/src/main/java/me/bobert/netneutrality/tasks/music/LoadTrackFromPlaylistTask.java new file mode 100644 index 0000000..f9cd164 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/music/LoadTrackFromPlaylistTask.java @@ -0,0 +1,42 @@ +package me.bobert.netneutrality.tasks.music; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import me.bobert.netneutrality.music.MusicManager; +import me.bobert.netneutrality.music.SourceFinder; +import me.bobert.netneutrality.tasks.generic.AcceptButton; +import me.bobert.netneutrality.utils.MessageFunction; +import me.bobert.netneutrality.utils.MusicUtils; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; + +public class LoadTrackFromPlaylistTask extends AcceptButton { + private final AudioTrack track; + + public LoadTrackFromPlaylistTask(LoadPlaylistTask parentTask, String buttonLabel, AudioTrack track) { + super(parentTask, ButtonStyle.PRIMARY, buttonLabel, context -> { + MusicManager manager = MusicManager.get(context.getGuild()); + Member member = context.getGuildUser(); + + if (MusicUtils.connect(context)) { + manager.getQueueManager().queue(track, member); + context.reply(MessageFunction.buildAddingTrackMessage(track, member, SourceFinder.AudioSourceType.Youtube)); + + return Lifecycle.REMOVE_SELF_AND_CHILDREN; + } + + return Lifecycle.KEEP; + }); + + this.track = track; + } + + @Override + public String getName() { + return String.format("load-track-%s", track.getIdentifier()); + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleFirstStep.java b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleFirstStep.java new file mode 100644 index 0000000..65c3213 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleFirstStep.java @@ -0,0 +1,47 @@ +package me.bobert.netneutrality.tasks.reaction; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.impl.StepTask; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; + +public class EmojiRoleFirstStep extends StepTask { + private final Message target; + + public EmojiRoleFirstStep(Message target) { + this.target = target; + } + + @Override + public String getName() { + return "role-first-step"; + } + + @Override + public StepType getStepType() { + return StepType.ROLE; + } + + @Override + public String getStepDescription() { + return String.format("Creating a new emoji role mapping in guild %s for message %s", + getGuild().getId(), + target.getJumpUrl()); + } + + @Override + public String getStepQuestion() { + return "Reply to this message with the role ID you want to configure"; + } + + @Override + public StepResult executeStep(Object answer) { + return StepResult.next(new EmojiRoleSecondStep(target, (Role) answer)); + } + +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleModify.java b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleModify.java new file mode 100644 index 0000000..40bf58e --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleModify.java @@ -0,0 +1,84 @@ +package me.bobert.netneutrality.tasks.reaction; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.core.tasks.impl.ButtonTask; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import me.bobert.netneutrality.tasks.generic.AcceptButton; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.interactions.components.ActionRow; + +import java.util.ArrayList; +import java.util.List; + +public class EmojiRoleModify extends ButtonTask implements WithChildren { + private final List childrens = new ArrayList<>(); + private final Message target; + private final EmojiReactionRoleTask task; + + public EmojiRoleModify(Message target, EmojiReactionRoleTask task) { + super(null, null); + this.target = target; + this.task = task; + } + + @Override + public String getName() { + return "modify emoji role"; + } + + @Override + public boolean isInsertable() { + return false; + } + + @Override + public void replyToInteraction(CommandContext context) { + Task addEmojiTask = AcceptButton.createSuccessButton(this, "Add emoji role mapping", taskContext -> { + if (task.getEmojiMapping().size() >= 10) { + taskContext.replyEphemeral("You cannot add more than 10 emoji roles on a message"); + return Lifecycle.KEEP; + } + + new EmojiRoleFirstStep(target).insert(context); + return Lifecycle.KEEP; + }); + + Task removeEmojiTask = AcceptButton.createSecondaryButton(this, "Remove emoji role mapping", taskContext -> { + if (task.getEmojiMapping().isEmpty()) { + taskContext.replyEphemeral("No emoji role mapping found for the message"); + return Lifecycle.KEEP; + } + + new EmojiRoleRemoveStep(target, task).insert(context); + return Lifecycle.KEEP; + }); + + Task deleteMappingTask = AcceptButton.createDangerButton(this, "Unregister task", taskContext -> { + if (!TaskManager.getInstance().hasTask(task.getId())) { + taskContext.replyEphemeral("This task no longer exists"); + return Lifecycle.REMOVE_SELF; + } + + task.delete(); + context.replyEphemeral("Task deleted successfully!"); + return Lifecycle.REMOVE_SELF; + }); + + ActionRow row = createActionRow(addEmojiTask, removeEmojiTask, deleteMappingTask); + context.replyEphemeral("Select what you want to do with this message", List.of(row)); + } + + @Override + public List getChildTasks() { + return childrens; + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleRemoveStep.java b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleRemoveStep.java new file mode 100644 index 0000000..0d59a89 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleRemoveStep.java @@ -0,0 +1,62 @@ +package me.bobert.netneutrality.tasks.reaction; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.impl.StepTask; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import net.dv8tion.jda.api.entities.Message; + +import java.util.Set; +import java.util.StringJoiner; + +public class EmojiRoleRemoveStep extends StepTask { + private final Message target; + private final EmojiReactionRoleTask task; + + public EmojiRoleRemoveStep(Message target, EmojiReactionRoleTask task) { + this.target = target; + this.task = task; + } + + @Override + public StepType getStepType() { + return StepType.EMOJI; + } + + @Override + public String getStepDescription() { + return String.format("Select which emoji you want to remove from the message %s", target.getJumpUrl()); + } + + @Override + public String getStepQuestion() { + StringJoiner joiner = new StringJoiner(" - "); + Set emojis = task.getEmojiMapping().keySet(); + for (String emoji : emojis) { + joiner.add(emoji); + } + + return joiner.toString(); + } + + @Override + public StepResult executeStep(Object answer) { + String emoji = (String) answer; + + if (!task.getEmojiMapping().containsKey(emoji)) { + return StepResult.failure("The emoji doesn't exist"); + } + + task.removeEmoji(emoji); + return StepResult.success("Removed emoji successfully!"); + } + + @Override + public String getName() { + return "emoji-role-remove"; + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleSecondStep.java b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleSecondStep.java new file mode 100644 index 0000000..4371d13 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleSecondStep.java @@ -0,0 +1,64 @@ +package me.bobert.netneutrality.tasks.reaction; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.core.tasks.impl.StepTask; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; + +import java.util.Map; + +public class EmojiRoleSecondStep extends StepTask { + private final Message target; + private final Role role; + + public EmojiRoleSecondStep(Message target, Role role) { + this.target = target; + this.role = role; + } + + @Override + public StepType getStepType() { + return StepType.EMOJI; + } + + @Override + public String getStepDescription() { + return String.format("Creating a new emoji role mapping in guild %s for message %s and role %s", + getGuild().getId(), + target.getJumpUrl(), + role.getName()); + } + + @Override + public String getStepQuestion() { + return "Reply with the emoji you want to add to the role"; + } + + @Override + public StepResult executeStep(Object answer) { + String emoji = (String) answer; + Task emojiTask = new EmojiReactionRoleTask(target.getGuildId(), + target.getChannelId(), + target.getId(), + Map.of(emoji, role.getId())); + + if (TaskManager.getInstance().hasTask(emojiTask.getId())) { + return StepResult.failure("The message ID is already registered with the bot, please use the modify mapping command instead"); + } + + return StepResult.success("Mapping registered successfully for role " + role.getName() + " with emoji " + emoji, emojiTask); + } + + @Override + public String getName() { + return "role-second-step"; + } +} diff --git a/src/main/java/me/bobert/netneutrality/thread/CleanupTaskThread.java b/src/main/java/me/bobert/netneutrality/thread/CleanupTaskThread.java new file mode 100644 index 0000000..35931ca --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/thread/CleanupTaskThread.java @@ -0,0 +1,35 @@ +package me.bobert.netneutrality.thread;/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.BuiltInTask; +import me.bobert.netneutrality.core.tasks.PersistableTask; +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.TaskManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; + +public class CleanupTaskThread implements Runnable { + private static final Logger logger = LogManager.getLogger(CleanupTaskThread.class); + + @Override + public void run() { + TaskManager manager = TaskManager.getInstance(); + + List expiredTasks = manager.getTasks() + .stream() + .filter(task -> !(task instanceof PersistableTask)) + .filter(task -> !(task instanceof BuiltInTask)) + .filter(Task::isExpired) + .toList(); + + if (!expiredTasks.isEmpty()) { + logger.info("Cleaning up {} expired tasks..", expiredTasks.size()); + expiredTasks.forEach(manager::removeTask); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/thread/MusicManagerCleanupThread.java b/src/main/java/me/bobert/netneutrality/thread/MusicManagerCleanupThread.java new file mode 100644 index 0000000..d52d064 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/thread/MusicManagerCleanupThread.java @@ -0,0 +1,39 @@ +package me.bobert.netneutrality.thread; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.music.MusicManager; +import me.bobert.netneutrality.music.TrackScheduler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class MusicManagerCleanupThread implements Runnable { + private static final Logger logger = LogManager.getLogger(MusicManagerCleanupThread.class); + + @Override + public void run() { + + try { + + for (MusicManager manager : MusicManager.getManagers()) { + processManager(manager); + } + + } catch (InterruptedException e) { + logger.error("Interrupted while running MusicManagerCleanupThread", e); + } + } + + private void processManager(MusicManager manager) throws InterruptedException { + TrackScheduler scheduler = manager.getScheduler(); + + // Context is still attached after bot is disconnected, the bot might have been kicked from the channel + if (scheduler.getContext() != null && !manager.isConnected()) { + manager.disconnect(); + manager.getQueueManager().clearQueue(); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/thread/MusicPlayerAutoDisconnectThread.java b/src/main/java/me/bobert/netneutrality/thread/MusicPlayerAutoDisconnectThread.java index e97e091..2efb483 100644 --- a/src/main/java/me/bobert/netneutrality/thread/MusicPlayerAutoDisconnectThread.java +++ b/src/main/java/me/bobert/netneutrality/thread/MusicPlayerAutoDisconnectThread.java @@ -5,51 +5,56 @@ package me.bobert.netneutrality.thread; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.NetNeutrality; import me.bobert.netneutrality.music.MusicManager; -import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.List; public class MusicPlayerAutoDisconnectThread implements Runnable { - - private final Logger logger = LogManager.getLogger(MusicPlayerAutoDisconnectThread.class); - private final MusicManager manager; - - //TODO: move to scheduler - public MusicPlayerAutoDisconnectThread(MusicManager musicManager) { - manager = musicManager; - } + private static final Logger logger = LogManager.getLogger(MusicPlayerAutoDisconnectThread.class); @Override public void run() { - Guild guild = manager.getGuild(); - VoiceChannel channel = (VoiceChannel) guild.getAudioManager().getConnectedChannel(); - try { - if (channel != null) { - if (conditionsMetToDisconnect(channel)) { - // Sleep 30 seconds then check again (prevents disconnecting right after a track ends) - Thread.sleep(30000); - if (conditionsMetToDisconnect(channel)) { - logger.debug("Guild {}: auto-disconnect player from voice channel: {}", guild.getId(), channel.getId()); - manager.leave(); - } - } + for (MusicManager manager : MusicManager.getManagers()) { + processManager(manager); } + } catch (InterruptedException e) { - throw new RuntimeException(e); + logger.error("Interrupted while running MusicPlayerAutoDisconnectThread", e); } } - private boolean conditionsMetToDisconnect(VoiceChannel channel) { - List members = channel.getMembers().stream().filter(member -> !member.getId().equals(NetNeutrality.getSelf().getId())).toList(); + private void processManager(MusicManager manager) throws InterruptedException { + AudioChannel channel = manager.getGuild().getAudioManager().getConnectedChannel(); - return members.isEmpty() || manager.getScheduler().getQueueManager().isEmpty(); + if (channel != null) { + logger.debug("Processing MusicPlayerAutoDisconnectThread for guild {}", manager.getGuild().getId()); + if (conditionsMetToDisconnect(manager)) { + logger.debug("Guild {}: Sleeping 30 seconds and checking again", manager.getGuild().getId()); + // Sleep 30 seconds then check again (prevents disconnecting right after a track ends) + Thread.sleep(30000); + + if (conditionsMetToDisconnect(manager)) { + logger.debug("Guild {}: auto-disconnect player from voice channel: {}", manager.getGuild().getId(), channel.getId()); + manager.disconnect(); + } + } + } + } + + private boolean conditionsMetToDisconnect(MusicManager manager) { + AudioChannel channel = manager.getGuild().getAudioManager().getConnectedChannel(); + if (channel != null) { + List members = channel.getMembers().stream().filter(member -> !member.getId().equals(channel.getJDA().getSelfUser().getId())).toList(); + + return members.isEmpty() || manager.getQueueManager().isEmpty(); + } + + return false; } } diff --git a/src/main/java/me/bobert/netneutrality/thread/ShutdownThread.java b/src/main/java/me/bobert/netneutrality/thread/ShutdownThread.java index c9900e0..cb19a71 100644 --- a/src/main/java/me/bobert/netneutrality/thread/ShutdownThread.java +++ b/src/main/java/me/bobert/netneutrality/thread/ShutdownThread.java @@ -11,7 +11,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configurator; -public class ShutdownThread implements Runnable { +public class ShutdownThread extends Thread { + private final NetNeutrality main; + + public ShutdownThread(NetNeutrality main) { + this.main = main; + } @Override public void run() { @@ -20,8 +25,8 @@ public class ShutdownThread implements Runnable { Configurator.shutdown((LoggerContext) LogManager.getContext()); } - if (NetNeutrality.getJDA() != null) { - NetNeutrality.shutdown(); + if (main.getJDA() != null) { + main.shutdown(); } } diff --git a/src/main/java/me/bobert/netneutrality/utils/ChannelMessagePurger.java b/src/main/java/me/bobert/netneutrality/utils/ChannelMessagePurger.java index 95ae067..bf9f955 100644 --- a/src/main/java/me/bobert/netneutrality/utils/ChannelMessagePurger.java +++ b/src/main/java/me/bobert/netneutrality/utils/ChannelMessagePurger.java @@ -5,16 +5,13 @@ package me.bobert.netneutrality.utils; * https://discord.gg/qUa6zNFZsd */ -import io.sentry.ISpan; -import io.sentry.ITransaction; -import io.sentry.Sentry; -import io.sentry.SpanStatus; import me.bobert.netneutrality.core.context.CommandContext; -import me.bobert.netneutrality.thread.ThreadManager; +import me.bobert.netneutrality.core.thread.ThreadManager; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageHistory; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageEditData; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -24,6 +21,7 @@ import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +//TODO: not useful since we can't delete messages that are more than 2 weeks old public class ChannelMessagePurger { private static final Logger logger = LogManager.getLogger(ChannelMessagePurger.class); @@ -67,21 +65,6 @@ public class ChannelMessagePurger { }); } - public void purgeChannel(CommandContext context) { - ThreadManager.getExecutor().submit(() -> { - if (lock.tryLock()) { - try { - int total = countAllChannelMessages(channel); - context.reply("Number of messages to purge: %d", total); - - safePurge(context, total); - } finally { - lock.unlock(); - } - } - }); - } - private synchronized int countAllChannelMessages(GuildMessageChannel channel) { logger.info("Guild {}: counting all messages from channel {}", channel.getGuild().getId(), channel.getId()); MessageHistory history = channel.getHistory(); @@ -100,9 +83,7 @@ public class ChannelMessagePurger { } private synchronized void safePurge(CommandContext context, int toPurge) { - ITransaction transaction = Sentry.startTransaction(String.format("purge messages from guild %s)", channel.getGuild().getId()), "task"); - transaction.setData("channel", channel.getId()); - transaction.setData("toPurge", toPurge); + logger.info("Safe purging {} messages from channel {}", toPurge, channel.getId()); if (toPurge == 1) { toPurge += 1; @@ -111,60 +92,48 @@ public class ChannelMessagePurger { int tracker = toPurge; int iteration = tracker / 100; boolean olderThan2Weeks = false; - context.replyToMessage("Purging iterations to do: %d", iteration); + context.edit(String.format("Purging iterations to do: %d", iteration)); try { for (int i = 0; i < iteration; i++) { - ISpan innerSpan = transaction.startChild("retrieve-messages", "Processing iteration " + (i + 1) + "/" + iteration); + logger.debug("Processing iteration {}/{}", (i + 1), iteration); try { List messages = channel.getHistory().retrievePast(100).complete(); channel.deleteMessages(messages).complete(); - tracker -= 100; - innerSpan.setStatus(SpanStatus.OK); } catch (IllegalArgumentException ex) { olderThan2Weeks = true; } catch (Exception e) { - innerSpan.setThrowable(e); - innerSpan.setStatus(SpanStatus.INTERNAL_ERROR); + logger.error("Error purging iteration {}/{}", (i + 1), iteration, e); } finally { - innerSpan.finish(); + tracker -= 100; } } if (tracker > 0) { - ISpan innerSpan = transaction.startChild("retrieve-messages", String.format("Processing final iteration (%d messages)", tracker)); + logger.debug("Processing final iteration ({} messages)", tracker); try { List messages = channel.getHistory().retrievePast(tracker).complete(); channel.deleteMessages(messages).complete(); - innerSpan.setStatus(SpanStatus.OK); } catch (IllegalArgumentException ex) { olderThan2Weeks = true; } catch (Exception e) { - innerSpan.setThrowable(e); - innerSpan.setStatus(SpanStatus.INTERNAL_ERROR); - } finally { - innerSpan.finish(); + logger.error("Error purging final iteration", e); } - - - innerSpan.finish(); } } catch (Exception e) { - transaction.setThrowable(e); - transaction.setStatus(SpanStatus.INTERNAL_ERROR); - } finally { - transaction.finish(); - MessageCreateBuilder builder = new MessageCreateBuilder() - .addContent("Finished purging ") - .addContent(String.valueOf(toPurge)) - .addContent(" messages."); - - if (olderThan2Weeks) { - builder.addContent("\n").addContent("Messages older than two weeks weren't deleted."); - } - context.reply(builder.build()); - lock.unlock(); + logger.error("Error purging messages", e); } + + MessageCreateBuilder builder = new MessageCreateBuilder() + .addContent("Finished purging ") + .addContent(String.valueOf(toPurge)) + .addContent(" messages."); + + if (olderThan2Weeks) { + builder.addContent("\n").addContent("Messages older than two weeks weren't deleted."); + } + context.edit(MessageEditData.fromCreateData(builder.build())); + lock.unlock(); } } diff --git a/src/main/java/me/bobert/netneutrality/utils/ContextUtils.java b/src/main/java/me/bobert/netneutrality/utils/ContextUtils.java new file mode 100644 index 0000000..059b6fe --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/utils/ContextUtils.java @@ -0,0 +1,78 @@ +package me.bobert.netneutrality.utils; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import java.util.ArrayList; +import java.util.List; + +public class ContextUtils { + + + public static List splitMessage(String input, int limit) { + List chunks = new ArrayList<>(); + int index = 0; + + while (index < input.length()) { + // Check if the remaining text is within the limit + if (input.length() - index <= limit) { + chunks.add(trimTrailingNewline(input.substring(index))); + break; + } + + // Determine the endpoint of the current chunk + int endIndex = Math.min(index + limit, input.length()); + String chunk = input.substring(index, endIndex); + + // Find the last suitable truncation boundary + int boundaryIndex = findLastBoundary(chunk); + + // Adjust the truncation index + if (boundaryIndex == -1 && endIndex < input.length()) { + // Hard truncate if no suitable boundary is found and not at the end + boundaryIndex = limit; + } else if (boundaryIndex > -1) { + // Include sentence-ending punctuation if present + boundaryIndex += 1; + } + + // Extract the chunk and trim unnecessary trailing '\n' + endIndex = index + (boundaryIndex > -1 ? boundaryIndex : limit); + String trimmedChunk = trimTrailingNewline(input.substring(index, endIndex)); + + // Add the chunk to the result list + chunks.add(trimmedChunk); + + // Move the index to the next chunk, skipping redundant newlines + index = endIndex; + while (index < input.length() && input.charAt(index) == '\n') { + index++; + } + } + + return chunks; + } + + private static int findLastBoundary(String chunk) { + // Find the last sentence-ending punctuation or space within the chunk + int lastSentenceBoundary = Math.max( + Math.max(chunk.lastIndexOf("."), Math.max(chunk.lastIndexOf("!"), chunk.lastIndexOf("?"))), + chunk.lastIndexOf("\n") + ); + int lastSpace = chunk.lastIndexOf(" "); + return Math.max(lastSentenceBoundary, lastSpace); + } + + private static String trimTrailingNewline(String chunk) { + // Trim trailing '\n' if present + if (chunk.endsWith("\n")) { + return chunk.substring(0, chunk.length() - 1).trim(); + } + + return chunk.trim(); + } + + +} diff --git a/src/main/java/me/bobert/netneutrality/utils/GuildUtils.java b/src/main/java/me/bobert/netneutrality/utils/GuildUtils.java new file mode 100644 index 0000000..66e8fcb --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/utils/GuildUtils.java @@ -0,0 +1,26 @@ +package me.bobert.netneutrality.utils; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.Context; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; + +public class GuildUtils { + + public static void addRolesToMember(Context context, Role role) { + addRolesToMember(context.getGuild(), context.getGuildUser(), role); + } + + public static void addRolesToMember(Guild guild, Member member, Role role) { + Member self = guild.getSelfMember(); + if (self.canInteract(role)) { + guild.addRoleToMember(member, role).queue(); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/utils/JpaUtils.java b/src/main/java/me/bobert/netneutrality/utils/JpaUtils.java index 2996950..b6b33fa 100644 --- a/src/main/java/me/bobert/netneutrality/utils/JpaUtils.java +++ b/src/main/java/me/bobert/netneutrality/utils/JpaUtils.java @@ -6,28 +6,30 @@ package me.bobert.netneutrality.utils; * https://discord.gg/qUa6zNFZsd */ -import io.sentry.Breadcrumb; -import io.sentry.Sentry; -import io.sentry.SentryLevel; -import me.bobert.netneutrality.core.GuildManager; -import me.bobert.netneutrality.core.SessionManager; +import me.bobert.netneutrality.core.db.SessionManager; import me.bobert.netneutrality.db.entities.OGuild; +import me.bobert.netneutrality.db.service.GuildService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.hibernate.HibernateException; import org.hibernate.ObjectNotFoundException; import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.exception.ConstraintViolationException; import java.io.Serializable; import java.util.function.Consumer; public class JpaUtils { + private static final Logger logger = LogManager.getLogger(JpaUtils.class); + /** * Tries to read the persistent state into the object. *

* If the object is not found in the database and is of type {@link OGuild}, tries to recreate entity and throws * an exception to block execution. *

- * The session is not closed * * @param session The session in which to load the persistent object * @param object The persistent object @@ -38,16 +40,14 @@ public class JpaUtils { session.load(object, id); } catch (HibernateException exception) { if (exception instanceof ObjectNotFoundException) { - Sentry.addBreadcrumb(Breadcrumb.error("ObjectNotFoundException")); - if (object instanceof OGuild oGuild) { String cachedId = oGuild.getGuildId(); - Sentry.captureMessage("Trying to load Guild[" + cachedId + "] while entity doesn't exist in database, reloading..", SentryLevel.ERROR); + logger.error("Trying to load Guild[{}] while entity doesn't exist in database, reloading..", cachedId); - GuildManager guildManager = GuildManager.getManager(cachedId); - guildManager.reloadEntity(); - guildManager.getBlacklistCache().clear(); - Sentry.captureMessage("Guild[" + cachedId + "] reloaded", SentryLevel.INFO); + GuildService service = GuildService.get(cachedId); + service.reloadEntity(); + service.getBlacklistService().removeGuildBlacklist(); + logger.info("Guild[{}] reloaded", cachedId); } } @@ -56,31 +56,51 @@ public class JpaUtils { } } - public static void updateWithTransaction(Object persistantEntity, Runnable runnable) { - Session session = SessionManager.getSessionFactory().openSession(); - session.beginTransaction(); - - runnable.run(); - - session.merge(persistantEntity); - session.getTransaction().commit(); - session.close(); + public static void runWithSession(Consumer consumer) { + try(Session session = SessionManager.getSessionFactory().openSession()) { + consumer.accept(session); + } } - public static void getWithSession(OGuild oGuild, Consumer consumer) { - getWithSession(oGuild, oGuild.getGuildId(), consumer); + public static void runWithTransaction(Consumer consumer) { + try(Session session = SessionManager.getSessionFactory().openSession()) { + session.beginTransaction(); + consumer.accept(session); + session.getTransaction().commit(); + } + } + + public static void updateWithTransaction(Object persistantEntity, Runnable runnable) { + try(Session session = SessionManager.getSessionFactory().openSession()) { + Transaction transaction = session.beginTransaction(); + + try { + runnable.run(); + + session.merge(persistantEntity); + transaction.commit(); + } catch (ConstraintViolationException e) { + logger.error(e); + throw e; + } catch (Exception e) { + logger.error(e); + transaction.rollback(); + } + } } /** + * Loads the entity and relations from the database and executes the given consumer. + * This will load all Lazy relationships defined in the entity. + * * @param persistantEntity The entity to load * @param id The identifier for the entity * @param consumer The comsumer, which exposes the session */ public static void getWithSession(Object persistantEntity, Serializable id, Consumer consumer) { - Session session = SessionManager.getSessionFactory().openSession(); - tryLoad(session, persistantEntity, id); - - consumer.accept(session); - session.close(); + try(Session session = SessionManager.getSessionFactory().openSession()) { + tryLoad(session, persistantEntity, id); + consumer.accept(session); + } } } diff --git a/src/main/java/me/bobert/netneutrality/utils/MessageFunction.java b/src/main/java/me/bobert/netneutrality/utils/MessageFunction.java index 3cd26b6..0586fd1 100644 --- a/src/main/java/me/bobert/netneutrality/utils/MessageFunction.java +++ b/src/main/java/me/bobert/netneutrality/utils/MessageFunction.java @@ -10,50 +10,50 @@ import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import io.sentry.Sentry; -import me.bobert.netneutrality.command.AbstractCommand; -import me.bobert.netneutrality.command.CommandManager; -import me.bobert.netneutrality.command.CommandType; +import me.bobert.netneutrality.core.command.Command; +import me.bobert.netneutrality.core.command.CommandCategory; +import me.bobert.netneutrality.core.command.CommandContainer; +import me.bobert.netneutrality.core.command.CommandManager; import me.bobert.netneutrality.core.context.CommandContext; -import me.bobert.netneutrality.exceptions.CommandException; +import me.bobert.netneutrality.core.context.Context; +import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.exceptions.ContextException; import me.bobert.netneutrality.music.MusicManager; -import me.bobert.netneutrality.music.QueueManager; import me.bobert.netneutrality.music.SourceFinder; +import me.bobert.netneutrality.music.TrackInfo; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import org.apache.commons.lang3.time.DurationFormatUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.awt.*; import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.*; public class MessageFunction { - private final static Logger logger = LogManager.getLogger(MessageFunction.class); + private static final Logger logger = LogManager.getLogger(MessageFunction.class); public static MessageEmbed buildCurrentlyPlayingMessage(MusicManager manager) { AudioTrack current = manager.getCurrentlyPlaying(); EmbedBuilder message = new EmbedBuilder(); - message.setAuthor("Link to track", String.format("https://youtu.be/%s", current.getIdentifier())); + message.setAuthor("Link to track (click me)", String.format("https://youtu.be/%s", current.getIdentifier())); message.setThumbnail(String.format("https://img.youtube.com/vi/%s/default.jpg", current.getIdentifier())); - message.setTitle("Currently playing:"); + //message.setTitle("Currently playing:"); message.setColor(Color.BLUE); - message.addField("Track title: ", current.getInfo().title, false); - message.addField("Duration: ", DurationFormatUtils.formatDuration(current.getPosition(), "**HH:mm:ss**", true) + " / " + DurationFormatUtils.formatDuration(current.getDuration(), "**HH:mm:ss**", true), false); + message.addField("Currently playing: ", current.getInfo().title, true); + message.addField("Duration: ", DurationFormatUtils.formatDuration(current.getPosition(), "HH:mm:ss", true) + " / " + DurationFormatUtils.formatDuration(current.getDuration(), "HH:mm:ss", true), true); message.setTimestamp(OffsetDateTime.now()); return message.build(); } - public static MessageEmbed buildAddingTrackMessage(AudioTrack track, SourceFinder.AudioSourceType type) { + public static MessageEmbed buildAddingTrackMessage(AudioTrack track, Member member, SourceFinder.AudioSourceType type) { EmbedBuilder message = new EmbedBuilder(); String url = track.getIdentifier(); String thumbnail = null; @@ -74,37 +74,24 @@ public class MessageFunction { message.setColor(Color.BLUE); message.addField("Track title: ", String.format("[**`%s`**](%s)", track.getInfo().title, url), false); message.addField("Duration: ", DurationFormatUtils.formatDuration(track.getDuration(), "**HH:mm:ss**", true), false); + message.setFooter(member.getEffectiveName(), member.getEffectiveAvatarUrl()); message.setTimestamp(OffsetDateTime.now()); return message.build(); } - public static MessageEmbed buildSearchResultsMessage(List tracks) { + public static MessageEmbed buildAddingPlaylistMessage(List tracks, String playlistName, Member member, SourceFinder.AudioSourceType type) { EmbedBuilder message = new EmbedBuilder(); - - message.setTitle("Select the track to add to the queue"); - message.setColor(Color.BLUE); - - int i = 0; - for (AudioTrack track : tracks) { - message.addField(++i + ": ", String.format("**`%s`**", track.getInfo().title), false); - } - - return message.build(); - } - - public static MessageEmbed buildAddingPlaylistMessage(AudioPlaylist playlist, String identifier, SourceFinder.AudioSourceType type) { - EmbedBuilder message = new EmbedBuilder(); - String url = identifier; + String url = ""; String thumbnail = null; if (type == SourceFinder.AudioSourceType.Youtube || type == SourceFinder.AudioSourceType.YTSearch) { - url = String.format("https://youtu.be/%s", playlist.getTracks().get(0).getIdentifier()); - thumbnail = String.format("https://i3.ytimg.com/vi/%s/hqdefault.jpg", playlist.getTracks().get(0).getIdentifier()); + url = String.format("https://youtu.be/%s", tracks.get(0).getIdentifier()); + thumbnail = String.format("https://i3.ytimg.com/vi/%s/hqdefault.jpg", tracks.get(0).getIdentifier()); } if (type == SourceFinder.AudioSourceType.YTMusic) { - thumbnail = String.format("https://i3.ytimg.com/vi/%s/hqdefault.jpg", playlist.getTracks().get(0).getIdentifier()); + thumbnail = String.format("https://i3.ytimg.com/vi/%s/hqdefault.jpg", tracks.get(0).getIdentifier()); } if (thumbnail != null) { @@ -112,44 +99,52 @@ public class MessageFunction { } message.setTitle("Adding playlist to queue.."); message.setColor(Color.BLUE); - message.addField("Playlist: ", String.format("[**`%s`**](%s)", playlist.getName(), url), false); - message.addField("Number of tracks: ", String.valueOf(playlist.getTracks().size()), false); + message.addField("Playlist: ", String.format("[**`%s`**](%s)", playlistName, url), false); + message.addField("Number of tracks: ", String.valueOf(tracks.size()), false); + message.setFooter(member.getEffectiveName(), member.getEffectiveAvatarUrl()); message.setTimestamp(OffsetDateTime.now()); return message.build(); } - public static MessageEmbed buildViewQueueMessage(MusicManager manager, boolean currentlyPlaying) { - ArrayList tracks = manager.getScheduler().getQueueManager().getAllQueuedTrack(); - AudioTrack current = manager.getCurrentlyPlaying(); - int numberofTracksShown = Math.min(tracks.size(), 10); + public static String buildViewQueueMessage(MusicManager manager) { + List tracks = manager.getQueueManager().getAllQueuedTracks(); + if (tracks.isEmpty()) { + return "Queue is empty!"; + } - EmbedBuilder message = new EmbedBuilder(); - message.setTitle(String.format("Current queue (showing %d out of %d items):", numberofTracksShown, tracks.size())); - message.setColor(Color.BLUE); + int limit = 10; + AudioTrack current = manager.getCurrentlyPlaying(); + int numberofTracksShown = Math.min(tracks.size(), limit); + + StringBuilder message = new StringBuilder(); + message.append("Here is the current queue of the bot ") + .append("(showing ") + .append(numberofTracksShown) + .append(" out of ") + .append(tracks.size()) + .append(" items):\n"); int index = 0; for (AudioTrack track : tracks) { + index++; AudioTrackInfo info = track.getInfo(); - if (current != null && current.getInfo().title.equals(info.title) && currentlyPlaying) { - message.addField("Track #" + ++index + " (Currently playing)", info.title, false); + message.append("\n"); + if (current != null && current.getInfo().title.equals(info.title)) { + message.append("- ").append("**").append(info.title).append("**"); } else { - message.addField("Track #" + ++index, info.title, false); + message.append("- ").append(info.title); } - if (index == 10) break; + if (index == limit) break; } - message.setTimestamp(OffsetDateTime.now()); - - return message.build(); + return message.toString(); } public static MessageEmbed buildUserTracksMessage(MusicManager manager, Member member) { - List tracks = manager.getScheduler() - .getQueueManager() - .getQueueWithFilter(trackInfo -> member.equals(trackInfo.getRequester())); + List tracks = manager.getQueueManager().getQueueWithFilter(trackInfo -> member.equals(trackInfo.getRequester())); AudioTrack current = manager.getCurrentlyPlaying(); int numberofTracksShown = Math.min(tracks.size(), 10); @@ -158,7 +153,7 @@ public class MessageFunction { message.setColor(Color.BLUE); int index = 0; - for (QueueManager.TrackInfo track : tracks) { + for (TrackInfo track : tracks) { AudioTrackInfo info = track.getTrack().getInfo(); if (current != null && current.getInfo().title.equals(info.title)) { @@ -174,42 +169,86 @@ public class MessageFunction { return message.build(); } - public static ArrayList buildHelpMessage(Member member) { - HashMap help = new HashMap<>(); + public static MessageEmbed buildSearchResultsMessage(List tracks, Member member) { + EmbedBuilder embed = new EmbedBuilder(); - for (AbstractCommand command : CommandManager.commands.values()) { - if (command.getType() == null) + embed.setTitle("Search results"); + embed.setDescription("I found the following tracks for your query, please select which one you want to play:"); + embed.setColor(Color.MAGENTA); + + int i = 0; + for (AudioTrack track : tracks) { + embed.addField(++i + ": ", track.getInfo().title, false); + } + + embed.setFooter(member.getEffectiveName(), member.getEffectiveAvatarUrl()); + embed.setTimestamp(OffsetDateTime.now()); + + return embed.build(); + } + + public static MessageEmbed buildPlaylistLoadMessage(AudioPlaylist playlist, Member member) { + EmbedBuilder embed = new EmbedBuilder(); + + embed.setTitle(String.format("Loading playlist: %s", playlist.getName())); + embed.setDescription(String.format("You are about to load %d tracks from this playlist, are you sure?", playlist.getTracks().size())); + embed.setColor(Color.CYAN); + + embed.setFooter(member.getEffectiveName(), member.getEffectiveAvatarUrl()); + embed.setTimestamp(OffsetDateTime.now()); + + return embed.build(); + } + + public static String buildHelpMessage() { + Map help = new HashMap<>(); + + for (CommandContainer container : CommandManager.commands.values()) { + Command command = container.getCommand(); + if (command.getCategory() == null) { continue; + } - String description = "No description yet"; + if (command.isEnabled() && command.getCategory() != CommandCategory.UNLISTED) { + String description = "No description yet"; - if (command.isEnabled() && command.getType() != CommandType.UNLISTED) { - if (command.getType() == CommandType.ADMINISTRATION && !UserFunction.isAdmin(member)) - continue; - - if (command.getDescription() != null) + if (command.getDescription() != null) { description = command.getDescription(); + } - EmbedBuilder message; - if (help.containsKey(command.getType())) { - message = help.get(command.getType()); + String message; + if (help.containsKey(command.getCategory())) { + message = help.get(command.getCategory()); } else { - help.put(command.getType(), message = new EmbedBuilder()); + help.put(command.getCategory(), message = ""); } if (message.isEmpty()) { - message.setTitle(command.getType().getDescription() + " commands for NetNeutrality"); - message.setColor(Color.BLUE); + message = "## " + command.getCategory() + " commands"; } - message.addField(String.format("%s", command.getCommandName()), description, false); + message += "\n- /" + command.getCommandName() + " -> " + description; + help.put(command.getCategory(), message); } } - ArrayList messages = new ArrayList<>(); - help.values().forEach(embedBuilder -> messages.add(embedBuilder.build())); + String subtext = "\n\n" + + "-# The bot supports both types of commands: slash (/) and message ($), " + + "however not all slash commands might have a corresponding message command.\n" + + "-# To get help for a specific command, please use /help "; - return messages; + return String.join("\n", help.values()) + subtext; + } + + public static String buildCommandHelpMessage(CommandContainer container) { + + String message = "## Command info for /" + container.getCommand().getCommandName(); + if (container.getCommand().getDescription() != null) { + message += "\n- Description: " + container.getCommand().getDescription(); + } + message += "\n" + container.getCommandOptions().printUsage(); + + return message; } public static MessageEmbed buildSentryErrorMessage(Exception ex) { @@ -223,23 +262,34 @@ public class MessageFunction { return message.build(); } - public static MessageEmbed buildDebugMessage(CommandContext context, CommandException error) { + public static MessageEmbed buildDebugMessage(ContextException exception) { + Context context = exception.getContext(); + EmbedBuilder message = new EmbedBuilder(); message.setColor(Color.RED); - message.setTitle(":warning: Exception in command execution"); - message.addField("Caused by command:", context.getCommand().getCommandName(), false); - message.addField("Exception:", error.getMessage(), false); - if (error.getCause() != null) { + message.setTitle(":warning: Exception in context"); + + if (context instanceof CommandContext commandContext) { + message.addField("Caused by command:", commandContext.getCommand().getCommandName(), false); + } + + if (context instanceof TaskContext taskContext) { + message.addField("Caused by task with ID:", taskContext.getId(), false); + } + + message.addField("Exception:", exception.getMessage(), false); + if (exception.getCause() != null) { String cause; - if (error.getCause().getMessage() != null && !error.getCause().getMessage().isEmpty()) { - cause = error.getCause().getMessage(); + if (exception.getCause().getMessage() != null && !exception.getCause().getMessage().isEmpty()) { + cause = exception.getCause().getMessage().substring(0, Math.min(exception.getCause().getMessage().length(), 64)); } else { - cause = error.getCause().getClass().getName(); + cause = exception.getCause().getClass().getName(); } + message.addField("Caused by:", cause, false); } - message.setFooter("Requested by " + context.getGuildUser().getEffectiveName(), context.getGuildUser().getAvatarUrl()); + message.setFooter("User in context: " + context.getDiscordUser().getEffectiveName(), context.getDiscordUser().getAvatarUrl()); message.setTimestamp(OffsetDateTime.now()); return message.build(); @@ -267,19 +317,4 @@ public class MessageFunction { return editEmbed.build(); } - public static void testMessageAgainstBlacklist(Message message, MessageChannel channel, Set cache) { - boolean found = false; - for (String word : cache) { - if (message.getContentRaw().toLowerCase().contains(word)) { - found = true; - break; - } - } - - if (found) { - message.delete().queue(); - channel.sendMessage("Not in my christian server").queue(); - } - } - } diff --git a/src/main/java/me/bobert/netneutrality/utils/MusicUtils.java b/src/main/java/me/bobert/netneutrality/utils/MusicUtils.java index 47a36e7..7dac00a 100644 --- a/src/main/java/me/bobert/netneutrality/utils/MusicUtils.java +++ b/src/main/java/me/bobert/netneutrality/utils/MusicUtils.java @@ -5,20 +5,58 @@ package me.bobert.netneutrality.utils; * https://discord.gg/qUa6zNFZsd */ -import me.bobert.netneutrality.core.context.CommandContext; -import me.bobert.netneutrality.exceptions.CommandExecutionException; +import me.bobert.netneutrality.context.MusicChannelContext; +import me.bobert.netneutrality.context.command.SlashCommandContext; +import me.bobert.netneutrality.core.context.Context; +import me.bobert.netneutrality.exceptions.ContextException; import me.bobert.netneutrality.music.MusicManager; +import net.dv8tion.jda.api.entities.GuildVoiceState; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; public class MusicUtils { - public static boolean checkIfConnected(MusicManager manager, CommandContext context) { - if (context.getGuildUser().getVoiceState() == null) { - throw new CommandExecutionException("Voice state is null, this should not happen", context, null); + public static boolean connect(Context context) { + return connect(context, false); + } + + public static boolean connect(Context context, boolean checkOnly) { + MusicManager manager = MusicManager.get(context.getGuild()); + Member member = context.getGuildUser(); + if (member == null) { + context.reply("Context doesn't have a Guild user, can't continue"); + return false; } - if (!context.getGuildUser().getVoiceState().inAudioChannel()) { + AudioChannel audioChannel = getVoiceChannel(context); + if (audioChannel == null) { + context.reply("You are not in a voice channel"); + return false; + } + + if (!isUserDj(member) && (manager.isConnected() && manager.isNotConnectedTo(audioChannel))) { + context.reply("The bot isn't connected to your voice channel"); + return false; + } + + if (!checkOnly && manager.isNotConnectedTo(audioChannel)) { + manager.connect(audioChannel); + manager.getScheduler().attachContext(new MusicChannelContext((GuildMessageChannel) context.getChannel(), context.getGuildUser())); + } + + return true; + } + + public static boolean canConnect(Context context) { + return connect(context, true); + } + + public static boolean checkIfConnected(Context context) { + MusicManager manager = MusicManager.get(context.getGuild()); + + AudioChannel voiceChannel = getVoiceChannel(context); + if (voiceChannel == null) { context.reply("You are not in a voice channel"); return false; } @@ -28,8 +66,7 @@ public class MusicUtils { return false; } - VoiceChannel vChannel = (VoiceChannel) context.getGuildUser().getVoiceState().getChannel(); - if (manager.isNotConnectedTo(vChannel)) { + if (manager.isNotConnectedTo(voiceChannel)) { context.reply("The bot isn't in your voice channel"); return false; } @@ -37,13 +74,36 @@ public class MusicUtils { return true; } - public static boolean isUserDj(Member member) { - MusicManager manager = MusicManager.get(member.getGuild()); + public static AudioChannel getVoiceChannel(Context context) { + GuildVoiceState memberVoiceState = context.getGuildUser().getVoiceState(); - if (manager.isCurrentlyPlaying()) { - return manager.getRequesterOfCurrentTrack().equals(member); + if (memberVoiceState == null) { + throw new ContextException("Voice state is null", context, null); } - return false; + if (!memberVoiceState.inAudioChannel()) { + return null; + } + + return memberVoiceState.getChannel(); + } + + public static boolean isUserDj(Context context) { + if (context instanceof SlashCommandContext) { + return true; + } + + return isUserDj(context.getGuildUser()); + } + + public static boolean isUserDj(Member member) { + MusicManager manager = MusicManager.get(member.getGuild()); + Member dj = manager.getScheduler().getDj(); + + if (manager.isCurrentlyPlaying() && dj != null) { + return dj.equals(member); + } + + return manager.getQueueSize() == 0 || !manager.isCurrentlyPlaying(); } } diff --git a/src/main/java/me/bobert/netneutrality/utils/Settings.java b/src/main/java/me/bobert/netneutrality/utils/Settings.java index e1084da..2a9415b 100644 --- a/src/main/java/me/bobert/netneutrality/utils/Settings.java +++ b/src/main/java/me/bobert/netneutrality/utils/Settings.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Map; import java.util.Properties; +import java.util.concurrent.TimeUnit; public class Settings { @@ -53,6 +54,10 @@ public class Settings { return INSTANCE; } + public static String getVersion() { + return Settings.getInstance().getMavenVersion(); + } + public String get(String key) { String property = prop.getProperty(key); @@ -72,12 +77,16 @@ public class Settings { return property; } + public String getPrefix() { + return get("NETNEUTRALITY_BOT_PREFIX", "$"); + } + public String getMavenVersion() { String version = null; try { Properties p = new Properties(); - InputStream is = getClass().getResourceAsStream("/app.properties"); + InputStream is = getClass().getResourceAsStream("/application.properties"); if (is != null) { p.load(is); version = p.getProperty("app.version", ""); @@ -96,7 +105,7 @@ public class Settings { String gitlabProjectId = "22"; String latestReleaseUrl = String.format("%s/api/v4/projects/%s/releases/", gitlabHost, gitlabProjectId); - JSONArray json = new WebModule(latestReleaseUrl).request().getAsArray(); + JSONArray json = new WebModule.Builder(latestReleaseUrl).cache(1, TimeUnit.DAYS).build().request().getAsArray(); if (json.isNull(0)) { return "0.0.0"; } diff --git a/src/main/java/me/bobert/netneutrality/utils/UserFunction.java b/src/main/java/me/bobert/netneutrality/utils/UserFunction.java index f3ef127..1f99800 100644 --- a/src/main/java/me/bobert/netneutrality/utils/UserFunction.java +++ b/src/main/java/me/bobert/netneutrality/utils/UserFunction.java @@ -6,12 +6,22 @@ package me.bobert.netneutrality.utils; * https://discord.gg/qUa6zNFZsd */ +import me.bobert.netneutrality.context.command.SlashCommandContext; +import me.bobert.netneutrality.core.context.Context; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; public class UserFunction { - + + public static boolean isAdmin(Context context) { + if (context instanceof SlashCommandContext) { + return true; + } + + return isAdmin(context.getGuildUser()); + } + public static boolean isAdmin(Member user) { boolean isAdmin = false; for(Role role : user.getRoles()) { diff --git a/src/main/resources/app.properties b/src/main/resources/application.properties similarity index 100% rename from src/main/resources/app.properties rename to src/main/resources/application.properties diff --git a/src/main/resources/hibernate.cfg.xml b/src/main/resources/hibernate.cfg.xml index 817a6be..955a01e 100644 --- a/src/main/resources/hibernate.cfg.xml +++ b/src/main/resources/hibernate.cfg.xml @@ -8,12 +8,13 @@ com.mysql.cj.jdbc.Driver org.hibernate.hikaricp.internal.HikariCPConnectionProvider - + - + + \ No newline at end of file diff --git a/src/main/resources/persistable-tasks.json b/src/main/resources/persistable-tasks.json new file mode 100644 index 0000000..556499f --- /dev/null +++ b/src/main/resources/persistable-tasks.json @@ -0,0 +1,19 @@ +{ + "tasks": [ + { + "class": "me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask", + "params": [ + "guildId", + "channelId", + "messageId", + "emojiMapping" + ], + "types": [ + "string", + "string", + "string", + "map" + ] + } + ] +} \ No newline at end of file diff --git a/src/test/java/netneutrality/music/source/UdioAudioSourceManagerTest.java b/src/test/java/me/bobert/netneutrality/music/source/UdioAudioSourceManagerTest.java similarity index 77% rename from src/test/java/netneutrality/music/source/UdioAudioSourceManagerTest.java rename to src/test/java/me/bobert/netneutrality/music/source/UdioAudioSourceManagerTest.java index 745e5e2..438a1db 100644 --- a/src/test/java/netneutrality/music/source/UdioAudioSourceManagerTest.java +++ b/src/test/java/me/bobert/netneutrality/music/source/UdioAudioSourceManagerTest.java @@ -1,4 +1,4 @@ -package netneutrality.music.source; +package me.bobert.netneutrality.music.source; /* * Copyright (c) anthorob * @@ -8,12 +8,10 @@ package netneutrality.music.source; import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioReference; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; -import me.bobert.netneutrality.music.source.UdioAudioSourceManager; -import me.bobert.netneutrality.music.source.UdioAudioTrack; -import org.junit.Test; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; public class UdioAudioSourceManagerTest { diff --git a/src/test/java/me/bobert/netneutrality/utils/SettingsTest.java b/src/test/java/me/bobert/netneutrality/utils/SettingsTest.java new file mode 100644 index 0000000..076025a --- /dev/null +++ b/src/test/java/me/bobert/netneutrality/utils/SettingsTest.java @@ -0,0 +1,21 @@ +package me.bobert.netneutrality.utils; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SettingsTest { + + @Test + public void getMavenVersion() { + String version = Settings.getInstance().getMavenVersion(); + + assertNotNull(version); + } +} \ No newline at end of file diff --git a/src/test/java/netneutrality/utils/SettingsTest.java b/src/test/java/netneutrality/utils/SettingsTest.java deleted file mode 100644 index 3e13580..0000000 --- a/src/test/java/netneutrality/utils/SettingsTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package netneutrality.utils; - -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import me.bobert.netneutrality.utils.Settings; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class SettingsTest { - - @Test - public void getMavenVersion() { - String version = Settings.getInstance().getMavenVersion(); - - assertNotNull(version); - } - - @Test - public void getGitVersion() { - String version = Settings.getInstance().getGitVersion(); - - assertNotNull(version); - } -} \ No newline at end of file diff --git a/startup.sh b/startup.sh index 7af7a26..96f6dc0 100644 --- a/startup.sh +++ b/startup.sh @@ -1,9 +1,5 @@ #!/bin/sh -prop() { - grep "${1}" bot.properties | cut -d'=' -f2 -} - _term() { echo "Caught SIGTERM/SIGINT signal, graceful shutdown of bot.." if [ -z "$pid" ]; then @@ -29,11 +25,11 @@ if [ -z $API_PORT ]; then fi if [ -z $MYSQL_HOST ]; then - MYSQL_HOST="$(prop 'MYSQL_HOST')" + MYSQL_HOST="localhost" fi if [ -z $MYSQL_PORT ]; then - MYSQL_PORT=$(prop 'MYSQL_PORT') + MYSQL_PORT=3306 fi echo "Waiting 10 seconds for database to start.."