package wtf.beatrice.hidekobot.services; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.commands.build.CommandData; import net.dv8tion.jda.api.interactions.components.LayoutComponent; import net.dv8tion.jda.api.requests.RestAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import wtf.beatrice.hidekobot.Cache; import wtf.beatrice.hidekobot.HidekoBot; import wtf.beatrice.hidekobot.objects.commands.SlashCommand; import java.util.ArrayList; import java.util.List; @Component public class CommandService { private static final Logger LOGGER = LoggerFactory.getLogger(CommandService.class); private final DatabaseService databaseService; public CommandService(@Autowired DatabaseService databaseService) { this.databaseService = databaseService; } /** * Function to delete a message when a user clicks the "delete" button attached to that message. * This will check in the database if that user ran the command originally. * * @param event the button interaction event. */ public void deleteUserLinkedMessage(ButtonInteractionEvent event) { // check if the user interacting is the same one who ran the command if (!databaseService.isUserTrackedFor(event.getUser().getId(), event.getMessageId())) { event.reply("❌ You did not run this command!").setEphemeral(true).queue(); return; } // Acknowledge immediately so the interaction token stays valid event.deferEdit().queue(hook -> { // Try deleting via the interaction webhook (works for original interaction responses) hook.deleteOriginal().queue( success -> { /* optional: databaseService.untrackExpiredMessage(event.getMessageId()); */ }, failure -> { // Fallback to channel delete (works even if webhook token expired) event.getChannel().deleteMessageById(event.getMessageId()).queue( null, __ -> { /* ignore if already deleted */ } ); } ); }, failure -> { // If we failed to acknowledge (interaction already expired), try channel delete anyway event.getChannel().deleteMessageById(event.getMessageId()).queue( null, __ -> { /* ignore if already deleted */ } ); } ); } /** * Method to update slash commands registered on Discord's side. * It runs automatically every time the bot starts, but only updates the commands in case differences * are found, unless forced. * * @param force a boolean specifying if the update should be forced even if no differences were found. */ public void updateSlashCommands(boolean force) { // populate commands list from registered commands List allCommands = new ArrayList<>(); for (SlashCommand cmd : Cache.getSlashCommandListener().getRegisteredCommands()) { allCommands.add(cmd.getSlashCommandData()); } JDA jdaInstance = HidekoBot.getAPI(); // get all the already registered commands List registeredCommands = jdaInstance.retrieveCommands().complete(); boolean update = false; if (force) { update = true; } else { // for each command that we have already registered... for (Command currRegCmd : registeredCommands) { boolean found = false; // iterate through all "recognized" commands for (CommandData cmdData : allCommands) { // if we find the same command... if (cmdData.getName().equals(currRegCmd.getName())) { // quit the loop since we found it. found = true; break; } } // if no match was found, we need to send an updated command list because // an old command was probably removed. if (!found) { update = true; // quit the loop since we only need to trigger this once. break; } } // if an update is not already queued... if (!update) { // for each "recognized" valid command for (CommandData currCmdData : allCommands) { boolean found = false; // iterate through all already registered commands. for (Command cmd : registeredCommands) { // if this command was already registered... if (cmd.getName().equals(currCmdData.getName())) { // quit the loop since we found a match. found = true; break; } } // if no match was found, we need to send an updated command list because // a new command was probably added. if (!found) { update = true; // quit the loop since we only need to trigger this once. break; } } } } LOGGER.info("Found {} commands.", registeredCommands.size()); if (update) { // send updated command list. jdaInstance.updateCommands().addCommands(allCommands).queue(); LOGGER.info("Commands updated. New total: {}.", allCommands.size()); } } /** * Method to disable all buttons from an expired message. * * @param messageId the message id to disable. */ public void disableExpired(String messageId) { String channelId = databaseService.getQueuedExpiringMessageChannel(messageId); // todo: warning, the following method + related if check are thread-locking. // todo: we should probably merge the two tables somehow, since they have redundant information. ChannelType msgChannelType = databaseService.getTrackedMessageChannelType(messageId); MessageChannel textChannel = null; // this should never happen, but only message channels are supported. if (!msgChannelType.isMessage()) { databaseService.untrackExpiredMessage(messageId); return; } // if this is a DM if (!(msgChannelType.isGuild())) { String userId = databaseService.getTrackedReplyUserId(messageId); User user = userId == null ? null : HidekoBot.getAPI().retrieveUserById(userId).complete(); if (user == null) { // if user is not found, consider it expired // (deleted profile, or blocked the bot) databaseService.untrackExpiredMessage(messageId); return; } textChannel = user.openPrivateChannel().complete(); } else { String guildId = databaseService.getQueuedExpiringMessageGuild(messageId); Guild guild = guildId == null ? null : HidekoBot.getAPI().getGuildById(guildId); if (guild == null) { // if guild is not found, consider it expired // (server was deleted or bot was kicked) databaseService.untrackExpiredMessage(messageId); return; } textChannel = guild.getTextChannelById(channelId); } if (textChannel == null) { // if channel is not found, count it as expired // (channel was deleted or bot permissions restricted) databaseService.untrackExpiredMessage(messageId); return; } RestAction retrieveAction = textChannel.retrieveMessageById(messageId); if (Cache.isVerbose()) LOGGER.info("cleaning up: {}", messageId); retrieveAction.queue( message -> { if (message == null) { databaseService.untrackExpiredMessage(messageId); return; } List components = message.getComponents(); List newComponents = new ArrayList<>(); for (LayoutComponent component : components) { component = component.asDisabled(); newComponents.add(component); } message.editMessageComponents(newComponents).queue(); databaseService.untrackExpiredMessage(messageId); }, error -> databaseService.untrackExpiredMessage(messageId)); } }