Skip to content

Commit 11d2f53

Browse files
committed
Archive instead of simply deleting voice channels
Currently there is no way to moderate ephemeral voice channels. Members could very easily break the rules in the channel, send NSFW content and it can go undetected by the moderation team. Introduce a new archival system where the ephemeral voice channels are instead stored in an archival category. Depending on the archival strategy, channels are removed once they are not needed any more. Routines are not being used since we are able to get away with attempting cleanup every time a user leaves an ephemeral voice channel. This results in superior performance and no scheduling involved. Do _not_ archive ephemeral voice channels with no contents sent by members. Signed-off-by: Chris Sdogkos <work@chris-sdogkos.com>
1 parent 2679a2f commit 11d2f53

File tree

3 files changed

+118
-10
lines changed

3 files changed

+118
-10
lines changed

application/src/main/java/org/togetherjava/tjbot/features/voicechat/DynamicVoiceChat.java

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
import net.dv8tion.jda.api.entities.Guild;
55
import net.dv8tion.jda.api.entities.Member;
66
import net.dv8tion.jda.api.entities.MessageEmbed;
7+
import net.dv8tion.jda.api.entities.MessageHistory;
8+
import net.dv8tion.jda.api.entities.channel.concrete.Category;
79
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
810
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
911
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
1012
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
13+
import net.dv8tion.jda.api.managers.channel.middleman.AudioChannelManager;
14+
import net.dv8tion.jda.api.requests.RestAction;
1115
import org.jetbrains.annotations.NotNull;
1216
import org.slf4j.Logger;
1317
import org.slf4j.LoggerFactory;
@@ -16,21 +20,34 @@
1620
import org.togetherjava.tjbot.features.VoiceReceiverAdapter;
1721

1822
import java.util.List;
23+
import java.util.Optional;
1924
import java.util.regex.Pattern;
2025

2126
/**
2227
* Handles dynamic voice channel creation and deletion based on user activity.
2328
* <p>
2429
* When a member joins a configured root channel, a temporary copy is created and the member is
25-
* moved into it. Once the channel becomes empty, it is deleted.
30+
* moved into it. Once the channel becomes empty, it is archived and further deleted using a
31+
* {@link VoiceChatCleanupStrategy}.
2632
*/
2733
public final class DynamicVoiceChat extends VoiceReceiverAdapter {
2834
private static final Logger logger = LoggerFactory.getLogger(DynamicVoiceChat.class);
35+
36+
// @christolis: Unless somebody is willing to make the category name configurable,
37+
// I will leave this here as a constant since I don't see a justification for naming
38+
// this category name into something different.
39+
private static final String ARCHIVE_CATEGORY_NAME = "Voice Channel Archives";
40+
private static final int CLEAN_CHANNELS_AMOUNT = 2;
41+
private static final int MINIMUM_CHANNELS_AMOUNT = 3;
42+
43+
private final VoiceChatCleanupStrategy voiceChatCleanupStrategy;
2944
private final List<Pattern> dynamicVoiceChannelPatterns;
3045

3146
public DynamicVoiceChat(Config config) {
3247
this.dynamicVoiceChannelPatterns =
3348
config.getDynamicVoiceChannelPatterns().stream().map(Pattern::compile).toList();
49+
this.voiceChatCleanupStrategy =
50+
new OldestVoiceChatCleanup(CLEAN_CHANNELS_AMOUNT, MINIMUM_CHANNELS_AMOUNT);
3451
}
3552

3653
@Override
@@ -45,7 +62,17 @@ public void onVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
4562

4663
if (channelLeft != null && !eventHappenOnDynamicRootChannel(channelLeft)) {
4764
logger.debug("Event happened on left channel {}", channelLeft);
48-
deleteDynamicVoiceChannel(channelLeft);
65+
66+
MessageHistory messageHistory = channelLeft.asVoiceChannel().getHistory();
67+
messageHistory.retrievePast(2).queue(messages -> {
68+
// Don't forget that there is always one
69+
// embed message sent by the bot every time.
70+
if (messages.size() > 1) {
71+
archiveDynamicVoiceChannel(channelLeft);
72+
} else {
73+
channelLeft.delete().queue();
74+
}
75+
});
4976
}
5077
}
5178

@@ -82,20 +109,40 @@ private void moveMember(Guild guild, Member member, AudioChannel channel) {
82109
member.getNickname(), channel.getName(), error));
83110
}
84111

