-
-
Notifications
You must be signed in to change notification settings - Fork 105
feat(DynamicVoiceChat): implement main logic #1370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
christolis
wants to merge
8
commits into
Together-Java:develop
Choose a base branch
from
christolis:feat/custom-vcs
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
41a827f
feat(DynamicVoiceChat): implement main logic
christolis cabd30a
DynamicVoiceChat.java: use trace instead of info
christolis 42d61f6
DynamicVoiceChat.java: more trace instead of info
christolis e93861d
Make class final and add JavaDoc
christolis 2679a2f
Resolve merge conflicts
christolis 11d2f53
Archive instead of simply deleting voice channels
christolis 7903563
Use proper constant values
christolis 3ba0b7a
DynamicVoiceChat.java: Remove defensive comment
christolis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
application/src/main/java/org/togetherjava/tjbot/features/voicechat/DynamicVoiceChat.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| package org.togetherjava.tjbot.features.voicechat; | ||
|
|
||
| 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.MessageHistory; | ||
| import net.dv8tion.jda.api.entities.channel.concrete.Category; | ||
| import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; | ||
| import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; | ||
| import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion; | ||
| import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; | ||
| import net.dv8tion.jda.api.managers.channel.middleman.AudioChannelManager; | ||
| import net.dv8tion.jda.api.requests.RestAction; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import org.togetherjava.tjbot.config.Config; | ||
| import org.togetherjava.tjbot.features.VoiceReceiverAdapter; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| /** | ||
| * Handles dynamic voice channel creation and deletion based on user activity. | ||
| * <p> | ||
| * When a member joins a configured root channel, a temporary copy is created and the member is | ||
| * moved into it. Once the channel becomes empty, it is archived and further deleted using a | ||
| * {@link VoiceChatCleanupStrategy}. | ||
| */ | ||
| public final class DynamicVoiceChat extends VoiceReceiverAdapter { | ||
| private static final Logger logger = LoggerFactory.getLogger(DynamicVoiceChat.class); | ||
|
|
||
| private static final String ARCHIVE_CATEGORY_NAME = "Voice Channel Archives"; | ||
| private static final int CLEAN_CHANNELS_AMOUNT = 25; | ||
| private static final int MINIMUM_CHANNELS_AMOUNT = 50; | ||
|
|
||
| private final VoiceChatCleanupStrategy voiceChatCleanupStrategy; | ||
| private final List<Pattern> dynamicVoiceChannelPatterns; | ||
|
|
||
| public DynamicVoiceChat(Config config) { | ||
| this.dynamicVoiceChannelPatterns = | ||
| config.getDynamicVoiceChannelPatterns().stream().map(Pattern::compile).toList(); | ||
| this.voiceChatCleanupStrategy = | ||
| new OldestVoiceChatCleanup(CLEAN_CHANNELS_AMOUNT, MINIMUM_CHANNELS_AMOUNT); | ||
| } | ||
|
|
||
| @Override | ||
| public void onVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) { | ||
| AudioChannelUnion channelJoined = event.getChannelJoined(); | ||
| AudioChannelUnion channelLeft = event.getChannelLeft(); | ||
|
|
||
| if (channelJoined != null && eventHappenOnDynamicRootChannel(channelJoined)) { | ||
| logger.debug("Event happened on joined channel {}", channelJoined); | ||
| createDynamicVoiceChannel(event, channelJoined.asVoiceChannel()); | ||
| } | ||
|
|
||
| if (channelLeft != null && !eventHappenOnDynamicRootChannel(channelLeft)) { | ||
| logger.debug("Event happened on left channel {}", channelLeft); | ||
|
|
||
| MessageHistory messageHistory = channelLeft.asVoiceChannel().getHistory(); | ||
| messageHistory.retrievePast(2).queue(messages -> { | ||
| // Don't forget that there is always one | ||
| // embed message sent by the bot every time. | ||
| if (messages.size() > 1) { | ||
| archiveDynamicVoiceChannel(channelLeft); | ||
| } else { | ||
| channelLeft.delete().queue(); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private boolean eventHappenOnDynamicRootChannel(AudioChannelUnion channel) { | ||
| return dynamicVoiceChannelPatterns.stream() | ||
| .anyMatch(pattern -> pattern.matcher(channel.getName()).matches()); | ||
| } | ||
|
|
||
| private void createDynamicVoiceChannel(@NotNull GuildVoiceUpdateEvent event, | ||
| VoiceChannel channel) { | ||
| Guild guild = event.getGuild(); | ||
| Member member = event.getMember(); | ||
| String newChannelName = "%s's %s".formatted(member.getEffectiveName(), channel.getName()); | ||
|
|
||
| channel.createCopy() | ||
| .setName(newChannelName) | ||
| .setPosition(channel.getPositionRaw()) | ||
| .onSuccess(newChannel -> { | ||
| moveMember(guild, member, newChannel); | ||
| sendWarningEmbed(newChannel); | ||
| }) | ||
| .queue(newChannel -> logger.trace("Successfully created {} voice channel.", | ||
| newChannel.getName()), | ||
| error -> logger.error("Failed to create dynamic voice channel", error)); | ||
| } | ||
|
|
||
| private void moveMember(Guild guild, Member member, AudioChannel channel) { | ||
| guild.moveVoiceMember(member, channel) | ||
| .queue(_ -> logger.trace( | ||
| "Successfully moved {} to newly created dynamic voice channel {}", | ||
| member.getEffectiveName(), channel.getName()), | ||
| error -> logger.error( | ||
| "Failed to move user into dynamically created voice channel {}, {}", | ||
| member.getNickname(), channel.getName(), error)); | ||
| } | ||
|
|
||
| private void archiveDynamicVoiceChannel(AudioChannelUnion channel) { | ||
| int memberCount = channel.getMembers().size(); | ||
| String channelName = channel.getName(); | ||
|
|
||
| if (memberCount > 0) { | ||
| logger.debug("Voice channel {} not empty ({} members), so not removing.", channelName, | ||
| memberCount); | ||
| return; | ||
| } | ||
|
|
||
| Optional<Category> archiveCategoryOptional = channel.getGuild() | ||
| .getCategoryCache() | ||
| .stream() | ||
| .filter(c -> c.getName().equalsIgnoreCase(ARCHIVE_CATEGORY_NAME)) | ||
| .findFirst(); | ||
|
|
||
| AudioChannelManager<?, ?> channelManager = channel.getManager(); | ||
| RestAction<Void> restActionChain = | ||
| channelManager.setName(String.format("%s (Archived)", channelName)) | ||
| .and(channel.getPermissionContainer().getManager().clearOverridesAdded()); | ||
|
|
||
| if (archiveCategoryOptional.isEmpty()) { | ||
| logger.warn("Could not find archive category. Attempting to create one..."); | ||
| channel.getGuild() | ||
| .createCategory(ARCHIVE_CATEGORY_NAME) | ||
| .queue(newCategory -> restActionChain.and(channelManager.setParent(newCategory)) | ||
| .queue()); | ||
| return; | ||
| } | ||
|
|
||
| archiveCategoryOptional.ifPresent(archiveCategory -> restActionChain | ||
| .and(channelManager.setParent(archiveCategory)) | ||
| .queue(_ -> voiceChatCleanupStrategy.cleanup(archiveCategory.getVoiceChannels()), | ||
| err -> logger.error("Could not archive dynamic voice chat", err))); | ||
| } | ||
|
|
||
| private void sendWarningEmbed(VoiceChannel channel) { | ||
| MessageEmbed messageEmbed = new EmbedBuilder() | ||
| .addField("👋 Heads up!", | ||
| """ | ||
| This is a **temporary** voice chat channel. Messages sent here will be *cleared* once \ | ||
| the channel is deleted when everyone leaves. If you need to keep something important, \ | ||
| make sure to save it elsewhere. 💬 | ||
| """, | ||
| false) | ||
| .build(); | ||
|
|
||
| channel.sendMessageEmbeds(messageEmbed).queue(); | ||
| } | ||
| } | ||
40 changes: 40 additions & 0 deletions
40
...ation/src/main/java/org/togetherjava/tjbot/features/voicechat/OldestVoiceChatCleanup.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package org.togetherjava.tjbot.features.voicechat; | ||
|
|
||
| import net.dv8tion.jda.api.entities.ISnowflake; | ||
| import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; | ||
|
|
||
| import java.util.Comparator; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Cleans up voice chats from an archive prioritizing the oldest {@link VoiceChannel}. | ||
| * <p> | ||
| * Considering a list of voice channels is provided with all of them obviously having a different | ||
| * addition time, the first few {@link VoiceChannel} elements provided, amounting to the value of | ||
| * <code>cleanChannelsAmount</code> will be removed from the guild. | ||
| * <p> | ||
| * The cleanup strategy will <i>not</i> be executed if the amount of voice channels does not exceed | ||
| * the value of <code>minimumChannelsAmountToTrigger</code>. | ||
| */ | ||
| final class OldestVoiceChatCleanup implements VoiceChatCleanupStrategy { | ||
|
|
||
| private final int cleanChannelsAmount; | ||
| private final int minimumChannelsAmountToTrigger; | ||
|
|
||
| OldestVoiceChatCleanup(int cleanChannelsAmount, int minimumChannelsAmountToTrigger) { | ||
| this.cleanChannelsAmount = cleanChannelsAmount; | ||
| this.minimumChannelsAmountToTrigger = minimumChannelsAmountToTrigger; | ||
| } | ||
|
|
||
| @Override | ||
| public void cleanup(List<VoiceChannel> voiceChannels) { | ||
| if (voiceChannels.size() < minimumChannelsAmountToTrigger) { | ||
| return; | ||
| } | ||
|
|
||
| voiceChannels.stream() | ||
| .sorted(Comparator.comparing(ISnowflake::getTimeCreated)) | ||
| .limit(cleanChannelsAmount) | ||
| .forEach(voiceChannel -> voiceChannel.delete().queue()); | ||
| } | ||
| } |
21 changes: 21 additions & 0 deletions
21
...ion/src/main/java/org/togetherjava/tjbot/features/voicechat/VoiceChatCleanupStrategy.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package org.togetherjava.tjbot.features.voicechat; | ||
|
|
||
| import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Voice chat cleanup strategy interface for handling voice chat archive removal. | ||
| * <p> | ||
| * See provided implementation {@link OldestVoiceChatCleanup} for a more concrete usage example. | ||
| */ | ||
| public interface VoiceChatCleanupStrategy { | ||
|
|
||
| /** | ||
| * Attempts to delete the {@link VoiceChannel} channels from the Discord guild found in the | ||
| * inputted list. | ||
| * | ||
| * @param voiceChannels a list of voice channels to be considered for removal | ||
| */ | ||
| void cleanup(List<VoiceChannel> voiceChannels); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you simply use
MessageHistory#sizeinstead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to make use of
MessageHistory#size, a method likeMessageHistory#retrievePastneeds to be invoked first because otherwise the size method will always return 0 regardless of the channel's history.