Bunch of improvements to tasks and DB logic

Use locks when executing tasks
Decouple GuildManager into services
Add more logging when failing to load a persistable task
Add ping method to WebModule
Improve Task interface and implementations
Start work on step tasks
This commit is contained in:
anthorob committed 2025-02-14 05:43:49 -05:00
1 parent 72d8bcef20
commit 6429e42384
73 files changed
+1551 -1005

No files matched your search

@@ -45,7 +45,7 @@ public class NetNeutrality {
options.setEnvironment(env); options.setEnvironment(env);
options.setTracesSampleRate(1.0); options.setTracesSampleRate(1.0);
options.setDebug(false); options.setDebug(false);
options.setRelease(VERSION); options.setRelease(Settings.getVersion());
}); });
} }
@@ -11,7 +11,8 @@ import me.bobert.netneutrality.core.command.options.CommandOption;
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.db.entities.OBlacklist; import me.bobert.netneutrality.db.entities.OBlacklist;
import me.bobert.netneutrality.db.repository.BlacklistRepository; 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.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
@@ -54,14 +55,14 @@ public class BlacklistCommand extends AbstractAdminCommand {
public CommandExecutionResult execute(CommandContext context) { public CommandExecutionResult execute(CommandContext context) {
String sub = context.getSubcommand(); String sub = context.getSubcommand();
String word = context.getOption("word", "", CommandOption::getAsString); String word = context.getOption("word", "", CommandOption::getAsString);
BlacklistRepository repository = new BlacklistRepository(); BlacklistService service = GuildService.get(context.getGuild()).getBlacklistService();
switch (sub) { switch (sub) {
case "get" -> context.async("Retrieving list, please wait...", asyncMessage -> { case "get" -> context.async("Retrieving list, please wait...", asyncMessage -> {
List<OBlacklist> blacklists = repository.getBlacklistForGuild(context.getGuild()); List<OBlacklist> blacklists = service.getBlacklistForGuild();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("List of blacklisted words for this server [").append(context.getGuild().getName()).append("]: \n"); sb.append("List of blacklisted words for this server: \n");
for (OBlacklist bl : blacklists) { for (OBlacklist bl : blacklists) {
sb.append(bl.getWord()).append(" -> ").append(bl.getEnabled() ? "enabled" : "disabled").append("\n"); sb.append(bl.getWord()).append(" -> ").append(bl.getEnabled() ? "enabled" : "disabled").append("\n");
} }
@@ -73,10 +74,10 @@ public class BlacklistCommand extends AbstractAdminCommand {
asyncMessage.edit(sb.toString()); asyncMessage.edit(sb.toString());
}); });
case "add" -> { case "add" -> {
return repository.add(context.getGuild(), context.getGuildUser(), word); return service.add(context.getGuildUser(), word);
} }
case "remove" -> { case "remove" -> {
return repository.delete(context.getGuild(), word); return service.delete(word);
} }
} }
@@ -17,6 +17,9 @@ 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.SubcommandData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
import java.util.List;
import java.util.Map;
public class ReactionCommand extends AbstractAdminCommand { public class ReactionCommand extends AbstractAdminCommand {
@Override @Override
@@ -43,6 +46,7 @@ public class ReactionCommand extends AbstractAdminCommand {
public void registerCommandOptions(CommandOptionsBuilder builder) { public void registerCommandOptions(CommandOptionsBuilder builder) {
builder.addSubcommandGroups( builder.addSubcommandGroups(
new SubcommandGroupData("emoji", "Give roles to users using emojis").addSubcommands( 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 SubcommandData("setup", "Add a role to a message").addOptions(
new OptionData(OptionType.STRING, "messageid", "The ID of the message", true) new OptionData(OptionType.STRING, "messageid", "The ID of the message", true)
.setRequiredLength(19, 19), .setRequiredLength(19, 19),
@@ -77,65 +81,78 @@ public class ReactionCommand extends AbstractAdminCommand {
String group = context.getSubcommandGroup(); String group = context.getSubcommandGroup();
String sub = context.getSubcommand(); String sub = context.getSubcommand();
switch (group) { return switch (group) {
case "emoji" -> { case "emoji" -> executeEmoji(context, guildId, messageId, sub);
if ("setup".equals(sub)) { default -> CommandExecutionResult.none();
String emoji = context.getOption("emoji", CommandOption::getAsString); };
String role = context.getOption("role", CommandOption::getAsString); }
EmojiReactionRoleTask task = new EmojiReactionRoleTask(guildId, messageId); private CommandExecutionResult executeEmoji(CommandContext context, String guildId, String messageId, String sub) {
if (TaskManager.getInstance().exists(task.getId())) { String id = String.format("%s:%s", guildId, messageId);
return CommandExecutionResult.error("The message ID is already registered, use the add subcommand instead."); EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id);
}
task.addEmoji(emoji, role);
TaskManager.getInstance().insert(context, task); switch (sub) {
return CommandExecutionResult.success("Added emoji to message successfully!"); case "list" -> {
List<EmojiReactionRoleTask> tasks = TaskManager.getInstance().findTasksByPartialId(EmojiReactionRoleTask.class, guildId);
if (tasks.isEmpty()) {
return CommandExecutionResult.info("No tasks found for this guild");
} }
if ("add".equals(sub)) { StringBuilder builder = new StringBuilder();
String emoji = context.getOption("emoji", CommandOption::getAsString); builder.append("I have found the following tasks for this guild:");
String role = context.getOption("role", CommandOption::getAsString);
String id = String.format("%s:%s", guildId, messageId); for (EmojiReactionRoleTask item : tasks) {
EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id); builder.append("\n- Task ID: ").append(item.getId()).append("\n")
if (task == null) { .append(" Message ID: ").append(item.getMessageId()).append("\n")
return CommandExecutionResult.error("The message ID provided isn't registered with the bot"); .append(" Emoji Mapping: ").append(item.getEmojiMapping()).append("\n");
}
task.addEmoji(emoji, role);
TaskManager.getInstance().getTaskService().persistTask(task);
return CommandExecutionResult.success("Added emoji to message successfully!");
} }
if ("remove".equals(sub)) { return CommandExecutionResult.success(builder.toString());
String emoji = context.getOption("emoji", CommandOption::getAsString); }
case "setup" -> {
String id = String.format("%s:%s", guildId, messageId); if (task != null) {
EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id); return CommandExecutionResult.error("The message ID is already registered, use the add subcommand instead.");
if (task == null) {
return CommandExecutionResult.error("The message ID provided isn't registered with the bot");
}
task.removeEmoji(emoji);
TaskManager.getInstance().getTaskService().persistTask(task);
return CommandExecutionResult.success("Removed emoji from message successfully!");
} }
if ("delete".equals(sub)) { String emoji = context.getOption("emoji", CommandOption::getAsString);
String id = String.format("%s:%s", guildId, messageId); String role = context.getOption("role", CommandOption::getAsString);
EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id); task = new EmojiReactionRoleTask(guildId, messageId, Map.of(emoji, role));
if (task == null) { task.insert(context);
return CommandExecutionResult.error("The message ID provided isn't registered with the bot");
}
TaskManager.getInstance().getTaskService().unpersistTask(task); return CommandExecutionResult.success("Added emoji to message successfully!");
return CommandExecutionResult.success("Unregistered the message ID successfully!"); }
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);
task.save();
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);
task.save();
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(); return CommandExecutionResult.none();
} }
} }
@@ -61,9 +61,9 @@ public class HelpCommand extends AbstractCommand {
public CommandExecutionResult execute(CommandContext context) { public CommandExecutionResult execute(CommandContext context) {
String command = context.getOption("command", null, CommandOption::getAsString); String command = context.getOption("command", null, CommandOption::getAsString);
if (command == null) { if (command == null) {
Button commands = BuiltInTask.createButton(CommandHelp.class).primary("Commands"); Button commands = BuiltInTask.createButton(CommandHelp.class);
Button info = BuiltInTask.createButton(BotInfo.class).primary("Info"); Button info = BuiltInTask.createButton(BotInfo.class);
Button reportbug = BuiltInTask.createButton(ReportBug.class).danger("Report bug"); Button reportbug = BuiltInTask.createButton(ReportBug.class);
ActionRow row = ActionRow.of(commands, info, reportbug); 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)); context.replyEphemeral("Thanks for using the Net Neutrality Bot! What do you need help with?", List.of(row));
return CommandExecutionResult.none(); return CommandExecutionResult.none();
@@ -5,13 +5,13 @@ package me.bobert.netneutrality.command.bot;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.command.AbstractAdminCommand; import me.bobert.netneutrality.core.command.AbstractAdminCommand;
import me.bobert.netneutrality.core.command.CommandExecutionResult; import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.command.options.CommandOption; import me.bobert.netneutrality.core.command.options.CommandOption;
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.db.GSettings; import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.music.MusicManager;
import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
@@ -63,7 +63,7 @@ public class ConfigureCommand extends AbstractAdminCommand {
@Override @Override
public CommandExecutionResult execute(CommandContext context) { public CommandExecutionResult execute(CommandContext context) {
String sub = context.getSubcommand(); String sub = context.getSubcommand();
GuildManager guildManager = GuildManager.getManager(context.getGuild()); GuildService guildService = GuildService.get(context.getGuild().getId());
context.ephemeral(); context.ephemeral();
if ("music-role".equals(sub)) { if ("music-role".equals(sub)) {
@@ -71,13 +71,13 @@ public class ConfigureCommand extends AbstractAdminCommand {
assert role != null; assert role != null;
if (role.getPosition() == -1) { 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"); return CommandExecutionResult.success("Everyone can now use music commands");
} }
if (role.getGuild().getId().equals(context.getGuild().getId())) { 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())); return CommandExecutionResult.success(String.format("Successfully set %s as the role needed for music command", role.getAsMention()));
} }
@@ -87,14 +87,14 @@ public class ConfigureCommand extends AbstractAdminCommand {
GuildChannelUnion channel = context.getOption("channel", CommandOption::getAsChannel); GuildChannelUnion channel = context.getOption("channel", CommandOption::getAsChannel);
if (channel == null) { 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"); return CommandExecutionResult.success("No channel restrictions for music commands");
} }
if (channel instanceof TextChannel) { if (channel instanceof TextChannel) {
if (channel.getGuild().getId().equals(context.getGuild().getId())) { 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())); return CommandExecutionResult.success(String.format("Successfully set %s as the channel required to execute music command", channel.getAsMention()));
} }
@@ -104,7 +104,7 @@ public class ConfigureCommand extends AbstractAdminCommand {
if ("music-volume".equals(sub)) { if ("music-volume".equals(sub)) {
int volume = context.getOption("volume", 30, CommandOption::getAsInt); int volume = context.getOption("volume", 30, CommandOption::getAsInt);
guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_VOLUME, String.valueOf(volume)); guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_VOLUME, String.valueOf(volume));
MusicManager.get(context.getGuild()).setVolume(volume); MusicManager.get(context.getGuild()).setVolume(volume);
return CommandExecutionResult.success(String.format("Default volume for music player is now %d", volume)); return CommandExecutionResult.success(String.format("Default volume for music player is now %d", volume));
@@ -112,13 +112,13 @@ public class ConfigureCommand extends AbstractAdminCommand {
if ("error-logging".equals(sub)) { if ("error-logging".equals(sub)) {
boolean debug = context.getOption("debug", true, CommandOption::getAsBoolean); boolean debug = context.getOption("debug", true, CommandOption::getAsBoolean);
guildManager.setDebug(debug); guildService.updateDebugMode(debug);
return CommandExecutionResult.success("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)) { if ("show-all".equals(sub)) {
Map<String, String> settings = guildManager.getGuildConfigManager().getSettings(); Map<String, String> settings = guildService.getGuildSettingService().getSettings();
MessageCreateBuilder builder = new MessageCreateBuilder(); MessageCreateBuilder builder = new MessageCreateBuilder();
for (GSettings o : GSettings.values()) { for (GSettings o : GSettings.values()) {
@@ -11,10 +11,12 @@ import me.bobert.netneutrality.core.command.CommandCategory;
import me.bobert.netneutrality.core.command.CommandExecutionResult; import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.tasks.TaskManager; import me.bobert.netneutrality.http.WebModule;
import me.bobert.netneutrality.tasks.git.ReportIssueTask; import me.bobert.netneutrality.tasks.git.ReportIssueTask;
import me.bobert.netneutrality.utils.Settings; import me.bobert.netneutrality.utils.Settings;
import java.util.concurrent.TimeUnit;
public class ReportIssueCommand extends AbstractCommand { public class ReportIssueCommand extends AbstractCommand {
@Override @Override
public boolean needAdmin() { public boolean needAdmin() {
@@ -66,8 +68,10 @@ public class ReportIssueCommand extends AbstractCommand {
return CommandExecutionResult.error("Command is not configured, failed to create issue."); return CommandExecutionResult.error("Command is not configured, failed to create issue.");
} }
TaskManager.getInstance().insert(context, new ReportIssueTask(context instanceof SlashCommandContext)); if (new WebModule.Builder(gitlabHost).ping().timeout(2, TimeUnit.SECONDS).build().request().isSuccess()) {
return CommandExecutionResult.task(new ReportIssueTask(context));
}
return CommandExecutionResult.none(); return CommandExecutionResult.error("Unable to create bug reports at this time, please try again later.");
} }
} }
@@ -34,6 +34,14 @@ public class AsyncContext extends AbstractContext {
this.message = message; this.message = message;
} }
public String getMessageId() {
if (hook != null) {
return hook.getId();
}
return message.getId();
}
@Override @Override
public Event getEvent() { public Event getEvent() {
return null; return null;
@@ -60,6 +68,10 @@ public class AsyncContext extends AbstractContext {
@Override @Override
public boolean isAcknowledged() { public boolean isAcknowledged() {
if (hook != null) {
return hook.getInteraction().isAcknowledged();
}
return true; return true;
} }
@@ -6,19 +6,17 @@ package me.bobert.netneutrality.context;
*/ */
import me.bobert.netneutrality.core.context.AbstractContext; 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.Member;
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.GuildMessageChannel;
import net.dv8tion.jda.api.events.Event; 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.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.MessageCreateData;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
public class MusicChannelContext extends AbstractContext { public class MusicChannelContext extends AbstractContext implements NoMessageContext {
public MusicChannelContext(GuildMessageChannel channel, Member member) { public MusicChannelContext(GuildMessageChannel channel, Member member) {
super(channel.getJDA(), channel.getGuild(), channel, member, member.getUser()); super(channel.getJDA(), channel.getGuild(), channel, member, member.getUser());
@@ -34,21 +32,6 @@ public class MusicChannelContext extends AbstractContext {
return String.format("music:%s", getChannel().getId()); return String.format("music:%s", getChannel().getId());
} }
@Override
public ChannelType getType() {
return getChannel().getType();
}
@Override
public InteractionHook getInteractionHook() {
return null;
}
@Override
public boolean isAcknowledged() {
return true;
}
@Override @Override
public boolean isFromGuild() { public boolean isFromGuild() {
return true; return true;
@@ -59,25 +42,10 @@ public class MusicChannelContext extends AbstractContext {
} }
@Override
public void reply(Modal modal) {
}
@Override @Override
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) { public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (getChannel().canTalk()) { if (getChannel().canTalk()) {
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message))); getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
} }
} }
@Override
public void defer(boolean ephemeral, Consumer<AsyncContext> consumer) {
reply("Bot is thinking", consumer);
}
@Override
public void ack(boolean ephemeral) {
}
} }
@@ -5,31 +5,25 @@ package me.bobert.netneutrality.context.command;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.context.AsyncContext;
import me.bobert.netneutrality.core.command.options.CommandOption; import me.bobert.netneutrality.core.command.options.CommandOption;
import me.bobert.netneutrality.core.command.options.MessageCommandOption; import me.bobert.netneutrality.core.command.options.MessageCommandOption;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.MessageContext;
import me.bobert.netneutrality.utils.Settings; import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.OptionType; 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.OptionData;
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.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.StringJoiner; import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public class MessageCommandContext extends CommandContext { public class MessageCommandContext extends CommandContext implements MessageContext {
private final MessageReceivedEvent event; private final MessageReceivedEvent event;
private final Message message; private final Message message;
@@ -54,63 +48,10 @@ public class MessageCommandContext extends CommandContext {
} }
@Override @Override
public ChannelType getType() {
return event.getChannelType();
}
public Message getMessage() { public Message getMessage() {
return message; return message;
} }
@Override
public boolean isAcknowledged() {
return true;
}
@Override
public boolean isFromGuild() {
return event.isFromGuild();
}
@Override
public InteractionHook getInteractionHook() {
return null;
}
@Override
public void defer(boolean ephemeral, Consumer<AsyncContext> consumer) {
reply("Bot is thinking...", consumer);
}
@Override
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (ephemeral) {
sendMessageInPrivate(messageCreateData, components);
return;
}
if (getChannel() != null && getChannel().canTalk()) {
message.reply(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
}
}
@Override
public void reply(Modal modal) {
}
@Override
public void ack(boolean ephemeral) {
}
@Override
public void deleteMessage() {
if (message != null && getType() != ChannelType.PRIVATE) {
message.delete().queueAfter(1, TimeUnit.SECONDS);
}
}
@Override @Override
public List<CommandOption> parseOptions(String[] args, List<OptionData> commandOptions) { public List<CommandOption> parseOptions(String[] args, List<OptionData> commandOptions) {
List<CommandOption> options = new ArrayList<>(); List<CommandOption> options = new ArrayList<>();
@@ -6,6 +6,7 @@ package me.bobert.netneutrality.context.tasks;
*/ */
import me.bobert.netneutrality.context.AsyncContext; import me.bobert.netneutrality.context.AsyncContext;
import me.bobert.netneutrality.core.context.NoMessageContext;
import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.context.TaskContext;
import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
@@ -17,7 +18,8 @@ import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
public class EmojiTaskContext extends TaskContext { public class EmojiTaskContext extends TaskContext implements NoMessageContext {
public static final String ID = "et:%s:%s";
private final MessageReactionAddEvent event; private final MessageReactionAddEvent event;
public EmojiTaskContext(MessageReactionAddEvent event) { public EmojiTaskContext(MessageReactionAddEvent event) {
@@ -32,29 +34,9 @@ public class EmojiTaskContext extends TaskContext {
return event; return event;
} }
@Override
public ChannelType getType() {
return event.getChannelType();
}
@Override @Override
public String getId() { public String getId() {
return String.format("%s:%s", getGuild().getId(), event.getMessageId()); return String.format(ID, getGuild().getId(), event.getMessageId());
}
@Override
public InteractionHook getInteractionHook() {
return null;
}
@Override
public void defer(boolean ephemeral, Consumer<AsyncContext> consumer) {
reply("Bot is thinking...", consumer);
}
@Override
public boolean isAcknowledged() {
return true;
} }
@Override @Override
@@ -62,29 +44,10 @@ public class EmojiTaskContext extends TaskContext {
return event.isFromGuild(); return event.isFromGuild();
} }
@Override
public void reply(Modal modal) {
}
@Override @Override
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) { public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (getChannel() != null && getChannel().canTalk()) { if (getChannel() != null && getChannel().canTalk()) {
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message))); getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
} }
} }
@Override
public void replyEphemeral(MessageCreateData messageCreateData) {
if (getType() == ChannelType.PRIVATE) {
return;
}
sendMessageInPrivate(messageCreateData);
}
@Override
public void ack(boolean ephemeral) {
}
} }
@@ -0,0 +1,55 @@
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) {
return;
}
sendMessageInPrivate(messageCreateData);
}
@Override
public Message getMessage() {
return message;
}
}
@@ -5,9 +5,9 @@ package me.bobert.netneutrality.core;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.music.MusicManager;
import me.bobert.netneutrality.music.TrackScheduler; import me.bobert.netneutrality.music.TrackScheduler;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -21,18 +21,21 @@ public class BotState {
//TODO: is it needed? //TODO: is it needed?
private static void saveGuildsState() { private static void saveGuildsState() {
Map<String, GuildManager> managers = GuildManager.getManagers(); Map<String, GuildService> services = GuildService.getServices();
JSONObject main = new JSONObject(); JSONObject main = new JSONObject();
JSONArray guildsArray = new JSONArray(); JSONArray guildsArray = new JSONArray();
for (GuildManager manager : managers.values()) { for (MusicManager manager : MusicManager.getManagers()) {
MusicManager musicManager = MusicManager.get(manager.getGuild()); TrackScheduler trackScheduler = manager.getScheduler();
TrackScheduler trackScheduler = musicManager.getScheduler(); }
for (GuildService service : services.values()) {
JSONObject guild = new JSONObject(); JSONObject guild = new JSONObject();
JSONObject music = new JSONObject(); JSONObject music = new JSONObject();
guild.put("id", manager.getGuild().getId()); guild.put("id", service.getEntity().getGuildId());
// MessageChannel currentChannel = trackScheduler.getTextChannel(); // MessageChannel currentChannel = trackScheduler.getTextChannel();
// if (currentChannel == null) { // if (currentChannel == null) {
@@ -1,157 +0,0 @@
package me.bobert.netneutrality.core;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.NetNeutrality;
import me.bobert.netneutrality.db.entities.OBlacklist;
import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.db.repository.GuildRepository;
import me.bobert.netneutrality.utils.JpaUtils;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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 final GuildRepository guildRepository = new GuildRepository();
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;
loadGuildFromDb(guild.getId());
this.guildConfigManager = new GuildConfigManager(guild, this.guildEntity);
reloadBlacklistCache();
}
public static GuildManager getManager(Guild guild) {
if (managers.containsKey(guild.getId())) {
return managers.get(guild.getId());
}
GuildManager manager = new GuildManager(guild);
managers.put(guild.getId(), manager);
return manager;
}
public static GuildManager getManager(String guildId) {
if (managers.containsKey(guildId)) {
return managers.get(guildId);
}
Guild temp = NetNeutrality.getInstance().getJDA().getGuildById(guildId);
if (temp == null) {
throw new IllegalArgumentException("Guild does not exist in current cache");
}
return getManager(temp);
}
// 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 addRolesToMember(Member member, Role role) {
Member self = guild.getSelfMember();
if (!self.canInteract(role)) {
logger.error("Missing permissions to interact with role {} in guild {}", role.getId(), guild.getId());
}
guild.addRoleToMember(member, role).queue();
}
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, guildEntity.getGuildId(), session -> {
Set<OBlacklist> oBlacklistSet = guildEntity.getBlacklists();
for (OBlacklist bl : oBlacklistSet) {
if (bl.getEnabled()) {
blacklistCache.add(bl.getWord());
}
}
});
}
private void loadGuildFromDb(String guildId) {
JpaUtils.runWithTransaction(session -> {
OGuild entity = guildRepository.findById(guildId);
if (entity == null) {
logger.debug("Guild[{}] doesn't exist in DB, creating entity..", guildId);
entity = new OGuild();
entity.setGuildId(guildId);
session.persist(entity);
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());
}
guildEntity = entity;
});
}
public void reloadEntity() {
loadGuildFromDb(guild.getId());
}
public void removeFromDB() {
guildRepository.deleteBy(guild.getId());
managers.remove(guild.getId());
}
}
@@ -9,6 +9,7 @@ import io.sentry.Sentry;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.Context; import me.bobert.netneutrality.core.context.Context;
import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.exceptions.ContextException; import me.bobert.netneutrality.exceptions.ContextException;
import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MessageFunction;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
@@ -49,13 +50,12 @@ public class LogHandler {
} }
public static void sendMessageToDebugChannel(Guild guild, String message) { public static void sendMessageToDebugChannel(Guild guild, String message) {
if (GuildManager.getManager(guild).isDebugEnabled()) { guild.getTextChannels()
guild.getTextChannels() .stream()
.stream() .filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log"))
.filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log")) .filter(TextChannel::canTalk)
.filter(TextChannel::canTalk) .forEach(textChannel -> textChannel.sendMessage(message).queue());
.forEach(textChannel -> textChannel.sendMessage(message).queue());
}
} }
private static void logInternally(ContextException exception) { private static void logInternally(ContextException exception) {
@@ -80,7 +80,7 @@ public class LogHandler {
Context context = exception.getContext(); Context context = exception.getContext();
if (context.getGuild() != null) { if (context.getGuild() != null) {
if (context.getGuildManager().isDebugEnabled()) { if (GuildService.get(context.getGuild()).isDebugEnabled()) {
context.getGuild() context.getGuild()
.getTextChannels() .getTextChannels()
.stream() .stream()
@@ -17,9 +17,14 @@ public abstract class AbstractMessageMenuCommand extends AbstractCommand {
return null; return null;
} }
@Override
public boolean isGuildOnly() {
return true;
}
@Override @Override
public boolean isSlashCommandOnly() { public boolean isSlashCommandOnly() {
return false; return true;
} }
@Override @Override
@@ -27,6 +32,11 @@ public abstract class AbstractMessageMenuCommand extends AbstractCommand {
// no options for context commands // no options for context commands
} }
@Override
public CommandCategory getCategory() {
return CommandCategory.UNLISTED;
}
@Override @Override
public Type getType() { public Type getType() {
return Type.MESSAGE; return Type.MESSAGE;
@@ -5,8 +5,8 @@ package me.bobert.netneutrality.core.command;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.db.GSettings; import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.utils.UserFunction; import me.bobert.netneutrality.utils.UserFunction;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@@ -39,7 +39,8 @@ public abstract class AbstractMusicCommand extends AbstractCommand {
} }
Guild guild = member.getGuild(); Guild guild = member.getGuild();
String roleDb = GuildManager.getManager(guild).getGuildConfigManager().getSetting(GSettings.MUSIC_ROLE);
String roleDb = GuildService.get(guild.getId()).getGuildSettingService().getSetting(GSettings.MUSIC_ROLE);
if (roleDb != null && !roleDb.isEmpty()) { if (roleDb != null && !roleDb.isEmpty()) {
if (UserFunction.isAdmin(member)) { if (UserFunction.isAdmin(member)) {
@@ -49,7 +50,7 @@ public abstract class AbstractMusicCommand extends AbstractCommand {
Role neededRole = guild.getRoleById(roleDb); Role neededRole = guild.getRoleById(roleDb);
if (neededRole == null) { if (neededRole == null) {
// Role doesn't exist on guild anymore, remove from db // 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; return true;
} }
@@ -65,7 +66,7 @@ public abstract class AbstractMusicCommand extends AbstractCommand {
if (channel.getType() != ChannelType.TEXT) if (channel.getType() != ChannelType.TEXT)
return false; return false;
String channelDb = GuildManager.getManager(guildMessageChannel.getGuild()).getGuildConfigManager().getSetting(GSettings.MUSIC_CHANNEL); String channelDb = GuildService.get(guildMessageChannel.getGuild()).getGuildSettingService().getSetting(GSettings.MUSIC_CHANNEL);
if (channelDb != null && !channelDb.isEmpty()) { if (channelDb != null && !channelDb.isEmpty()) {
return channelDb.equals(channel.getId()); return channelDb.equals(channel.getId());
} }
@@ -5,26 +5,38 @@ package me.bobert.netneutrality.core.command;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.tasks.Task;
public class CommandExecutionResult { public class CommandExecutionResult {
public enum Result { public enum Result {
SUCCESS, INFO, WARN, ERROR, NOACTION SUCCESS, INFO, WARN, ERROR, NOACTION, TASK
} }
private final Result result; private final Result result;
private final String message; private final String message;
private final Object data;
private final Exception exception; private final Exception exception;
private CommandExecutionResult(Result result, String message, Exception exception) { private CommandExecutionResult(Result result, String message, Object data, Exception exception) {
this.result = result; this.result = result;
this.message = message; this.message = message;
this.data = data;
this.exception = exception; this.exception = exception;
} }
private CommandExecutionResult(Result result, String message, Exception exception) {
this(result, message, null, exception);
}
private CommandExecutionResult(Result result, String message) { private CommandExecutionResult(Result result, String message) {
this(result, message, null); this(result, message, null);
} }
private CommandExecutionResult(Result result, Object data) {
this(result, null, data, null);
}
public Result getResult() { public Result getResult() {
return result; return result;
} }
@@ -33,6 +45,10 @@ public class CommandExecutionResult {
return message; return message;
} }
public Object getData() {
return data;
}
public Exception getException() { public Exception getException() {
return exception; return exception;
} }
@@ -65,4 +81,7 @@ public class CommandExecutionResult {
return new CommandExecutionResult(Result.NOACTION, null); return new CommandExecutionResult(Result.NOACTION, null);
} }
public static CommandExecutionResult task(Task task) {
return new CommandExecutionResult(Result.TASK, null, task, null);
}
} }
@@ -14,15 +14,13 @@ import org.apache.logging.log4j.Logger;
import org.reflections.Reflections; import org.reflections.Reflections;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.util.Set;
public class CommandManager { public class CommandManager {
private static final Logger logger = LogManager.getLogger(CommandManager.class); private static final Logger logger = LogManager.getLogger(CommandManager.class);
public static final HashMap<String, CommandContainer> commands = new HashMap<>(); public static final Map<String, CommandContainer> commands = new ConcurrentHashMap<>();
public static void init() { public static void init() {
logger.info("Loading commands.."); logger.info("Loading commands..");
@@ -11,7 +11,10 @@ import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.core.command.options.CommandOptions; import me.bobert.netneutrality.core.command.options.CommandOptions;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.InteractionContext; 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.CommandExecutionException;
import me.bobert.netneutrality.exceptions.GracefulContextError;
import me.bobert.netneutrality.utils.UserFunction; import me.bobert.netneutrality.utils.UserFunction;
import net.dv8tion.jda.api.interactions.commands.OptionType; 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.OptionData;
@@ -26,8 +29,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static java.util.function.Predicate.not; import static java.util.function.Predicate.not;
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.ERROR; import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.*;
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.NOACTION;
public class CommandProcessor { public class CommandProcessor {
@@ -56,13 +58,13 @@ public class CommandProcessor {
return; return;
} }
if (command.isSlashCommandOnly() && !(context instanceof SlashCommandContext)) { if (command.isSlashCommandOnly() && !(context instanceof InteractionContext)) {
context.replyEphemeral("This command is only available as a slash command"); context.replyEphemeral("This command is only available as a slash command");
return; return;
} }
try { try {
logger.info("{} ID: {}", context.getClass().getSimpleName(), context.getId()); logger.info("Context ID: {}", context.getId());
logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId()); logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId());
configureSentry(command); configureSentry(command);
@@ -95,17 +97,28 @@ public class CommandProcessor {
return; return;
} }
if (result == TASK) {
TaskManager.getInstance().insert(context, (Task) commandExecutionResult.getData());
}
String message = commandExecutionResult.getMessage(); String message = commandExecutionResult.getMessage();
if (result == ERROR) { if (result == ERROR) {
logger.error("Context[{}]: Error from command: {}", context.getId(), message, commandExecutionResult.getException()); logger.error("Context[{}]: Error from command: {}", context.getId(), message, commandExecutionResult.getException());
} }
context.reply(message); if (result == WARN) {
} catch (Exception exception) { logger.warn("Context[{}]: Warning from command: {}", context.getId(), message);
if (exception instanceof CommandExecutionException) {
LogHandler.log(exception);
} }
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)); LogHandler.log(new CommandExecutionException("Error during command execution", context, exception));
} }
} }
@@ -6,6 +6,7 @@ package me.bobert.netneutrality.core.context;
*/ */
import io.sentry.Sentry; import io.sentry.Sentry;
import me.bobert.netneutrality.context.command.SlashCommandContext;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@@ -6,7 +6,6 @@ package me.bobert.netneutrality.core.context;
*/ */
import me.bobert.netneutrality.context.AsyncContext; import me.bobert.netneutrality.context.AsyncContext;
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.utils.ContextUtils; import me.bobert.netneutrality.utils.ContextUtils;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.*;
@@ -55,10 +54,6 @@ public interface Context {
void ack(boolean ephemeral); void ack(boolean ephemeral);
default GuildManager getGuildManager() {
return GuildManager.getManager(getGuild());
}
default void ack() { default void ack() {
ack(false); ack(false);
} }
@@ -160,33 +155,39 @@ public interface Context {
replyEphemeral(MessageCreateData.fromEmbeds(embeds)); replyEphemeral(MessageCreateData.fromEmbeds(embeds));
} }
default void sendMessageInPrivate(MessageCreateData messageCreateData, List<LayoutComponent> components) { default void sendMessageInPrivate(MessageCreateData messageCreateData, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
User user = getDiscordUser(); User user = getDiscordUser();
if (user != null) { if (user != null) {
user.openPrivateChannel().flatMap(channel -> channel.sendMessage(messageCreateData).setComponents(components)).queue(); 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) { default void sendMessageInPrivate(MessageCreateData messageCreateData) {
sendMessageInPrivate(messageCreateData, List.of()); sendMessageInPrivate(messageCreateData, List.of());
} }
default void sendMessageInPrivate(String message) {
sendMessageInPrivateWithLimit(message);
}
default void sendMessageInPrivate(String message, List<LayoutComponent> components) {
sendMessageInPrivate(MessageCreateData.fromContent(message), components);
}
default void sendMessageInPrivate(MessageEmbed embed) {
sendMessageInPrivate(List.of(embed));
}
default void sendMessageInPrivate(List<MessageEmbed> embeds) { default void sendMessageInPrivate(List<MessageEmbed> embeds) {
sendMessageInPrivate(MessageCreateData.fromEmbeds(embeds)); sendMessageInPrivate(MessageCreateData.fromEmbeds(embeds));
} }
default void sendMessageInPrivate(String message) {
sendMessageInPrivateWithLimit(message);
}
private void replyWithLimit(String message) { private void replyWithLimit(String message) {
for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) { for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) {
reply(MessageCreateData.fromContent(chunk)); reply(MessageCreateData.fromContent(chunk));
@@ -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) {
}
}
@@ -11,15 +11,15 @@ import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public abstract class TaskContext extends AbstractContext { public abstract class TaskContext extends AbstractContext {
private final Map<String, Object> contextData; private final Map<String, Object> contextData;
public TaskContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) { public TaskContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) {
super(jda, guild, channel, member, user); super(jda, guild, channel, member, user);
this.contextData = new HashMap<>(); this.contextData = new ConcurrentHashMap<>();
} }
@Override @Override
@@ -6,6 +6,7 @@ package me.bobert.netneutrality.core.tasks;
*/ */
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.TaskContext;
public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltInTask { public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltInTask {
@@ -19,11 +20,6 @@ public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltI
return true; return true;
} }
@Override
public boolean oneshot() {
return false;
}
@Override @Override
public boolean isExpired() { public boolean isExpired() {
return false; return false;
@@ -33,4 +29,12 @@ public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltI
public void replyToInteraction(CommandContext context) { public void replyToInteraction(CommandContext context) {
// Task is loaded at runtime, this will never be called // 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);
} }
@@ -15,11 +15,6 @@ public abstract class AbstractPersistableTask extends AbstractTask implements Pe
return true; return true;
} }
@Override
public boolean oneshot() {
return false;
}
@Override @Override
public boolean isExpired() { public boolean isExpired() {
return false; return false;
@@ -30,6 +25,30 @@ public abstract class AbstractPersistableTask extends AbstractTask implements Pe
// Task loaded from database, this will never be called // 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 @Override
public String toPayload() { public String toPayload() {
JSONObject payload = new JSONObject(this); JSONObject payload = new JSONObject(this);
@@ -5,6 +5,8 @@ package me.bobert.netneutrality.core.tasks;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.context.CommandContext;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.Objects; import java.util.Objects;
@@ -26,8 +28,8 @@ public abstract class AbstractTask implements Task {
} }
@Override @Override
public TaskButtonCreator createButton() { public void insert(CommandContext context) {
return new TaskButtonCreator(getId()); TaskManager.getInstance().insert(context, this);
} }
@Override @Override
@@ -46,6 +48,6 @@ public abstract class AbstractTask implements Task {
@Override @Override
public String toString() { public String toString() {
return String.format("Task{id=%s, class=%s, name=%s, oneshot=%b, creationTime=%s}", getId(), getClass().getSimpleName(), getName(), oneshot(), getCreationTime()); return String.format("Task{id=%s, lockId=%s, class=%s, name=%s, creationTime=%s}", getId(), getLockId(), getClass().getSimpleName(), getName(), getCreationTime());
} }
} }
@@ -5,18 +5,21 @@ package me.bobert.netneutrality.core.tasks;
* https://discord.gg/qUa6zNFZsd * 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. * Represents a task that has a known ID and is loaded at runtime.
* This can be any task and is not necessarily a button. * This can be any task and is not necessarily a button.
*/ */
public interface BuiltInTask extends Task { public interface BuiltInTask extends Task {
static TaskButtonCreator createButton(Class<? extends BuiltInTask> clazz) { static Button createButton(Class<? extends BuiltInTask> clazz) {
BuiltInTask builtInTask = TaskManager.getInstance().getTaskMap().values().stream().filter(clazz::isInstance).map(task -> (BuiltInTask) task).findFirst().orElse(null); BuiltInTask builtInTask = TaskManager.getInstance().findBuiltInTaskByClass(clazz);
if (builtInTask == null) { if (builtInTask == null) {
throw new IllegalArgumentException("Task class " + clazz.getName() + " was not registered by the task manager."); throw new IllegalArgumentException("Task class " + clazz.getName() + " was not registered by the task manager.");
} }
return new TaskButtonCreator(builtInTask.getId()); 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());
}
}
@@ -5,10 +5,19 @@ package me.bobert.netneutrality.core.tasks;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.context.CommandContext;
/** /**
* Represents a task that will be saved in the database and loaded when the bot is started * 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 { public interface PersistableTask extends Task {
void insert(CommandContext context);
void save();
void delete();
String toPayload(); String toPayload();
} }
@@ -13,17 +13,35 @@ import net.dv8tion.jda.api.interactions.components.buttons.Button;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
public interface Task { public interface Task {
enum Lifecycle {
BUILTIN,
PARENT,
KEEP,
REMOVE_SELF,
REMOVE_SELF_AND_CHILDREN
}
/** /**
* <p>This method should generate a unique ID to differentiate it from other tasks.</p> * This method should generate a unique ID to differentiate it from other tasks.
* <p> * <p>
* This determines what triggers a task to be executed. * This determines what triggers a task to be executed.
* For example, a modal or button would use the component id. * For example, a modal or button would use the component id.
* For an emoji reaction, the guild and message id would be combined. * For an emoji reaction, the guild and message id would be combined.
* </p> * </p>
*
* @return a unique ID for this task * @return a unique ID for this task
*/ */
String getId(); 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. * This is only used for debugging purpose.
* @return Name of the task * @return Name of the task
@@ -36,9 +54,14 @@ public interface Task {
boolean isInsertable(); boolean isInsertable();
/** /**
* This determines if the task should be cancelled after execution. * When this returns true, the task will get removed by the cleanup thread.
* @return If the task is expired
*/ */
boolean oneshot(); boolean isExpired();
OffsetDateTime getCreationTime();
Button createButton();
/** /**
* <p>This method is used to reply to the interaction that triggered this task.</p> * <p>This method is used to reply to the interaction that triggered this task.</p>
@@ -48,41 +71,9 @@ public interface Task {
/** /**
* Executes the task. * Executes the task.
* @param context The context of a task containing the event that triggererd it. * @param context The context for this task.
*/ */
void execute(TaskContext context); Lifecycle execute(TaskContext context);
/** void insert(CommandContext context);
* When this returns true, the task will get removed by the cleanup thread.
* @return If the task is expired
*/
boolean isExpired();
OffsetDateTime getCreationTime();
TaskButtonCreator createButton();
class TaskButtonCreator {
private final String id;
public TaskButtonCreator(String id) {
this.id = id;
}
public Button primary(String label) {
return Button.primary(id, label);
}
public Button secondary(String label) {
return Button.secondary(id, label);
}
public Button success(String label) {
return Button.success(id, label);
}
public Button danger(String label) {
return Button.danger(id, label);
}
}
} }
@@ -5,17 +5,18 @@ package me.bobert.netneutrality.core.tasks;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.context.tasks.ButtonTaskContext;
import me.bobert.netneutrality.context.tasks.ModalTaskContext;
import me.bobert.netneutrality.core.LogHandler; import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.core.context.CommandContext; 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.context.TaskContext;
import me.bobert.netneutrality.core.tasks.impl.StepTask;
import me.bobert.netneutrality.core.tasks.trait.Deletable; import me.bobert.netneutrality.core.tasks.trait.Deletable;
import me.bobert.netneutrality.core.tasks.trait.NotAcknowledgeable; import me.bobert.netneutrality.core.tasks.trait.NotAcknowledgeable;
import me.bobert.netneutrality.core.tasks.trait.WithChildren; import me.bobert.netneutrality.core.tasks.trait.WithChildren;
import me.bobert.netneutrality.core.tasks.trait.WithParent; import me.bobert.netneutrality.core.tasks.trait.WithParent;
import me.bobert.netneutrality.core.thread.ThreadManager; import me.bobert.netneutrality.core.thread.ThreadManager;
import me.bobert.netneutrality.db.service.TaskService; import me.bobert.netneutrality.db.service.TaskService;
import me.bobert.netneutrality.exceptions.GracefulContextError;
import me.bobert.netneutrality.exceptions.TaskExecutionException; import me.bobert.netneutrality.exceptions.TaskExecutionException;
import me.bobert.netneutrality.thread.CleanupTaskThread; import me.bobert.netneutrality.thread.CleanupTaskThread;
import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.InteractionHook;
@@ -26,19 +27,27 @@ import org.reflections.Reflections;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class TaskManager { public class TaskManager {
private static final Logger logger = LogManager.getLogger(TaskManager.class); private static final Logger logger = LogManager.getLogger(TaskManager.class);
private static TaskManager instance; private static TaskManager instance;
private final TaskService taskService; private final TaskService taskService;
private final Map<String, Task> tasks; private final Map<String, Task> tasks;
private final List<Task> pendingTasks;
private final Map<String, CommandContext> deletableTasksContext; private final Map<String, CommandContext> deletableTasksContext;
private final Map<String, ReentrantLock> tasksLock;
private TaskManager() { private TaskManager() {
this.tasks = new HashMap<>(); this.tasks = new ConcurrentHashMap<>();
this.deletableTasksContext = new HashMap<>(); this.pendingTasks = new CopyOnWriteArrayList<>();
this.taskService = new TaskService(this); this.deletableTasksContext = new ConcurrentHashMap<>();
this.tasksLock = new ConcurrentHashMap<>();
this.taskService = new TaskService();
} }
public static TaskManager getInstance() { public static TaskManager getInstance() {
@@ -56,7 +65,7 @@ public class TaskManager {
ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new CleanupTaskThread(), 1, 5, TimeUnit.MINUTES); ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new CleanupTaskThread(), 1, 5, TimeUnit.MINUTES);
} }
public TaskService getTaskService() { protected TaskService getTaskService() {
return taskService; return taskService;
} }
@@ -64,22 +73,22 @@ public class TaskManager {
return tasks.get(id); return tasks.get(id);
} }
public Collection<Task> getTasks() { 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(); return tasks.values();
} }
public Map<String, Task> getTaskMap() { public synchronized Collection<Task> getPendingTasks() {
return tasks; return pendingTasks;
}
public boolean exists(String id) {
return tasks.containsKey(id);
} }
public void insert(CommandContext context, Task task) { public void insert(CommandContext context, Task task) {
String id = task.getId(); String id = task.getId();
if (tasks.containsKey(id)) { if (hasTask(id)) {
context.reply("A task with the same ID already exists!"); context.reply("A task with the same ID already exists!");
return; return;
} }
@@ -93,7 +102,11 @@ public class TaskManager {
taskService.persistTask(persistableTask); taskService.persistTask(persistableTask);
} }
tasks.put(id, task); if (task instanceof StepTask) {
pendingTasks.add(task);
} else {
tasks.put(id, task);
}
} }
if (task instanceof Deletable) { if (task instanceof Deletable) {
@@ -102,6 +115,10 @@ public class TaskManager {
if (task instanceof WithChildren taskWithChildren) { if (task instanceof WithChildren taskWithChildren) {
List<Task> childrens = taskWithChildren.getChildTasks(); List<Task> childrens = taskWithChildren.getChildTasks();
if (childrens == null || childrens.isEmpty()) {
logger.warn("Task [{}]: parent task doesn't create any childs", task.getId());
return;
}
for (Task child : childrens) { for (Task child : childrens) {
insert(context, child); insert(context, child);
@@ -109,51 +126,107 @@ public class TaskManager {
} }
} }
public void removeTask(Task task) { public synchronized void activateTask(Task task) {
if (pendingTasks.remove(task)) {
tasks.put(task.getId(), task);
logger.info("Updating task {} with new id: {}", task.getName(), task.getId());
}
}
public synchronized void removeTask(Task task) {
String id = task.getId(); String id = task.getId();
if (tasks.containsKey(id)) { if (tasks.containsKey(id)) {
logger.debug("Removing task: {}", task); logger.debug("Removing task: {}", task);
tasks.remove(id); tasks.remove(id);
} }
tasksLock.remove(task.getLockId());
} }
public void handle(TaskContext context) { public void handle(TaskContext context) {
String taskId = context.getId(); String taskId = context.getId();
Task task = tasks.get(taskId); Task task = getTask(taskId);
if (task == null && (context instanceof ModalTaskContext || context instanceof ButtonTaskContext)) { 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"); context.replyEphemeral("This task is no longer available, someone might have already used it or it might have expired");
} }
if (task != null) { if (task != null) {
logger.debug("Executing task: {}", task); String lockId = task.getLockId();
ReentrantLock lock = tasksLock.computeIfAbsent(lockId, k -> {
if (task instanceof PersistableTask || task instanceof BuiltInTask) {
return null;
}
return new ReentrantLock();
});
try { try {
// Cancel task during execution to prevent spam if (lock != null) {
removeTaskAndChildTasks(task); logger.debug("Aquiring lock for task: {}", taskId);
lock.lock();
logger.debug("Aquired lock for: {}", lockId);
task.execute(context); if (!tasks.containsKey(taskId)) {
logger.debug("Task {} is no longer available after aquiring lock", taskId);
if (task.isInsertable() && !task.oneshot()) { return;
reinsertTaskAndChildTasks(task); }
} }
logger.debug("Executing task: {}", task);
Task.Lifecycle status = task.execute(context);
handleTaskLifecycle(task, status);
deleteInteraction(task); deleteInteraction(task);
// If interaction is still not acknowledged after execution, send a placeholder response. // If interaction is still not acknowledged after execution, send a placeholder response.
if (!context.isAcknowledged() && !(task instanceof NotAcknowledgeable)) { 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"); 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) { } catch (TaskExecutionException exception) {
LogHandler.log(exception); LogHandler.log(exception);
} catch (Exception exception) { } catch (Exception exception) {
LogHandler.log(new TaskExecutionException("Error during task execution", context, 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> 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() { private void loadBuiltInTasks() {
logger.info("Loading built-in tasks..."); logger.info("Loading built-in tasks...");
@@ -177,7 +250,7 @@ public class TaskManager {
private void deleteInteraction(Task task) { private void deleteInteraction(Task task) {
if (task instanceof WithParent<?> taskWithParent) { if (task instanceof WithParent<?> taskWithParent) {
Task parent = (Task) taskWithParent.getParentTask(); Task parent = taskWithParent.getParentTask();
if (parent instanceof Deletable deletable && task.isInsertable()) { if (parent instanceof Deletable deletable && task.isInsertable()) {
deleteInteraction(parent.getId(), deletable); deleteInteraction(parent.getId(), deletable);
@@ -212,37 +285,28 @@ public class TaskManager {
deletable.onInteractionDelete(); deletable.onInteractionDelete();
} }
private void removeTaskAndChildTasks(Task task) { private void handleTaskLifecycle(Task task, Task.Lifecycle status) {
if (task instanceof BuiltInTask) { if (status == null) {
return; throw new IllegalArgumentException("Task lifecycle status cannot be null");
} }
tasks.remove(task.getId()); switch (status) {
case REMOVE_SELF:
removeTask(task);
break;
if (task instanceof WithParent<?> taskWithParent && !taskWithParent.shouldKeepChilds()) { case REMOVE_SELF_AND_CHILDREN:
List<Task> childrens = taskWithParent.getParentTask().getChildTasks(); if (task instanceof WithChildren taskWithChildren) {
for (Task child : childrens) { for (Task child : taskWithChildren.getChildTasks()) {
if (!task.equals(child)) { removeTask(child);
tasks.remove(child.getId()); }
} }
} removeTask(task);
} break;
}
private void reinsertTaskAndChildTasks(Task task) { case KEEP:
if (task instanceof BuiltInTask) { default:
return; break;
}
tasks.put(task.getId(), task);
if (task instanceof WithParent<?> taskWithParent && taskWithParent.shouldKeepChilds()) {
List<Task> childrens = taskWithParent.getParentTask().getChildTasks();
for (Task child : childrens) {
if (!task.equals(child)) {
tasks.put(child.getId(), child);
}
}
} }
} }
} }
@@ -0,0 +1,36 @@
package me.bobert.netneutrality.core.tasks.impl;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.tasks.GenericRandomIdTask;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
import java.time.OffsetDateTime;
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());
}
}
@@ -0,0 +1,54 @@
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 messageId;
public EmojiTask(String guildId, String messageId) {
this.guildId = guildId;
this.messageId = messageId;
}
public String getGuildId() {
return guildId;
}
public String getMessageId() {
return messageId;
}
public void addEmoji(String emoji, String snowflake) {
emojiMapping.put(emoji, snowflake);
}
public void removeEmoji(String emoji) {
emojiMapping.remove(emoji);
}
public Map<String, String> getEmojiMapping() {
return emojiMapping;
}
@Override
public String getId() {
return String.format(EmojiTaskContext.ID, getGuildId(), getMessageId());
}
@Override
public Button createButton() {
return null;
}
}
@@ -1,4 +1,4 @@
package me.bobert.netneutrality.tasks; package me.bobert.netneutrality.core.tasks.impl;
/* /*
* Copyright (c) anthorob * Copyright (c) anthorob
* *
@@ -6,25 +6,13 @@ package me.bobert.netneutrality.tasks;
*/ */
import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.AbstractTask; import me.bobert.netneutrality.core.tasks.GenericRandomIdTask;
import net.dv8tion.jda.api.interactions.modals.ModalMapping; import net.dv8tion.jda.api.interactions.modals.ModalMapping;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.UUID;
public abstract class ModalTask extends AbstractTask { public abstract class ModalTask extends GenericRandomIdTask {
private final UUID id = UUID.randomUUID();
@Override
public String getId() {
return id.toString();
}
@Override
public boolean oneshot() {
return true;
}
@Override @Override
public boolean isExpired() { public boolean isExpired() {
@@ -0,0 +1,110 @@
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.TaskManager;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.OffsetDateTime;
import java.util.UUID;
public abstract class StepTask extends AbstractTask {
private static final Logger logger = LoggerFactory.getLogger(StepTask.class);
public enum StepType {
STRING, ROLE, EMOJI
}
private Guild guild;
private String id = UUID.randomUUID().toString();
@Override
public String getId() {
return id;
}
@Override
public void replyToInteraction(CommandContext context) {
if (context.isFromGuild()) {
guild = context.getGuild();
}
if (guild == null && getStepType() == StepType.ROLE) {
throw new IllegalArgumentException("Step task if of type role but it was inserted in a non-guild context");
}
if (inDM()) {
context.replyEphemeral("Please follow the instructions sent in DM.");
context.sendMessageInPrivate(getStepDescription() + "\n" + getStepQuestion(), asyncContext -> assignId(asyncContext.getMessageId()));
} else {
context.reply(getStepQuestion(), asyncContext -> id = asyncContext.getMessageId());
}
System.out.println("step id : " + id);
}
@Override
public Lifecycle execute(TaskContext context) {
if (context instanceof MessageContext messageContext) {
Message message = messageContext.getMessage();
String answer = message.getContentRaw();
if (validateAnswer(answer)) {
StepTask nextStep = executeStep(answer);
//TODO: to finish
}
}
return Lifecycle.REMOVE_SELF;
}
@Override
public boolean isExpired() {
return getCreationTime().plusMinutes(30).isBefore(OffsetDateTime.now());
}
@Override
public Button createButton() {
return null;
}
private boolean validateAnswer(String answer) {
}
private void assignId(String newId) {
if (newId == null || newId.isBlank()) {
throw new IllegalArgumentException("Task ID cannot be null or empty.");
}
synchronized (this) {
this.id = newId;
TaskManager.getInstance().activateTask(this);
}
this.id = newId;
}
public abstract boolean inDM();
public abstract StepType getStepType();
public abstract String getStepDescription();
public abstract String getStepQuestion();
public abstract StepTask executeStep(String answer);
}
@@ -6,64 +6,76 @@ package me.bobert.netneutrality.core.tasks.persistence;
*/ */
import me.bobert.netneutrality.core.tasks.PersistableTask; import me.bobert.netneutrality.core.tasks.PersistableTask;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
public class TaskFactory { public class TaskFactory {
private static final Logger logger = LogManager.getLogger(TaskFactory.class); 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 { public static PersistableTask createTaskInstance(String className, JSONArray params, JSONArray types, JSONObject payload) throws Exception {
if (params.length() != types.length()) { if (params.length() != types.length()) {
throw new RuntimeException("Wrong number of parameters and types for " + className); throw new RuntimeException("Wrong number of parameters and types.");
} }
Class<?> clazz = Class.forName(className); Class<?> clazz = Class.forName(className);
if (!PersistableTask.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Class does not implement PersistableTask.");
}
Class<?>[] classTypes = new Class<?>[types.length()]; Class<?>[] classTypes = new Class<?>[types.length()];
Object[] classParams = new Object[params.length()]; Object[] classParams = new Object[params.length()];
for (int i = 0; i < params.length(); i++) { for (int i = 0; i < params.length(); i++) {
String type = types.getString(i); ParamType paramType = ParamType.fromString(types.getString(i));
String param = params.getString(i); String paramName = params.getString(i);
Class<?> classType = parseClassTypeFromString(type); classTypes[i] = paramType.getTypeClass();
Object classParam = castParamToType(classType, param, type, payload); classParams[i] = extractValue(paramType, paramName, payload);
classTypes[i] = classType;
classParams[i] = classParam;
} }
return (PersistableTask) clazz.getConstructor(classTypes).newInstance(classParams); 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 Class<?> parseClassTypeFromString(String param) { private static Object extractValue(ParamType type, String paramName, JSONObject payload) {
return switch (param) { return switch (type) {
case "string" -> String.class; case STRING -> payload.getString(paramName);
case "int" -> Integer.class; case INT -> payload.getInt(paramName);
case "map" -> Map.class; case MAP -> payload.getJSONObject(paramName).toMap();
default -> throw new IllegalArgumentException("Param " + param + " is not implemented yet.");
}; };
} }
private static Object castParamToType(Class<?> type, String paramName, String typeString, JSONObject payload) {
if (type == String.class) {
return payload.getString(paramName);
}
if (type == Integer.class) {
return payload.getInt(paramName);
}
if (type == Map.class) {
JSONObject mapJson = payload.getJSONObject(paramName);
return mapJson.toMap();
}
throw new IllegalArgumentException("Type " + typeString + " is not implemented yet.");
}
} }
@@ -26,43 +26,67 @@ public class TaskRegistry {
private static final Logger logger = LogManager.getLogger(TaskRegistry.class); private static final Logger logger = LogManager.getLogger(TaskRegistry.class);
private static final Map<String, TaskCreator> taskCreators = new HashMap<>(); private static final Map<String, TaskCreator> taskCreators = new HashMap<>();
private static final String FILE_NAME = "persistable-tasks.json";
public TaskRegistry() { // JSON keys as constants
loadTaskConfig(); 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) { public static TaskCreator getTaskCreator(String className) {
return taskCreators.get(className); return taskCreators.get(className);
} }
private static void loadTaskConfig() { public static void loadTaskConfig() {
String fileName = "persistable-tasks.json"; logger.info("Reading task mappings from {}..", FILE_NAME);
try (InputStream is = TaskFactory.class.getClassLoader().getResourceAsStream(fileName)) { try (InputStream is = TaskFactory.class.getClassLoader().getResourceAsStream(FILE_NAME)) {
if (is == null) { if (is == null) {
throw new FileNotFoundException("Could not load: " + fileName); throw new FileNotFoundException("Could not load: " + FILE_NAME);
} }
String jsonString = IOUtils.toString(is, StandardCharsets.UTF_8); String jsonString = IOUtils.toString(is, StandardCharsets.UTF_8);
JSONObject json = new JSONObject(jsonString); JSONObject json = new JSONObject(jsonString);
JSONArray tasksArray = json.optJSONArray(KEY_TASKS);
json.getJSONArray("tasks").forEach(task -> { if (tasksArray == null) {
JSONObject taskJson = (JSONObject) task; logger.warn("No tasks found in JSON: {}", FILE_NAME);
return;
}
String className = taskJson.getString("class"); for (int i = 0; i < tasksArray.length(); i++) {
JSONArray params = taskJson.getJSONArray("params"); JSONObject taskJson = tasksArray.getJSONObject(i);
JSONArray types = taskJson.getJSONArray("types");
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 -> { taskCreators.put(className, payload -> {
try { try {
return TaskFactory.createTaskInstance(className, params, types, payload); return TaskFactory.createTaskInstance(className, params, types, payload);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); logger.error("Failed to create instance of '{}': {}", className, e.getMessage());
return null;
} }
}); });
}); }
logger.info("{} mappings registered", taskCreators.size());
} catch (Exception e) { } catch (Exception e) {
logger.error(e); logger.error(e);
} }
@@ -9,5 +9,11 @@ package me.bobert.netneutrality.core.tasks.trait;
* Represents a task that will remove its interaction from the channel after execution * Represents a task that will remove its interaction from the channel after execution
*/ */
public interface Deletable { 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(); void onInteractionDelete();
} }
@@ -6,7 +6,7 @@ package me.bobert.netneutrality.core.tasks.trait;
*/ */
/** /**
* If a task implements this interface, it means that the interaction will not get acknowledged during execution. * Represents a task that will not acknowledge the interaction when executed.
*/ */
public interface NotAcknowledgeable { public interface NotAcknowledgeable {
} }
@@ -5,17 +5,21 @@ package me.bobert.netneutrality.core.tasks.trait;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.Task; import me.bobert.netneutrality.core.tasks.Task;
import me.bobert.netneutrality.core.tasks.TaskManager;
import java.util.List; import java.util.List;
public interface WithChildren { public interface WithChildren extends Task {
@Override
default Lifecycle execute(TaskContext context) {
return Lifecycle.PARENT;
}
List<Task> getChildTasks(); List<Task> getChildTasks();
default void removeAllChildTasks() { default void addChild(Task... tasks) {
for (Task child : getChildTasks()) { getChildTasks().addAll(List.of(tasks));
TaskManager.getInstance().removeTask(child);
}
} }
} }
@@ -5,12 +5,30 @@ package me.bobert.netneutrality.core.tasks.trait;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
public interface WithParent<E extends WithChildren> { import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.tasks.Task;
E getParentTask(); public interface WithParent<E extends WithChildren> extends Task {
/** /**
* Whether execution of this task should remove all childs of its parent task during execution * Retrieves the lock ID associated with the parent task.
*
* @return the lock ID of the parent task
*/ */
boolean shouldKeepChilds(); @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();
} }
@@ -22,11 +22,12 @@ public class ThreadManager {
); );
} }
public static ExecutorService getExecutor(String name) {
return executors.computeIfAbsent(name, s -> Executors.newCachedThreadPool(new BaseThreadFactory(s)));
}
public static ExecutorService getExecutor() { public static ExecutorService getExecutor() {
return executors.computeIfAbsent( return getExecutor("general-executor");
"general-executor",
s -> Executors.newCachedThreadPool(new BaseThreadFactory(s))
);
} }
public static void shutdownExecutor() { public static void shutdownExecutor() {
@@ -5,14 +5,11 @@ package me.bobert.netneutrality.db.repository;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.db.SessionManager; import me.bobert.netneutrality.core.db.SessionManager;
import me.bobert.netneutrality.db.Repository; import me.bobert.netneutrality.db.Repository;
import me.bobert.netneutrality.db.entities.OBlacklist; import me.bobert.netneutrality.db.entities.OBlacklist;
import me.bobert.netneutrality.db.entities.OGuild; import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.utils.JpaUtils; 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.Member;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.query.Query; import org.hibernate.query.Query;
@@ -53,15 +50,26 @@ public class BlacklistRepository implements Repository<OBlacklist> {
}); });
} }
public CommandExecutionResult add(Guild guild, Member member, String word) { public List<OBlacklist> findByGuildId(String guildId) {
GuildManager guildManager = GuildManager.getManager(guild); List<OBlacklist> blacklists = new ArrayList<>();
JpaUtils.runWithSession(session -> {
Query<OBlacklist> query = session.createQuery("SELECT o FROM OBlacklist o WHERE o.guild.guildId = :guildId", OBlacklist.class);
query.setParameter("guildId", guildId);
blacklists.addAll(query.list());
});
return blacklists;
}
public void add(OGuild oGuild, Member member, String word) {
JpaUtils.runWithTransaction(session -> { JpaUtils.runWithTransaction(session -> {
OBlacklist bl = getWordFromDb(word, guild.getId()); OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word);
if (bl == null) { if (bl == null) {
bl = new OBlacklist(); bl = new OBlacklist();
bl.setGuild(guildManager.getGuildEntity()); bl.setGuild(oGuild);
bl.setCreatorId(member.getId()); bl.setCreatorId(member.getId());
bl.setWord(word); bl.setWord(word);
bl.setEnabled(true); bl.setEnabled(true);
@@ -72,37 +80,18 @@ public class BlacklistRepository implements Repository<OBlacklist> {
session.merge(bl); session.merge(bl);
} }
}); });
guildManager.reloadBlacklistCache();
return CommandExecutionResult.success("Added the word [%s] to the blacklist.", word);
} }
public CommandExecutionResult delete(Guild guild, String word) { public void delete(OGuild oGuild, String word) {
GuildManager guildManager = GuildManager.getManager(guild);
JpaUtils.runWithTransaction(session -> { JpaUtils.runWithTransaction(session -> {
OBlacklist bl = getWordFromDb(word, guild.getId()); OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word);
if (bl != null) { if (bl != null) {
session.remove(bl); session.remove(bl);
} }
}); });
guildManager.reloadBlacklistCache();
return CommandExecutionResult.success("Removed the word [%s] to the blacklist.", word);
} }
public List<OBlacklist> getBlacklistForGuild(Guild guild) { private OBlacklist getWordFromDb(String guildId, String word) {
GuildManager manager = GuildManager.getManager(guild);
OGuild guildEntity = manager.getGuildEntity();
List<OBlacklist> blacklist = new ArrayList<>();
JpaUtils.getWithSession(guildEntity, guildEntity.getGuildId(), session -> blacklist.addAll(guildEntity.getBlacklists()));
return blacklist;
}
private OBlacklist getWordFromDb(String word, String guildId) {
try(Session session = SessionManager.getSessionFactory().openSession()) { try(Session session = SessionManager.getSessionFactory().openSession()) {
Query<OBlacklist> query = session.createQuery("SELECT o FROM OBlacklist o WHERE upper(o.word) = upper(:word) AND o.guild.guildId = :guildId", OBlacklist.class); Query<OBlacklist> query = session.createQuery("SELECT o FROM OBlacklist o WHERE upper(o.word) = upper(:word) AND o.guild.guildId = :guildId", OBlacklist.class);
query.setParameter("guildId", guildId); query.setParameter("guildId", guildId);
@@ -5,9 +5,9 @@ package me.bobert.netneutrality.db.repository;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.db.SessionManager; import me.bobert.netneutrality.core.db.SessionManager;
import me.bobert.netneutrality.db.Repository; import me.bobert.netneutrality.db.Repository;
import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.db.entities.OLogMessage; import me.bobert.netneutrality.db.entities.OLogMessage;
import me.bobert.netneutrality.utils.JpaUtils; import me.bobert.netneutrality.utils.JpaUtils;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@@ -56,10 +56,11 @@ public class MessageRepository implements Repository<OLogMessage> {
}); });
} }
public void log(GuildManager guildManager, MessageChannel channel, Member author, Message message) { //TODO: move to a service
public void log(OGuild oGuild, MessageChannel channel, Member author, Message message) {
JpaUtils.runWithTransaction(session -> { JpaUtils.runWithTransaction(session -> {
OLogMessage logMessage = new OLogMessage(); OLogMessage logMessage = new OLogMessage();
logMessage.setGuild(guildManager.getGuildEntity()); logMessage.setGuild(oGuild);
logMessage.setMessageId(message.getId()); logMessage.setMessageId(message.getId());
logMessage.setMessage(message.getContentRaw().trim()); logMessage.setMessage(message.getContentRaw().trim());
logMessage.setChannelId(channel.getId()); logMessage.setChannelId(channel.getId());
@@ -0,0 +1,98 @@
package me.bobert.netneutrality.db.service;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.db.entities.OBlacklist;
import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.db.repository.BlacklistRepository;
import me.bobert.netneutrality.utils.JpaUtils;
import net.dv8tion.jda.api.entities.Member;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class BlacklistService {
private static final Logger logger = LoggerFactory.getLogger(BlacklistService.class);
private static final Map<String, Set<String>> blacklistCache = new ConcurrentHashMap<>();
private final BlacklistRepository blacklistRepository;
private final GuildService guildService;
public BlacklistService(GuildService guildService) {
this.blacklistRepository = new BlacklistRepository();
this.guildService = guildService;
init();
}
private void init() {
reloadBlacklistCache();
}
public void reloadBlacklistCache() {
OGuild oGuild = guildService.getEntity();
logger.debug("Reloading blacklist cache for guild: {}", oGuild.getGuildId());
JpaUtils.getWithSession(oGuild, oGuild.getGuildId(), session -> {
Set<String> blacklistWords = oGuild.getBlacklists().stream()
.filter(OBlacklist::getEnabled)
.map(OBlacklist::getWord)
.collect(Collectors.toSet());
blacklistCache.put(oGuild.getGuildId(), blacklistWords);
});
}
public CommandExecutionResult add(Member member, String word) {
try {
blacklistRepository.add(guildService.getEntity(), member, word);
reloadBlacklistCache();
return CommandExecutionResult.success("Added the word [%s] to the blacklist.", word);
} catch (Exception e) {
return CommandExecutionResult.warn("Failed to add word to blacklist: " + e.getMessage());
}
}
public CommandExecutionResult delete(String word) {
try {
blacklistRepository.delete(guildService.getEntity(), word);
reloadBlacklistCache();
return CommandExecutionResult.success("Removed the word [%s] to the blacklist.", word);
} catch (Exception e) {
return CommandExecutionResult.warn("Failed to remove word from blacklist: " + e.getMessage());
}
}
public List<OBlacklist> getBlacklistForGuild() {
try {
return blacklistRepository.findByGuildId(guildService.getEntity().getGuildId());
} catch (Exception e) {
return List.of();
}
}
public void removeGuildBlacklist() {
logger.info("Clearing blacklist cache for guild: {}", guildService.getEntity().getGuildId());
blacklistCache.remove(guildService.getEntity().getGuildId());
}
public static boolean isWordBlacklisted(String guildId, String word) {
Set<String> blacklistWords = blacklistCache.get(guildId);
return blacklistWords != null && blacklistWords.contains(word);
}
public static Set<String> getBlacklist(String guildId) {
return blacklistCache.getOrDefault(guildId, Collections.emptySet());
}
}
@@ -0,0 +1,85 @@
package me.bobert.netneutrality.db.service;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.db.repository.GuildRepository;
import me.bobert.netneutrality.utils.JpaUtils;
import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class GuildService {
private static final Map<String, GuildService> services = new ConcurrentHashMap<>();
private static final Logger logger = LoggerFactory.getLogger(GuildService.class);
private final GuildRepository guildRepository = new GuildRepository();
private final BlacklistService blacklistService;
private final GuildSettingService guildSettingService;
private OGuild oGuild;
private GuildService(String guildId) {
this.oGuild = findOrCreateGuild(guildId);
this.blacklistService = new BlacklistService(this);
this.guildSettingService = new GuildSettingService(this);
}
public static GuildService get(String guildId) {
return services.computeIfAbsent(guildId, GuildService::new);
}
public static GuildService get(Guild guild) {
return get(guild.getId());
}
public static Map<String, GuildService> getServices() {
return services;
}
public OGuild getEntity() {
return oGuild;
}
public BlacklistService getBlacklistService() {
return blacklistService;
}
public GuildSettingService getGuildSettingService() {
return guildSettingService;
}
public OGuild findOrCreateGuild(String guildId) {
OGuild guildEntity = guildRepository.findById(guildId);
if (guildEntity == null) {
logger.debug("Guild[{}] doesn't exist in DB, creating entity..", guildId);
guildEntity = new OGuild();
guildEntity.setGuildId(guildId);
guildRepository.save(guildEntity);
}
return guildEntity;
}
public boolean isDebugEnabled() {
return oGuild.getDebug();
}
public void updateDebugMode(boolean debug) {
JpaUtils.updateWithTransaction(oGuild, () -> oGuild.setDebug(debug));
}
public void deleteGuild() {
guildRepository.deleteBy(oGuild.getGuildId());
}
public void reloadEntity() {
this.oGuild = findOrCreateGuild(oGuild.getGuildId());
}
}
@@ -1,4 +1,4 @@
package me.bobert.netneutrality.core; package me.bobert.netneutrality.db.service;
/* /*
* Copyright (c) anthorob * Copyright (c) anthorob
@@ -7,30 +7,23 @@ package me.bobert.netneutrality.core;
*/ */
import me.bobert.netneutrality.db.GSettings; import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.db.entities.OGuildSetting; import me.bobert.netneutrality.db.entities.OGuildSetting;
import me.bobert.netneutrality.db.repository.GuildSettingRepository; import me.bobert.netneutrality.db.repository.GuildSettingRepository;
import net.dv8tion.jda.api.entities.Guild; import org.slf4j.Logger;
import org.apache.logging.log4j.LogManager; import org.slf4j.LoggerFactory;
import org.apache.logging.log4j.Logger;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class GuildConfigManager { public class GuildSettingService {
private static final Logger logger = LoggerFactory.getLogger(GuildSettingService.class);
private static final Logger logger = LogManager.getLogger(GuildConfigManager.class);
private final Guild guild;
private final OGuild guildEntity;
private final GuildSettingRepository guildSettingRepository = new GuildSettingRepository(); private final GuildSettingRepository guildSettingRepository = new GuildSettingRepository();
private final GuildService guildService;
private final Map<String, String> settingsCache = new ConcurrentHashMap<>();
private final Map<String, String> settingsCache = new HashMap<>(); public GuildSettingService(GuildService guildService) {
this.guildService = guildService;
public GuildConfigManager(Guild guild, OGuild guildEntity) {
this.guild = guild;
this.guildEntity = guildEntity;
reloadSettingsCache(); reloadSettingsCache();
} }
@@ -49,19 +42,19 @@ public class GuildConfigManager {
} }
public void setSettingValue(GSettings setting, String value) { public void setSettingValue(GSettings setting, String value) {
guildSettingRepository.setSettingValue(guildEntity, setting, value); guildSettingRepository.setSettingValue(guildService.getEntity(), setting, value);
reloadSettingsCache(); reloadSettingsCache();
} }
public void reloadSettingsCache() { public void reloadSettingsCache() {
logger.debug("Reloading settings cache for guild: {}", guild.getId()); logger.debug("Reloading settings cache for guild: {}", guildService.getEntity().getGuildId());
settingsCache.clear(); settingsCache.clear();
List<OGuildSetting> settings = guildSettingRepository.findByGuild(guild.getId());
List<OGuildSetting> settings = guildSettingRepository.findByGuild(guildService.getEntity().getGuildId());
for (OGuildSetting setting : settings) { for (OGuildSetting setting : settings) {
settingsCache.put(setting.getName(), setting.getValue()); settingsCache.put(setting.getName(), setting.getValue());
} }
} }
} }
@@ -15,23 +15,27 @@ import org.apache.logging.log4j.Logger;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.List; import java.util.List;
import java.util.Objects;
public class TaskService { public class TaskService {
private static final Logger logger = LogManager.getLogger(TaskService.class); private static final Logger logger = LogManager.getLogger(TaskService.class);
private final TaskManager taskManager;
private final TaskRepository taskRepository; private final TaskRepository taskRepository;
public TaskService(TaskManager taskManager) { public TaskService() {
this.taskManager = taskManager;
this.taskRepository = new TaskRepository(); this.taskRepository = new TaskRepository();
} }
public void loadTasks() { public void loadTasks() {
TaskRegistry.loadTaskConfig();
logger.info("Loading persistable tasks..."); logger.info("Loading persistable tasks...");
List<OTask> tasks = taskRepository.findAll(); List<OTask> tasks = taskRepository.findAll();
for (OTask oTask : tasks) { for (OTask oTask : tasks) {
String taskId = oTask.getTaskId();
String taskClass = oTask.getTaskClass(); String taskClass = oTask.getTaskClass();
logger.debug("Trying to load task with class: {}", taskClass);
String payload = oTask.getPayload(); String payload = oTask.getPayload();
try { try {
@@ -42,20 +46,38 @@ public class TaskService {
JSONObject json = new JSONObject(payload); JSONObject json = new JSONObject(payload);
PersistableTask task = creator.fromPayload(json); PersistableTask task = creator.fromPayload(json);
if (task == null) {
if (TaskManager.getInstance().exists(task.getId())) {
logger.error("Task with id {} already exists", task.getId());
continue; continue;
} }
logger.info("Loaded task: {}", task); validateTask(task, oTask);
TaskManager.getInstance().getTaskMap().put(task.getId(), task); TaskManager.getInstance().insertPersistableTask(task);
} catch (Exception e) { } catch (Exception e) {
logger.error("Error loading task: {}", payload, e); logger.error("Error loading task with ID {}: {}", taskId, e.getMessage());
} }
} }
} }
private void validateTask(PersistableTask task, OTask oTask) throws IllegalArgumentException {
String taskId = oTask.getTaskId();
String payload = oTask.getPayload();
if (!Objects.equals(taskId, task.getId())) {
throw new IllegalArgumentException("Task ID mismatch, expected: " + task.getId());
}
JSONObject json = new JSONObject(payload);
String payloadId = json.getString("id");
if (!Objects.equals(payloadId, task.getId())) {
throw new IllegalArgumentException("Task payload ID mismatch, expected: " + task.getId());
}
String payloadLockId = json.getString("lockId");
if (!Objects.equals(payloadLockId, task.getLockId())) {
throw new IllegalArgumentException("Task lock ID mismatch, expected: " + task.getLockId());
}
}
public void persistTask(PersistableTask task) { public void persistTask(PersistableTask task) {
String taskClass = task.getClass().getName(); String taskClass = task.getClass().getName();
if (TaskRegistry.getTaskCreator(taskClass) == null) { if (TaskRegistry.getTaskCreator(taskClass) == null) {
@@ -67,10 +89,10 @@ public class TaskService {
OTask oTask = taskRepository.findById(task.getId()); OTask oTask = taskRepository.findById(task.getId());
if (oTask == null) { if (oTask == null) {
oTask = new OTask(); oTask = new OTask();
oTask.setTaskId(task.getId());
oTask.setTaskClass(task.getClass().getName());
} }
oTask.setTaskId(task.getId());
oTask.setTaskClass(task.getClass().getName());
oTask.setPayload(task.toPayload()); oTask.setPayload(task.toPayload());
taskRepository.save(oTask); taskRepository.save(oTask);
@@ -81,6 +103,5 @@ public class TaskService {
TaskRepository taskRepository = new TaskRepository(); TaskRepository taskRepository = new TaskRepository();
taskRepository.deleteBy(task.getId()); taskRepository.deleteBy(task.getId());
taskManager.getTaskMap().remove(task.getId());
} }
} }
@@ -0,0 +1,19 @@
package me.bobert.netneutrality.exceptions;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.context.Context;
/**
* When thrown, this will call {@link Context#replyEphemeral(String)} with the specified message
*/
public class GracefulContextError extends RuntimeException {
public GracefulContextError(String message) {
super(message);
}
}
@@ -7,22 +7,30 @@ package me.bobert.netneutrality.hooks;
*/ */
import me.bobert.netneutrality.context.command.MessageCommandContext; import me.bobert.netneutrality.context.command.MessageCommandContext;
import me.bobert.netneutrality.context.command.MessageMenuContext;
import me.bobert.netneutrality.context.command.SlashCommandContext; import me.bobert.netneutrality.context.command.SlashCommandContext;
import me.bobert.netneutrality.context.command.UserMenuContext;
import me.bobert.netneutrality.context.tasks.ButtonTaskContext; import me.bobert.netneutrality.context.tasks.ButtonTaskContext;
import me.bobert.netneutrality.context.tasks.EmojiTaskContext; import me.bobert.netneutrality.context.tasks.EmojiTaskContext;
import me.bobert.netneutrality.context.tasks.MessageTaskContext;
import me.bobert.netneutrality.context.tasks.ModalTaskContext; import me.bobert.netneutrality.context.tasks.ModalTaskContext;
import me.bobert.netneutrality.core.GuildManager; import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.core.command.CommandProcessor; import me.bobert.netneutrality.core.command.CommandProcessor;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.Context; import me.bobert.netneutrality.core.context.Context;
import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.TaskManager; import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.utils.Settings; import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.Event; import net.dv8tion.jda.api.events.Event;
import net.dv8tion.jda.api.events.GenericEvent; import net.dv8tion.jda.api.events.GenericEvent;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent; import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
import net.dv8tion.jda.api.events.guild.GuildLeaveEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
@@ -45,21 +53,30 @@ public class EventsHandler implements EventListener {
if (event.getAuthor().isBot()) return null; if (event.getAuthor().isBot()) return null;
if (event.isWebhookMessage()) return null; if (event.isWebhookMessage()) return null;
String rawMessage = event.getMessage().getContentDisplay().trim(); Message message = event.getMessage();
String rawMessage = message.getContentDisplay().trim();
String prefix = Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$"); String prefix = Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$");
if (rawMessage.startsWith(prefix)) { if (rawMessage.startsWith(prefix)) {
return new MessageCommandContext(event); return new MessageCommandContext(event);
} }
return null; return new MessageTaskContext(event);
}); });
addListener(UserContextInteractionEvent.class, UserMenuContext::new);
addListener(MessageContextInteractionEvent.class, MessageMenuContext::new);
addListener(SlashCommandInteractionEvent.class, SlashCommandContext::new); addListener(SlashCommandInteractionEvent.class, SlashCommandContext::new);
addListener(ModalInteractionEvent.class, ModalTaskContext::new); addListener(ModalInteractionEvent.class, ModalTaskContext::new);
addListener(ButtonInteractionEvent.class, ButtonTaskContext::new); addListener(ButtonInteractionEvent.class, ButtonTaskContext::new);
addListener(MessageReactionAddEvent.class, EmojiTaskContext::new); addListener(MessageReactionAddEvent.class, EmojiTaskContext::new);
addGenericListener(GuildJoinEvent.class, event -> GuildManager.getManager(event.getGuild()).removeFromDB()); addGenericListener(GuildJoinEvent.class, event -> {
GuildService service = GuildService.get(event.getGuild());
if (service.isDebugEnabled()) {
LogHandler.sendMessageToDebugChannel(event.getGuild(), "Error logging enabled on this channel");
}
});
addGenericListener(GuildLeaveEvent.class, event -> GuildService.get(event.getGuild()).deleteGuild());
} }
/** /**
@@ -104,11 +121,6 @@ public class EventsHandler implements EventListener {
Function<Event, Context> function = (Function<Event, Context>) listeners.get(eventClass); Function<Event, Context> function = (Function<Event, Context>) listeners.get(eventClass);
Context context = function.apply(event); Context context = function.apply(event);
if (context == null) {
logger.debug("Handling event {} by generic listener", event);
return;
}
if (context instanceof CommandContext commandContext) { if (context instanceof CommandContext commandContext) {
handleCommand(commandContext); handleCommand(commandContext);
return; return;
@@ -9,8 +9,7 @@ package me.bobert.netneutrality.http;
import com.google.gson.Gson; import com.google.gson.Gson;
import io.sentry.Breadcrumb; import io.sentry.Breadcrumb;
import io.sentry.Sentry; import io.sentry.Sentry;
import me.bobert.netneutrality.NetNeutrality; import me.bobert.netneutrality.utils.Settings;
import me.bobert.netneutrality.core.LogHandler;
import okhttp3.*; import okhttp3.*;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -18,15 +17,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.*;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class WebModule { public class WebModule {
private enum Method { private enum Method {
GET, POST PING, GET, POST
} }
private static final String USER_AGENT = "Net Neutrality/%s (Discord bot, discord.gg/trAqhQ5Wsr)"; private static final String USER_AGENT = "Net Neutrality/%s (Discord bot, discord.gg/trAqhQ5Wsr)";
@@ -45,7 +41,6 @@ public class WebModule {
private final long cacheExpirationMillis; private final long cacheExpirationMillis;
private final OkHttpClient client; private final OkHttpClient client;
private final Map<String, String> headers; private final Map<String, String> headers;
private final Map<String, String> params;
private final Request request; private final Request request;
private CachedResponse cachedResponse; private CachedResponse cachedResponse;
@@ -54,9 +49,8 @@ public class WebModule {
this.url = builder.url; this.url = builder.url;
this.client = builder.client; this.client = builder.client;
this.headers = builder.headers; this.headers = builder.headers;
this.params = builder.params;
this.cacheExpirationMillis = builder.timeUnit.toMillis(builder.timeout); this.cacheExpirationMillis = builder.timeUnit.toMillis(builder.timeout);
this.request = createRequest(); this.request = builder.request;
} }
/** /**
@@ -70,19 +64,6 @@ public class WebModule {
cache.keySet().removeIf(s -> s.contains(url)); cache.keySet().removeIf(s -> s.contains(url));
} }
private Request createRequest() {
Request.Builder builder = new Request.Builder().url(url);
builder.addHeader("User-Agent", String.format(USER_AGENT, NetNeutrality.VERSION));
headers.forEach(builder::addHeader);
if (method == Method.POST && !params.isEmpty()) {
RequestBody body = RequestBody.create(new Gson().toJson(params), MediaType.parse("application/json; charset=utf-8"));
builder.post(body);
}
return builder.build();
}
/** /**
* Sends the HTTP request based on the configuration and retrieves the response. * Sends the HTTP request based on the configuration and retrieves the response.
* <p> * <p>
@@ -107,24 +88,37 @@ public class WebModule {
} }
} }
long startTime = System.currentTimeMillis();
try (Response response = client.newCall(request).execute()) { try (Response response = client.newCall(request).execute()) {
long elapsedTime = System.currentTimeMillis() - startTime;
Sentry.addBreadcrumb(Breadcrumb.http(url, method.name(), response.code())); Sentry.addBreadcrumb(Breadcrumb.http(url, method.name(), response.code()));
String body = response.body() != null ? response.body().string() : null; String body = response.body() != null ? response.body().string() : null;
this.cachedResponse = new CachedResponse( this.cachedResponse = new CachedResponse(
response.code(), response.code(),
response.headers(), response.headers(),
elapsedTime,
body, body,
System.currentTimeMillis() + cacheExpirationMillis System.currentTimeMillis() + cacheExpirationMillis,
null
); );
if (method == Method.GET && cacheKey != null) {
log.debug("Caching request: {}", cacheKey);
cache.put(cacheKey, this.cachedResponse);
}
} catch (IOException e) { } catch (IOException e) {
long elapsedTime = System.currentTimeMillis() - startTime;
log.error("Request failed: {}", e.getMessage(), e);
Sentry.captureException(e); Sentry.captureException(e);
throw new RuntimeException("Failed to execute request", e);
this.cachedResponse = new CachedResponse(
0, // Status code 0 indicates a failure
new Headers.Builder().build(),
elapsedTime,
null,
System.currentTimeMillis() + cacheExpirationMillis,
e.getMessage()
);
}
if (method == Method.GET && cacheKey != null) {
cache.put(cacheKey, this.cachedResponse);
} }
return this; return this;
@@ -145,28 +139,27 @@ public class WebModule {
} }
public JSONObject getAsObject() { public JSONObject getAsObject() {
try { return cachedResponse.getBody()
return new JSONObject(cachedResponse.getBody()); .map(JSONObject::new)
} catch (NullPointerException e) { .orElseThrow(() -> new IllegalStateException("No body available for this response"));
LogHandler.log(e);
return new JSONObject();
}
} }
public JSONArray getAsArray() { public JSONArray getAsArray() {
try { return cachedResponse.getBody()
return new JSONArray(cachedResponse.getBody()); .map(JSONArray::new)
} catch (NullPointerException e) { .orElseThrow(() -> new IllegalStateException("No body available for this response"));
LogHandler.log(e);
return new JSONArray();
}
} }
public String getRawData() { public String getRawData() {
return cachedResponse.getBody(); return cachedResponse.getBody().orElse(null);
}
public boolean isSuccess() {
return cachedResponse != null && cachedResponse.getStatusCode() >= 200 && cachedResponse.getStatusCode() < 400;
} }
public static class Builder { public static class Builder {
private Request request;
private final String url; private final String url;
private Method method = Method.GET; private Method method = Method.GET;
private long timeout = 5; private long timeout = 5;
@@ -187,6 +180,11 @@ public class WebModule {
this.url = url; this.url = url;
} }
public Builder ping() {
this.method = Method.PING;
return this;
}
/** /**
* Configures the request to use the HTTP GET method. * Configures the request to use the HTTP GET method.
* *
@@ -224,7 +222,7 @@ public class WebModule {
/** /**
* Configures caching for the request. * Configures caching for the request.
* *
* @param timeout the cache expiration duration * @param timeout the cache expiration duration
* @param timeUnit the unit of time for the cache expiration * @param timeUnit the unit of time for the cache expiration
* @return the current {@code Builder} instance for chaining * @return the current {@code Builder} instance for chaining
*/ */
@@ -234,10 +232,17 @@ public class WebModule {
return this; return this;
} }
public Builder timeout(long timeout, TimeUnit timeUnit) {
this.connectTimeout = timeUnit.toMillis(timeout);
this.readTimeout = timeUnit.toMillis(timeout);
this.writeTimeout = timeUnit.toMillis(timeout);
return this;
}
/** /**
* Sets the connection timeout for the HTTP client. * Sets the connection timeout for the HTTP client.
* *
* @param timeout the timeout duration * @param timeout the timeout duration
* @param timeUnit the unit of time for the timeout * @param timeUnit the unit of time for the timeout
* @return the current {@code Builder} instance for chaining * @return the current {@code Builder} instance for chaining
*/ */
@@ -249,7 +254,7 @@ public class WebModule {
/** /**
* Sets the read timeout for the HTTP client. * Sets the read timeout for the HTTP client.
* *
* @param timeout the timeout duration * @param timeout the timeout duration
* @param timeUnit the unit of time for the timeout * @param timeUnit the unit of time for the timeout
* @return the current {@code Builder} instance for chaining * @return the current {@code Builder} instance for chaining
*/ */
@@ -261,7 +266,7 @@ public class WebModule {
/** /**
* Sets the write timeout for the HTTP client. * Sets the write timeout for the HTTP client.
* *
* @param timeout the timeout duration * @param timeout the timeout duration
* @param timeUnit the unit of time for the timeout * @param timeUnit the unit of time for the timeout
* @return the current {@code Builder} instance for chaining * @return the current {@code Builder} instance for chaining
*/ */
@@ -270,6 +275,27 @@ public class WebModule {
return this; return this;
} }
private Request createRequest() {
Request.Builder builder = new Request.Builder().url(url);
builder.addHeader("User-Agent", String.format(USER_AGENT, Settings.getVersion()));
headers.forEach(builder::addHeader);
if (method == Method.PING) {
builder.head();
}
if (method == Method.GET) {
builder.get();
}
if (method == Method.POST && !params.isEmpty()) {
RequestBody body = RequestBody.create(new Gson().toJson(params), MediaType.parse("application/json; charset=utf-8"));
builder.post(body);
}
return builder.build();
}
/** /**
* Builds and returns a configured {@link WebModule} instance. * Builds and returns a configured {@link WebModule} instance.
* *
@@ -281,6 +307,7 @@ public class WebModule {
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS) .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS) .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.build(); .build();
this.request = createRequest();
return new WebModule(this); return new WebModule(this);
} }
@@ -289,14 +316,18 @@ public class WebModule {
private static class CachedResponse { private static class CachedResponse {
private final int statusCode; private final int statusCode;
private final Headers headers; private final Headers headers;
private final long elapsedTime;
private final String body; private final String body;
private final long expirationTime; private final long expirationTime;
private final String errorMessage;
public CachedResponse(int statusCode, Headers headers, String body, long expirationTime) { public CachedResponse(int statusCode, Headers headers, long elapsedTime, String body, long expirationTime, String errorMessage) {
this.statusCode = statusCode; this.statusCode = statusCode;
this.headers = headers; this.headers = headers;
this.elapsedTime = elapsedTime;
this.body = body; this.body = body;
this.expirationTime = expirationTime; this.expirationTime = expirationTime;
this.errorMessage = errorMessage;
} }
public boolean isExpired() { public boolean isExpired() {
@@ -311,8 +342,16 @@ public class WebModule {
return headers; return headers;
} }
public String getBody() { public long getElapsedTime() {
return body; return elapsedTime;
}
public Optional<String> getBody() {
return Optional.ofNullable(body);
}
public Optional<String> getErrorMessage() {
return Optional.ofNullable(errorMessage);
} }
} }
} }
@@ -9,7 +9,7 @@ package me.bobert.netneutrality.http.server;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import me.bobert.netneutrality.NetNeutrality; import me.bobert.netneutrality.NetNeutrality;
import me.bobert.netneutrality.core.GuildManager; import me.bobert.netneutrality.db.service.GuildService;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.json.JSONObject; import org.json.JSONObject;
@@ -49,8 +49,8 @@ public class ActionHandler extends AbstractHandler {
Guild guild = NetNeutrality.getInstance().getJDA().getGuildById(guildId); Guild guild = NetNeutrality.getInstance().getJDA().getGuildById(guildId);
if (guild != null) { if (guild != null) {
GuildManager manager = GuildManager.getManager(guild); GuildService service = GuildService.get(guild);
Map<String, String> settings = manager.getGuildConfigManager().getSettings(); Map<String, String> settings = service.getGuildSettingService().getSettings();
JSONObject setting = new JSONObject(settings); JSONObject setting = new JSONObject(settings);
json.put("status", "ok"); json.put("status", "ok");
@@ -68,13 +68,8 @@ public class ActionHandler extends AbstractHandler {
String[] args = exchange.getRequestURI().toString().split("/"); String[] args = exchange.getRequestURI().toString().split("/");
String guildId = args[args.length - 1]; String guildId = args[args.length - 1];
Guild guild = NetNeutrality.getInstance().getJDA().getGuildById(guildId);
boolean ok = true; //TODO: add bool boolean ok = true; //TODO: add bool
if (guild == null) GuildService.get(guildId).getGuildSettingService().reloadSettingsCache();
GuildManager.getManager(guildId).getGuildConfigManager().reloadSettingsCache();
else
GuildManager.getManager(guild).getGuildConfigManager().reloadSettingsCache();
json.put("status", String.valueOf(ok)); json.put("status", String.valueOf(ok));
sendJsonResponse(exchange, new Gson().toJson(json)); sendJsonResponse(exchange, new Gson().toJson(json));
@@ -13,11 +13,11 @@ import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager; import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import dev.lavalink.youtube.YoutubeAudioSourceManager; import dev.lavalink.youtube.YoutubeAudioSourceManager;
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.command.CommandExecutionResult; import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.thread.ThreadManager; import me.bobert.netneutrality.core.thread.ThreadManager;
import me.bobert.netneutrality.db.GSettings; import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.source.UdioAudioSourceManager; import me.bobert.netneutrality.music.source.UdioAudioSourceManager;
import me.bobert.netneutrality.thread.MusicManagerCleanupThread; import me.bobert.netneutrality.thread.MusicManagerCleanupThread;
import me.bobert.netneutrality.thread.MusicPlayerAutoDisconnectThread; import me.bobert.netneutrality.thread.MusicPlayerAutoDisconnectThread;
@@ -47,7 +47,7 @@ public class MusicManager {
this.guild = guild; this.guild = guild;
this.player = playerManager.createPlayer(); this.player = playerManager.createPlayer();
this.scheduler = new TrackScheduler(this); this.scheduler = new TrackScheduler(this);
this.baseVolume = Integer.parseInt(GuildManager.getManager(guild).getGuildConfigManager().getSetting(GSettings.MUSIC_VOLUME)); this.baseVolume = Integer.parseInt(GuildService.get(guild).getGuildSettingService().getSetting(GSettings.MUSIC_VOLUME));
guild.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(player)); guild.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(player));
player.addListener(scheduler); player.addListener(scheduler);
@@ -104,8 +104,9 @@ public class MusicManager {
return guild; return guild;
} }
public boolean isNotConnectedTo(AudioChannel vChannel) { public synchronized boolean isNotConnectedTo(AudioChannel vChannel) {
return vChannel == null || !vChannel.equals(guild.getAudioManager().getConnectedChannel()); AudioChannel current = guild.getAudioManager().getConnectedChannel();
return vChannel == null || current == null || !vChannel.getId().equals(current.getId());
} }
public synchronized void connect(AudioChannel audioChannel) { public synchronized void connect(AudioChannel audioChannel) {
@@ -120,7 +121,7 @@ public class MusicManager {
} }
public synchronized void disconnect() { public synchronized void disconnect() {
if (guild != null && guild.getAudioManager().getConnectedChannel() != null) { if (guild.getAudioManager().getConnectedChannel() != null) {
logger.debug("Guild {}: disconnecting from voice channel {}", guild.getId(), guild.getAudioManager().getConnectedChannel().getId()); logger.debug("Guild {}: disconnecting from voice channel {}", guild.getId(), guild.getAudioManager().getConnectedChannel().getId());
player.stopTrack(); player.stopTrack();
guild.getAudioManager().closeAudioConnection(); guild.getAudioManager().closeAudioConnection();
@@ -129,8 +130,8 @@ public class MusicManager {
scheduler.detachContext(); scheduler.detachContext();
} }
public boolean isConnected() { public synchronized boolean isConnected() {
return guild != null && guild.getAudioManager().getConnectedChannel() != null; return guild.getAudioManager().getConnectedChannel() != null;
} }
public synchronized AudioTrack getCurrentlyPlaying() { public synchronized AudioTrack getCurrentlyPlaying() {
@@ -57,7 +57,7 @@ public class TrackLoadingHandler implements AudioLoadResultHandler {
public void playlistLoaded(AudioPlaylist playlist) { public void playlistLoaded(AudioPlaylist playlist) {
logger.debug("Guild {}: playlist loaded: {}", guild.getId(), identifier); logger.debug("Guild {}: playlist loaded: {}", guild.getId(), identifier);
TaskManager.getInstance().insert(context, new LoadPlaylistTask(playlist, type)); new LoadPlaylistTask(playlist, type).insert(context);
} }
@Override @Override
@@ -1,30 +0,0 @@
package me.bobert.netneutrality.tasks;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.tasks.AbstractTask;
import java.time.OffsetDateTime;
import java.util.UUID;
public abstract class ButtonTask extends AbstractTask {
private final UUID id = UUID.randomUUID();
@Override
public String getId() {
return id.toString();
}
@Override
public boolean oneshot() {
return true;
}
@Override
public boolean isExpired() {
return getCreationTime().plusMinutes(15).isBefore(OffsetDateTime.now());
}
}
@@ -1,31 +0,0 @@
package me.bobert.netneutrality.tasks;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.tasks.AbstractPersistableTask;
public abstract class EmojiTask extends AbstractPersistableTask {
private final String guildId;
private final String messageId;
public EmojiTask(String guildId, String messageId) {
this.guildId = guildId;
this.messageId = messageId;
}
public String getGuildId() {
return guildId;
}
public String getMessageId() {
return messageId;
}
@Override
public String getId() {
return String.format("%s:%s", getGuildId(), getMessageId());
}
}
@@ -6,35 +6,21 @@ package me.bobert.netneutrality.tasks.emoji;
*/ */
import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.tasks.EmojiTask; import me.bobert.netneutrality.core.tasks.impl.EmojiTask;
import me.bobert.netneutrality.utils.GuildUtils;
import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.Role;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class EmojiReactionRoleTask extends EmojiTask { public class EmojiReactionRoleTask extends EmojiTask {
private final Map<String, String> emojiMapping; public EmojiReactionRoleTask(String guildId, String channelId, String messageId) {
public EmojiReactionRoleTask(String guildId, String messageId) {
super(guildId, messageId); super(guildId, messageId);
this.emojiMapping = new HashMap<>();
} }
// constructor for auto loading
public EmojiReactionRoleTask(String guildId, String messageId, Map<String, String> emojiMapping) { public EmojiReactionRoleTask(String guildId, String messageId, Map<String, String> emojiMapping) {
super(guildId, messageId); super(guildId, messageId);
this.emojiMapping = emojiMapping; getEmojiMapping().putAll(emojiMapping);
}
public void addEmoji(String emoji, String role) {
emojiMapping.put(emoji, role);
}
public void removeEmoji(String emoji) {
emojiMapping.remove(emoji);
}
public Map<String, String> getEmojiMapping() {
return emojiMapping;
} }
@Override @Override
@@ -43,15 +29,17 @@ public class EmojiReactionRoleTask extends EmojiTask {
} }
@Override @Override
public void execute(TaskContext context) { public Lifecycle execute(TaskContext context) {
String emoji = (String) context.getContextData("emoji-code"); String emoji = (String) context.getContextData("emoji-code");
if (getEmojiMapping().containsKey(emoji)) { if (getEmojiMapping().containsKey(emoji)) {
Role role = context.getGuild().getRoleById(getEmojiMapping().get(emoji)); Role role = context.getGuild().getRoleById(getEmojiMapping().get(emoji));
if (role != null) { if (role != null) {
context.getGuildManager().addRolesToMember(context.getGuildUser(), role); GuildUtils.addRolesToMember(context, role);
} }
} }
return Lifecycle.KEEP;
} }
} }
@@ -0,0 +1,53 @@
package me.bobert.netneutrality.tasks.generic;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.trait.WithChildren;
import me.bobert.netneutrality.core.tasks.trait.WithParent;
import me.bobert.netneutrality.core.tasks.impl.ButtonTask;
import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
import java.util.function.Function;
/**
* A generic button task that represents a button to accept a choice.
* @param <P> the type of the parent task, which must implement WithChildren
*/
public class AcceptButton<P extends WithChildren> extends ButtonTask implements WithParent<P> {
private final P parentTask;
private final Function<TaskContext, Lifecycle> function;
protected AcceptButton(P parentTask, ButtonStyle buttonStyle, String buttonLabel, Function<TaskContext, Lifecycle> function) {
super(buttonStyle, buttonLabel);
this.parentTask = parentTask;
this.function = function;
}
public static <P extends WithChildren> AcceptButton<P> createPrimaryButton(P parentTask, String buttonLabel, Function<TaskContext, Lifecycle> function) {
return new AcceptButton<>(parentTask, ButtonStyle.PRIMARY, buttonLabel, function);
}
public static <P extends WithChildren> AcceptButton<P> createSuccessButton(P parentTask, String buttonLabel, Function<TaskContext, Lifecycle> function) {
return new AcceptButton<>(parentTask, ButtonStyle.SUCCESS, buttonLabel, function);
}
@Override
public P getParentTask() {
return parentTask;
}
@Override
public String getName() {
return "accept-" + parentTask.getName();
}
@Override
public Lifecycle execute(TaskContext context) {
return function.apply(context);
}
}
@@ -0,0 +1,46 @@
package me.bobert.netneutrality.tasks.generic;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.trait.NotAcknowledgeable;
import me.bobert.netneutrality.core.tasks.trait.WithChildren;
import me.bobert.netneutrality.core.tasks.trait.WithParent;
import me.bobert.netneutrality.core.tasks.impl.ButtonTask;
import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
import java.util.function.Function;
public class CancelButton<P extends WithChildren> extends ButtonTask implements WithParent<P>, NotAcknowledgeable {
private final P parentTask;
private final Function<TaskContext, Lifecycle> function;
protected CancelButton(P parentTask, ButtonStyle buttonStyle, String buttonLabel, Function<TaskContext, Lifecycle> function) {
super(buttonStyle, buttonLabel);
this.parentTask = parentTask;
this.function = function;
}
public static <P extends WithChildren> CancelButton<P> create(P parentTask, String buttonLabel, Function<TaskContext, Lifecycle> function) {
return new CancelButton<>(parentTask, ButtonStyle.DANGER, buttonLabel, function);
}
@Override
public P getParentTask() {
return parentTask;
}
@Override
public String getName() {
return "cancel-" + parentTask.getName();
}
@Override
public Lifecycle execute(TaskContext context) {
return function.apply(context);
}
}
@@ -6,10 +6,11 @@ package me.bobert.netneutrality.tasks.git;
*/ */
import io.sentry.Sentry; import io.sentry.Sentry;
import me.bobert.netneutrality.context.command.SlashCommandContext;
import me.bobert.netneutrality.core.LogHandler; import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.tasks.ModalTask; import me.bobert.netneutrality.core.tasks.impl.ModalTask;
import me.bobert.netneutrality.utils.Settings; import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.ActionRow;
@@ -27,8 +28,8 @@ public class ReportIssueTask extends ModalTask {
private static final Logger logger = LogManager.getLogger(ReportIssueTask.class); private static final Logger logger = LogManager.getLogger(ReportIssueTask.class);
private final boolean insertable; private final boolean insertable;
public ReportIssueTask(boolean insertable) { public ReportIssueTask(CommandContext context) {
this.insertable = insertable; this.insertable = context instanceof SlashCommandContext;
} }
@Override @Override
@@ -63,7 +64,7 @@ public class ReportIssueTask extends ModalTask {
} }
@Override @Override
public void execute(TaskContext context) { public Lifecycle execute(TaskContext context) {
context.ephemeral(); context.ephemeral();
String gitlabHost = Settings.getInstance().get("GITLAB_HOST"); String gitlabHost = Settings.getInstance().get("GITLAB_HOST");
@@ -73,7 +74,7 @@ public class ReportIssueTask extends ModalTask {
if (gitlabHost.isBlank() || gitlabToken.isBlank() || projectId.isBlank()) { if (gitlabHost.isBlank() || gitlabToken.isBlank() || projectId.isBlank()) {
logger.error("Gitlab is not configured in the bot settings, can't execute task"); logger.error("Gitlab is not configured in the bot settings, can't execute task");
context.reply("Command is not configured, failed to create issue."); context.reply("Command is not configured, failed to create issue.");
return; return null;
} }
ModalMapping modalTitle = getValue("title", context); ModalMapping modalTitle = getValue("title", context);
@@ -103,5 +104,7 @@ public class ReportIssueTask extends ModalTask {
context.reply("Failed to create issue."); context.reply("Failed to create issue.");
LogHandler.log(e); LogHandler.log(e);
} }
return Lifecycle.REMOVE_SELF;
} }
} }
@@ -7,6 +7,7 @@ package me.bobert.netneutrality.tasks.help;
import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask; import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
public class BotInfo extends AbstractBuiltInTask { public class BotInfo extends AbstractBuiltInTask {
@@ -16,7 +17,12 @@ public class BotInfo extends AbstractBuiltInTask {
} }
@Override @Override
public void execute(TaskContext context) { public Button createButton() {
return Button.primary(getId(), "Info");
}
@Override
public void executeTask(TaskContext context) {
context.sendMessageInPrivate("For information about the bot and to follow the development, " + context.sendMessageInPrivate("For information about the bot and to follow the development, " +
"you can join the discord here: https://discord.gg/udVJwTgV"); "you can join the discord here: https://discord.gg/udVJwTgV");
} }
@@ -9,6 +9,7 @@ import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask; import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask;
import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MessageFunction;
import me.bobert.netneutrality.utils.Settings; import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
public class CommandHelp extends AbstractBuiltInTask { public class CommandHelp extends AbstractBuiltInTask {
@@ -18,7 +19,12 @@ public class CommandHelp extends AbstractBuiltInTask {
} }
@Override @Override
public void execute(TaskContext context) { public Button createButton() {
return Button.primary(getId(), "Commands");
}
@Override
public void executeTask(TaskContext context) {
context.replyEphemeral("The prefix for the bot is " + Settings.getInstance().getPrefix()); context.replyEphemeral("The prefix for the bot is " + Settings.getInstance().getPrefix());
context.replyEphemeral(MessageFunction.buildHelpMessage()); context.replyEphemeral(MessageFunction.buildHelpMessage());
} }
@@ -7,6 +7,7 @@ package me.bobert.netneutrality.tasks.help;
import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask; import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
public class ReportBug extends AbstractBuiltInTask { public class ReportBug extends AbstractBuiltInTask {
@@ -16,7 +17,12 @@ public class ReportBug extends AbstractBuiltInTask {
} }
@Override @Override
public void execute(TaskContext context) { public Button createButton() {
return Button.danger(getId(), "Report bug");
}
@Override
public void executeTask(TaskContext context) {
context.sendMessageInPrivate("To report a bug with the bot, you can either use the /report-bug command " + context.sendMessageInPrivate("To report a bug with the bot, you can either use the /report-bug command " +
"or join the discord here: https://discord.gg/udVJwTgV"); "or join the discord here: https://discord.gg/udVJwTgV");
} }
@@ -1,61 +0,0 @@
package me.bobert.netneutrality.tasks.music;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.trait.WithParent;
import me.bobert.netneutrality.music.MusicManager;
import me.bobert.netneutrality.music.SourceFinder;
import me.bobert.netneutrality.tasks.ButtonTask;
import me.bobert.netneutrality.utils.MessageFunction;
import me.bobert.netneutrality.utils.MusicUtils;
import net.dv8tion.jda.api.entities.Member;
import java.util.List;
public class AddAllTracksTask extends ButtonTask implements WithParent<LoadPlaylistTask> {
private final LoadPlaylistTask parentTask;
List<AudioTrack> tracks;
public AddAllTracksTask(LoadPlaylistTask parentTask, List<AudioTrack> tracks) {
this.parentTask = parentTask;
this.tracks = tracks;
}
@Override
public String getName() {
return "add-all-tracks";
}
@Override
public void replyToInteraction(CommandContext context) {
}
@Override
public void execute(TaskContext context) {
MusicManager manager = MusicManager.get(context.getGuild());
Member member = context.getGuildUser();
if (MusicUtils.connect(context)) {
manager.getQueueManager().queue(tracks, member);
context.reply(MessageFunction.buildAddingPlaylistMessage(tracks, parentTask.getPlaylist().getName(), member, SourceFinder.AudioSourceType.Youtube));
}
}
@Override
public LoadPlaylistTask getParentTask() {
return parentTask;
}
@Override
public boolean shouldKeepChilds() {
return false;
}
}
@@ -1,45 +0,0 @@
package me.bobert.netneutrality.tasks.music;
/*
* 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.NotAcknowledgeable;
import me.bobert.netneutrality.core.tasks.trait.WithParent;
import me.bobert.netneutrality.tasks.ButtonTask;
public class CancelLoadTask extends ButtonTask implements WithParent<LoadPlaylistTask>, NotAcknowledgeable {
private final LoadPlaylistTask parentTask;
public CancelLoadTask(LoadPlaylistTask parentTask) {
this.parentTask = parentTask;
}
@Override
public String getName() {
return "cancel-playlist-loading";
}
@Override
public void replyToInteraction(CommandContext context) {
}
@Override
public LoadPlaylistTask getParentTask() {
return parentTask;
}
@Override
public boolean shouldKeepChilds() {
return false;
}
@Override
public void execute(TaskContext context) {
// Interaction will get removed after execution
}
}
@@ -8,16 +8,20 @@ package me.bobert.netneutrality.tasks.music;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.Task; import me.bobert.netneutrality.core.tasks.Task;
import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.core.tasks.trait.Deletable; import me.bobert.netneutrality.core.tasks.trait.Deletable;
import me.bobert.netneutrality.core.tasks.trait.WithChildren; import me.bobert.netneutrality.core.tasks.trait.WithChildren;
import me.bobert.netneutrality.db.GSettings; import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.music.MusicManager;
import me.bobert.netneutrality.music.SourceFinder; import me.bobert.netneutrality.music.SourceFinder;
import me.bobert.netneutrality.tasks.ButtonTask; import me.bobert.netneutrality.core.tasks.impl.ButtonTask;
import me.bobert.netneutrality.tasks.generic.AcceptButton;
import me.bobert.netneutrality.tasks.generic.CancelButton;
import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MessageFunction;
import me.bobert.netneutrality.utils.MusicUtils; import me.bobert.netneutrality.utils.MusicUtils;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.ItemComponent; import net.dv8tion.jda.api.interactions.components.ItemComponent;
@@ -31,6 +35,7 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
private final SourceFinder.AudioSourceType sourceType; private final SourceFinder.AudioSourceType sourceType;
public LoadPlaylistTask(AudioPlaylist playlist, SourceFinder.AudioSourceType sourceType) { public LoadPlaylistTask(AudioPlaylist playlist, SourceFinder.AudioSourceType sourceType) {
super(null, null);
this.playlist = playlist; this.playlist = playlist;
this.sourceType = sourceType; this.sourceType = sourceType;
} }
@@ -57,8 +62,8 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
} }
int searchResultsLimit = 5; // Max number of buttons per row int searchResultsLimit = 5; // Max number of buttons per row
int maxPlaylistSizeBeforeAsking = Integer.parseInt(context.getGuildManager() int maxPlaylistSizeBeforeAsking = Integer.parseInt(GuildService.get(context.getGuild())
.getGuildConfigManager() .getGuildSettingService()
.getSetting(GSettings.MUSIC_MAXPLAYLISTSIZE)); .getSetting(GSettings.MUSIC_MAXPLAYLISTSIZE));
// Search results // Search results
@@ -68,10 +73,10 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
for (int i = 0; i < searchResults.size(); i++) { for (int i = 0; i < searchResults.size(); i++) {
AudioTrack track = searchResults.get(i); AudioTrack track = searchResults.get(i);
Task buttonTask = new LoadTrackFromPlaylistTask(this, track); Task buttonTask = new LoadTrackFromPlaylistTask(this, String.valueOf(i + 1), track);
buttonList.add(buttonTask.createButton().primary(String.valueOf(i + 1))); buttonList.add(buttonTask.createButton());
childrens.add(buttonTask); addChild(buttonTask);
} }
ActionRow firstRow = ActionRow.of(buttonList); ActionRow firstRow = ActionRow.of(buttonList);
@@ -99,21 +104,27 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
} }
} }
@Override
public void execute(TaskContext context) {
// Only the buttons will get executed, this task is only used to insert them
}
private ActionRow commonButtons(List<AudioTrack> tracks, String addAllText, String cancelText) { private ActionRow commonButtons(List<AudioTrack> tracks, String addAllText, String cancelText) {
Task addAllTracksTask = new AddAllTracksTask(this, tracks); Task accept = AcceptButton.createSuccessButton(this, addAllText, context -> {
childrens.add(addAllTracksTask); MusicManager manager = MusicManager.get(context.getGuild());
Member member = context.getGuildUser();
Task cancelTask = new CancelLoadTask(this); if (MusicUtils.connect(context)) {
childrens.add(cancelTask); manager.getQueueManager().queue(tracks, member);
context.reply(MessageFunction.buildAddingPlaylistMessage(tracks, playlist.getName(), member, SourceFinder.AudioSourceType.Youtube));
return Lifecycle.REMOVE_SELF_AND_CHILDREN;
}
return Lifecycle.KEEP;
});
Task cancel = CancelButton.create(this, cancelText, context -> Lifecycle.REMOVE_SELF_AND_CHILDREN);
addChild(accept, cancel);
return ActionRow.of( return ActionRow.of(
addAllTracksTask.createButton().success(addAllText).withEmoji(Emoji.fromFormatted("U+1F60E")), accept.createButton().withEmoji(Emoji.fromFormatted("U+1F60E")),
cancelTask.createButton().danger(cancelText).withEmoji(Emoji.fromFormatted("U+1F92E")) cancel.createButton().withEmoji(Emoji.fromFormatted("U+1F92E"))
); );
} }
@@ -124,6 +135,8 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
@Override @Override
public void onInteractionDelete() { public void onInteractionDelete() {
removeAllChildTasks(); for (Task child : getChildTasks()) {
TaskManager.getInstance().removeTask(child);
}
} }
} }
@@ -6,22 +6,32 @@ package me.bobert.netneutrality.tasks.music;
*/ */
import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.trait.WithParent;
import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.music.MusicManager;
import me.bobert.netneutrality.music.SourceFinder; import me.bobert.netneutrality.music.SourceFinder;
import me.bobert.netneutrality.tasks.ButtonTask; import me.bobert.netneutrality.tasks.generic.AcceptButton;
import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MessageFunction;
import me.bobert.netneutrality.utils.MusicUtils; import me.bobert.netneutrality.utils.MusicUtils;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
public class LoadTrackFromPlaylistTask extends ButtonTask implements WithParent<LoadPlaylistTask> { public class LoadTrackFromPlaylistTask extends AcceptButton<LoadPlaylistTask> {
private final LoadPlaylistTask parentTask;
private final AudioTrack track; private final AudioTrack track;
public LoadTrackFromPlaylistTask(LoadPlaylistTask parentTask, AudioTrack track) { public LoadTrackFromPlaylistTask(LoadPlaylistTask parentTask, String buttonLabel, AudioTrack track) {
this.parentTask = parentTask; super(parentTask, ButtonStyle.PRIMARY, buttonLabel, context -> {
MusicManager manager = MusicManager.get(context.getGuild());
Member member = context.getGuildUser();
if (MusicUtils.connect(context)) {
manager.getQueueManager().queue(track, member);
context.reply(MessageFunction.buildAddingTrackMessage(track, member, SourceFinder.AudioSourceType.Youtube));
return Lifecycle.REMOVE_SELF_AND_CHILDREN;
}
return Lifecycle.KEEP;
});
this.track = track; this.track = track;
} }
@@ -29,31 +39,4 @@ public class LoadTrackFromPlaylistTask extends ButtonTask implements WithParent<
public String getName() { public String getName() {
return String.format("load-track-%s", track.getIdentifier()); return String.format("load-track-%s", track.getIdentifier());
} }
@Override
public void replyToInteraction(CommandContext context) {
}
@Override
public void execute(TaskContext context) {
MusicManager manager = MusicManager.get(context.getGuild());
Member member = context.getGuildUser();
if (MusicUtils.connect(context)) {
manager.getQueueManager().queue(track, member);
context.reply(MessageFunction.buildAddingTrackMessage(track, member, SourceFinder.AudioSourceType.Youtube));
}
}
@Override
public LoadPlaylistTask getParentTask() {
return parentTask;
}
@Override
public boolean shouldKeepChilds() {
return false;
}
} }
@@ -26,13 +26,11 @@ public class CleanupTaskThread implements Runnable {
.toList(); .toList();
if (!tasks.isEmpty()) { if (!tasks.isEmpty()) {
logger.info("Cleaning up tasks.."); logger.info("Cleaning up {} expired tasks..", tasks.size());
for (Task task : tasks) { for (Task task : tasks) {
TaskManager.getInstance().removeTask(task); TaskManager.getInstance().removeTask(task);
} }
} }
} }
} }
@@ -0,0 +1,26 @@
package me.bobert.netneutrality.utils;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.context.Context;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
public class GuildUtils {
public static void addRolesToMember(Context context, Role role) {
addRolesToMember(context.getGuild(), context.getGuildUser(), role);
}
public static void addRolesToMember(Guild guild, Member member, Role role) {
Member self = guild.getSelfMember();
if (self.canInteract(role)) {
guild.addRoleToMember(member, role).queue();
}
}
}
@@ -6,9 +6,9 @@ package me.bobert.netneutrality.utils;
* https://discord.gg/qUa6zNFZsd * https://discord.gg/qUa6zNFZsd
*/ */
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.db.SessionManager; import me.bobert.netneutrality.core.db.SessionManager;
import me.bobert.netneutrality.db.entities.OGuild; import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.db.service.GuildService;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
@@ -30,7 +30,6 @@ public class JpaUtils {
* If the object is not found in the database and is of type {@link OGuild}, tries to recreate entity and throws * If the object is not found in the database and is of type {@link OGuild}, tries to recreate entity and throws
* an exception to block execution. * an exception to block execution.
* <p> * <p>
* The session is not closed
* *
* @param session The session in which to load the persistent object * @param session The session in which to load the persistent object
* @param object The persistent object * @param object The persistent object
@@ -45,9 +44,9 @@ public class JpaUtils {
String cachedId = oGuild.getGuildId(); String cachedId = oGuild.getGuildId();
logger.error("Trying to load Guild[{}] while entity doesn't exist in database, reloading..", cachedId); logger.error("Trying to load Guild[{}] while entity doesn't exist in database, reloading..", cachedId);
GuildManager guildManager = GuildManager.getManager(cachedId); GuildService service = GuildService.get(cachedId);
guildManager.reloadEntity(); service.reloadEntity();
guildManager.getBlacklistCache().clear(); service.getBlacklistService().removeGuildBlacklist();
logger.info("Guild[{}] reloaded", cachedId); logger.info("Guild[{}] reloaded", cachedId);
} }
} }
@@ -54,6 +54,10 @@ public class Settings {
return INSTANCE; return INSTANCE;
} }
public static String getVersion() {
return Settings.getInstance().getMavenVersion();
}
public String get(String key) { public String get(String key) {
String property = prop.getProperty(key); String property = prop.getProperty(key);