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.setTracesSampleRate(1.0);
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.context.CommandContext;
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.build.SubcommandData;
@@ -54,14 +55,14 @@ public class BlacklistCommand extends AbstractAdminCommand {
public CommandExecutionResult execute(CommandContext context) {
String sub = context.getSubcommand();
String word = context.getOption("word", "", CommandOption::getAsString);
BlacklistRepository repository = new BlacklistRepository();
BlacklistService service = GuildService.get(context.getGuild()).getBlacklistService();
switch (sub) {
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();
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) {
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());
});
case "add" -> {
return repository.add(context.getGuild(), context.getGuildUser(), word);
return service.add(context.getGuildUser(), word);
}
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.SubcommandGroupData;
import java.util.List;
import java.util.Map;
public class ReactionCommand extends AbstractAdminCommand {
@Override
@@ -43,6 +46,7 @@ public class ReactionCommand extends AbstractAdminCommand {
public void registerCommandOptions(CommandOptionsBuilder builder) {
builder.addSubcommandGroups(
new SubcommandGroupData("emoji", "Give roles to users using emojis").addSubcommands(
new SubcommandData("list", "Lists all registered tasks for this guild"),
new SubcommandData("setup", "Add a role to a message").addOptions(
new OptionData(OptionType.STRING, "messageid", "The ID of the message", true)
.setRequiredLength(19, 19),
@@ -77,65 +81,78 @@ public class ReactionCommand extends AbstractAdminCommand {
String group = context.getSubcommandGroup();
String sub = context.getSubcommand();
switch (group) {
case "emoji" -> {
if ("setup".equals(sub)) {
String emoji = context.getOption("emoji", CommandOption::getAsString);
String role = context.getOption("role", CommandOption::getAsString);
return switch (group) {
case "emoji" -> executeEmoji(context, guildId, messageId, sub);
default -> CommandExecutionResult.none();
};
}
EmojiReactionRoleTask task = new EmojiReactionRoleTask(guildId, messageId);
if (TaskManager.getInstance().exists(task.getId())) {
return CommandExecutionResult.error("The message ID is already registered, use the add subcommand instead.");
}
task.addEmoji(emoji, role);
private CommandExecutionResult executeEmoji(CommandContext context, String guildId, String messageId, String sub) {
String id = String.format("%s:%s", guildId, messageId);
EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id);
TaskManager.getInstance().insert(context, task);
return CommandExecutionResult.success("Added emoji to message successfully!");
switch (sub) {
case "list" -> {
List<EmojiReactionRoleTask> tasks = TaskManager.getInstance().findTasksByPartialId(EmojiReactionRoleTask.class, guildId);
if (tasks.isEmpty()) {
return CommandExecutionResult.info("No tasks found for this guild");
}
if ("add".equals(sub)) {
String emoji = context.getOption("emoji", CommandOption::getAsString);
String role = context.getOption("role", CommandOption::getAsString);
StringBuilder builder = new StringBuilder();
builder.append("I have found the following tasks for this guild:");
String id = String.format("%s:%s", guildId, messageId);
EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id);
if (task == null) {
return CommandExecutionResult.error("The message ID provided isn't registered with the bot");
}
task.addEmoji(emoji, role);
TaskManager.getInstance().getTaskService().persistTask(task);
return CommandExecutionResult.success("Added emoji to message successfully!");
for (EmojiReactionRoleTask item : tasks) {
builder.append("\n- Task ID: ").append(item.getId()).append("\n")
.append(" Message ID: ").append(item.getMessageId()).append("\n")
.append(" Emoji Mapping: ").append(item.getEmojiMapping()).append("\n");
}
if ("remove".equals(sub)) {
String emoji = context.getOption("emoji", CommandOption::getAsString);
String id = String.format("%s:%s", guildId, messageId);
EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id);
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!");
return CommandExecutionResult.success(builder.toString());
}
case "setup" -> {
if (task != null) {
return CommandExecutionResult.error("The message ID is already registered, use the add subcommand instead.");
}
if ("delete".equals(sub)) {
String id = String.format("%s:%s", guildId, messageId);
EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id);
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 = new EmojiReactionRoleTask(guildId, messageId, Map.of(emoji, role));
task.insert(context);
TaskManager.getInstance().getTaskService().unpersistTask(task);
return CommandExecutionResult.success("Unregistered the message ID successfully!");
return CommandExecutionResult.success("Added emoji to message 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();
}
}
@@ -61,9 +61,9 @@ public class HelpCommand extends AbstractCommand {
public CommandExecutionResult execute(CommandContext context) {
String command = context.getOption("command", null, CommandOption::getAsString);
if (command == null) {
Button commands = BuiltInTask.createButton(CommandHelp.class).primary("Commands");
Button info = BuiltInTask.createButton(BotInfo.class).primary("Info");
Button reportbug = BuiltInTask.createButton(ReportBug.class).danger("Report bug");
Button commands = BuiltInTask.createButton(CommandHelp.class);
Button info = BuiltInTask.createButton(BotInfo.class);
Button reportbug = BuiltInTask.createButton(ReportBug.class);
ActionRow row = ActionRow.of(commands, info, reportbug);
context.replyEphemeral("Thanks for using the Net Neutrality Bot! What do you need help with?", List.of(row));
return CommandExecutionResult.none();
@@ -5,13 +5,13 @@ package me.bobert.netneutrality.command.bot;
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.command.AbstractAdminCommand;
import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.command.options.CommandOption;
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.MusicManager;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
@@ -63,7 +63,7 @@ public class ConfigureCommand extends AbstractAdminCommand {
@Override
public CommandExecutionResult execute(CommandContext context) {
String sub = context.getSubcommand();
GuildManager guildManager = GuildManager.getManager(context.getGuild());
GuildService guildService = GuildService.get(context.getGuild().getId());
context.ephemeral();
if ("music-role".equals(sub)) {
@@ -71,13 +71,13 @@ public class ConfigureCommand extends AbstractAdminCommand {
assert role != null;
if (role.getPosition() == -1) {
guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_ROLE, null);
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, null);
return CommandExecutionResult.success("Everyone can now use music commands");
}
if (role.getGuild().getId().equals(context.getGuild().getId())) {
guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_ROLE, role.getId());
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, role.getId());
return CommandExecutionResult.success(String.format("Successfully set %s as the role needed for music command", role.getAsMention()));
}
@@ -87,14 +87,14 @@ public class ConfigureCommand extends AbstractAdminCommand {
GuildChannelUnion channel = context.getOption("channel", CommandOption::getAsChannel);
if (channel == null) {
guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_CHANNEL, null);
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_CHANNEL, null);
return CommandExecutionResult.success("No channel restrictions for music commands");
}
if (channel instanceof TextChannel) {
if (channel.getGuild().getId().equals(context.getGuild().getId())) {
guildManager.getGuildConfigManager().setSettingValue(GSettings.MUSIC_CHANNEL, channel.getId());
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_CHANNEL, channel.getId());
return CommandExecutionResult.success(String.format("Successfully set %s as the channel required to execute music command", channel.getAsMention()));
}
@@ -104,7 +104,7 @@ public class ConfigureCommand extends AbstractAdminCommand {
if ("music-volume".equals(sub)) {
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);
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)) {
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"));
}
if ("show-all".equals(sub)) {
Map<String, String> settings = guildManager.getGuildConfigManager().getSettings();
Map<String, String> settings = guildService.getGuildSettingService().getSettings();
MessageCreateBuilder builder = new MessageCreateBuilder();
for (GSettings o : GSettings.values()) {
@@ -11,10 +11,12 @@ import me.bobert.netneutrality.core.command.CommandCategory;
import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.http.WebModule;
import me.bobert.netneutrality.tasks.git.ReportIssueTask;
import me.bobert.netneutrality.utils.Settings;
import java.util.concurrent.TimeUnit;
public class ReportIssueCommand extends AbstractCommand {
@Override
public boolean needAdmin() {
@@ -66,8 +68,10 @@ public class ReportIssueCommand extends AbstractCommand {
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;
}
public String getMessageId() {
if (hook != null) {
return hook.getId();
}
return message.getId();
}
@Override
public Event getEvent() {
return null;
@@ -60,6 +68,10 @@ public class AsyncContext extends AbstractContext {
@Override
public boolean isAcknowledged() {
if (hook != null) {
return hook.getInteraction().isAcknowledged();
}
return true;
}
@@ -6,19 +6,17 @@ package me.bobert.netneutrality.context;
*/
import me.bobert.netneutrality.core.context.AbstractContext;
import me.bobert.netneutrality.core.context.NoMessageContext;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.events.Event;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
import net.dv8tion.jda.api.interactions.modals.Modal;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import java.util.List;
import java.util.function.Consumer;
public class MusicChannelContext extends AbstractContext {
public class MusicChannelContext extends AbstractContext implements NoMessageContext {
public MusicChannelContext(GuildMessageChannel channel, Member member) {
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());
}
@Override
public ChannelType getType() {
return getChannel().getType();
}
@Override
public InteractionHook getInteractionHook() {
return null;
}
@Override
public boolean isAcknowledged() {
return true;
}
@Override
public boolean isFromGuild() {
return true;
@@ -59,25 +42,10 @@ public class MusicChannelContext extends AbstractContext {
}
@Override
public void reply(Modal modal) {
}
@Override
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (getChannel().canTalk()) {
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
}
}
@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
*/
import me.bobert.netneutrality.context.AsyncContext;
import me.bobert.netneutrality.core.command.options.CommandOption;
import me.bobert.netneutrality.core.command.options.MessageCommandOption;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.MessageContext;
import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import 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.List;
import java.util.Set;
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 Message message;
@@ -54,63 +48,10 @@ public class MessageCommandContext extends CommandContext {
}
@Override
public ChannelType getType() {
return event.getChannelType();
}
public Message getMessage() {
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
public List<CommandOption> parseOptions(String[] args, List<OptionData> commandOptions) {
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.core.context.NoMessageContext;
import me.bobert.netneutrality.core.context.TaskContext;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
@@ -17,7 +18,8 @@ import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import java.util.List;
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;
public EmojiTaskContext(MessageReactionAddEvent event) {
@@ -32,29 +34,9 @@ public class EmojiTaskContext extends TaskContext {
return event;
}
@Override
public ChannelType getType() {
return event.getChannelType();
}
@Override
public String getId() {
return String.format("%s:%s", 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;
return String.format(ID, getGuild().getId(), event.getMessageId());
}
@Override
@@ -62,29 +44,10 @@ public class EmojiTaskContext extends TaskContext {
return event.isFromGuild();
}
@Override
public void reply(Modal modal) {
}
@Override
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (getChannel() != null && getChannel().canTalk()) {
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
}
}
@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
*/
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.MusicManager;
import me.bobert.netneutrality.music.TrackScheduler;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -21,18 +21,21 @@ public class BotState {
//TODO: is it needed?
private static void saveGuildsState() {
Map<String, GuildManager> managers = GuildManager.getManagers();
Map<String, GuildService> services = GuildService.getServices();
JSONObject main = new JSONObject();
JSONArray guildsArray = new JSONArray();
for (GuildManager manager : managers.values()) {
MusicManager musicManager = MusicManager.get(manager.getGuild());
TrackScheduler trackScheduler = musicManager.getScheduler();
for (MusicManager manager : MusicManager.getManagers()) {
TrackScheduler trackScheduler = manager.getScheduler();
}
for (GuildService service : services.values()) {
JSONObject guild = new JSONObject();
JSONObject music = new JSONObject();
guild.put("id", manager.getGuild().getId());
guild.put("id", service.getEntity().getGuildId());
// MessageChannel currentChannel = trackScheduler.getTextChannel();
// if (currentChannel == null) {
@@ -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.Context;
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.exceptions.ContextException;
import me.bobert.netneutrality.utils.MessageFunction;
import net.dv8tion.jda.api.entities.Guild;
@@ -49,13 +50,12 @@ public class LogHandler {
}
public static void sendMessageToDebugChannel(Guild guild, String message) {
if (GuildManager.getManager(guild).isDebugEnabled()) {
guild.getTextChannels()
.stream()
.filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log"))
.filter(TextChannel::canTalk)
.forEach(textChannel -> textChannel.sendMessage(message).queue());
}
guild.getTextChannels()
.stream()
.filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log"))
.filter(TextChannel::canTalk)
.forEach(textChannel -> textChannel.sendMessage(message).queue());
}
private static void logInternally(ContextException exception) {
@@ -80,7 +80,7 @@ public class LogHandler {
Context context = exception.getContext();
if (context.getGuild() != null) {
if (context.getGuildManager().isDebugEnabled()) {
if (GuildService.get(context.getGuild()).isDebugEnabled()) {
context.getGuild()
.getTextChannels()
.stream()
@@ -17,9 +17,14 @@ public abstract class AbstractMessageMenuCommand extends AbstractCommand {
return null;
}
@Override
public boolean isGuildOnly() {
return true;
}
@Override
public boolean isSlashCommandOnly() {
return false;
return true;
}
@Override
@@ -27,6 +32,11 @@ public abstract class AbstractMessageMenuCommand extends AbstractCommand {
// no options for context commands
}
@Override
public CommandCategory getCategory() {
return CommandCategory.UNLISTED;
}
@Override
public Type getType() {
return Type.MESSAGE;
@@ -5,8 +5,8 @@ package me.bobert.netneutrality.core.command;
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.utils.UserFunction;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
@@ -39,7 +39,8 @@ public abstract class AbstractMusicCommand extends AbstractCommand {
}
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 (UserFunction.isAdmin(member)) {
@@ -49,7 +50,7 @@ public abstract class AbstractMusicCommand extends AbstractCommand {
Role neededRole = guild.getRoleById(roleDb);
if (neededRole == null) {
// Role doesn't exist on guild anymore, remove from db
GuildManager.getManager(guild).getGuildConfigManager().setSettingValue(GSettings.MUSIC_ROLE, null);
GuildService.get(guild.getId()).getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, null);
return true;
}
@@ -65,7 +66,7 @@ public abstract class AbstractMusicCommand extends AbstractCommand {
if (channel.getType() != ChannelType.TEXT)
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()) {
return channelDb.equals(channel.getId());
}
@@ -5,26 +5,38 @@ package me.bobert.netneutrality.core.command;
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.tasks.Task;
public class CommandExecutionResult {
public enum Result {
SUCCESS, INFO, WARN, ERROR, NOACTION
SUCCESS, INFO, WARN, ERROR, NOACTION, TASK
}
private final Result result;
private final String message;
private final Object data;
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.message = message;
this.data = data;
this.exception = exception;
}
private CommandExecutionResult(Result result, String message, Exception exception) {
this(result, message, null, exception);
}
private CommandExecutionResult(Result result, String message) {
this(result, message, null);
}
private CommandExecutionResult(Result result, Object data) {
this(result, null, data, null);
}
public Result getResult() {
return result;
}
@@ -33,6 +45,10 @@ public class CommandExecutionResult {
return message;
}
public Object getData() {
return data;
}
public Exception getException() {
return exception;
}
@@ -65,4 +81,7 @@ public class CommandExecutionResult {
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 java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class CommandManager {
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() {
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.context.CommandContext;
import me.bobert.netneutrality.core.context.InteractionContext;
import me.bobert.netneutrality.core.tasks.Task;
import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.exceptions.CommandExecutionException;
import me.bobert.netneutrality.exceptions.GracefulContextError;
import me.bobert.netneutrality.utils.UserFunction;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
@@ -26,8 +29,7 @@ import java.util.List;
import java.util.Map;
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.NOACTION;
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.*;
public class CommandProcessor {
@@ -56,13 +58,13 @@ public class CommandProcessor {
return;
}
if (command.isSlashCommandOnly() && !(context instanceof SlashCommandContext)) {
if (command.isSlashCommandOnly() && !(context instanceof InteractionContext)) {
context.replyEphemeral("This command is only available as a slash command");
return;
}
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());
configureSentry(command);
@@ -95,17 +97,28 @@ public class CommandProcessor {
return;
}
if (result == TASK) {
TaskManager.getInstance().insert(context, (Task) commandExecutionResult.getData());
}
String message = commandExecutionResult.getMessage();
if (result == ERROR) {
logger.error("Context[{}]: Error from command: {}", context.getId(), message, commandExecutionResult.getException());
}
context.reply(message);
} catch (Exception exception) {
if (exception instanceof CommandExecutionException) {
LogHandler.log(exception);
if (result == WARN) {
logger.warn("Context[{}]: Warning from command: {}", context.getId(), message);
}
if (message != null) {
context.reply(message);
}
} catch (GracefulContextError exception) {
logger.error(exception.getMessage());
context.replyEphemeral(exception.getMessage());
} catch (CommandExecutionException exception) {
LogHandler.log(exception);
} catch (Exception exception) {
LogHandler.log(new CommandExecutionException("Error during command execution", context, exception));
}
}
@@ -6,6 +6,7 @@ package me.bobert.netneutrality.core.context;
*/
import io.sentry.Sentry;
import me.bobert.netneutrality.context.command.SlashCommandContext;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
@@ -6,7 +6,6 @@ package me.bobert.netneutrality.core.context;
*/
import me.bobert.netneutrality.context.AsyncContext;
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.utils.ContextUtils;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.*;
@@ -55,10 +54,6 @@ public interface Context {
void ack(boolean ephemeral);
default GuildManager getGuildManager() {
return GuildManager.getManager(getGuild());
}
default void ack() {
ack(false);
}
@@ -160,33 +155,39 @@ public interface Context {
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();
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) {
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) {
sendMessageInPrivate(MessageCreateData.fromEmbeds(embeds));
}
default void sendMessageInPrivate(String message) {
sendMessageInPrivateWithLimit(message);
}
private void replyWithLimit(String message) {
for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) {
reply(MessageCreateData.fromContent(chunk));
@@ -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.channel.middleman.MessageChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public abstract class TaskContext extends AbstractContext {
private final Map<String, Object> contextData;
public TaskContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) {
super(jda, guild, channel, member, user);
this.contextData = new HashMap<>();
this.contextData = new ConcurrentHashMap<>();
}
@Override
@@ -6,6 +6,7 @@ package me.bobert.netneutrality.core.tasks;
*/
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.TaskContext;
public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltInTask {
@@ -19,11 +20,6 @@ public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltI
return true;
}
@Override
public boolean oneshot() {
return false;
}
@Override
public boolean isExpired() {
return false;
@@ -33,4 +29,12 @@ public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltI
public void replyToInteraction(CommandContext context) {
// Task is loaded at runtime, this will never be called
}
@Override
public Lifecycle execute(TaskContext context) {
executeTask(context);
return Lifecycle.BUILTIN;
}
public abstract void executeTask(TaskContext context);
}
@@ -15,11 +15,6 @@ public abstract class AbstractPersistableTask extends AbstractTask implements Pe
return true;
}
@Override
public boolean oneshot() {
return false;
}
@Override
public boolean isExpired() {
return false;
@@ -30,6 +25,30 @@ public abstract class AbstractPersistableTask extends AbstractTask implements Pe
// Task loaded from database, this will never be called
}
@Override
public void insert(CommandContext context) {
if (TaskManager.getInstance().hasTask(getId())) {
throw new RuntimeException("Cannot insert a task that already exists");
}
TaskManager.getInstance().insert(context, this);
}
@Override
public void save() {
if (!TaskManager.getInstance().hasTask(getId())) {
throw new RuntimeException("Cannot save a task that doesn't exist yet");
}
TaskManager.getInstance().getTaskService().persistTask(this);
}
@Override
public void delete() {
TaskManager.getInstance().getTaskService().unpersistTask(this);
TaskManager.getInstance().removeTask(this);
}
@Override
public String toPayload() {
JSONObject payload = new JSONObject(this);
@@ -5,6 +5,8 @@ package me.bobert.netneutrality.core.tasks;
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.context.CommandContext;
import java.time.OffsetDateTime;
import java.util.Objects;
@@ -26,8 +28,8 @@ public abstract class AbstractTask implements Task {
}
@Override
public TaskButtonCreator createButton() {
return new TaskButtonCreator(getId());
public void insert(CommandContext context) {
TaskManager.getInstance().insert(context, this);
}
@Override
@@ -46,6 +48,6 @@ public abstract class AbstractTask implements Task {
@Override
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
*/
import net.dv8tion.jda.api.interactions.components.buttons.Button;
/**
* Represents a task that has a known ID and is loaded at runtime.
* This can be any task and is not necessarily a button.
*/
public interface BuiltInTask extends Task {
static TaskButtonCreator createButton(Class<? extends BuiltInTask> clazz) {
BuiltInTask builtInTask = TaskManager.getInstance().getTaskMap().values().stream().filter(clazz::isInstance).map(task -> (BuiltInTask) task).findFirst().orElse(null);
static Button createButton(Class<? extends BuiltInTask> clazz) {
BuiltInTask builtInTask = TaskManager.getInstance().findBuiltInTaskByClass(clazz);
if (builtInTask == null) {
throw new IllegalArgumentException("Task class " + clazz.getName() + " was not registered by the task manager.");
}
return 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
*/
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 {
void insert(CommandContext context);
void save();
void delete();
String toPayload();
}
@@ -13,17 +13,35 @@ import net.dv8tion.jda.api.interactions.components.buttons.Button;
import java.time.OffsetDateTime;
public interface Task {
enum Lifecycle {
BUILTIN,
PARENT,
KEEP,
REMOVE_SELF,
REMOVE_SELF_AND_CHILDREN
}
/**
* <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>
* This determines what triggers a task to be executed.
* For example, a modal or button would use the component id.
* For an emoji reaction, the guild and message id would be combined.
* </p>
*
* @return a unique ID for this task
*/
String getId();
/**
* Retrieves the lock ID that is used to synchronize tasks
* that require a locking mechanism.
*
* @return the lock ID associated with this task
*/
default String getLockId() {
return getId();
}
/**
* This is only used for debugging purpose.
* @return Name of the task
@@ -36,9 +54,14 @@ public interface Task {
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>
@@ -48,41 +71,9 @@ public interface 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);
/**
* 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);
}
}
void insert(CommandContext context);
}
@@ -5,17 +5,18 @@ package me.bobert.netneutrality.core.tasks;
* 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.context.CommandContext;
import me.bobert.netneutrality.core.context.InteractionContext;
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.NotAcknowledgeable;
import me.bobert.netneutrality.core.tasks.trait.WithChildren;
import me.bobert.netneutrality.core.tasks.trait.WithParent;
import me.bobert.netneutrality.core.thread.ThreadManager;
import me.bobert.netneutrality.db.service.TaskService;
import me.bobert.netneutrality.exceptions.GracefulContextError;
import me.bobert.netneutrality.exceptions.TaskExecutionException;
import me.bobert.netneutrality.thread.CleanupTaskThread;
import net.dv8tion.jda.api.interactions.InteractionHook;
@@ -26,19 +27,27 @@ import org.reflections.Reflections;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class TaskManager {
private static final Logger logger = LogManager.getLogger(TaskManager.class);
private static TaskManager instance;
private final TaskService taskService;
private final Map<String, Task> tasks;
private final List<Task> pendingTasks;
private final Map<String, CommandContext> deletableTasksContext;
private final Map<String, ReentrantLock> tasksLock;
private TaskManager() {
this.tasks = new HashMap<>();
this.deletableTasksContext = new HashMap<>();
this.taskService = new TaskService(this);
this.tasks = new ConcurrentHashMap<>();
this.pendingTasks = new CopyOnWriteArrayList<>();
this.deletableTasksContext = new ConcurrentHashMap<>();
this.tasksLock = new ConcurrentHashMap<>();
this.taskService = new TaskService();
}
public static TaskManager getInstance() {
@@ -56,7 +65,7 @@ public class TaskManager {
ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new CleanupTaskThread(), 1, 5, TimeUnit.MINUTES);
}
public TaskService getTaskService() {
protected TaskService getTaskService() {
return taskService;
}
@@ -64,22 +73,22 @@ public class TaskManager {
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();
}
public Map<String, Task> getTaskMap() {
return tasks;
}
public boolean exists(String id) {
return tasks.containsKey(id);
public synchronized Collection<Task> getPendingTasks() {
return pendingTasks;
}
public void insert(CommandContext context, Task task) {
String id = task.getId();
if (tasks.containsKey(id)) {
if (hasTask(id)) {
context.reply("A task with the same ID already exists!");
return;
}
@@ -93,7 +102,11 @@ public class TaskManager {
taskService.persistTask(persistableTask);
}
tasks.put(id, task);
if (task instanceof StepTask) {
pendingTasks.add(task);
} else {
tasks.put(id, task);
}
}
if (task instanceof Deletable) {
@@ -102,6 +115,10 @@ public class TaskManager {
if (task instanceof WithChildren taskWithChildren) {
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) {
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();
if (tasks.containsKey(id)) {
logger.debug("Removing task: {}", task);
tasks.remove(id);
}
tasksLock.remove(task.getLockId());
}
public void handle(TaskContext context) {
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");
}
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 {
// Cancel task during execution to prevent spam
removeTaskAndChildTasks(task);
if (lock != null) {
logger.debug("Aquiring lock for task: {}", taskId);
lock.lock();
logger.debug("Aquired lock for: {}", lockId);
task.execute(context);
if (task.isInsertable() && !task.oneshot()) {
reinsertTaskAndChildTasks(task);
if (!tasks.containsKey(taskId)) {
logger.debug("Task {} is no longer available after aquiring lock", taskId);
return;
}
}
logger.debug("Executing task: {}", task);
Task.Lifecycle status = task.execute(context);
handleTaskLifecycle(task, status);
deleteInteraction(task);
// If interaction is still not acknowledged after execution, send a placeholder response.
if (!context.isAcknowledged() && !(task instanceof NotAcknowledgeable)) {
logger.warn("Interaction not acknowledged");
context.replyEphemeral("Task executed successfully but interaction wasn't acknowledged, report this bug by using the command /report-bug");
}
} catch (GracefulContextError exception) {
logger.error(exception.getMessage());
context.replyEphemeral(exception.getMessage());
} catch (TaskExecutionException exception) {
LogHandler.log(exception);
} catch (Exception exception) {
LogHandler.log(new TaskExecutionException("Error during task execution", context, exception));
} finally {
if (lock != null) {
logger.debug("Releasing lock for task: {}", taskId);
lock.unlock();
}
}
}
}
public <E extends Task> List<E> findTasksByPartialId(Class<E> clazz, String partialId) {
return tasks.values().stream()
.filter(clazz::isInstance)
.map(clazz::cast)
.filter(task -> task.getId().contains(partialId))
.collect(Collectors.toList());
}
public BuiltInTask findBuiltInTaskByClass(Class<? extends BuiltInTask> clazz) {
return tasks.values().stream()
.filter(clazz::isInstance)
.map(task -> (BuiltInTask) task)
.findFirst()
.orElse(null);
}
public void insertPersistableTask(PersistableTask task) {
if (tasks.containsKey(task.getId())) {
logger.error("Trying to load persistable task but ID already exists: {}", task.getId());
return;
}
logger.info("Loaded persistable task: {}", task);
tasks.put(task.getId(), task);
}
private void loadBuiltInTasks() {
logger.info("Loading built-in tasks...");
@@ -177,7 +250,7 @@ public class TaskManager {
private void deleteInteraction(Task task) {
if (task instanceof WithParent<?> taskWithParent) {
Task parent = (Task) taskWithParent.getParentTask();
Task parent = taskWithParent.getParentTask();
if (parent instanceof Deletable deletable && task.isInsertable()) {
deleteInteraction(parent.getId(), deletable);
@@ -212,37 +285,28 @@ public class TaskManager {
deletable.onInteractionDelete();
}
private void removeTaskAndChildTasks(Task task) {
if (task instanceof BuiltInTask) {
return;
private void handleTaskLifecycle(Task task, Task.Lifecycle status) {
if (status == null) {
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()) {
List<Task> childrens = taskWithParent.getParentTask().getChildTasks();
for (Task child : childrens) {
if (!task.equals(child)) {
tasks.remove(child.getId());
case REMOVE_SELF_AND_CHILDREN:
if (task instanceof WithChildren taskWithChildren) {
for (Task child : taskWithChildren.getChildTasks()) {
removeTask(child);
}
}
}
}
}
removeTask(task);
break;
private void reinsertTaskAndChildTasks(Task task) {
if (task instanceof BuiltInTask) {
return;
}
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);
}
}
case KEEP:
default:
break;
}
}
}
@@ -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
*
@@ -6,25 +6,13 @@ package me.bobert.netneutrality.tasks;
*/
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 java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public abstract class ModalTask extends AbstractTask {
private final UUID id = UUID.randomUUID();
@Override
public String getId() {
return id.toString();
}
@Override
public boolean oneshot() {
return true;
}
public abstract class ModalTask extends GenericRandomIdTask {
@Override
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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONObject;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Map;
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 {
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);
if (!PersistableTask.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Class does not implement PersistableTask.");
}
Class<?>[] classTypes = new Class<?>[types.length()];
Object[] classParams = new Object[params.length()];
for (int i = 0; i < params.length(); i++) {
String type = types.getString(i);
String param = params.getString(i);
ParamType paramType = ParamType.fromString(types.getString(i));
String paramName = params.getString(i);
Class<?> classType = parseClassTypeFromString(type);
Object classParam = castParamToType(classType, param, type, payload);
classTypes[i] = classType;
classParams[i] = classParam;
classTypes[i] = paramType.getTypeClass();
classParams[i] = extractValue(paramType, paramName, payload);
}
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) {
return switch (param) {
case "string" -> String.class;
case "int" -> Integer.class;
case "map" -> Map.class;
default -> throw new IllegalArgumentException("Param " + param + " is not implemented yet.");
private static Object extractValue(ParamType type, String paramName, JSONObject payload) {
return switch (type) {
case STRING -> payload.getString(paramName);
case INT -> payload.getInt(paramName);
case MAP -> payload.getJSONObject(paramName).toMap();
};
}
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 Map<String, TaskCreator> taskCreators = new HashMap<>();
private static final String FILE_NAME = "persistable-tasks.json";
public TaskRegistry() {
loadTaskConfig();
}
// JSON keys as constants
private static final String KEY_TASKS = "tasks";
private static final String KEY_CLASS = "class";
private static final String KEY_PARAMS = "params";
private static final String KEY_TYPES = "types";
public static TaskCreator getTaskCreator(String className) {
return taskCreators.get(className);
}
private static void loadTaskConfig() {
String fileName = "persistable-tasks.json";
public static void loadTaskConfig() {
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) {
throw new FileNotFoundException("Could not load: " + fileName);
throw new FileNotFoundException("Could not load: " + FILE_NAME);
}
String jsonString = IOUtils.toString(is, StandardCharsets.UTF_8);
JSONObject json = new JSONObject(jsonString);
JSONArray tasksArray = json.optJSONArray(KEY_TASKS);
json.getJSONArray("tasks").forEach(task -> {
JSONObject taskJson = (JSONObject) task;
if (tasksArray == null) {
logger.warn("No tasks found in JSON: {}", FILE_NAME);
return;
}
String className = taskJson.getString("class");
JSONArray params = taskJson.getJSONArray("params");
JSONArray types = taskJson.getJSONArray("types");
for (int i = 0; i < tasksArray.length(); i++) {
JSONObject taskJson = tasksArray.getJSONObject(i);
String className = taskJson.optString(KEY_CLASS, "").trim();
if (className.isEmpty()) {
logger.warn("Missing JSON key '{}' at index {}", KEY_CLASS, i);
continue;
}
JSONArray params = taskJson.optJSONArray(KEY_PARAMS);
if (params == null) {
logger.warn("Missing JSON key '{}' for class: {}", KEY_PARAMS, className);
continue;
}
JSONArray types = taskJson.optJSONArray(KEY_TYPES);
if (types == null) {
logger.warn("Missing JSON key '{}' for class: {}", KEY_TYPES, className);
continue;
}
taskCreators.put(className, payload -> {
try {
return TaskFactory.createTaskInstance(className, params, types, payload);
} catch (RuntimeException e) {
throw 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) {
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
*/
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();
}
@@ -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 {
}
@@ -5,17 +5,21 @@ package me.bobert.netneutrality.core.tasks.trait;
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.Task;
import me.bobert.netneutrality.core.tasks.TaskManager;
import java.util.List;
public interface WithChildren {
public interface WithChildren extends Task {
@Override
default Lifecycle execute(TaskContext context) {
return Lifecycle.PARENT;
}
List<Task> getChildTasks();
default void removeAllChildTasks() {
for (Task child : getChildTasks()) {
TaskManager.getInstance().removeTask(child);
}
default void addChild(Task... tasks) {
getChildTasks().addAll(List.of(tasks));
}
}
@@ -5,12 +5,30 @@ package me.bobert.netneutrality.core.tasks.trait;
* 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() {
return executors.computeIfAbsent(
"general-executor",
s -> Executors.newCachedThreadPool(new BaseThreadFactory(s))
);
return getExecutor("general-executor");
}
public static void shutdownExecutor() {
@@ -5,14 +5,11 @@ package me.bobert.netneutrality.db.repository;
* 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.db.Repository;
import me.bobert.netneutrality.db.entities.OBlacklist;
import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.utils.JpaUtils;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import org.hibernate.Session;
import org.hibernate.query.Query;
@@ -53,15 +50,26 @@ public class BlacklistRepository implements Repository<OBlacklist> {
});
}
public CommandExecutionResult add(Guild guild, Member member, String word) {
GuildManager guildManager = GuildManager.getManager(guild);
public List<OBlacklist> findByGuildId(String guildId) {
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 -> {
OBlacklist bl = getWordFromDb(word, guild.getId());
OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word);
if (bl == null) {
bl = new OBlacklist();
bl.setGuild(guildManager.getGuildEntity());
bl.setGuild(oGuild);
bl.setCreatorId(member.getId());
bl.setWord(word);
bl.setEnabled(true);
@@ -72,37 +80,18 @@ public class BlacklistRepository implements Repository<OBlacklist> {
session.merge(bl);
}
});
guildManager.reloadBlacklistCache();
return CommandExecutionResult.success("Added the word [%s] to the blacklist.", word);
}
public CommandExecutionResult delete(Guild guild, String word) {
GuildManager guildManager = GuildManager.getManager(guild);
public void delete(OGuild oGuild, String word) {
JpaUtils.runWithTransaction(session -> {
OBlacklist bl = getWordFromDb(word, guild.getId());
OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word);
if (bl != null) {
session.remove(bl);
}
});
guildManager.reloadBlacklistCache();
return CommandExecutionResult.success("Removed the word [%s] to the blacklist.", word);
}
public List<OBlacklist> getBlacklistForGuild(Guild guild) {
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) {
private OBlacklist getWordFromDb(String guildId, String word) {
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.setParameter("guildId", guildId);
@@ -5,9 +5,9 @@ package me.bobert.netneutrality.db.repository;
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.db.SessionManager;
import me.bobert.netneutrality.db.Repository;
import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.db.entities.OLogMessage;
import me.bobert.netneutrality.utils.JpaUtils;
import net.dv8tion.jda.api.entities.Member;
@@ -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 -> {
OLogMessage logMessage = new OLogMessage();
logMessage.setGuild(guildManager.getGuildEntity());
logMessage.setGuild(oGuild);
logMessage.setMessageId(message.getId());
logMessage.setMessage(message.getContentRaw().trim());
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
@@ -7,30 +7,23 @@ package me.bobert.netneutrality.core;
*/
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.repository.GuildSettingRepository;
import net.dv8tion.jda.api.entities.Guild;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class GuildConfigManager {
private static final Logger logger = LogManager.getLogger(GuildConfigManager.class);
private final Guild guild;
private final OGuild guildEntity;
public class GuildSettingService {
private static final Logger logger = LoggerFactory.getLogger(GuildSettingService.class);
private final GuildSettingRepository guildSettingRepository = new GuildSettingRepository();
private final GuildService guildService;
private final Map<String, String> settingsCache = new ConcurrentHashMap<>();
private final Map<String, String> settingsCache = new HashMap<>();
public GuildConfigManager(Guild guild, OGuild guildEntity) {
this.guild = guild;
this.guildEntity = guildEntity;
public GuildSettingService(GuildService guildService) {
this.guildService = guildService;
reloadSettingsCache();
}
@@ -49,19 +42,19 @@ public class GuildConfigManager {
}
public void setSettingValue(GSettings setting, String value) {
guildSettingRepository.setSettingValue(guildEntity, setting, value);
guildSettingRepository.setSettingValue(guildService.getEntity(), setting, value);
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();
List<OGuildSetting> settings = guildSettingRepository.findByGuild(guild.getId());
List<OGuildSetting> settings = guildSettingRepository.findByGuild(guildService.getEntity().getGuildId());
for (OGuildSetting setting : settings) {
settingsCache.put(setting.getName(), setting.getValue());
}
}
}
@@ -15,23 +15,27 @@ import org.apache.logging.log4j.Logger;
import org.json.JSONObject;
import java.util.List;
import java.util.Objects;
public class TaskService {
private static final Logger logger = LogManager.getLogger(TaskService.class);
private final TaskManager taskManager;
private final TaskRepository taskRepository;
public TaskService(TaskManager taskManager) {
this.taskManager = taskManager;
public TaskService() {
this.taskRepository = new TaskRepository();
}
public void loadTasks() {
TaskRegistry.loadTaskConfig();
logger.info("Loading persistable tasks...");
List<OTask> tasks = taskRepository.findAll();
for (OTask oTask : tasks) {
String taskId = oTask.getTaskId();
String taskClass = oTask.getTaskClass();
logger.debug("Trying to load task with class: {}", taskClass);
String payload = oTask.getPayload();
try {
@@ -42,20 +46,38 @@ public class TaskService {
JSONObject json = new JSONObject(payload);
PersistableTask task = creator.fromPayload(json);
if (TaskManager.getInstance().exists(task.getId())) {
logger.error("Task with id {} already exists", task.getId());
if (task == null) {
continue;
}
logger.info("Loaded task: {}", task);
TaskManager.getInstance().getTaskMap().put(task.getId(), task);
validateTask(task, oTask);
TaskManager.getInstance().insertPersistableTask(task);
} 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) {
String taskClass = task.getClass().getName();
if (TaskRegistry.getTaskCreator(taskClass) == null) {
@@ -67,10 +89,10 @@ public class TaskService {
OTask oTask = taskRepository.findById(task.getId());
if (oTask == null) {
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());
taskRepository.save(oTask);
@@ -81,6 +103,5 @@ public class TaskService {
TaskRepository taskRepository = new TaskRepository();
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.MessageMenuContext;
import me.bobert.netneutrality.context.command.SlashCommandContext;
import me.bobert.netneutrality.context.command.UserMenuContext;
import me.bobert.netneutrality.context.tasks.ButtonTaskContext;
import me.bobert.netneutrality.context.tasks.EmojiTaskContext;
import me.bobert.netneutrality.context.tasks.MessageTaskContext;
import me.bobert.netneutrality.context.tasks.ModalTaskContext;
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.core.command.CommandProcessor;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.Context;
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.Event;
import net.dv8tion.jda.api.events.GenericEvent;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
import net.dv8tion.jda.api.events.guild.GuildLeaveEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
@@ -45,21 +53,30 @@ public class EventsHandler implements EventListener {
if (event.getAuthor().isBot()) 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", "$");
if (rawMessage.startsWith(prefix)) {
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(ModalInteractionEvent.class, ModalTaskContext::new);
addListener(ButtonInteractionEvent.class, ButtonTaskContext::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);
Context context = function.apply(event);
if (context == null) {
logger.debug("Handling event {} by generic listener", event);
return;
}
if (context instanceof CommandContext commandContext) {
handleCommand(commandContext);
return;
@@ -9,8 +9,7 @@ package me.bobert.netneutrality.http;
import com.google.gson.Gson;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import me.bobert.netneutrality.NetNeutrality;
import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.utils.Settings;
import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -18,15 +17,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class WebModule {
private enum Method {
GET, POST
PING, GET, POST
}
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 OkHttpClient client;
private final Map<String, String> headers;
private final Map<String, String> params;
private final Request request;
private CachedResponse cachedResponse;
@@ -54,9 +49,8 @@ public class WebModule {
this.url = builder.url;
this.client = builder.client;
this.headers = builder.headers;
this.params = builder.params;
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));
}
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.
* <p>
@@ -107,24 +88,37 @@ public class WebModule {
}
}
long startTime = System.currentTimeMillis();
try (Response response = client.newCall(request).execute()) {
long elapsedTime = System.currentTimeMillis() - startTime;
Sentry.addBreadcrumb(Breadcrumb.http(url, method.name(), response.code()));
String body = response.body() != null ? response.body().string() : null;
this.cachedResponse = new CachedResponse(
response.code(),
response.headers(),
elapsedTime,
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) {
long elapsedTime = System.currentTimeMillis() - startTime;
log.error("Request failed: {}", e.getMessage(), 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;
@@ -145,28 +139,27 @@ public class WebModule {
}
public JSONObject getAsObject() {
try {
return new JSONObject(cachedResponse.getBody());
} catch (NullPointerException e) {
LogHandler.log(e);
return new JSONObject();
}
return cachedResponse.getBody()
.map(JSONObject::new)
.orElseThrow(() -> new IllegalStateException("No body available for this response"));
}
public JSONArray getAsArray() {
try {
return new JSONArray(cachedResponse.getBody());
} catch (NullPointerException e) {
LogHandler.log(e);
return new JSONArray();
}
return cachedResponse.getBody()
.map(JSONArray::new)
.orElseThrow(() -> new IllegalStateException("No body available for this response"));
}
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 {
private Request request;
private final String url;
private Method method = Method.GET;
private long timeout = 5;
@@ -187,6 +180,11 @@ public class WebModule {
this.url = url;
}
public Builder ping() {
this.method = Method.PING;
return this;
}
/**
* Configures the request to use the HTTP GET method.
*
@@ -224,7 +222,7 @@ public class WebModule {
/**
* 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
* @return the current {@code Builder} instance for chaining
*/
@@ -234,10 +232,17 @@ public class WebModule {
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.
*
* @param timeout the timeout duration
* @param timeout the timeout duration
* @param timeUnit the unit of time for the timeout
* @return the current {@code Builder} instance for chaining
*/
@@ -249,7 +254,7 @@ public class WebModule {
/**
* 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
* @return the current {@code Builder} instance for chaining
*/
@@ -261,7 +266,7 @@ public class WebModule {
/**
* 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
* @return the current {@code Builder} instance for chaining
*/
@@ -270,6 +275,27 @@ public class WebModule {
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.
*
@@ -281,6 +307,7 @@ public class WebModule {
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.build();
this.request = createRequest();
return new WebModule(this);
}
@@ -289,14 +316,18 @@ public class WebModule {
private static class CachedResponse {
private final int statusCode;
private final Headers headers;
private final long elapsedTime;
private final String body;
private final long expirationTime;
private final String errorMessage;
public CachedResponse(int statusCode, Headers headers, String body, long expirationTime) {
public CachedResponse(int statusCode, Headers headers, long elapsedTime, String body, long expirationTime, String errorMessage) {
this.statusCode = statusCode;
this.headers = headers;
this.elapsedTime = elapsedTime;
this.body = body;
this.expirationTime = expirationTime;
this.errorMessage = errorMessage;
}
public boolean isExpired() {
@@ -311,8 +342,16 @@ public class WebModule {
return headers;
}
public String getBody() {
return body;
public long getElapsedTime() {
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.sun.net.httpserver.HttpExchange;
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 org.apache.logging.log4j.LogManager;
import org.json.JSONObject;
@@ -49,8 +49,8 @@ public class ActionHandler extends AbstractHandler {
Guild guild = NetNeutrality.getInstance().getJDA().getGuildById(guildId);
if (guild != null) {
GuildManager manager = GuildManager.getManager(guild);
Map<String, String> settings = manager.getGuildConfigManager().getSettings();
GuildService service = GuildService.get(guild);
Map<String, String> settings = service.getGuildSettingService().getSettings();
JSONObject setting = new JSONObject(settings);
json.put("status", "ok");
@@ -68,13 +68,8 @@ public class ActionHandler extends AbstractHandler {
String[] args = exchange.getRequestURI().toString().split("/");
String guildId = args[args.length - 1];
Guild guild = NetNeutrality.getInstance().getJDA().getGuildById(guildId);
boolean ok = true; //TODO: add bool
if (guild == null)
GuildManager.getManager(guildId).getGuildConfigManager().reloadSettingsCache();
else
GuildManager.getManager(guild).getGuildConfigManager().reloadSettingsCache();
GuildService.get(guildId).getGuildSettingService().reloadSettingsCache();
json.put("status", String.valueOf(ok));
sendJsonResponse(exchange, 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.track.AudioTrack;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.thread.ThreadManager;
import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.source.UdioAudioSourceManager;
import me.bobert.netneutrality.thread.MusicManagerCleanupThread;
import me.bobert.netneutrality.thread.MusicPlayerAutoDisconnectThread;
@@ -47,7 +47,7 @@ public class MusicManager {
this.guild = guild;
this.player = playerManager.createPlayer();
this.scheduler = new TrackScheduler(this);
this.baseVolume = Integer.parseInt(GuildManager.getManager(guild).getGuildConfigManager().getSetting(GSettings.MUSIC_VOLUME));
this.baseVolume = Integer.parseInt(GuildService.get(guild).getGuildSettingService().getSetting(GSettings.MUSIC_VOLUME));
guild.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(player));
player.addListener(scheduler);
@@ -104,8 +104,9 @@ public class MusicManager {
return guild;
}
public boolean isNotConnectedTo(AudioChannel vChannel) {
return vChannel == null || !vChannel.equals(guild.getAudioManager().getConnectedChannel());
public synchronized boolean isNotConnectedTo(AudioChannel vChannel) {
AudioChannel current = guild.getAudioManager().getConnectedChannel();
return vChannel == null || current == null || !vChannel.getId().equals(current.getId());
}
public synchronized void connect(AudioChannel audioChannel) {
@@ -120,7 +121,7 @@ public class MusicManager {
}
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());
player.stopTrack();
guild.getAudioManager().closeAudioConnection();
@@ -129,8 +130,8 @@ public class MusicManager {
scheduler.detachContext();
}
public boolean isConnected() {
return guild != null && guild.getAudioManager().getConnectedChannel() != null;
public synchronized boolean isConnected() {
return guild.getAudioManager().getConnectedChannel() != null;
}
public synchronized AudioTrack getCurrentlyPlaying() {
@@ -57,7 +57,7 @@ public class TrackLoadingHandler implements AudioLoadResultHandler {
public void playlistLoaded(AudioPlaylist playlist) {
logger.debug("Guild {}: playlist loaded: {}", guild.getId(), identifier);
TaskManager.getInstance().insert(context, new LoadPlaylistTask(playlist, type));
new LoadPlaylistTask(playlist, type).insert(context);
}
@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.tasks.EmojiTask;
import me.bobert.netneutrality.core.tasks.impl.EmojiTask;
import me.bobert.netneutrality.utils.GuildUtils;
import net.dv8tion.jda.api.entities.Role;
import java.util.HashMap;
import java.util.Map;
public class EmojiReactionRoleTask extends EmojiTask {
private final Map<String, String> emojiMapping;
public EmojiReactionRoleTask(String guildId, String messageId) {
public EmojiReactionRoleTask(String guildId, String channelId, String messageId) {
super(guildId, messageId);
this.emojiMapping = new HashMap<>();
}
// constructor for auto loading
public EmojiReactionRoleTask(String guildId, String messageId, Map<String, String> emojiMapping) {
super(guildId, messageId);
this.emojiMapping = 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;
getEmojiMapping().putAll(emojiMapping);
}
@Override
@@ -43,15 +29,17 @@ public class EmojiReactionRoleTask extends EmojiTask {
}
@Override
public void execute(TaskContext context) {
public Lifecycle execute(TaskContext context) {
String emoji = (String) context.getContextData("emoji-code");
if (getEmojiMapping().containsKey(emoji)) {
Role role = context.getGuild().getRoleById(getEmojiMapping().get(emoji));
if (role != null) {
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 me.bobert.netneutrality.context.command.SlashCommandContext;
import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.tasks.ModalTask;
import me.bobert.netneutrality.core.tasks.impl.ModalTask;
import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.interactions.components.ActionRow;
@@ -27,8 +28,8 @@ public class ReportIssueTask extends ModalTask {
private static final Logger logger = LogManager.getLogger(ReportIssueTask.class);
private final boolean insertable;
public ReportIssueTask(boolean insertable) {
this.insertable = insertable;
public ReportIssueTask(CommandContext context) {
this.insertable = context instanceof SlashCommandContext;
}
@Override
@@ -63,7 +64,7 @@ public class ReportIssueTask extends ModalTask {
}
@Override
public void execute(TaskContext context) {
public Lifecycle execute(TaskContext context) {
context.ephemeral();
String gitlabHost = Settings.getInstance().get("GITLAB_HOST");
@@ -73,7 +74,7 @@ public class ReportIssueTask extends ModalTask {
if (gitlabHost.isBlank() || gitlabToken.isBlank() || projectId.isBlank()) {
logger.error("Gitlab is not configured in the bot settings, can't execute task");
context.reply("Command is not configured, failed to create issue.");
return;
return null;
}
ModalMapping modalTitle = getValue("title", context);
@@ -103,5 +104,7 @@ public class ReportIssueTask extends ModalTask {
context.reply("Failed to create issue.");
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.tasks.AbstractBuiltInTask;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
public class BotInfo extends AbstractBuiltInTask {
@@ -16,7 +17,12 @@ public class BotInfo extends AbstractBuiltInTask {
}
@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, " +
"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.utils.MessageFunction;
import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
public class CommandHelp extends AbstractBuiltInTask {
@@ -18,7 +19,12 @@ public class CommandHelp extends AbstractBuiltInTask {
}
@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(MessageFunction.buildHelpMessage());
}
@@ -7,6 +7,7 @@ package me.bobert.netneutrality.tasks.help;
import me.bobert.netneutrality.core.context.TaskContext;
import me.bobert.netneutrality.core.tasks.AbstractBuiltInTask;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
public class ReportBug extends AbstractBuiltInTask {
@@ -16,7 +17,12 @@ public class ReportBug extends AbstractBuiltInTask {
}
@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 " +
"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.AudioTrack;
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.TaskManager;
import me.bobert.netneutrality.core.tasks.trait.Deletable;
import me.bobert.netneutrality.core.tasks.trait.WithChildren;
import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.MusicManager;
import me.bobert.netneutrality.music.SourceFinder;
import me.bobert.netneutrality.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.MusicUtils;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
@@ -31,6 +35,7 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
private final SourceFinder.AudioSourceType sourceType;
public LoadPlaylistTask(AudioPlaylist playlist, SourceFinder.AudioSourceType sourceType) {
super(null, null);
this.playlist = playlist;
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 maxPlaylistSizeBeforeAsking = Integer.parseInt(context.getGuildManager()
.getGuildConfigManager()
int maxPlaylistSizeBeforeAsking = Integer.parseInt(GuildService.get(context.getGuild())
.getGuildSettingService()
.getSetting(GSettings.MUSIC_MAXPLAYLISTSIZE));
// Search results
@@ -68,10 +73,10 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
for (int i = 0; i < searchResults.size(); 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)));
childrens.add(buttonTask);
buttonList.add(buttonTask.createButton());
addChild(buttonTask);
}
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) {
Task addAllTracksTask = new AddAllTracksTask(this, tracks);
childrens.add(addAllTracksTask);
Task accept = AcceptButton.createSuccessButton(this, addAllText, context -> {
MusicManager manager = MusicManager.get(context.getGuild());
Member member = context.getGuildUser();
Task cancelTask = new CancelLoadTask(this);
childrens.add(cancelTask);
if (MusicUtils.connect(context)) {
manager.getQueueManager().queue(tracks, member);
context.reply(MessageFunction.buildAddingPlaylistMessage(tracks, playlist.getName(), member, SourceFinder.AudioSourceType.Youtube));
return Lifecycle.REMOVE_SELF_AND_CHILDREN;
}
return Lifecycle.KEEP;
});
Task cancel = CancelButton.create(this, cancelText, context -> Lifecycle.REMOVE_SELF_AND_CHILDREN);
addChild(accept, cancel);
return ActionRow.of(
addAllTracksTask.createButton().success(addAllText).withEmoji(Emoji.fromFormatted("U+1F60E")),
cancelTask.createButton().danger(cancelText).withEmoji(Emoji.fromFormatted("U+1F92E"))
accept.createButton().withEmoji(Emoji.fromFormatted("U+1F60E")),
cancel.createButton().withEmoji(Emoji.fromFormatted("U+1F92E"))
);
}
@@ -124,6 +135,8 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
@Override
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 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.tasks.generic.AcceptButton;
import me.bobert.netneutrality.utils.MessageFunction;
import me.bobert.netneutrality.utils.MusicUtils;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
public class LoadTrackFromPlaylistTask extends ButtonTask implements WithParent<LoadPlaylistTask> {
private final LoadPlaylistTask parentTask;
public class LoadTrackFromPlaylistTask extends AcceptButton<LoadPlaylistTask> {
private final AudioTrack track;
public LoadTrackFromPlaylistTask(LoadPlaylistTask parentTask, AudioTrack track) {
this.parentTask = parentTask;
public LoadTrackFromPlaylistTask(LoadPlaylistTask parentTask, String buttonLabel, AudioTrack track) {
super(parentTask, ButtonStyle.PRIMARY, buttonLabel, context -> {
MusicManager manager = MusicManager.get(context.getGuild());
Member member = context.getGuildUser();
if (MusicUtils.connect(context)) {
manager.getQueueManager().queue(track, member);
context.reply(MessageFunction.buildAddingTrackMessage(track, member, SourceFinder.AudioSourceType.Youtube));
return Lifecycle.REMOVE_SELF_AND_CHILDREN;
}
return Lifecycle.KEEP;
});
this.track = track;
}
@@ -29,31 +39,4 @@ public class LoadTrackFromPlaylistTask extends ButtonTask implements WithParent<
public String getName() {
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();
if (!tasks.isEmpty()) {
logger.info("Cleaning up tasks..");
logger.info("Cleaning up {} expired tasks..", tasks.size());
for (Task task : tasks) {
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
*/
import me.bobert.netneutrality.core.GuildManager;
import me.bobert.netneutrality.core.db.SessionManager;
import me.bobert.netneutrality.db.entities.OGuild;
import me.bobert.netneutrality.db.service.GuildService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.HibernateException;
@@ -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
* an exception to block execution.
* <p>
* The session is not closed
*
* @param session The session in which to load the persistent object
* @param object The persistent object
@@ -45,9 +44,9 @@ public class JpaUtils {
String cachedId = oGuild.getGuildId();
logger.error("Trying to load Guild[{}] while entity doesn't exist in database, reloading..", cachedId);
GuildManager guildManager = GuildManager.getManager(cachedId);
guildManager.reloadEntity();
guildManager.getBlacklistCache().clear();
GuildService service = GuildService.get(cachedId);
service.reloadEntity();
service.getBlacklistService().removeGuildBlacklist();
logger.info("Guild[{}] reloaded", cachedId);
}
}
@@ -54,6 +54,10 @@ public class Settings {
return INSTANCE;
}
public static String getVersion() {
return Settings.getInstance().getMavenVersion();
}
public String get(String key) {
String property = prop.getProperty(key);