diff --git a/pom.xml b/pom.xml index 591badd..fc91d51 100644 --- a/pom.xml +++ b/pom.xml @@ -159,6 +159,11 @@ spotify-web-api-java 8.4.0 + + com.github.ben-manes.caffeine + caffeine + 3.1.8 + diff --git a/src/main/java/cc/fascinated/bat/features/drag/DragFeature.java b/src/main/java/cc/fascinated/bat/features/drag/DragFeature.java new file mode 100644 index 0000000..0ca83a1 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/drag/DragFeature.java @@ -0,0 +1,23 @@ +package cc.fascinated.bat.features.drag; + +import cc.fascinated.bat.command.Category; +import cc.fascinated.bat.features.Feature; +import cc.fascinated.bat.features.drag.command.DragCommand; +import cc.fascinated.bat.service.CommandService; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @author Fascinated (fascinated7) + */ +@Component +public class DragFeature extends Feature { + @Autowired + public DragFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { + super("Drag", true,Category.GENERAL); + + super.registerCommand(commandService, context.getBean(DragCommand.class)); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/drag/DragRequest.java b/src/main/java/cc/fascinated/bat/features/drag/DragRequest.java new file mode 100644 index 0000000..4372f41 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/drag/DragRequest.java @@ -0,0 +1,50 @@ +package cc.fascinated.bat.features.drag; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.interactions.InteractionHook; + +import java.util.Date; + +/** + * @author Fascinated (fascinated7) + */ +@RequiredArgsConstructor +@Getter @Setter +public class DragRequest { + /** + * The date the request was made + */ + private final Date requestDate = new Date(); + + /** + * The user that wants to join the voice channel + */ + private final Member member; + + /** + * The user that the member wants to join + */ + private final Member target; + + /** + * The voice channel the user wants to join + */ + private final VoiceChannel voiceChannel; + + /** + * The interaction hook that the request was made from + */ + private final InteractionHook interactionHook; + + /** + * The request message sent in the voice channel + */ + private Message requestMessage; + +} diff --git a/src/main/java/cc/fascinated/bat/features/drag/command/DragCommand.java b/src/main/java/cc/fascinated/bat/features/drag/command/DragCommand.java new file mode 100644 index 0000000..f4a7fe9 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/drag/command/DragCommand.java @@ -0,0 +1,21 @@ +package cc.fascinated.bat.features.drag.command; + +import cc.fascinated.bat.command.BatCommand; +import cc.fascinated.bat.command.CommandInfo; +import io.sentry.protocol.App; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @author Fascinated (fascinated7) + */ +@Component +@CommandInfo(name = "drag", description = "Drag command") +public class DragCommand extends BatCommand { + @Autowired + public DragCommand(@NonNull ApplicationContext context) { + super.addSubCommand(context.getBean(RequestSubCommand.class)); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/drag/command/RequestSubCommand.java b/src/main/java/cc/fascinated/bat/features/drag/command/RequestSubCommand.java new file mode 100644 index 0000000..c19ecba --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/drag/command/RequestSubCommand.java @@ -0,0 +1,152 @@ +package cc.fascinated.bat.features.drag.command; + +import cc.fascinated.bat.command.BatSubCommand; +import cc.fascinated.bat.command.CommandInfo; +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.common.TimerUtils; +import cc.fascinated.bat.event.EventListener; +import cc.fascinated.bat.features.drag.DragRequest; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.GuildVoiceState; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; + +/** + * Handles requests to be moved to a voice channel. + * Author: Fascinated (fascinated7) + */ +@Component +@CommandInfo(name = "request", description = "Request to be moved to a voice channel") +public class RequestSubCommand extends BatSubCommand implements EventListener { + /** + * A list of join requests + */ + public static final Set JOIN_REQUESTS = new HashSet<>(); + + private final long requestTimeout = Duration.ofMinutes(30).toMillis(); + private final long checkInterval = Duration.ofSeconds(10).toMillis(); + + public RequestSubCommand() { + super.addOption(OptionType.USER, "user", "The user you want to join", true); + + TimerUtils.scheduleRepeating(() -> { + Set toRemove = new HashSet<>(); + for (DragRequest joinRequest : JOIN_REQUESTS) { + if (System.currentTimeMillis() - joinRequest.getRequestDate().getTime() < requestTimeout) { + return; + } + // The request has timed out + joinRequest.getInteractionHook().editOriginalEmbeds(EmbedUtils.errorEmbed() + .setDescription("The request to join %s's voice channel has timed out.".formatted(joinRequest.getTarget().getAsMention())) + .build()).queue(); + joinRequest.getVoiceChannel().sendMessageEmbeds(EmbedUtils.errorEmbed() + .setDescription("%s's request to join your voice channel has timed out.".formatted(joinRequest.getMember().getAsMention())) + .build()).queue(); + joinRequest.getRequestMessage().delete().queue(); + toRemove.add(joinRequest); + } + JOIN_REQUESTS.removeAll(toRemove); + }, checkInterval, checkInterval); + } + + @Override + public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) { + GuildVoiceState voiceState = member.getVoiceState(); + // Check if the user is in a voice channel + if (voiceState == null || voiceState.getChannel() == null) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("You are not in a voice channel.") + .build()) + .setEphemeral(true) + .queue(); + return; + } + + OptionMapping userOption = event.getOption("user"); + if (userOption == null) return; + + // Check if the user is in a voice channel + Member target = userOption.getAsMember(); + if (target == null || target.getId().equals(member.getId())) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("You cannot request to join your own voice channel.") + .build()) + .setEphemeral(true) + .queue(); + return; + } + + // Check if the target user is in a voice channel + GuildVoiceState targetVoiceState = target.getVoiceState(); + if (targetVoiceState == null || targetVoiceState.getChannel() == null) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("The user %s is not in a voice channel.".formatted(target.getAsMention())) + .build()) + .setEphemeral(true) + .queue(); + return; + } + + VoiceChannel targetChannel = targetVoiceState.getChannel().asVoiceChannel(); + + // User is already in the target channel + if (voiceState.getChannel().getId().equals(targetChannel.getId())) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("You are already in the voice channel %s.".formatted(voiceState.getChannel().getAsMention())) + .build()) + .setEphemeral(true) + .queue(); + return; + } + + // Check if the user has already requested to join the target channel + DragRequest existingRequest = JOIN_REQUESTS.stream() + .filter(request -> request.getMember().getId().equals(member.getId()) && request.getVoiceChannel().getId().equals(targetChannel.getId())) + .findFirst() + .orElse(null); + if (existingRequest != null) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("You have already requested to join %s's voice channel.".formatted(target.getAsMention())) + .build()) + .setEphemeral(true) + .queue(); + return; + } + + // Add the request to the list + JOIN_REQUESTS.add(new DragRequest(member, target, targetChannel, event.getHook())); + + // Send the request to the target user + targetChannel.sendMessage(target.getAsMention()).queue(); + targetChannel.sendMessageEmbeds(EmbedUtils.successEmbed() + .setDescription("User %s has requested to join your voice channel.".formatted(member.getAsMention())) + .build()) + .addComponents(ActionRow.of( + Button.primary("drag-request-accept", "Accept"), + Button.danger("drag-request-decline", "Decline") + )) + .queue(message -> { + JOIN_REQUESTS.stream() + .filter(r -> r.getVoiceChannel().getId().equals(targetChannel.getId())) + .findFirst().ifPresent(request -> request.setRequestMessage(message)); + }); + event.replyEmbeds(EmbedUtils.successEmbed() + .setDescription("Request to join %s's voice channel has been sent.".formatted(target.getAsMention())) + .build()) + .setComponents(ActionRow.of(Button.secondary("drag-request-cancel", "Cancel"))) + .queue(); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/features/drag/listeners/request/RequestListener.java b/src/main/java/cc/fascinated/bat/features/drag/listeners/request/RequestListener.java new file mode 100644 index 0000000..406df14 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/drag/listeners/request/RequestListener.java @@ -0,0 +1,43 @@ +package cc.fascinated.bat.features.drag.listeners.request; + +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.event.EventListener; +import cc.fascinated.bat.features.drag.DragRequest; +import cc.fascinated.bat.features.drag.command.RequestSubCommand; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import lombok.NonNull; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.InteractionHook; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * @author Fascinated (fascinated7) + */ +@Component +public class RequestListener implements EventListener { + @Override + public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) { + if (!event.getComponentId().equals("drag-request-cancel")) { + return; + } + Optional optionalDragRequest = RequestSubCommand.JOIN_REQUESTS.stream() + .filter(request -> request.getMember().getId().equals(event.getUser().getId())) + .findFirst(); + if (optionalDragRequest.isEmpty()) { + return; + } + DragRequest dragRequest = optionalDragRequest.get(); + InteractionHook interactionHook = dragRequest.getInteractionHook(); + interactionHook.editOriginalEmbeds(EmbedUtils.errorEmbed() + .setDescription("You have cancelled your request to join %s's voice channel.".formatted(dragRequest.getTarget().getAsMention())) + .build()).queue(message -> message.editMessageComponents().queue()); + dragRequest.getVoiceChannel().sendMessageEmbeds(EmbedUtils.errorEmbed() + .setDescription("%s has cancelled their request to join your voice channel.".formatted(dragRequest.getMember().getAsMention())) + .build()).queue(); + dragRequest.getRequestMessage().delete().queue(); + RequestSubCommand.JOIN_REQUESTS.remove(dragRequest); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/drag/listeners/request/TargetChannelListener.java b/src/main/java/cc/fascinated/bat/features/drag/listeners/request/TargetChannelListener.java new file mode 100644 index 0000000..27d38be --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/drag/listeners/request/TargetChannelListener.java @@ -0,0 +1,54 @@ +package cc.fascinated.bat.features.drag.listeners.request; + +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.event.EventListener; +import cc.fascinated.bat.features.drag.DragRequest; +import cc.fascinated.bat.features.drag.command.RequestSubCommand; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import org.springframework.stereotype.Component; + +/** + * @author Fascinated (fascinated7) + */ +@Component +public class TargetChannelListener implements EventListener { + @Override + public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) { + User buttonUser = event.getUser(); + Member member = guild.getDiscordGuild().getMember(buttonUser); + if (member == null) return; + + DragRequest joinRequest = RequestSubCommand.JOIN_REQUESTS.stream() + .filter(request -> request.getVoiceChannel().getId().equals(event.getChannel().getId())) + .findFirst() + .orElse(null); + if (joinRequest == null) return; + + if (event.getComponentId().equals("drag-request-accept")) { + joinRequest.getVoiceChannel().getGuild().moveVoiceMember(joinRequest.getMember(), joinRequest.getVoiceChannel()).queue(); + event.replyEmbeds(EmbedUtils.successEmbed() + .setDescription("You have accepted %s's request to join your voice channel!".formatted(joinRequest.getMember().getAsMention())) + .build()) + .queue(); + } else if (event.getComponentId().equals("drag-request-decline")) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("You have declined %s's request to join your voice channel!".formatted(joinRequest.getMember().getAsMention())) + .build()) + .queue(); + joinRequest.getInteractionHook().retrieveOriginal().queue(message -> { + message.editMessageEmbeds(EmbedUtils.errorEmbed() + .setDescription("%s has declined your request to join their voice channel.".formatted(joinRequest.getTarget().getAsMention())) + .build()).queue(); + message.editMessageComponents().queue(); + }); + } + RequestSubCommand.JOIN_REQUESTS.remove(joinRequest); + // Remove the buttons from the embed + event.getInteraction().getMessage().editMessageComponents().queue(); + } +}