85-
private void deleteDynamicVoiceChannel(AudioChannelUnion channel) {
112+
private void archiveDynamicVoiceChannel(AudioChannelUnion channel) {
86113
int memberCount = channel.getMembers().size();
114+
String channelName = channel.getName();
87115

88116
if (memberCount > 0) {
89-
logger.debug("Voice channel {} not empty ({} members), so not removing.",
90-
channel.getName(), memberCount);
117+
logger.debug("Voice channel {} not empty ({} members), so not removing.", channelName,
118+
memberCount);
119+
return;
120+
}
121+
122+
Optional<Category> archiveCategoryOptional = channel.getGuild()
123+
.getCategoryCache()
124+
.stream()
125+
.filter(c -> c.getName().equalsIgnoreCase(ARCHIVE_CATEGORY_NAME))
126+
.findFirst();
127+
128+
AudioChannelManager<?, ?> channelManager = channel.getManager();
129+
RestAction<Void> restActionChain =
130+
channelManager.setName(String.format("%s (Archived)", channelName))
131+
.and(channel.getPermissionContainer().getManager().clearOverridesAdded());
132+
133+
if (archiveCategoryOptional.isEmpty()) {
134+
logger.warn("Could not find archive category. Attempting to create one...");
135+
channel.getGuild()
136+
.createCategory(ARCHIVE_CATEGORY_NAME)
137+
.queue(newCategory -> restActionChain.and(channelManager.setParent(newCategory))
138+
.queue());
91139
return;
92140
}
93141

94-
channel.delete()
95-
.queue(_ -> logger.trace("Deleted dynamically created voice channel: {} ",
96-
channel.getName()),
97-
error -> logger.error("Failed to delete dynamically created voice channel: {} ",
98-
channel.getName(), error));
142+
archiveCategoryOptional.ifPresent(archiveCategory -> restActionChain
143+
.and(channelManager.setParent(archiveCategory))
144+
.queue(_ -> voiceChatCleanupStrategy.cleanup(archiveCategory.getVoiceChannels()),
145+
err -> logger.error("Could not archive dynamic voice chat", err)));
99146
}
100147

101148
private void sendWarningEmbed(VoiceChannel channel) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.togetherjava.tjbot.features.voicechat;
2+
3+
import net.dv8tion.jda.api.entities.ISnowflake;
4+
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
5+
6+
import java.util.Comparator;
7+
import java.util.List;
8+
9+
/**
10+
* Cleans up voice chats from an archive prioritizing the oldest {@link VoiceChannel}.
11+
* <p>
12+
* Considering a list of voice channels is provided with all of them obviously having a different
13+
* addition time, the first few {@link VoiceChannel} elements provided, amounting to the value of
14+
* <code>cleanChannelsAmount</code> will be removed from the guild.
15+
* <p>
16+
* The cleanup strategy will <i>not</i> be executed if the amount of voice channels does not exceed
17+
* the value of <code>minimumChannelsAmountToTrigger</code>.
18+
*/
19+
final class OldestVoiceChatCleanup implements VoiceChatCleanupStrategy {
20+
21+
private final int cleanChannelsAmount;
22+
private final int minimumChannelsAmountToTrigger;
23+
24+
OldestVoiceChatCleanup(int cleanChannelsAmount, int minimumChannelsAmountToTrigger) {
25+
this.cleanChannelsAmount = cleanChannelsAmount;
26+
this.minimumChannelsAmountToTrigger = minimumChannelsAmountToTrigger;
27+
}
28+
29+
@Override
30+
public void cleanup(List<VoiceChannel> voiceChannels) {
31+
if (voiceChannels.size() < minimumChannelsAmountToTrigger) {
32+
return;
33+
}
34+
35+
voiceChannels.stream()
36+
.sorted(Comparator.comparing(ISnowflake::getTimeCreated))
37+
.limit(cleanChannelsAmount)
38+
.forEach(voiceChannel -> voiceChannel.delete().queue());
39+
}
40+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.togetherjava.tjbot.features.voicechat;
2+
3+
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
4+
5+
import java.util.List;
6+
7+
/**
8+
* Voice chat cleanup strategy interface for handling voice chat archive removal.
9+
* <p>
10+
* See provided implementation {@link OldestVoiceChatCleanup} for a more concrete usage example.
11+
*/
12+
public interface VoiceChatCleanupStrategy {
13+
14+
/**
15+
* Attempts to delete the {@link VoiceChannel} channels from the Discord guild found in the
16+
* inputted list.
17+
*
18+
* @param voiceChannels a list of voice channels to be considered for removal
19+
*/
20+
void cleanup(List<VoiceChannel> voiceChannels);
21+
}

0 commit comments

Comments
 (0)