Version 3.0

This commit is contained in:
anthorob committed 2025-03-09 20:08:43 +00:00
1 parent 5fb29ea4ed
commit 1242118b0d
174 files changed
+8366 -3263

No files matched your search

+7 -1
View File
@@ -10,4 +10,10 @@ MYSQL_PASSWORD=
MYSQL_DATABASE=
MYSQL_PORT=3306
NETNEUTRALITY_SENTRY_DSN=
NETNEUTRALITY_PO_TOKEN=
NETNEUTRALITY_VISITOR_DATA=
NETNEUTRALITY_SENTRY_DSN=
GITLAB_HOST=
GITLAB_TOKEN=
GITLAB_PROJECT_ID=
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JpaPluginProjectSettings">
<option name="lastSelectedLanguage" value="Java" />
</component>
</project>
-29
View File
@@ -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
+1 -1
View File
@@ -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 ./
+7 -1
View File
@@ -10,4 +10,10 @@ MYSQL_PASSWORD=
MYSQL_DATABASE=
MYSQL_PORT=3306
NETNEUTRALITY_SENTRY_DSN=
NETNEUTRALITY_PO_TOKEN=
NETNEUTRALITY_VISITOR_DATA=
NETNEUTRALITY_SENTRY_DSN=
GITLAB_HOST=
GITLAB_TOKEN=
GITLAB_PROJECT_ID=
Regular → Executable
View File
File mode changed.
+2 -1
View File
@@ -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'
+43 -14
View File
@@ -6,7 +6,7 @@
<groupId>me.bobert</groupId>
<artifactId>netneutrality</artifactId>
<version>2.0.1</version>
<version>3.0.0</version>
<name>netneutrality-discord-bot</name>
<description>Discord bot</description>
<packaging>jar</packaging>
@@ -28,6 +28,16 @@
</resources>
<plugins>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>10.17.0</version>
<configuration>
<url>jdbc:mysql://localhost:3306/netneutrality</url>
<user>root</user>
<password></password>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
@@ -84,24 +94,23 @@
<name>Lavalink Repository</name>
<url>https://maven.lavalink.dev/releases</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.0.0</version>
<version>5.1.2</version>
</dependency>
<dependency>
<groupId>dev.arbjerg</groupId>
<artifactId>lavaplayer</artifactId>
<version>2.1.2</version>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>dev.lavalink.youtube</groupId>
<artifactId>v2</artifactId>
<version>1.4.0</version>
<version>1.11.5</version>
</dependency>
<dependency>
@@ -113,23 +122,43 @@
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.1.7.Final</version>
<version>6.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-hikaricp</artifactId>
<version>6.1.7.Final</version>
<version>6.5.2.Final</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
<version>9.0.0</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>10.17.0</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>10.17.0</version>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry</artifactId>
<version>7.6.0</version>
<version>7.12.1</version>
</dependency>
<dependency>
<groupId>org.gitlab4j</groupId>
<artifactId>gitlab4j-api</artifactId>
<version>6.0.0-rc.5</version>
</dependency>
<dependency>
<groupId>net.fellbaum</groupId>
<artifactId>jemoji</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
@@ -163,12 +192,12 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
<version>3.15.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
@@ -177,9 +206,9 @@
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
</dependency>
</dependencies>
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"renovate/renovate-config"
]
}
+55 -54
View File
@@ -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 */;
@@ -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();
}
}
}
@@ -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<String, AbstractCommand> 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<Class<? extends AbstractCommand>> classes = reflections.getSubTypesOf(AbstractCommand.class);
for (Class<? extends AbstractCommand> 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<CommandData> 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();
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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<OBlacklist> blacklists = service.getBlacklistForGuild();
context.getHook().editOriginal("Retrieving list, please wait...").queue(t -> {
ArrayList<OBlacklist> 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();
@@ -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<EmojiReactionRoleTask> 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<String, String> 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();
}
}
@@ -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<MessageEmbed> 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<MessageEmbed> help = MessageFunction.buildHelpMessage(context.getGuildUser());
context.sendMessageInPrivate(help);
return CommandExecutionResult.success("Help has been sent in DM!");
}
}
@@ -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()));
}
@@ -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<Role> 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<TextChannel> 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 <value between 0 and 100>'");
}
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<String, String> settings = guildManager.getGuildConfigManager().getSettings();
Map<String, String> 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);
}
}
@@ -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;
}
}
}
@@ -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.");
}
}
@@ -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?");
}
}
@@ -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();
}
}
@@ -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));
}
@@ -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");
}
@@ -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();
@@ -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();
}
}
@@ -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);
}
}
}
@@ -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();
}
}
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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();
}
}
@@ -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);
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (hook != null) {
hook.sendMessage(messageCreateData).queue();
}
if (message != null) {
message.reply(messageCreateData).queue();
}
}
@Override
public void defer(boolean ephemeral, Consumer<AsyncContext> consumer) {
}
@Override
public void ack(boolean ephemeral) {
}
@Override
public void edit(MessageEditData messageEditData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (hook != null) {
hook.editOriginal(messageEditData).queue();
}
if (message != null) {
message.editMessage(messageEditData).queue();
}
}
}
@@ -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<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (getChannel().canTalk()) {
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
}
}
}
@@ -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<CommandOption> parseOptions(String[] args, List<OptionData> commandOptions) {
List<CommandOption> 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<ChannelType> requiredChannelTypes = data.getChannelTypes();
List<Command.Choice> 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<Command.Choice> 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<Command.Choice> 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<Command.Choice> 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<Command.Choice> choices) {
StringJoiner joiner = new StringJoiner(", ");
for (Command.Choice choice : choices) {
joiner.add(choice.getAsString());
}
return joiner.toString();
}
private String validTypes(Set<ChannelType> 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();
}
}
@@ -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<CommandOption> parseOptions(String[] args, List<OptionData> 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();
}
}
@@ -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<CommandOption> parseOptions(String[] args, List<OptionData> 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();
}
}
@@ -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<CommandOption> parseOptions(String[] args, List<OptionData> 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();
}
}
@@ -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();
}
}
@@ -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<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (getChannel() != null && getChannel().canTalk()) {
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
}
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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<String, GuildManager> managers = GuildManager.getManagers();
Map<String, GuildService> 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);
@@ -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());
});
}
}
@@ -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<String, String> settingsCache = new HashMap<>();
public GuildConfigManager(Guild guild, OGuild guildEntity) {
this.guild = guild;
this.guildEntity = guildEntity;
reloadSettingsCache();
}
public Map<String, String> 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<OGuildSetting> settingSet = guildEntity.getGuildSettings();
for (OGuildSetting setting : settingSet) {
settingsCache.put(setting.getName(), setting.getValue());
}
});
}
}
@@ -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<String, GuildManager> managers = new ConcurrentHashMap<>();
private static final SessionFactory sessionFactory = SessionManager.getSessionFactory();
private final Set<String> 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<String, GuildManager> getManagers() {
return managers;
}
public Guild getGuild() {
return guild;
}
public OGuild getGuildEntity() {
return guildEntity;
}
public Set<String> 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<OBlacklist> 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());
}
}
@@ -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));
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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;
@@ -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);
}
@@ -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);
}
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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<String, CommandContainer> 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<Class<? extends Command>> classes = new Reflections("me.bobert.netneutrality.command").getSubTypesOf(Command.class);
for (Class<? extends Command> 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<CommandData> 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();
}
}
@@ -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<OptionData, String> 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<SubcommandGroupData> groups = commandOptions.getSubcommandGroups();
List<SubcommandData> subcommands = commandOptions.getSubcommands();
List<OptionData> 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<OptionData> options) {
if (context instanceof InteractionContext) {
context.setCurrentCommandOptions(args, options);
return true; // slash commands are validated by discord
}
List<String> 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<String> 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());
});
}
}
@@ -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.
* <br>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.
* <br>This will automatically convert the value to a string if the type is not {@link OptionType#STRING OptionType.STRING}.
* <br>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.
* <br>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.
* <br>This will be the ID of any resolved entity such as {@link Role} or {@link Member}.
*
* <p><b>It is highly recommended to assert int values by using {@link OptionData#setRequiredRange(long, long)}</b>
*
* @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.
* <br>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.
* <br>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();
}
@@ -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<OptionData> options;
private final List<SubcommandData> subcommands;
private final List<SubcommandGroupData> 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<OptionData> getOptions() {
return options;
}
public List<SubcommandData> getSubcommands() {
return subcommands;
}
public List<SubcommandGroupData> 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<OptionData> 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<net.dv8tion.jda.api.interactions.commands.Command.Choice> 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<ChannelType> 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();
}
}
@@ -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<DiscordLocale, String> 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<DiscordLocale, String> 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<? super OptionData> condition) {
return commandData.removeOptions(condition);
}
@Override
public boolean removeSubcommands(@Nonnull Predicate<? super SubcommandData> condition) {
return commandData.removeSubcommands(condition);
}
@Override
public boolean removeSubcommandGroups(@Nonnull Predicate<? super SubcommandGroupData> condition) {
return commandData.removeSubcommandGroups(condition);
}
@Override
public @Nonnull List<SubcommandData> getSubcommands() {
return commandData.getSubcommands();
}
@Override
public @Nonnull List<SubcommandGroupData> getSubcommandGroups() {
return commandData.getSubcommandGroups();
}
@Override
public @Nonnull List<OptionData> 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();
}
}
@@ -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 +
'}';
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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 <OptionData, String> invalidOptions = new HashMap<>();
private final List<CommandOption> 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<OptionData> 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<Map.Entry<OptionData, String>> getInvalidOptions() {
return invalidOptions.entrySet();
}
public <T> T getOption(String name, Supplier<? extends T> fallback, Function<? super CommandOption, ? extends T> 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> T getOption(String name, T fallback, Function<? super CommandOption, ? extends T> resolver) {
Objects.requireNonNull(resolver, "resolver cannot be null");
CommandOption mapping = getOption(name);
if (mapping != null)
return resolver.apply(mapping);
return fallback;
}
public <T> T getOption(String name, Function<? super CommandOption, ? extends T> resolver) {
return getOption(name, null, resolver);
}
public CommandOption getOption(String name) {
List<CommandOption> options = getOptionsByName(name);
return options.isEmpty() ? null : options.get(0);
}
public List<CommandOption> 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<CommandOption> parseOptions(String[] args, List<OptionData> commandOptions);
public abstract String parseCommandName();
public abstract String getFullCommand();
}
@@ -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<LayoutComponent> components, Consumer<AsyncContext> consumer);
void defer(boolean ephemeral, Consumer<AsyncContext> 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<LayoutComponent> components) {
reply(messageCreateData, false, components, asyncMessage -> {});
}
default void reply(MessageCreateData messageCreateData, Consumer<AsyncContext> consumer) {
reply(messageCreateData, false, List.of(), consumer);
}
default void reply(String message) {
replyWithLimit(message);
}
default void reply(String message, Consumer<AsyncContext> consumer) {
reply(MessageCreateData.fromContent(message), consumer);
}
default void reply(MessageEmbed embed) {
reply(List.of(embed));
}
default void reply(List<MessageEmbed> embeds) {
reply(MessageCreateData.fromEmbeds(embeds));
}
default void edit(MessageEditData messageEditData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> 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<LayoutComponent> 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<MessageEmbed> embeds, List<LayoutComponent> components) {
edit(MessageEditData.fromEmbeds(embeds), components);
}
default void edit(MessageEmbed embed, List<LayoutComponent> components) {
edit(List.of(embed), components);
}
default void async(MessageCreateData messageCreateData, Consumer<AsyncContext> consumer) {
reply(messageCreateData, false, List.of(), consumer);
}
default void async(String message, Consumer<AsyncContext> consumer) {
async(MessageCreateData.fromContent(message), consumer);
}
default void async(Consumer<AsyncContext> consumer) {
defer(false, consumer);
}
default void replyEphemeral(MessageCreateData messageCreateData, List<LayoutComponent> 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<LayoutComponent> components) {
replyEphemeral(MessageCreateData.fromContent(message), components);
}
default void replyEphemeral(List<MessageEmbed> embeds) {
replyEphemeral(MessageCreateData.fromEmbeds(embeds));
}
default void sendMessageInPrivate(MessageCreateData messageCreateData, List<LayoutComponent> components, Consumer<AsyncContext> 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<LayoutComponent> components, Consumer<AsyncContext> consumer) {
sendMessageInPrivate(MessageCreateData.fromContent(message), components, consumer);
}
default void sendMessageInPrivate(MessageCreateData messageCreateData, List<LayoutComponent> components) {
sendMessageInPrivate(messageCreateData, components, asyncContext -> {});
}
default void sendMessageInPrivate(String message, Consumer<AsyncContext> consumer) {
sendMessageInPrivate(message, List.of(), consumer);
}
default void sendMessageInPrivate(MessageCreateData messageCreateData) {
sendMessageInPrivate(messageCreateData, List.of());
}
default void sendMessageInPrivate(List<MessageEmbed> 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));
}
}
}
@@ -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<AsyncContext> 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<LayoutComponent> components, Consumer<AsyncContext> 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);
}
}
}
@@ -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<? extends MessageEmbed> 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());
}
}
@@ -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<LayoutComponent> components, Consumer<AsyncContext> 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)));
}
}
}
@@ -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<AsyncContext> consumer) {
reply("Bot is thinking...", consumer);
}
@Override
default void ack(boolean ephemeral) {
}
}
@@ -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<? extends MessageEmbed> 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);
}
}
@@ -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<String, Object> 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);
}
}
@@ -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);
}
}
}
@@ -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();
@@ -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);
}
@@ -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();
}
}
@@ -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());
}
}
@@ -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<? extends BuiltInTask> 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();
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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);
}
@@ -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();
}
@@ -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.
* <p>
* 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.
* </p>
*
* @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();
/**
* <p>If this returns false, the task will not get inserted and executed.</p>
*/
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();
/**
* <p>This method is used to reply to the interaction that triggered this task.</p>
* <p>If the task is implementing {@link WithChildren}, the child tasks must be instanciated in this method.</p>
*/
void replyToInteraction(CommandContext context);
/**
* Executes the task.
* @param context The context for this task.
*/
Lifecycle execute(TaskContext context);
void insert(CommandContext context);
}
@@ -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<String, Task> tasks;
private final List<Task> pendingTasks;
private final Map<String, CommandContext> deletableTasksContext;
private final Map<String, ReentrantLock> 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<Task> 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<Task> 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 <E extends Task> Optional<E> findTaskByPartialId(Class<E> clazz, String partialId) {
List<E> tasks = findTasksByPartialId(clazz, partialId);
return Optional.ofNullable(tasks.isEmpty() ? null : tasks.get(0));
}
public <E extends Task> List<E> findTasksByPartialId(Class<E> 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<? extends BuiltInTask> 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<Class<? extends BuiltInTask>> classes = new Reflections("me.bobert.netneutrality.tasks").getSubTypesOf(BuiltInTask.class);
for (Class<? extends BuiltInTask> 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;
}
}
}
@@ -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<Task> 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));
}
}
@@ -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<String, String> 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<String, String> getEmojiMapping() {
return emojiMapping;
}
@Override
public String getId() {
return String.format(EmojiTaskContext.ID, getGuildId(), getMessageId());
}
@Override
public Button createButton() {
return null;
}
}
@@ -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);
}
}
@@ -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<Emoji> 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);
}
}
}
@@ -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();
};
}
}
@@ -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<String, TaskCreator> 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);
}
}
}
@@ -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();
}
@@ -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 {
}
@@ -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<Task> getChildTasks();
default void addChild(List<Task> tasks) {
getChildTasks().addAll(tasks);
}
default void addChild(Task... tasks) {
addChild(List.of(tasks));
}
}
@@ -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<E extends WithChildren> 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();
}
@@ -1,4 +1,4 @@
package me.bobert.netneutrality.thread;
package me.bobert.netneutrality.core.thread;
/*
* Copyright (c) anthorob
Loaded 100 of 174 files, more files were not shown because too many files have changed in this diff. Show more