diff --git a/build.gradle.kts b/build.gradle.kts index c985d92..c3f603e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,27 +38,23 @@ tasks { } } -// create("relocateJars") { -// target = shadowJar.get() -// prefix = "${project.name}.lib" -// } -// -// shadowJar { -// dependsOn(getByName("relocateJars") as ConfigureShadowRelocation) -// archiveFileName.set("${project.name}-${project.version}.jar") -// minimize() -// configurations = listOf(project.configurations.shadow.get()) -// } -// + shadowJar { + archiveFileName.set(rootProject.name + ".jar") + } + build { dependsOn(shadowJar) } + jar { + enabled = false + } + } dependencies { // JDA - implementation("net.dv8tion:JDA:5.0.0-alpha.19") { + implementation("net.dv8tion:JDA:5.0.0-alpha.20") { exclude("opus-java") // exclude audio } // MySQL @@ -67,4 +63,8 @@ dependencies { // Configurate implementation("org.spongepowered:configurate-yaml:4.1.2") + + + compileOnly("org.projectlombok:lombok:1.18.24") + annotationProcessor("org.projectlombok:lombok:1.18.24") } \ No newline at end of file diff --git a/example.requests.yml b/example.requests.yml new file mode 100644 index 0000000..322beab --- /dev/null +++ b/example.requests.yml @@ -0,0 +1,34 @@ +request: + guild: '776590138296893480' + category: '776590138296893481' + channel: '1017787342561476709' + message: '1017839462111256667' +types: + bug-report: + category: '776590138296893481' + channel: '820222354180800525' + name: Bug report + title: What should the title be? + description: Report a new bug. + message: Describe the bug. + act-request: + category: '776590138296893481' + channel: '820222354180800525' + name: Act request + title: What should the title be? + description: Make a new act request. + message: Describe the act request. + admin-act-request: + category: '776590138296893481' + channel: '820222354180800525' + name: Admin act request + title: What should the title be? + description: Make a new admin act request. + message: Describe the admin act request. + feature-request: + category: '776590138296893481' + channel: '820222354180800525' + name: Feature request + title: What should the title be? + description: Make a new feature request. + message: Describe the feature request. diff --git a/src/main/java/com/alttd/AltitudeBot.java b/src/main/java/com/alttd/AltitudeBot.java index 9151aec..554f58a 100644 --- a/src/main/java/com/alttd/AltitudeBot.java +++ b/src/main/java/com/alttd/AltitudeBot.java @@ -30,8 +30,8 @@ public class AltitudeBot { private void start() { Logger.info("Starting bot..."); initConfigs(); - ConsoleCommandManager.startConsoleCommands(jda); jda = JDABuilder.createDefault(SettingsConfig.TOKEN).build(); + ConsoleCommandManager.startConsoleCommands(jda); DatabaseTables.createTables(Database.getDatabase().getConnection()); // try { // jda.getPresence().setPresence( @@ -41,7 +41,6 @@ public class AltitudeBot { // Logger.exception(e); // } initListeners(); - //TODO init permissionManager } private void initListeners() { diff --git a/src/main/java/com/alttd/buttonManager/ButtonManager.java b/src/main/java/com/alttd/buttonManager/ButtonManager.java index 607113f..2516ac4 100644 --- a/src/main/java/com/alttd/buttonManager/ButtonManager.java +++ b/src/main/java/com/alttd/buttonManager/ButtonManager.java @@ -1,5 +1,7 @@ package com.alttd.buttonManager; +import com.alttd.buttonManager.buttons.remindMeConfirm.ButtonRemindMeCancel; +import com.alttd.buttonManager.buttons.remindMeConfirm.ButtonRemindMeConfirm; import com.alttd.buttonManager.buttons.suggestionReview.ButtonSuggestionReviewAccept; import com.alttd.buttonManager.buttons.suggestionReview.ButtonSuggestionReviewDeny; import com.alttd.util.Util; @@ -22,7 +24,9 @@ public class ButtonManager extends ListenerAdapter { public ButtonManager() { buttons = List.of( new ButtonSuggestionReviewAccept(), - new ButtonSuggestionReviewDeny()); + new ButtonSuggestionReviewDeny(), + new ButtonRemindMeCancel(), + new ButtonRemindMeConfirm()); } @Override diff --git a/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeCancel.java b/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeCancel.java new file mode 100644 index 0000000..65351e2 --- /dev/null +++ b/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeCancel.java @@ -0,0 +1,36 @@ +package com.alttd.buttonManager.buttons.remindMeConfirm; + +import com.alttd.buttonManager.DiscordButton; +import com.alttd.database.queries.QueriesReminders.Reminder; +import com.alttd.util.Util; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ButtonRemindMeCancel extends DiscordButton { + @Override + public String getButtonId() { + return "remind_me_cancel"; + } + + @Override + public void execute(ButtonInteractionEvent event) { + HookAndReminder hookAndReminder = ButtonRemindMeConfirm.removeReminder(event.getUser().getIdLong()); + if (hookAndReminder == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Your reminder was already cancelled!")) + .setEphemeral(true).queue(); + return; + } + event.replyEmbeds(Util.genericSuccessEmbed("Success", "Cancelled your reminder!")) + .setEphemeral(true).queue(); + hookAndReminder.interactionHook().editOriginalComponents(List.of()).queue(); + } + + @Override + public Button getButton() { + return Button.danger(getButtonId(), "Cancel"); + } +} diff --git a/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeConfirm.java b/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeConfirm.java new file mode 100644 index 0000000..7076c00 --- /dev/null +++ b/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeConfirm.java @@ -0,0 +1,84 @@ +package com.alttd.buttonManager.buttons.remindMeConfirm; + +import com.alttd.buttonManager.DiscordButton; +import com.alttd.database.queries.QueriesReminders.QueriesReminders; +import com.alttd.database.queries.QueriesReminders.Reminder; +import com.alttd.reminders.ReminderScheduler; +import com.alttd.util.Util; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +import java.util.HashMap; +import java.util.List; + +public class ButtonRemindMeConfirm extends DiscordButton { + + private static final HashMap unconfirmedReminders = new HashMap<>(); + + public static synchronized void putReminder(long id, InteractionHook defer, Reminder reminder) { + unconfirmedReminders.put(id, new HookAndReminder(reminder, defer)); + } + + public static synchronized HookAndReminder removeReminder(long id) { + return unconfirmedReminders.remove(id); + } + + @Override + public String getButtonId() { + return "remind_me_confirm"; + } + + @Override + public void execute(ButtonInteractionEvent event) { + HookAndReminder hookAndReminder = removeReminder(event.getUser().getIdLong()); + + if (storeReminder(hookAndReminder.reminder(), event)) { + event.replyEmbeds(Util.genericSuccessEmbed("Success", "Your reminder was successfully created!")) + .setEphemeral(true).queue(); + } + hookAndReminder.interactionHook().editOriginalComponents(List.of()).queue(); + } + + private boolean storeReminder(Reminder reminder, ButtonInteractionEvent event) { + if (reminder == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to retrieve reminder data for this button")) + .setEphemeral(true).queue(); + return false; + } + int id = QueriesReminders.storeReminder(reminder); + if (id == 0) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to store reminder in the database")) + .setEphemeral(true).queue(); + return false; + } + + reminder = new Reminder( + id, + reminder.title(), + reminder.description(), + reminder.userId(), + reminder.guildId(), + reminder.channelId(), + reminder.messageId(), + reminder.shouldRepeat(), + reminder.creationDate(), + reminder.remindDate()); + + ReminderScheduler instance = ReminderScheduler.getInstance(event.getJDA()); + if (instance == null) { + QueriesReminders.removeReminder(reminder.id()); + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to start reminder, removing it from the database...")) + .setEphemeral(true).queue(); + return false; + } + + instance.addReminder(reminder); + return true; + } + + @Override + public Button getButton() { + return Button.success(getButtonId(), "Confirm"); + } +} diff --git a/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/HookAndReminder.java b/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/HookAndReminder.java new file mode 100644 index 0000000..bc8945f --- /dev/null +++ b/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/HookAndReminder.java @@ -0,0 +1,8 @@ +package com.alttd.buttonManager.buttons.remindMeConfirm; + +import com.alttd.database.queries.QueriesReminders.Reminder; +import net.dv8tion.jda.api.interactions.InteractionHook; + +record HookAndReminder(Reminder reminder, InteractionHook interactionHook) { + +} diff --git a/src/main/java/com/alttd/buttonManager/buttons/suggestionReview/ButtonSuggestionReviewAccept.java b/src/main/java/com/alttd/buttonManager/buttons/suggestionReview/ButtonSuggestionReviewAccept.java index 7f2a3c1..be63c1d 100644 --- a/src/main/java/com/alttd/buttonManager/buttons/suggestionReview/ButtonSuggestionReviewAccept.java +++ b/src/main/java/com/alttd/buttonManager/buttons/suggestionReview/ButtonSuggestionReviewAccept.java @@ -8,10 +8,15 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; import java.awt.*; import java.util.List; @@ -26,16 +31,6 @@ public class ButtonSuggestionReviewAccept extends DiscordButton { @Override public void execute(ButtonInteractionEvent event) { Message message = event.getMessage(); - long suggestionChannelId = CommandOutputChannels.getOutputChannel(message.getGuild().getIdLong(), OutputType.SUGGESTION); - long modLogChannelId = CommandOutputChannels.getOutputChannel(message.getGuild().getIdLong(), OutputType.MOD_LOG); - - List embeds = message.getEmbeds(); - if (embeds.size() != 1) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "This message contains no embeds, can't be a suggestion")) - .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); - return; - } - Guild guild = event.getGuild(); if (guild == null) { event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to retrieve guild")) @@ -43,16 +38,9 @@ public class ButtonSuggestionReviewAccept extends DiscordButton { return; } - GuildMessageChannel suggestionChannel = guild.getChannelById(GuildMessageChannel.class, suggestionChannelId); - if (suggestionChannel == null) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "This server does not have a valid suggestion channel")) - .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); - return; - } - - GuildMessageChannel modLogChannel = guild.getChannelById(GuildMessageChannel.class, modLogChannelId); - if (modLogChannel == null) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "This server does not have a valid suggestion channel")) + List embeds = message.getEmbeds(); + if (embeds.size() != 1) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This message contains no embeds, can't be a suggestion")) .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); return; } @@ -65,21 +53,86 @@ public class ButtonSuggestionReviewAccept extends DiscordButton { return; } + GuildChannel suggestionGuildChannel = CommandOutputChannels.getOutputChannel(message.getGuild(), OutputType.SUGGESTION); + GuildChannel modLogGuildChannel = CommandOutputChannels.getOutputChannel(message.getGuild(), OutputType.MOD_LOG); + + TextChannel modLogChannel = validModLogChannel(event, modLogGuildChannel); + if (modLogChannel == null) + return; + + if (suggestionGuildChannel == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This server does not have a valid suggestion channel.")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + String mentionMember = reviewMessage.getDescription(); + if (mentionMember == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This message contains no description, can't be a suggestion")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + MessageEmbed suggestionMessage = new EmbedBuilder(reviewMessage) .clearFields() .setColor(Color.GRAY) .setTitle(fields.get(0).getName()) .setDescription(fields.get(0).getValue()) + .addField("Suggestion by", mentionMember, false) .build(); + + if (suggestionGuildChannel instanceof ForumChannel forumChannel) { + sendSuggestionInForum(forumChannel, modLogChannel, fields.get(0), suggestionMessage, mentionMember, event); + } else if (suggestionGuildChannel instanceof TextChannel forumChannel) { + sendSuggestionEmbed(forumChannel, modLogChannel, suggestionMessage, event); + } else { + event.replyEmbeds(Util.genericErrorEmbed("Error", suggestionGuildChannel.getType().name() + " is not a valid suggestion channel")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + } + + private TextChannel validModLogChannel(ButtonInteractionEvent event, GuildChannel guildChannel) { + if (guildChannel == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This server does not have a valid mod log channel")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return null; + } + + if (!(guildChannel instanceof TextChannel channel)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "A mod log channel can't be of type: " + guildChannel.getType().name())) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return null; + } + return channel; + } + + public void sendSuggestionEmbed(TextChannel suggestionChannel, TextChannel modLog, MessageEmbed suggestionMessage, ButtonInteractionEvent event) { suggestionChannel.sendMessageEmbeds(suggestionMessage).queue(success -> { - message.delete().queue(RestAction.getDefaultSuccess(), Util::handleFailure); + event.getMessage().delete().queue(RestAction.getDefaultSuccess(), Util::handleFailure); event.replyEmbeds(Util.genericSuccessEmbed("Success", "The suggestion was accepted and posted in the suggestion channel")).setEphemeral(true).queue(); - modLogChannel.sendMessageEmbeds(new EmbedBuilder(suggestionMessage).addField("Accepted", event.getUser().getAsMention(), false).setColor(Color.GREEN).build()) - .queue(RestAction.getDefaultSuccess(), Util::handleFailure); + sendModLog(modLog, suggestionMessage, event); }, failure -> event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to send suggestion to the suggestion channel")) .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure)); } + public void sendSuggestionInForum(ForumChannel forumChannel, TextChannel modLog, MessageEmbed.Field field, MessageEmbed suggestionMessage, String mentionMember, ButtonInteractionEvent event) { + MessageCreateData messageCreateData = new MessageCreateBuilder().addContent("**Suggestion by: " + mentionMember + "**\n\n" + field.getValue() + "\u200B").build(); + + forumChannel.createForumPost(field.getName(), messageCreateData).queue(success -> { + event.getMessage().delete().queue(RestAction.getDefaultSuccess(), Util::handleFailure); + event.replyEmbeds(Util.genericSuccessEmbed("Success", "The suggestion was accepted and posted in the suggestion channel")).setEphemeral(true).queue(); + sendModLog(modLog, suggestionMessage, event); + success.getMessage().addReaction(Emoji.fromUnicode("\uD83D\uDC4D")).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + success.getMessage().addReaction(Emoji.fromUnicode("\uD83D\uDC4E")).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + }, failure -> event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to send suggestion to the suggestion channel")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure)); + } + + public void sendModLog(TextChannel modLog, MessageEmbed suggestionMessage, ButtonInteractionEvent event) { + modLog.sendMessageEmbeds(new EmbedBuilder(suggestionMessage).addField("Accepted", event.getUser().getAsMention(), false).setColor(Color.GREEN).build()) + .queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + @Override public Button getButton() { return Button.success(getButtonId(), "Accept Suggestion"); diff --git a/src/main/java/com/alttd/buttonManager/buttons/suggestionReview/ButtonSuggestionReviewDeny.java b/src/main/java/com/alttd/buttonManager/buttons/suggestionReview/ButtonSuggestionReviewDeny.java index 48b149c..74dd9d9 100644 --- a/src/main/java/com/alttd/buttonManager/buttons/suggestionReview/ButtonSuggestionReviewDeny.java +++ b/src/main/java/com/alttd/buttonManager/buttons/suggestionReview/ButtonSuggestionReviewDeny.java @@ -8,9 +8,11 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.requests.RestAction; import java.awt.*; import java.util.List; @@ -25,30 +27,30 @@ public class ButtonSuggestionReviewDeny extends DiscordButton { @Override public void execute(ButtonInteractionEvent event) { Message message = event.getMessage(); - long channelId = CommandOutputChannels.getOutputChannel(message.getGuild().getIdLong(), OutputType.MOD_LOG); + GuildChannel guildChannel = CommandOutputChannels.getOutputChannel(message.getGuild(), OutputType.MOD_LOG); + TextChannel channel = validModLogChannel(event, guildChannel); + if (channel == null) + return; List embeds = message.getEmbeds(); if (embeds.size() != 1) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "This message contains no embeds, can't be a suggestion")).setEphemeral(true).queue(); + event.replyEmbeds(Util.genericErrorEmbed("Error", "This message contains no embeds, can't be a suggestion")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); return; } Guild guild = event.getGuild(); if (guild == null) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to retrieve guild")).setEphemeral(true).queue(); - return; - } - - GuildMessageChannel channel = guild.getChannelById(GuildMessageChannel.class, channelId); - if (channel == null) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "This server does not have a valid mod log channel")).setEphemeral(true).queue(); + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to retrieve guild")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); return; } MessageEmbed reviewMessage = embeds.get(0); List fields = reviewMessage.getFields(); if (fields.size() != 1) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "This message's embed does not contain a field, can't be a suggestion")).setEphemeral(true).queue(); + event.replyEmbeds(Util.genericErrorEmbed("Error", "This message's embed does not contain a field, can't be a suggestion")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); return; } @@ -60,12 +62,29 @@ public class ButtonSuggestionReviewDeny extends DiscordButton { .build(); channel.sendMessageEmbeds(suggestionMessage).queue(success -> { message.delete().queue(); - event.replyEmbeds(Util.genericSuccessEmbed("Success", "The suggestion was denied and logged")).setEphemeral(true).queue(); + event.replyEmbeds(Util.genericSuccessEmbed("Success", "The suggestion was denied and logged")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); }, failure -> { - event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to send suggestion to the suggestion channel")).setEphemeral(true).queue(); + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to send suggestion to the suggestion channel")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); }); } + private TextChannel validModLogChannel(ButtonInteractionEvent event, GuildChannel guildChannel) { + if (guildChannel == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This server does not have a valid mod log channel")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return null; + } + + if (!(guildChannel instanceof TextChannel channel)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "A mod log channel can't be of type: " + guildChannel.getType().name())) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return null; + } + return channel; + } + @Override public Button getButton() { return Button.danger(getButtonId(), "Deny Suggestion"); diff --git a/src/main/java/com/alttd/commandManager/CommandManager.java b/src/main/java/com/alttd/commandManager/CommandManager.java index 56aa794..afdd48f 100644 --- a/src/main/java/com/alttd/commandManager/CommandManager.java +++ b/src/main/java/com/alttd/commandManager/CommandManager.java @@ -3,6 +3,7 @@ package com.alttd.commandManager; import com.alttd.commandManager.commands.AddCommand.CommandManage; import com.alttd.commandManager.commands.*; import com.alttd.commandManager.commands.PollCommand.CommandPoll; +import com.alttd.contextMenuManager.ContextMenuManager; import com.alttd.database.Database; import com.alttd.modalManager.ModalManager; import com.alttd.util.Logger; @@ -29,12 +30,13 @@ public class CommandManager extends ListenerAdapter { private final List commands; private final HashMap> commandList = new HashMap<>(); - public CommandManager(JDA jda, ModalManager modalManager) { + public CommandManager(JDA jda, ModalManager modalManager, ContextMenuManager contextMenuManager) { commandList.put("manage", new ArrayList<>(List.of(new ScopeInfo(CommandScope.GLOBAL, 0)))); loadCommands(); Logger.info("Loading commands..."); + CommandSetToggleableRoles commandSetToggleableRoles = new CommandSetToggleableRoles(jda, this); commands = List.of( - new CommandManage(jda, this), + new CommandManage(jda, this, contextMenuManager), new CommandHelp(jda, this), new CommandPoll(jda, this), new CommandSuggestion(jda, modalManager, this), @@ -43,8 +45,10 @@ public class CommandManager extends ListenerAdapter { new CommandEvidence(jda, modalManager, this), new CommandFlag(jda, this), new CommandHistory(jda, this), - new CommandSeen(jda, this) - ); + new CommandSeen(jda, this), + commandSetToggleableRoles, + new CommandToggleRole(commandSetToggleableRoles, jda, this), + new CommandRemindMe(jda, this, modalManager)); } @Override diff --git a/src/main/java/com/alttd/commandManager/commands/AddCommand/CommandManage.java b/src/main/java/com/alttd/commandManager/commands/AddCommand/CommandManage.java index 35e1275..f1a64d2 100644 --- a/src/main/java/com/alttd/commandManager/commands/AddCommand/CommandManage.java +++ b/src/main/java/com/alttd/commandManager/commands/AddCommand/CommandManage.java @@ -4,6 +4,7 @@ import com.alttd.commandManager.CommandManager; import com.alttd.commandManager.DiscordCommand; import com.alttd.commandManager.SubCommand; import com.alttd.commandManager.SubOption; +import com.alttd.contextMenuManager.ContextMenuManager; import com.alttd.util.Logger; import com.alttd.util.Util; import net.dv8tion.jda.api.JDA; @@ -23,7 +24,7 @@ public class CommandManage extends DiscordCommand { private final HashMap subOptionsMap = new HashMap<>(); private final CommandData commandData; - public CommandManage(JDA jda, CommandManager commandManager) { + public CommandManage(JDA jda, CommandManager commandManager, ContextMenuManager contextMenuManager) { commandData = Commands.slash(getName(), "Enable commands and assign permissions") .addSubcommands( new SubcommandData("enable", "Enable a command in a channel") @@ -34,8 +35,8 @@ public class CommandManage extends DiscordCommand { .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.ADMINISTRATOR)) .setGuildOnly(true); Util.registerSubOptions(subOptionsMap, - new SubCommandEnable(commandManager, null, this), - new SubCommandEnable(commandManager, null, this) + new SubCommandEnable(commandManager, contextMenuManager, null, this), + new SubCommandDisable(commandManager, null, this) ); Util.registerCommand(commandManager, jda, commandData, getName()); } diff --git a/src/main/java/com/alttd/commandManager/commands/AddCommand/SubCommandEnable.java b/src/main/java/com/alttd/commandManager/commands/AddCommand/SubCommandEnable.java index aa7ab7d..b9c7705 100644 --- a/src/main/java/com/alttd/commandManager/commands/AddCommand/SubCommandEnable.java +++ b/src/main/java/com/alttd/commandManager/commands/AddCommand/SubCommandEnable.java @@ -1,20 +1,19 @@ package com.alttd.commandManager.commands.AddCommand; import com.alttd.commandManager.*; +import com.alttd.contextMenuManager.ContextMenuManager; +import com.alttd.contextMenuManager.DiscordContextMenu; import com.alttd.database.Database; import com.alttd.templates.Parser; import com.alttd.templates.Template; import com.alttd.util.Logger; import com.alttd.util.Util; -import com.google.protobuf.GeneratedMessageV3; -import com.mysql.cj.log.Log; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -23,10 +22,12 @@ import java.util.stream.Collectors; public class SubCommandEnable extends SubCommand { private final CommandManager commandManager; + private final ContextMenuManager contextMenuManager; - protected SubCommandEnable(CommandManager commandManager, SubCommandGroup parentGroup, DiscordCommand parent) { + protected SubCommandEnable(CommandManager commandManager, ContextMenuManager contextMenuManager, SubCommandGroup parentGroup, DiscordCommand parent) { super(parentGroup, parent); this.commandManager = commandManager; + this.contextMenuManager = contextMenuManager; } @Override @@ -50,11 +51,22 @@ public class SubCommandEnable extends SubCommand { String commandName = option.getAsString(); DiscordCommand command = commandManager.getCommand(commandName); - if (command == null) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find a command called [" + commandName + "].")).setEphemeral(true).queue(); + if (command != null) { + tryEnableCommand(command, guild, commandName, event); return; } + DiscordContextMenu contextMenu = contextMenuManager.getContext(commandName); + if (contextMenu != null) { + tryEnableContextMenu(contextMenu, guild, commandName, event); + //todo stuff + return; + } + + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find a command called [" + commandName + "].")).setEphemeral(true).queue(); + } + + private void tryEnableCommand(DiscordCommand command, Guild guild, String commandName, SlashCommandInteractionEvent event) { if (enableCommand(command, guild.getIdLong())) { Util.registerCommand(guild, command.getCommandData(), command.getName()); event.replyEmbeds(Util.genericSuccessEmbed("Enabled command", @@ -71,6 +83,23 @@ public class SubCommandEnable extends SubCommand { } } + private void tryEnableContextMenu(DiscordContextMenu contextMenu, Guild guild, String commandName, SlashCommandInteractionEvent event) { + if (enableContextMenu(contextMenu, guild.getIdLong())) { + Util.registerCommand(guild, contextMenu.getUserContextInteraction(), contextMenu.getContextMenuId()); + event.replyEmbeds(Util.genericSuccessEmbed("Enabled command", + Parser.parse("Successfully enabled in !", + Template.of("command", commandName.toLowerCase()), + Template.of("guild", guild.getName()) + ))).setEphemeral(true).queue(); + } else { + event.replyEmbeds(Util.genericErrorEmbed("Failed to enable command", + Parser.parse("Unable to enable in , is it already enabled?", + Template.of("command", commandName.toLowerCase()), + Template.of("guild", guild.getName()) + ))).setEphemeral(true).queue(); + } + } + private boolean enableCommand(DiscordCommand command, long guildId) { if (!commandManager.enableCommand(command.getName(), new ScopeInfo(CommandScope.GUILD, guildId))) return false; @@ -100,6 +129,35 @@ public class SubCommandEnable extends SubCommand { return true; } + private boolean enableContextMenu(DiscordContextMenu contextMenu, long guildId) { + if (!commandManager.enableCommand(contextMenu.getContextMenuId(), new ScopeInfo(CommandScope.GUILD, guildId))) + return false; + String sql = "INSERT INTO commands (command_name, scope, location_id) VALUES(?, ?, ?)"; + PreparedStatement statement = null; + + try { + statement = Database.getDatabase().getConnection().prepareStatement(sql); + statement.setString(1, contextMenu.getContextMenuId()); + statement.setString(2, "GUILD"); + statement.setLong(3, guildId); + if (statement.executeUpdate() == 0) { + Logger.warning("Unable to enable command: % for guild: %", contextMenu.getContextMenuId(), String.valueOf(guildId)); + return false; + } + } catch (SQLException exception) { + Logger.sql(exception); + return false; + } finally { + try { + if (statement != null) + statement.close(); + } catch (SQLException exception) { + Logger.sql(exception); + } + } + return true; + } + @Override public void suggest(CommandAutoCompleteInteractionEvent event) { OptionMapping option = event.getOption("command"); @@ -110,13 +168,21 @@ public class SubCommandEnable extends SubCommand { } String commandName = option.getAsString().toLowerCase(); ScopeInfo scopeInfo = new ScopeInfo(CommandScope.GLOBAL, event.getGuild().getIdLong()); - event.replyChoiceStrings(commandManager.getCommands().stream() - .map(DiscordCommand::getName) - .filter(name -> name.toLowerCase().startsWith(commandName)) - .filter(name -> !commandManager.getActiveLocations(name).contains(scopeInfo)) - .limit(25) - .collect(Collectors.toList())) - .queue(); + List collect = commandManager.getCommands().stream() + .map(DiscordCommand::getName) + .filter(name -> name.toLowerCase().startsWith(commandName)) + .filter(name -> !commandManager.getActiveLocations(name).contains(scopeInfo)) + .limit(25) + .collect(Collectors.toList()); + + collect.addAll(contextMenuManager.getContexts().stream() + .map(DiscordContextMenu::getContextMenuId) + .filter(name -> name.toLowerCase().startsWith(commandName)) + .filter(name -> !commandManager.getActiveLocations(name).contains(scopeInfo)) + .limit(25) + .collect(Collectors.toList())); + + event.replyChoiceStrings(collect).queue(); } @Override diff --git a/src/main/java/com/alttd/commandManager/commands/CommandRemindMe.java b/src/main/java/com/alttd/commandManager/commands/CommandRemindMe.java index 11c95cc..1b5da6e 100644 --- a/src/main/java/com/alttd/commandManager/commands/CommandRemindMe.java +++ b/src/main/java/com/alttd/commandManager/commands/CommandRemindMe.java @@ -1,4 +1,186 @@ package com.alttd.commandManager.commands; -public class CommandRemindMe { +import com.alttd.commandManager.CommandManager; +import com.alttd.commandManager.DiscordCommand; +import com.alttd.modalManager.ModalManager; +import com.alttd.modalManager.modals.ModalRemindMe; +import com.alttd.util.Util; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.AutoCompleteQuery; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.components.Modal; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class CommandRemindMe extends DiscordCommand { + + private final CommandData commandData; + private final ModalManager modalManager; + + public CommandRemindMe(JDA jda, CommandManager commandManager, ModalManager modalManager) { + this.modalManager = modalManager; + commandData = Commands.slash(getName(), "Create a reminder") + .addOption(OptionType.CHANNEL, "channel", "The channel to send the reminder in", true) + .addOption(OptionType.STRING, "fromnow", "How long from now the reminder should send", true, true) + .setDefaultPermissions(DefaultMemberPermissions.ENABLED) + .setGuildOnly(true); + + Util.registerCommand(commandManager, jda, commandData, getName()); + } + + @Override + public String getName() { + return "remindme"; + } + + @Override + public void execute(SlashCommandInteractionEvent event) { + TextChannel channel = getValidChannel( + event.getInteraction().getOption("channel", OptionMapping::getAsChannel), event); + if (channel == null) + return; + + Long fromNow = getFromNow(event.getInteraction().getOption("fromnow", OptionMapping::getAsString), event); + if (fromNow == null) + return; + + Modal modal = modalManager.getModalFor("remindme"); + if (modal == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to retrieve remind me modal")) + .setEphemeral(true).queue(); + return; + } + + ModalRemindMe.putData(event.getUser().getIdLong(), channel, fromNow); + event.replyModal(modal).queue(); + } + + private TextChannel getValidChannel(GuildChannelUnion channel, SlashCommandInteractionEvent event) { + if (channel == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Couldn't find channel")) + .setEphemeral(true).queue(); + return null; + } + + if (!(channel instanceof TextChannel textChannel)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Not a valid TextChannel")) + .setEphemeral(true).queue(); + return null; + } + + if (!textChannel.canTalk()) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "I can't talk in this channel")) + .setEphemeral(true).queue(); + return null; + } + return textChannel; + } + + private Long getFromNow(String fromNow, SlashCommandInteractionEvent event) { + if (fromNow == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Couldn't find from now option")) + .setEphemeral(true).queue(); + return null; + } + + if (!fromNow.matches("[1-9][0-9]*[hdmy]")) { + return fromNowTimestamp(fromNow, event); + } + + int i; + try { + i = Integer.parseInt(fromNow.substring(0, fromNow.length() - 1)); + } catch (NumberFormatException e) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Invalid number")) + .setEphemeral(true).queue(); + return null; + } + + switch (fromNow.substring(fromNow.length() - 1)) { + case "h" -> { + return TimeUnit.HOURS.toMillis(i) + new Date().getTime(); + } + case "d" -> { + return TimeUnit.DAYS.toMillis(i) + new Date().getTime(); + } + case "m" -> { + Calendar instance = Calendar.getInstance(); + instance.setTime(new Date()); + instance.add(Calendar.MONTH, i); + return instance.getTimeInMillis(); + } + case "y" -> { + Calendar instance = Calendar.getInstance(); + instance.setTime(new Date()); + instance.add(Calendar.YEAR, i); + return instance.getTimeInMillis(); + } + default -> { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Invalid format? This shouldn't be possible...")) + .setEphemeral(true).queue(); + return null; + } + } + } + + private Long fromNowTimestamp(String fromNow, SlashCommandInteractionEvent event) { + if (!fromNow.matches("t:[1-9][0-9]*")) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Invalid from now format ex: `1d`")) + .setEphemeral(true).queue(); + return null; + } + long l; + try { + l = Long.parseLong(fromNow.substring(2)); + } catch (NumberFormatException e) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Invalid number")) + .setEphemeral(true).queue(); + return null; + } + return l; + } + + @Override + public void suggest(CommandAutoCompleteInteractionEvent event) { + AutoCompleteQuery focusedOption = event.getFocusedOption(); + if (!focusedOption.getName().equals("fromnow")) { + event.replyChoices(Collections.emptyList()).queue(); + return; + } + + String value = focusedOption.getValue(); + if (value.isBlank()) { + event.replyChoiceStrings(List.of("1h", "1d", "1m", "1y", "t:" + new Date().getTime())).queue(); + return; + } + + if (value.matches("[0-9]+")) + event.replyChoiceStrings(List.of(value + "h", value + "d", value + "m", value + "y")).queue(); + else if (value.startsWith("t:")) + event.replyChoiceStrings(List.of(value)).queue(); + else + event.replyChoices(Collections.emptyList()).queue(); + } + + @Override + public String getHelpMessage() { + return null; + } + + @Override + public CommandData getCommandData() { + return commandData; + } } diff --git a/src/main/java/com/alttd/commandManager/commands/CommandSetOutputChannel.java b/src/main/java/com/alttd/commandManager/commands/CommandSetOutputChannel.java index 44c7c65..a0220a5 100644 --- a/src/main/java/com/alttd/commandManager/commands/CommandSetOutputChannel.java +++ b/src/main/java/com/alttd/commandManager/commands/CommandSetOutputChannel.java @@ -8,6 +8,7 @@ import com.alttd.util.Util; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -71,10 +72,11 @@ public class CommandSetOutputChannel extends DiscordCommand { return; } - GuildChannelUnion channel = option.getAsChannel(); - switch (channel.getType()) { - case TEXT, NEWS, GUILD_NEWS_THREAD, GUILD_PUBLIC_THREAD, GUILD_PRIVATE_THREAD -> { - boolean success = CommandOutputChannels.setOutputChannel(guild.getIdLong(), outputType, channel.getIdLong()); + ChannelType channelType = option.getChannelType(); + switch (channelType) { + case TEXT, NEWS, GUILD_NEWS_THREAD, GUILD_PUBLIC_THREAD, GUILD_PRIVATE_THREAD, FORUM -> { + GuildChannelUnion channel = option.getAsChannel(); + boolean success = CommandOutputChannels.setOutputChannel(guild.getIdLong(), outputType, channel.getIdLong(), channelType); if (success) event.replyEmbeds(Util.genericSuccessEmbed("Success", "Set channel " + channel.getAsMention() + " as the output channel for " + outputType.name() + ".")) .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); @@ -82,7 +84,7 @@ public class CommandSetOutputChannel extends DiscordCommand { event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to store the new channel output in the database")) .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); } - default -> event.replyEmbeds(Util.genericErrorEmbed("Error", "The channel type " + channel.getType().name() + " is not a valid output channel type")) + default -> event.replyEmbeds(Util.genericErrorEmbed("Error", "The channel type " + channelType.name() + " is not a valid output channel type")) .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); } diff --git a/src/main/java/com/alttd/commandManager/commands/CommandSetToggleableRoles.java b/src/main/java/com/alttd/commandManager/commands/CommandSetToggleableRoles.java new file mode 100644 index 0000000..5179d1b --- /dev/null +++ b/src/main/java/com/alttd/commandManager/commands/CommandSetToggleableRoles.java @@ -0,0 +1,147 @@ +package com.alttd.commandManager.commands; + +import com.alttd.commandManager.CommandManager; +import com.alttd.commandManager.DiscordCommand; +import com.alttd.database.queries.QueriesToggleableRoles; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.requests.RestAction; + +import java.awt.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +public class CommandSetToggleableRoles extends DiscordCommand { + + private final HashMap> guildToRolesMap; + private final CommandData commandData; + + public CommandSetToggleableRoles(JDA jda, CommandManager commandManager) { + guildToRolesMap = QueriesToggleableRoles.getToggleableRoles(); + + commandData = Commands.slash(getName(), "Set which roles can be toggled") + .addOption(OptionType.ROLE, "role", "The role you want to toggle on/off", false) + .setDefaultPermissions(DefaultMemberPermissions.DISABLED); + + Util.registerCommand(commandManager, jda, commandData, getName()); + } + + @Override + public String getName() { + return "settoggleableroles"; + } + + @Override + public void execute(SlashCommandInteractionEvent event) { + Guild guild = event.getGuild(); + if (guild == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This command has to be ran in a guild")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + List options = event.getInteraction().getOptions(); + if (options.size() == 0) { + String toggleableRoles = getToggleableRoles(guild); + MessageEmbed messageEmbed = new EmbedBuilder() + .setTitle("Active roles") + .setColor(Color.GREEN) + .setDescription(toggleableRoles) + .build(); + + event.replyEmbeds(messageEmbed).setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + OptionMapping optionMapping = options.get(0); + Role role = optionMapping.getAsRole(); + if (containsRole(role)) { + if (!QueriesToggleableRoles.removeRoleToggleable(role)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to remove role from the database")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + removeRole(role); + event.replyEmbeds(Util.genericSuccessEmbed("Success", "Removed " + role.getAsMention() + " from the toggleable roles")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } else { + if (role.hasPermission(Permission.ADMINISTRATOR) || + role.hasPermission(Permission.MANAGE_ROLES) || + role.hasPermission(Permission.MANAGE_CHANNEL) || + role.hasPermission(Permission.MANAGE_THREADS) || + role.hasPermission(Permission.MANAGE_WEBHOOKS) || + role.hasPermission(Permission.MANAGE_SERVER) || + role.hasPermission(Permission.MANAGE_PERMISSIONS) || + role.hasPermission(Permission.MESSAGE_MANAGE) || + role.hasPermission(Permission.MODERATE_MEMBERS)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "For safety reason this bot can not add roles which have a manage or moderator permission")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + if (!QueriesToggleableRoles.addRoleToggleable(role)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to store role in the database")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + addRole(role); + event.replyEmbeds(Util.genericSuccessEmbed("Success", "Added " + role.getAsMention() + " to the toggleable roles")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + } + + private void addRole(Role role) { + long guild = role.getGuild().getIdLong(); + HashSet set = guildToRolesMap.getOrDefault(guild, new HashSet<>()); + set.add(role.getIdLong()); + guildToRolesMap.put(guild, set); + } + + private void removeRole(Role role) { + long guild = role.getGuild().getIdLong(); + HashSet set = guildToRolesMap.getOrDefault(guild, new HashSet<>()); + if (set.isEmpty()) + return; + set.remove(role.getIdLong()); + guildToRolesMap.put(guild, set); + } + + public boolean containsRole(Role role) { + return guildToRolesMap.getOrDefault(role.getGuild().getIdLong(), new HashSet<>()).contains(role.getIdLong()); + } + + public String getToggleableRoles(Guild guild) { + HashSet roleIds = guildToRolesMap.get(guild.getIdLong()); + return guild.getRoles().stream() + .filter(role -> roleIds.contains(role.getIdLong())) + .map(Role::getAsMention) + .collect(Collectors.joining("\n")); + } + + @Override + public void suggest(CommandAutoCompleteInteractionEvent event) { + event.replyChoices(Collections.emptyList()).queue(); + } + + @Override + public String getHelpMessage() { + return null; + } + + @Override + public CommandData getCommandData() { + return commandData; + } +} diff --git a/src/main/java/com/alttd/commandManager/commands/CommandToggleRole.java b/src/main/java/com/alttd/commandManager/commands/CommandToggleRole.java new file mode 100644 index 0000000..2d7f261 --- /dev/null +++ b/src/main/java/com/alttd/commandManager/commands/CommandToggleRole.java @@ -0,0 +1,108 @@ +package com.alttd.commandManager.commands; + +import com.alttd.commandManager.CommandManager; +import com.alttd.commandManager.DiscordCommand; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.requests.RestAction; + +import java.awt.*; +import java.util.Collections; +import java.util.List; + +public class CommandToggleRole extends DiscordCommand { + + private final CommandSetToggleableRoles commandSetToggleableRoles; + private final CommandData commandData; + + public CommandToggleRole(CommandSetToggleableRoles commandSetToggleableRoles, JDA jda, CommandManager commandManager) { + this.commandSetToggleableRoles = commandSetToggleableRoles; + commandData = Commands.slash(getName(), "Toggle a role") + .addOption(OptionType.ROLE, "role", "The role you want to toggle on/off (run the command without this option to see all available roles)", false) + .setDefaultPermissions(DefaultMemberPermissions.ENABLED); + + Util.registerCommand(commandManager, jda, commandData, getName()); + } + + @Override + public String getName() { + return "togglerole"; + } + + @Override + public void execute(SlashCommandInteractionEvent event) { + Guild guild = event.getGuild(); + if (guild == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This command has to be ran in a guild")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + List options = event.getInteraction().getOptions(); + if (options.size() == 0) { + String toggleableRoles = commandSetToggleableRoles.getToggleableRoles(guild); + MessageEmbed messageEmbed = new EmbedBuilder() + .setTitle("Toggleable roles") + .setColor(Color.GREEN) + .setDescription(toggleableRoles) + .build(); + + event.replyEmbeds(messageEmbed).setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + OptionMapping optionMapping = options.get(0); + Role role = optionMapping.getAsRole(); + if (!commandSetToggleableRoles.containsRole(role)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This role is not toggleable!")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + Member member = event.getMember(); + if (member == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This command has to be ran in a guild")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + if (member.getRoles().contains(role)) { + guild.removeRoleFromMember(member, role).queue(success -> + event.replyEmbeds(Util.genericSuccessEmbed("Role removed", "You no longer have " + role.getAsMention() + ".")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure), + error -> event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to manage your roles.")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure)); + } else { + guild.addRoleToMember(member, role).queue(success -> + event.replyEmbeds(Util.genericSuccessEmbed("Role add", "You now have " + role.getAsMention() + ".")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure), + error -> event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to manage your roles.")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure)); + } + } + + @Override + public void suggest(CommandAutoCompleteInteractionEvent event) { + event.replyChoices(Collections.emptyList()).queue(); + } + + @Override + public String getHelpMessage() { + return null; + } + + @Override + public CommandData getCommandData() { + return commandData; + } +} diff --git a/src/main/java/com/alttd/config/AbstractConfig.java b/src/main/java/com/alttd/config/AbstractConfig.java index feb1fcd..3289e8f 100644 --- a/src/main/java/com/alttd/config/AbstractConfig.java +++ b/src/main/java/com/alttd/config/AbstractConfig.java @@ -22,14 +22,14 @@ import java.util.Map; import java.util.regex.Pattern; @SuppressWarnings({"unused", "SameParameterValue"}) -abstract class AbstractConfig { +public abstract class AbstractConfig { private static final Pattern PATH_PATTERN = Pattern.compile("\\."); private static final String HEADER = ""; private YamlConfigurationLoader configLoader; private ConfigurationNode config; - AbstractConfig(String filename) { + protected AbstractConfig(String filename) { init(new File(new File(AltitudeBot.getInstance().getDataFolder()).getParentFile(), filename), filename); } @@ -60,7 +60,7 @@ abstract class AbstractConfig { } } - void readConfig(Class clazz, Object instance) { + protected void readConfig(Class clazz, Object instance) { for (Method method : clazz.getDeclaredMethods()) { if (Modifier.isPrivate(method.getModifiers())) { if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) { @@ -80,7 +80,7 @@ abstract class AbstractConfig { save(); } - private void save() { + protected void save() { try { configLoader.save(config); } catch (IOException ex) { @@ -101,6 +101,17 @@ abstract class AbstractConfig { } } + protected void update(String path, Object def) { + if(config.node(splitPath(path)).virtual()) { + set(path, def); + return; + } + try { + config.node(splitPath(path)).set(def); + } catch (SerializationException e) { + } + } + protected void setString(String path, String def) { try { if(config.node(splitPath(path)).virtual()) diff --git a/src/main/java/com/alttd/contextMenuManager/ContextMenuManager.java b/src/main/java/com/alttd/contextMenuManager/ContextMenuManager.java new file mode 100644 index 0000000..94e2ab6 --- /dev/null +++ b/src/main/java/com/alttd/contextMenuManager/ContextMenuManager.java @@ -0,0 +1,76 @@ +package com.alttd.contextMenuManager; + +import com.alttd.contextMenuManager.contextMenus.ContextMenuRespondSuggestion; +import com.alttd.modalManager.ModalManager; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.Nonnull; +import java.awt.*; +import java.util.List; +import java.util.Optional; + +public class ContextMenuManager extends ListenerAdapter { + + private final List contextMenus; + + public ContextMenuManager(ModalManager modalManager) { + contextMenus = List.of( + new ContextMenuRespondSuggestion(modalManager) + ); + } + + public DiscordContextMenu getContext(String name) { + for (DiscordContextMenu contextMenu : contextMenus) { + if (contextMenu.getContextMenuId().equalsIgnoreCase(name)) + return contextMenu; + } + return null; + } + + public List getContexts() { + return contextMenus; + } + + @Override + public void onUserContextInteraction(@Nonnull UserContextInteractionEvent event) { + String name = event.getInteraction().getName(); + Optional first = contextMenus.stream() + .filter(discordModal -> discordModal.getContextMenuId().equalsIgnoreCase(name)) + .findFirst(); + if (first.isEmpty()) { + event.replyEmbeds(new EmbedBuilder() + .setTitle("Invalid command") + .setDescription("Unable to process user context interaction with id: [" + name + "].") + .setColor(Color.RED) + .build()) + .setEphemeral(true) + .queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + first.get().execute(event); + } + + @Override + public void onMessageContextInteraction(@Nonnull MessageContextInteractionEvent event) { + String name = event.getInteraction().getName(); + Optional first = contextMenus.stream() + .filter(discordModal -> discordModal.getContextMenuId().equalsIgnoreCase(name)) + .findFirst(); + if (first.isEmpty()) { + event.replyEmbeds(new EmbedBuilder() + .setTitle("Invalid command") + .setDescription("Unable to process user context interaction with id: [" + name + "].") + .setColor(Color.RED) + .build()) + .setEphemeral(true) + .queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + first.get().execute(event); + } +} diff --git a/src/main/java/com/alttd/contextMenuManager/DiscordContextMenu.java b/src/main/java/com/alttd/contextMenuManager/DiscordContextMenu.java new file mode 100644 index 0000000..c01f5ce --- /dev/null +++ b/src/main/java/com/alttd/contextMenuManager/DiscordContextMenu.java @@ -0,0 +1,17 @@ +package com.alttd.contextMenuManager; + +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; + +public abstract class DiscordContextMenu { + + public abstract String getContextMenuId(); + + public abstract void execute(UserContextInteractionEvent event); + + public abstract void execute(MessageContextInteractionEvent event); + + public abstract CommandData getUserContextInteraction(); + +} diff --git a/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuRespondSuggestion.java b/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuRespondSuggestion.java new file mode 100644 index 0000000..7e63c3e --- /dev/null +++ b/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuRespondSuggestion.java @@ -0,0 +1,90 @@ +package com.alttd.contextMenuManager.contextMenus; + +import com.alttd.contextMenuManager.DiscordContextMenu; +import com.alttd.database.queries.commandOutputChannels.CommandOutputChannels; +import com.alttd.database.queries.commandOutputChannels.OutputType; +import com.alttd.modalManager.ModalManager; +import com.alttd.modalManager.modals.ModalReplySuggestion; +import com.alttd.util.Util; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.channel.unions.IThreadContainerUnion; +import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.components.Modal; +import net.dv8tion.jda.api.requests.RestAction; + +public class ContextMenuRespondSuggestion extends DiscordContextMenu { + + private final ModalManager modalManager; + + public ContextMenuRespondSuggestion(ModalManager modalManager) { + this.modalManager = modalManager; + } + + @Override + public String getContextMenuId() { + return "Respond To Suggestion"; + } + + @Override + public void execute(UserContextInteractionEvent event) { + event.getInteraction().replyEmbeds(Util.genericErrorEmbed("Error", "This interaction should have been a message interaction")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + + @Override + public void execute(MessageContextInteractionEvent event) { + Message message = event.getInteraction().getTarget(); + if (!isSuggestion(message)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This is not a suggestion")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + Modal replySuggestion = modalManager.getModalFor("reply_suggestion"); + if (replySuggestion == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find reply suggestion modal")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + ModalReplySuggestion.putMessage(event.getUser().getIdLong(), message); //TODO find a better way to do this + event.replyModal(replySuggestion).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + + @Override + public CommandData getUserContextInteraction() { + return Commands.message(getContextMenuId()) + .setGuildOnly(true) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.ADMINISTRATOR)); + } + + public boolean isSuggestion(Message message) { + GuildChannel channel = CommandOutputChannels.getOutputChannel(message.getGuild(), OutputType.SUGGESTION); + if (channel == null) + return false; + MessageChannelUnion messageChannel = message.getChannel(); + if (channel.getType().equals(ChannelType.FORUM)) { + if (messageChannel.getType() != ChannelType.GUILD_PUBLIC_THREAD) { + return false; + } + ThreadChannel threadChannel = messageChannel.asThreadChannel(); + IThreadContainerUnion parentChannel = threadChannel.getParentChannel(); + if (!parentChannel.getType().equals(ChannelType.FORUM)) + return false; + + return message.getIdLong() == messageChannel.getIdLong() && message.getAuthor().equals(message.getJDA().getSelfUser()); + } else { + return channel.equals(messageChannel); + } + + } +} diff --git a/src/main/java/com/alttd/database/DatabaseTables.java b/src/main/java/com/alttd/database/DatabaseTables.java index 683b837..4150d5f 100644 --- a/src/main/java/com/alttd/database/DatabaseTables.java +++ b/src/main/java/com/alttd/database/DatabaseTables.java @@ -65,7 +65,7 @@ public class DatabaseTables { connection.prepareStatement(sql).executeUpdate(); } catch (SQLException e) { Logger.sql(e); - Logger.severe("Unable to create polls table, shutting down..."); + Logger.severe("Unable to create commands table, shutting down..."); } } @@ -74,13 +74,50 @@ public class DatabaseTables { "guild BIGINT NOT NULL, " + "output_type VARCHAR(64) NOT NULL, " + "channel BIGINT NOT NULL, " + + "channel_type VARCHAR(64) NOT NULL, " + "PRIMARY KEY (guild, output_type, channel)" + ")"; try { connection.prepareStatement(sql).executeUpdate(); } catch (SQLException e) { Logger.sql(e); - Logger.severe("Unable to create polls table, shutting down..."); + Logger.severe("Unable to create output channel table, shutting down..."); + } + } + + private void createToggleableRolesTable() { + String sql = "CREATE TABLE IF NOT EXISTS toggleable_roles(" + + "guild BIGINT NOT NULL, " + + "role BIGINT NOT NULL, " + + "PRIMARY KEY (guild, role)" + + ")"; + try { + connection.prepareStatement(sql).executeUpdate(); + } catch (SQLException e) { + Logger.sql(e); + Logger.severe("Unable to create toggleable roles table, shutting down..."); + } + } + + private void createReminderTable() { + String sql = "CREATE TABLE IF NOT EXISTS new_reminders(" + + "id INT NOT NULL AUTO_INCREMENT, " + + "title VARCHAR(256) NOT NULL, " + + "description VARCHAR(4096) NOT NULL, " + + "user_id LONG NOT NULL, " + + "guild_id LONG NOT NULL, " + + "channel_id LONG NOT NULL, " + + "message_id LONG NOT NULL, " + + "should_repeat TINYINT(1) NOT NULL, " + + "creation_date LONG NOT NULL, " + + "remind_date LONG NOT NULL, " + + "PRIMARY KEY (id)" + + ")"; + try { + connection.prepareStatement(sql).executeUpdate(); + } catch (SQLException e) { + Logger.sql(e); + Logger.severe("Unable to create reminders table, shutting down..."); } } diff --git a/src/main/java/com/alttd/database/queries/QueriesReminders/QueriesReminders.java b/src/main/java/com/alttd/database/queries/QueriesReminders/QueriesReminders.java new file mode 100644 index 0000000..fba207c --- /dev/null +++ b/src/main/java/com/alttd/database/queries/QueriesReminders/QueriesReminders.java @@ -0,0 +1,90 @@ +package com.alttd.database.queries.QueriesReminders; + +import com.alttd.database.Database; +import com.alttd.util.Logger; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; + +public class QueriesReminders { + + public static int storeReminder(Reminder reminder) { + String sql = "INSERT INTO new_reminders " + + "(title, description, user_id, guild_id, channel_id, message_id, should_repeat, creation_date, remind_date) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + try { + PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + + preparedStatement.setString(1, reminder.title()); + preparedStatement.setString(2, reminder.description()); + preparedStatement.setLong(3, reminder.userId()); + preparedStatement.setLong(4, reminder.guildId()); + preparedStatement.setLong(5, reminder.channelId()); + preparedStatement.setLong(6, 0); + preparedStatement.setInt(7, reminder.shouldRepeat() ? 1 : 0); + preparedStatement.setLong(8, reminder.creationDate()); + preparedStatement.setLong(9, reminder.remindDate()); + + if (preparedStatement.executeUpdate() == 1) { + ResultSet generatedKeys = preparedStatement.getGeneratedKeys(); + if (generatedKeys.next()) { + return generatedKeys.getInt(1); + } + } + + return -1; + } catch (SQLException e) { + Logger.exception(e); + } + return -1; + } + + public static boolean removeReminder(int id) { + String sql = "DELETE FROM new_reminders WHERE id = ?"; + try { + PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); + + preparedStatement.setInt(1, id); + + return preparedStatement.executeUpdate() == 1; + } catch (SQLException e) { + Logger.exception(e); + } + return false; + } + + public static ArrayList getReminders() { + String sql = "SELECT * FROM new_reminders"; + try { + ArrayList reminders = new ArrayList<>(); + PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); + + ResultSet resultSet = preparedStatement.executeQuery(); + while (resultSet.next()) { + reminders.add(getReminder(resultSet)); + } + return reminders; + } catch (SQLException e) { + Logger.exception(e); + } + return null; + } + + private static Reminder getReminder(ResultSet resultSet) throws SQLException { + int id = resultSet.getInt("id"); + String title = resultSet.getString("title"); + String desc = resultSet.getString("description"); + long userId = resultSet.getLong("user_id"); + long guildId = resultSet.getLong("guild_id"); + long channelId = resultSet.getLong("channel_id"); + long messageId = resultSet.getLong("message_id"); + boolean shouldRepeat = resultSet.getInt("should_repeat") == 1; + long creationDate = resultSet.getLong("creation_date"); + long remindDate = resultSet.getLong("remind_date"); + return new Reminder(id, title, desc, userId, guildId, channelId, messageId, shouldRepeat, creationDate, remindDate); + } + +} diff --git a/src/main/java/com/alttd/database/queries/QueriesReminders/Reminder.java b/src/main/java/com/alttd/database/queries/QueriesReminders/Reminder.java new file mode 100644 index 0000000..6cb9934 --- /dev/null +++ b/src/main/java/com/alttd/database/queries/QueriesReminders/Reminder.java @@ -0,0 +1,33 @@ +package com.alttd.database.queries.QueriesReminders; + +import com.alttd.util.Logger; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; + +public record Reminder (int id, String title, String description, long userId, long guildId, long channelId, + long messageId, boolean shouldRepeat, long creationDate, long remindDate) { + public TextChannel getChannel(JDA jda) { + Guild guildById = getGuild(jda); + if (guildById == null) + return null; + + TextChannel textChannelById = guildById.getTextChannelById(this.channelId); + if (textChannelById == null) { + Logger.warning("Unable to find text channel for reminder, text channel id: [" + channelId + "]"); + return null; + } + + return textChannelById; + } + + public Guild getGuild(JDA jda) { + Guild guildById = jda.getGuildById(guildId); + if (guildById == null) { + Logger.warning("Unable to find guild for reminder, guild id: [" + guildId + "]"); + return null; + } + + return guildById; + } +} diff --git a/src/main/java/com/alttd/database/queries/QueriesToggleableRoles.java b/src/main/java/com/alttd/database/queries/QueriesToggleableRoles.java new file mode 100644 index 0000000..e9464dd --- /dev/null +++ b/src/main/java/com/alttd/database/queries/QueriesToggleableRoles.java @@ -0,0 +1,67 @@ +package com.alttd.database.queries; + +import com.alttd.database.Database; +import net.dv8tion.jda.api.entities.Role; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.HashSet; + +public class QueriesToggleableRoles { + + public static boolean addRoleToggleable(Role role) { + String sql = "INSERT INTO toggleable_roles (guild, role) VALUES (?, ?)"; + try { + PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); + + preparedStatement.setLong(1, role.getGuild().getIdLong()); + preparedStatement.setLong(2, role.getIdLong()); + + return preparedStatement.executeUpdate() == 1; + } catch (SQLException exception) { + exception.printStackTrace(); + } + return false; + } + + public static boolean removeRoleToggleable(Role role) { + String sql = "DELETE FROM toggleable_roles WHERE guild = ? AND role = ?"; + try { + PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); + + preparedStatement.setLong(1, role.getGuild().getIdLong()); + preparedStatement.setLong(2, role.getIdLong()); + + return preparedStatement.executeUpdate() == 1; + } catch (SQLException exception) { + exception.printStackTrace(); + } + return false; + } + + public static HashMap> getToggleableRoles() { + String sql = "SELECT * FROM toggleable_roles"; + try { + HashMap> map = new HashMap<>(); + PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); + + ResultSet resultSet = preparedStatement.executeQuery(); + + while (resultSet.next()) { + long guild = resultSet.getLong("guild"); + long role = resultSet.getLong("role"); + HashSet roles = map.getOrDefault(guild, new HashSet<>()); + roles.add(role); + map.put(guild, roles); + } + + return map; + } catch (SQLException exception) { + exception.printStackTrace(); + } + return null; + } + +} diff --git a/src/main/java/com/alttd/database/queries/commandOutputChannels/CommandOutputChannels.java b/src/main/java/com/alttd/database/queries/commandOutputChannels/CommandOutputChannels.java index b1a38a3..b15946b 100644 --- a/src/main/java/com/alttd/database/queries/commandOutputChannels/CommandOutputChannels.java +++ b/src/main/java/com/alttd/database/queries/commandOutputChannels/CommandOutputChannels.java @@ -2,6 +2,9 @@ package com.alttd.database.queries.commandOutputChannels; import com.alttd.database.Database; import com.alttd.util.Logger; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -9,14 +12,15 @@ import java.sql.SQLException; public class CommandOutputChannels { - public static boolean setOutputChannel(long guildId, OutputType outputType, long channelId) { - String sql = "INSERT INTO output_channels (guild, output_type, channel) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE channel = ?"; + public static boolean setOutputChannel(long guildId, OutputType outputType, long channelId, ChannelType channelType) { + String sql = "INSERT INTO output_channels (guild, output_type, channel, channel_type) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE channel = ?"; try { PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); preparedStatement.setLong(1, guildId); preparedStatement.setString(2, outputType.name()); preparedStatement.setLong(3, channelId); - preparedStatement.setLong(4, channelId); + preparedStatement.setString(4, channelType.name()); + preparedStatement.setLong(5, channelId); return preparedStatement.executeUpdate() == 1; } catch (SQLException e) { @@ -27,26 +31,46 @@ public class CommandOutputChannels { /** * Retrieve the channelId of the channel in the specified guild for the specified output type - * @param guildId id of the guild to check in + * @param guild guild to get the channel for * @param outputType output type to check for * @return long channel id or 0 if it errors or can't be found */ - public static long getOutputChannel(long guildId, OutputType outputType) { - String sql = "SELECT channel FROM output_channels WHERE guild = ? AND output_type = ?"; + public static GuildChannel getOutputChannel(Guild guild, OutputType outputType) { + String sql = "SELECT channel, channel_type FROM output_channels WHERE guild = ? AND output_type = ?"; try { PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); - preparedStatement.setLong(1, guildId); + preparedStatement.setLong(1, guild.getIdLong()); preparedStatement.setString(2, outputType.name()); ResultSet resultSet = preparedStatement.executeQuery(); - if (resultSet.next()) - return resultSet.getLong("channel"); - else - return 0L; + if (resultSet.next()) { + String stringChannelType = resultSet.getString("channel_type"); + ChannelType channelType; + try { + channelType = ChannelType.valueOf(stringChannelType); + } catch (IllegalArgumentException exception) { + return null; + } + long channelId = resultSet.getLong("channel"); + switch (channelType) { + case TEXT, NEWS -> { + return guild.getTextChannelById(channelId); + } + case GUILD_NEWS_THREAD, GUILD_PUBLIC_THREAD, GUILD_PRIVATE_THREAD -> { + return guild.getThreadChannelById(channelId); + } + case FORUM -> { + return guild.getForumChannelById(channelId); + } + default -> { + return null; + } + } + } } catch (SQLException e) { Logger.exception(e); - return 0L; } + return null; } } diff --git a/src/main/java/com/alttd/listeners/JDAListener.java b/src/main/java/com/alttd/listeners/JDAListener.java index 063e688..d117d58 100644 --- a/src/main/java/com/alttd/listeners/JDAListener.java +++ b/src/main/java/com/alttd/listeners/JDAListener.java @@ -2,10 +2,16 @@ package com.alttd.listeners; import com.alttd.buttonManager.ButtonManager; import com.alttd.commandManager.CommandManager; +import com.alttd.contextMenuManager.ContextMenuManager; import com.alttd.modalManager.ModalManager; +import com.alttd.reminders.ReminderScheduler; +import com.alttd.request.RequestManager; import com.alttd.util.Logger; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.events.ReadyEvent; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.jetbrains.annotations.NotNull; @@ -22,8 +28,38 @@ public class JDAListener extends ListenerAdapter { Logger.info("JDA ready to register commands."); ButtonManager buttonManager = new ButtonManager(); ModalManager modalManager = new ModalManager(buttonManager); - CommandManager commandManager = new CommandManager(jda, modalManager); - jda.addEventListener(buttonManager, modalManager, commandManager); + ContextMenuManager contextMenuManager = new ContextMenuManager(modalManager); + CommandManager commandManager = new CommandManager(jda, modalManager, contextMenuManager); + jda.addEventListener(buttonManager, modalManager, commandManager, contextMenuManager); + ReminderScheduler reminderScheduler = ReminderScheduler.getInstance(jda); + if (reminderScheduler == null) { + Logger.severe("Unable to start reminder scheduler!"); + } +// RequestManager.init(); + } + + @Override + public void onSelectMenuInteraction(@NotNull SelectMenuInteractionEvent event) { + String s = event.getComponentId(); + if (s.startsWith("request:")) { + RequestManager.onSelectMenuInteraction(event); + } + } + + @Override + public void onModalInteraction(@NotNull ModalInteractionEvent event) { + String s = event.getModalId(); + if (s.startsWith("request:")) { + RequestManager.onModalInteractionEvent(event); + } + } + + @Override + public void onButtonInteraction(ButtonInteractionEvent event) { + String s = event.getComponentId(); + if (s.startsWith("request:")) { + RequestManager.onButtonInteractionEvent(event); + } } } diff --git a/src/main/java/com/alttd/modalManager/ModalManager.java b/src/main/java/com/alttd/modalManager/ModalManager.java index 2cb30d1..ac2d6b7 100644 --- a/src/main/java/com/alttd/modalManager/ModalManager.java +++ b/src/main/java/com/alttd/modalManager/ModalManager.java @@ -2,6 +2,8 @@ package com.alttd.modalManager; import com.alttd.buttonManager.ButtonManager; import com.alttd.modalManager.modals.ModalEvidence; +import com.alttd.modalManager.modals.ModalRemindMe; +import com.alttd.modalManager.modals.ModalReplySuggestion; import com.alttd.modalManager.modals.ModalSuggestion; import com.alttd.util.Util; import net.dv8tion.jda.api.EmbedBuilder; @@ -23,7 +25,9 @@ public class ModalManager extends ListenerAdapter { public ModalManager(ButtonManager buttonManager) { modals = List.of( new ModalSuggestion(buttonManager), - new ModalEvidence()); + new ModalEvidence(), + new ModalReplySuggestion(), + new ModalRemindMe(buttonManager)); } @Override diff --git a/src/main/java/com/alttd/modalManager/modals/ModalEvidence.java b/src/main/java/com/alttd/modalManager/modals/ModalEvidence.java index 0ad1f2c..fb7a06a 100644 --- a/src/main/java/com/alttd/modalManager/modals/ModalEvidence.java +++ b/src/main/java/com/alttd/modalManager/modals/ModalEvidence.java @@ -8,6 +8,7 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.interactions.components.ActionRow; @@ -48,9 +49,15 @@ public class ModalEvidence extends DiscordModal { return; } - GuildMessageChannel channel = guild.getChannelById(GuildMessageChannel.class, CommandOutputChannels.getOutputChannel(guild.getIdLong(), OutputType.EVIDENCE)); - if (channel == null) { - event.replyEmbeds(Util.genericErrorEmbed("Error", "This guild does not have a suggestion review channel or it's not the right channel type")) + GuildChannel outputChannel = CommandOutputChannels.getOutputChannel(guild, OutputType.EVIDENCE); + if (outputChannel == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This guild does not have an evidence channel or it's not the right channel type")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + if (!(outputChannel instanceof GuildMessageChannel channel)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Invalid Evidence channel type: " + outputChannel.getType())) .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); return; } diff --git a/src/main/java/com/alttd/modalManager/modals/ModalRemindMe.java b/src/main/java/com/alttd/modalManager/modals/ModalRemindMe.java new file mode 100644 index 0000000..01d8186 --- /dev/null +++ b/src/main/java/com/alttd/modalManager/modals/ModalRemindMe.java @@ -0,0 +1,137 @@ +package com.alttd.modalManager.modals; + +import com.alttd.buttonManager.ButtonManager; +import com.alttd.buttonManager.buttons.remindMeConfirm.ButtonRemindMeConfirm; +import com.alttd.database.queries.QueriesReminders.Reminder; +import com.alttd.modalManager.DiscordModal; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Modal; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.TimeUtil; + +import java.util.Date; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +public class ModalRemindMe extends DiscordModal { + + private static final HashMap userToRemindMeMap = new HashMap<>(); + + public static synchronized void putData(long userId, TextChannel channel, long timestamp) { + userToRemindMeMap.put(userId, new RemindMeData(channel, timestamp)); + } + + private static synchronized RemindMeData pullData(long userId) { + return userToRemindMeMap.remove(userId); + } + + private final ButtonManager buttonManager; + + public ModalRemindMe(ButtonManager buttonManager) { + this.buttonManager = buttonManager; + } + + @Override + public String getModalId() { + return "remindme"; + } + + @Override + public void execute(ModalInteractionEvent event) { + String title = getValidString(event.getValue("title"), event, true); + if (title == null) + return; + + String desc = getValidString(event.getValue("description"), event, false); + if (desc == null) + desc = ""; + + long userId = event.getUser().getIdLong(); + RemindMeData remindMeData = pullData(userId); + if (remindMeData == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Couldn't find the data from the command that triggered this modal")) + .setEphemeral(true).queue(); + return; + } + + Reminder reminder = new Reminder( + -1, + title, + desc, + userId, + remindMeData.textChannel.getGuild().getIdLong(), + remindMeData.textChannel.getIdLong(), + 0, + false, + new Date().getTime(), + remindMeData.timestamp); + + Button remindMeConfirm = buttonManager.getButtonFor("remind_me_confirm"); + Button remindMeCancel = buttonManager.getButtonFor("remind_me_cancel"); + if (remindMeConfirm == null || remindMeCancel == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to retrieve continue/cancel buttons")) + .setEphemeral(true).queue(); + return; + } + + MessageEmbed messageEmbed = new EmbedBuilder() + .setTitle(reminder.title()) + .setDescription(reminder.description()) + .appendDescription("\n\nWill remind ") + .build(); + event.deferReply().setEphemeral(true).queue(defer -> { + ButtonRemindMeConfirm.putReminder(userId, defer, reminder); + defer.editOriginalEmbeds(messageEmbed).queue(message -> + defer.editOriginalComponents().setActionRow(remindMeConfirm, remindMeCancel) + .queue(RestAction.getDefaultSuccess(), Util::handleFailure)); + }); + } + + public String getValidString(ModalMapping modalMapping, ModalInteractionEvent event, boolean required) { + if (modalMapping == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Couldn't find modal")) + .setEphemeral(true).queue(); + return null; + } + + String string = modalMapping.getAsString(); + if (string.isEmpty()) { + if (required) + event.replyEmbeds(Util.genericErrorEmbed("Error", "Couldn't find contents of modal")) + .setEphemeral(true).queue(); + return null; + } + + return string; + } + + @Override + public Modal getModal() { + TextInput title = TextInput.create("title", "Title", TextInputStyle.SHORT) + .setPlaceholder("reminder title") + .setRequiredRange(1, 256) + .setRequired(true) + .build(); + + TextInput desc = TextInput.create("description", "Description", TextInputStyle.PARAGRAPH) + .setPlaceholder("optional reminder description") + .setRequiredRange(1, 4000) + .setRequired(false) + .build(); + + return Modal.create(getModalId(), "Remind Me") + .addActionRows(ActionRow.of(title), ActionRow.of(desc)) + .build(); + } + + private record RemindMeData(TextChannel textChannel, long timestamp) {} +} diff --git a/src/main/java/com/alttd/modalManager/modals/ModalReplySuggestion.java b/src/main/java/com/alttd/modalManager/modals/ModalReplySuggestion.java new file mode 100644 index 0000000..421a5aa --- /dev/null +++ b/src/main/java/com/alttd/modalManager/modals/ModalReplySuggestion.java @@ -0,0 +1,90 @@ +package com.alttd.modalManager.modals; + +import com.alttd.modalManager.DiscordModal; +import com.alttd.util.Util; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Modal; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; +import net.dv8tion.jda.api.requests.RestAction; + +import java.util.HashMap; + +public class ModalReplySuggestion extends DiscordModal { + + private static final HashMap userToMessageMap = new HashMap<>(); + + public static synchronized void putMessage(long userId, Message message) { + userToMessageMap.put(userId, message); + } + + private static synchronized Message pullMessage(long userId) { + return userToMessageMap.remove(userId); + } + + @Override + public String getModalId() { + return "reply_suggestion"; + } + + @Override + public void execute(ModalInteractionEvent event) { + ModalMapping modalMapping = event.getInteraction().getValue("response"); + if (modalMapping == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find response in modal")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + String response = modalMapping.getAsString(); + if (response.isEmpty()) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Response in modal is empty")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + Member member = event.getMember(); + if (member == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This modal only works from within a guild")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + Message message = pullMessage(member.getIdLong()); + if (message == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find a message for this modal")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + String[] split = message.getContentRaw().split("\u200B"); + if (split.length == 0) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "The suggestion to be edited has no content")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + message.editMessage(split[0] + "\u200B\n\n" + "**Response by: " + member.getAsMention() + "**\n_" + response.replaceAll("\u200B", "") + "_") + .queue(success -> event.replyEmbeds(Util.genericSuccessEmbed("Success", "Responded to the suggestion!")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure), + failure -> event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to edit the suggestion")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure)); + } + + @Override + public Modal getModal() { + TextInput body = TextInput.create("response", "Response", TextInputStyle.PARAGRAPH) + .setPlaceholder("Response...") + .setRequiredRange(10, 1024) + .setRequired(true) + .build(); + + return Modal.create(getModalId(), "Suggestion Response") + .addActionRows(ActionRow.of(body)) + .build(); + } +} diff --git a/src/main/java/com/alttd/modalManager/modals/ModalSuggestion.java b/src/main/java/com/alttd/modalManager/modals/ModalSuggestion.java index ce113d0..9928fe2 100644 --- a/src/main/java/com/alttd/modalManager/modals/ModalSuggestion.java +++ b/src/main/java/com/alttd/modalManager/modals/ModalSuggestion.java @@ -10,6 +10,7 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.interactions.components.ActionRow; @@ -44,8 +45,8 @@ public class ModalSuggestion extends DiscordModal { .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); return; } - String title = modalMappings.get(0).getAsString(); - String desc = modalMappings.get(1).getAsString(); + String title = modalMappings.get(0).getAsString().replaceAll("\u200B", ""); + String desc = modalMappings.get(1).getAsString().replaceAll("\u200B", ""); Guild guild = event.getGuild(); if (guild == null) { @@ -54,13 +55,19 @@ public class ModalSuggestion extends DiscordModal { return; } - GuildMessageChannel channel = guild.getChannelById(GuildMessageChannel.class, CommandOutputChannels.getOutputChannel(guild.getIdLong(), OutputType.SUGGESTION_REVIEW)); - if (channel == null) { + GuildChannel outputChannel = CommandOutputChannels.getOutputChannel(guild, OutputType.SUGGESTION_REVIEW); + if (outputChannel == null) { event.replyEmbeds(Util.genericErrorEmbed("Error", "This guild does not have a suggestion review channel or it's not the right channel type")) .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); return; } + if (!(outputChannel instanceof GuildMessageChannel channel)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", outputChannel.getType().name() + " is not a valid suggestion review channel type")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + Member member = event.getMember(); if (member == null) { event.replyEmbeds(Util.genericErrorEmbed("Error", "This command should only be executed from a guild")) @@ -101,7 +108,7 @@ public class ModalSuggestion extends DiscordModal { @Override public Modal getModal() { TextInput title = TextInput.create("title", "Title", TextInputStyle.SHORT) - .setPlaceholder("You suggestion in one sentence") + .setPlaceholder("Your suggestion in one sentence") .setRequiredRange(10, 100) .setRequired(true) .build(); diff --git a/src/main/java/com/alttd/reminders/ReminderScheduler.java b/src/main/java/com/alttd/reminders/ReminderScheduler.java new file mode 100644 index 0000000..89113b2 --- /dev/null +++ b/src/main/java/com/alttd/reminders/ReminderScheduler.java @@ -0,0 +1,113 @@ +package com.alttd.reminders; + +import com.alttd.database.queries.QueriesReminders.QueriesReminders; +import com.alttd.database.queries.QueriesReminders.Reminder; +import com.alttd.util.Logger; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.TimeUtil; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ReminderScheduler { + + private static ReminderScheduler instance = null; + private final ArrayList reminders; + private Reminder nextReminder; + private final JDA jda; + + private ReminderScheduler(JDA jda) { + instance = this; + this.jda = jda; + reminders = QueriesReminders.getReminders(); + if (reminders == null) { + Logger.severe("Unable to retrieve reminders"); + instance = null; + return; + } + reminders.sort(Comparator.comparingLong(Reminder::remindDate)); + if (reminders.size() == 0) + nextReminder = null; + else + nextReminder = reminders.get(0); + ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + scheduledExecutorService.scheduleWithFixedDelay(new ReminderRun(), 0, 1, TimeUnit.MINUTES); + + } + + public static ReminderScheduler getInstance(JDA jda) { + if (instance == null) + instance = new ReminderScheduler(jda); + return instance; + } + + public synchronized void addReminder(Reminder reminder) { + reminders.add(reminder); + reminders.sort(Comparator.comparingLong(Reminder::remindDate)); + nextReminder = reminders.get(0); + } + + public synchronized void removeReminder(Reminder reminder) { + reminders.remove(reminder); + if (reminders.size() == 0) + nextReminder = null; + else + nextReminder = reminders.get(0); + QueriesReminders.removeReminder(reminder.id()); + } + + private class ReminderRun implements Runnable { + + @Override + public void run() { + long time = new Date().getTime(); + while (nextReminder != null && time > nextReminder.remindDate()) { + //TODO run reminder + TextChannel channel = nextReminder.getChannel(jda); + if (channel == null || !channel.canTalk()) { + Logger.warning("Unable to run reminder: " + nextReminder.id() + + "\ntitle: [" + nextReminder.title() + + "]\ndescription: [" + nextReminder.description() + "]"); + return; + } + sendEmbed(nextReminder, channel); + removeReminder(nextReminder); + } + } + + private void sendEmbed(Reminder reminder, TextChannel channel) { + EmbedBuilder embedBuilder = new EmbedBuilder() + .setTitle(reminder.title()) + .setDescription(reminder.description()) + .appendDescription("\n\nRequested "); + Guild guild = reminder.getGuild(jda); + if (guild == null) { + sendEmbed(reminder, channel, embedBuilder); + return; + } + guild.retrieveMemberById(reminder.userId()).queue( + member -> sendEmbed(reminder, channel, embedBuilder, member), + failed -> sendEmbed(reminder, channel, embedBuilder)); + } + + private void sendEmbed(Reminder reminder, TextChannel channel, EmbedBuilder embedBuilder, Member member) { + embedBuilder.setAuthor(member.getEffectiveName(), null, member.getEffectiveAvatarUrl()); + channel.sendMessageEmbeds(embedBuilder.build()).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + + private void sendEmbed(Reminder reminder, TextChannel channel, EmbedBuilder embedBuilder) { + embedBuilder.setAuthor(reminder.userId() + ""); + channel.sendMessageEmbeds(embedBuilder.build()).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + } +} diff --git a/src/main/java/com/alttd/request/Request.java b/src/main/java/com/alttd/request/Request.java new file mode 100644 index 0000000..f22c2df --- /dev/null +++ b/src/main/java/com/alttd/request/Request.java @@ -0,0 +1,88 @@ +package com.alttd.request; + +import com.alttd.AltitudeBot; +import com.alttd.util.Pair; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Modal; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.requests.restaction.ThreadChannelAction; + +import java.awt.*; + +@AllArgsConstructor +public class Request { + // TODO check if all labels on the modal are max 45 in length + @Getter + private String id, category, channel, name, title, description, message; + + public Modal modal(Member member) { + TextInput requestTitle = TextInput + .create("title", title, TextInputStyle.SHORT) + .setPlaceholder(id) + .setRequired(false) + .build(); + + TextInput requestMessage = TextInput + .create("request", message, TextInputStyle.PARAGRAPH) + .build(); + + return Modal.create("request:" + id, name) + .addActionRow(requestTitle) + .addActionRow(requestMessage) + .build(); + } + + public void createThread(Member member, String title, String request) { + TextChannel channel = AltitudeBot.getInstance().getJDA().getGuildById(RequestConfig.REQUEST_GUILD_ID).getTextChannelById(getChannel()); + if (title == null || title.isEmpty()) title = id; + String finalTitle = title; + ThreadChannelAction threadChannelAction = channel.createThreadChannel(finalTitle); + threadChannelAction.queue(threadChannel -> { + threadChannel.addThreadMember(member).queue(); + sendEmbed(threadChannel, finalTitle, request); + channel.deleteMessageById(threadChannel.getId()).queue(); + // TODO store the request somewhere so it can be grabbed later + }); + } + + public void sendEmbed(ThreadChannel channel, String title, String request) { +// Pair pair = getRequestEmbed(channel.getId(), title, request); + // pairs are not really possible here :( + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setTitle(title) + .addField(getName(), request, false) + .setColor(new Color(41, 43, 47)); + channel.sendMessageEmbeds(embedBuilder.build()).queue(message1 -> + channel.editMessageEmbedsById(message1.getId(), embedBuilder.build()) + .setActionRow( + Button.primary("request:" + getId() + ":" + channel.getId() + ":" + message1.getId() + ":progress", "in progress"), + Button.success("request:" + getId() + ":" + channel.getId() + ":" + message1.getId() + ":complete", "complete"), + Button.danger("request:" + getId() + ":" + channel.getId() + ":" + message1.getId() + ":denied", "denied") + ).queue() + ); + } + + public Pair getRequestEmbed(String channellId, String title, String request) { + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setTitle("title") + .addField(getName(), request, false) + .setColor(new Color(41, 43, 47)); + + ActionRow actionRow = ActionRow.of( + Button.primary("request:" + getId() + ":" + channellId + ":progress", "in progress"), + Button.success("request:" + getId() + ":" + channellId + ":complete", "complete"), + Button.danger("request:" + getId() + ":" + channellId + ":denied", "denied") + ); + + return new Pair<>(embedBuilder, actionRow); + } + +} diff --git a/src/main/java/com/alttd/request/RequestConfig.java b/src/main/java/com/alttd/request/RequestConfig.java new file mode 100644 index 0000000..ce450aa --- /dev/null +++ b/src/main/java/com/alttd/request/RequestConfig.java @@ -0,0 +1,58 @@ +package com.alttd.request; + +import com.alttd.config.AbstractConfig; +import com.alttd.util.Logger; + +import java.util.ArrayList; +import java.util.List; + +public class RequestConfig extends AbstractConfig { + + static RequestConfig requestConfig; + + public RequestConfig() { + super("requests.yml"); + } + + public static void reload() { + requestConfig = new RequestConfig(); + requestConfig.readConfig(RequestConfig.class, requestConfig); + } + + public static String REQUEST_GUILD_ID = "776590138296893480"; + public static String REQUEST_CATEGORY = "776590138296893481"; + public static String REQUEST_CHANNEL = "1017787342561476709"; + public static String REQUEST_MESSAGE = ""; + private void settings() { + REQUEST_GUILD_ID = requestConfig.getString("request.guild", REQUEST_GUILD_ID); + REQUEST_CATEGORY = requestConfig.getString("request.category", REQUEST_CATEGORY); + REQUEST_CHANNEL = requestConfig.getString("request.channel", REQUEST_CHANNEL); + REQUEST_MESSAGE = requestConfig.getString("request.message", REQUEST_MESSAGE); + } + + public static void setRequestMessage(String messageId) { + REQUEST_MESSAGE = messageId; + requestConfig.update("request.message", REQUEST_MESSAGE); + requestConfig.save(); + } + + public static final List requests = new ArrayList<>(); + private void loadRequests() { + requests.clear(); + requestConfig.getNode("types").childrenMap().forEach((key, value) -> { + String id = key.toString(); + String category = value.node("category").getString(); + String channel = value.node("channel").getString(); + String name = value.node("name").getString(); + String title = value.node("title").getString(); + String description = value.node("description").getString(); + String message = value.node("message").getString(); + if (id == null || category == null || channel == null || name == null || description == null || message == null) { + Logger.warning("Requests are set up incorrectly!"); + } else { + requests.add(new Request(id, category, channel, name, title, description, message)); + } + }); + } + +} diff --git a/src/main/java/com/alttd/request/RequestManager.java b/src/main/java/com/alttd/request/RequestManager.java new file mode 100644 index 0000000..d2c0651 --- /dev/null +++ b/src/main/java/com/alttd/request/RequestManager.java @@ -0,0 +1,110 @@ +package com.alttd.request; + +import com.alttd.AltitudeBot; +import com.alttd.util.Pair; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.interactions.components.selections.SelectMenu; + +import java.awt.*; + +public class RequestManager { + + public static void init() { + RequestConfig.reload(); + if (RequestConfig.REQUEST_MESSAGE == null || RequestConfig.REQUEST_MESSAGE.isEmpty()) + sendRequestMessage(); + } + + public static Pair getRequestEmbed() { + EmbedBuilder embedBuilder = new EmbedBuilder(); + SelectMenu.Builder selectMenuBuilder = SelectMenu.create("request:create"); + embedBuilder.setDescription("Select an option below to open a request!\n") + .setTitle("Create a new request.") + .setColor(new Color(41, 43, 47)); + + for (Request request : RequestConfig.requests) { + embedBuilder.addField(request.getName(), request.getDescription(), false); + selectMenuBuilder.addOption(request.getName(), "request:open:" + request.getId(), request.getDescription(), null); + } + + return new Pair<>(embedBuilder, selectMenuBuilder); + } + + public static void sendRequestMessage() { + TextChannel channel = AltitudeBot.getInstance().getJDA().getGuildById(RequestConfig.REQUEST_GUILD_ID).getTextChannelById(RequestConfig.REQUEST_CHANNEL); + Pair pair = getRequestEmbed(); + channel.sendMessageEmbeds(pair.getValue0().build()).setActionRow( + pair.getValue1().build() + ).queue(m -> RequestConfig.setRequestMessage(m.getId())); + } + + public static void updateRequestMessage() { + TextChannel channel = AltitudeBot.getInstance().getJDA().getGuildById(RequestConfig.REQUEST_GUILD_ID).getTextChannelById(RequestConfig.REQUEST_CHANNEL); + Pair pair = getRequestEmbed(); + channel.editMessageEmbedsById(RequestConfig.REQUEST_MESSAGE, pair.getValue0().build()) + .setActionRow( + pair.getValue1().build() + ).queue(m -> RequestConfig.setRequestMessage(m.getId())); + } + + public static Request getRequestById(String id) { + return RequestConfig.requests.stream().filter(request -> request.getId().equalsIgnoreCase(id)).findFirst().orElse(null); + } + + public static void onSelectMenuInteraction(SelectMenuInteractionEvent event) { + String[] actions = event.getComponentId().split(":"); + if (actions[1].equals("create")) { + String[] selection = event.getSelectedOptions().get(0).getValue().split(":"); + if (selection[0].equals("request") && selection[1].equals("open")) { + String id = selection[2]; + event.replyModal(getRequestById(id).modal(event.getMember())).queue(); + updateRequestMessage(); // You can't use a select menu option twice in a row, updating it fixes that. + } + } + } + + public static void onModalInteractionEvent(ModalInteractionEvent event) { + String s = event.getModalId(); + String[] strings = s.split(":", 2); + getRequestById(strings[1]).createThread(event.getMember(), event.getValue("title").getAsString(), event.getValue("request").getAsString()); + event.reply("Thanks for your request!").setEphemeral(true).queue(); + } + + public static void onButtonInteractionEvent(ButtonInteractionEvent event) { + String s = event.getComponentId(); + String[] strings = s.split(":", 5); + String requestId = strings[1]; + String threadId = strings[2]; + String messageId = strings[3]; + String type = strings[4]; // progress, complete, denied + + // TODO update the stored request in the database + switch (type) { + case "denied" -> { + // TODO open a new modal to input a reason? + // could also do this by command? + event.reply("This request has been denied by " + event.getMember().getAsMention()).queue(); + ThreadChannel threadChannel = AltitudeBot.getInstance().getJDA().getGuildById(RequestConfig.REQUEST_GUILD_ID).getThreadChannelById(threadId); + threadChannel.getManager().setArchived(true).setLocked(true).queue(); + } + case "complete" -> { + // TODO open a new modal to input a reason? + // could also do this by command? + event.reply("This request has been completed by " + event.getMember().getAsMention()).queue(); + ThreadChannel threadChannel = AltitudeBot.getInstance().getJDA().getGuildById(RequestConfig.REQUEST_GUILD_ID).getThreadChannelById(threadId); + threadChannel.getManager().setArchived(true).setLocked(true).queue(); + } + case "progress" -> { + // TODO open a new modal to input a reason? + // edit the message to show who is working on it? + } + } + + } + +} diff --git a/src/main/java/com/alttd/util/Pair.java b/src/main/java/com/alttd/util/Pair.java new file mode 100644 index 0000000..987cfc7 --- /dev/null +++ b/src/main/java/com/alttd/util/Pair.java @@ -0,0 +1,21 @@ +package com.alttd.util; + +public class Pair { + + private final X x; + private final Y y; + + public Pair(X x, Y y) { + this.x = x; + this.y = y; + } + + public X getValue0() { + return x; + } + + public Y getValue1() { + return y; + } +} +