Skip to content

Commit 2f14b7d

Browse files
committed
feat: create dynamic voice channel system
1 parent 34e42d5 commit 2f14b7d

File tree

3 files changed

+204
-3
lines changed

3 files changed

+204
-3
lines changed

application/config.json.template

+5-1
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,9 @@
115115
"fallbackChannelPattern": "java-news-and-changes",
116116
"pollIntervalInMinutes": 10
117117
},
118-
"memberCountCategoryPattern": "Info"
118+
"memberCountCategoryPattern": "Info",
119+
"dynamicVoiceChannelPattern": [
120+
"Gaming",
121+
"Chit Chat"
122+
]
119123
}

application/src/main/java/org/togetherjava/tjbot/features/Features.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
148148
features.add(new PinnedNotificationRemover(config));
149149

150150
// Voice receivers
151-
features.add(new DynamicVoiceListener());
151+
features.add(new DynamicVoiceListener(config));
152152

153153
// Event receivers
154154
features.add(new RejoinModerationRoleListener(actionsStore, config));
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,211 @@
11
package org.togetherjava.tjbot.features.dynamicvc;
22

3+
import net.dv8tion.jda.api.entities.Guild;
4+
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
5+
import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildChannel;
6+
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
37
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
8+
import org.apache.commons.lang3.tuple.Pair;
49
import org.jetbrains.annotations.NotNull;
510

11+
import org.togetherjava.tjbot.config.Config;
612
import org.togetherjava.tjbot.features.VoiceReceiverAdapter;
713

14+
import java.util.ArrayList;
15+
import java.util.HashMap;
16+
import java.util.LinkedList;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Optional;
20+
import java.util.Queue;
21+
import java.util.concurrent.CompletableFuture;
22+
import java.util.concurrent.atomic.AtomicBoolean;
23+
import java.util.function.Predicate;
24+
import java.util.regex.Matcher;
25+
import java.util.regex.Pattern;
26+
import java.util.stream.IntStream;
27+
import java.util.stream.Stream;
28+
829
public class DynamicVoiceListener extends VoiceReceiverAdapter {
930

31+
private final Map<String, Predicate<String>> patterns = new HashMap<>();
32+
private static final Pattern channelTopicPattern = Pattern.compile("(\\s+\\d+)$");
33+
private static final Map<String, Queue<GuildVoiceUpdateEvent>> eventQueues = new HashMap<>();
34+
private static final Map<String, AtomicBoolean> isEventProcessing = new HashMap<>();
35+
36+
public DynamicVoiceListener(Config config) {
37+
config.getDynamicVoiceChannelPatterns().forEach(pattern -> {
38+
patterns.put(pattern, Pattern.compile(pattern).asMatchPredicate());
39+
isEventProcessing.put(pattern, new AtomicBoolean(false));
40+
eventQueues.put(pattern, new LinkedList<>());
41+
});
42+
}
43+
1044
@Override
1145
public void onVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
12-
// TODO: Complete
46+
AudioChannelUnion joinChannel = event.getChannelJoined();
47+
AudioChannelUnion leftChannel = event.getChannelLeft();
48+
49+
if (joinChannel != null) {
50+
insertEventToQueue(event, getChannelTopic(joinChannel.getName()));
51+
}
52+
53+
if (leftChannel != null) {
54+
insertEventToQueue(event, getChannelTopic(leftChannel.getName()));
55+
}
56+
}
57+
58+
private void insertEventToQueue(GuildVoiceUpdateEvent event, String channelTopic) {
59+
var eventQueue = eventQueues.get(channelTopic);
60+
61+
if (eventQueue == null) {
62+
return;
63+
}
64+
65+
eventQueue.add(event);
66+
67+
if (isEventProcessing.get(channelTopic).get()) {
68+
return;
69+
}
70+
71+
processEventFromQueue(channelTopic);
72+
}
73+
74+
private void processEventFromQueue(String channelTopic) {
75+
AtomicBoolean processing = isEventProcessing.get(channelTopic);
76+
processing.set(false);
77+
GuildVoiceUpdateEvent event = eventQueues.get(channelTopic).poll();
78+
79+
if (event == null) {
80+
return;
81+
}
82+
83+
processing.set(true);
84+
85+
handleTopicUpdate(event, channelTopic);
86+
}
87+
88+
private void handleTopicUpdate(GuildVoiceUpdateEvent event, String channelTopic) {
89+
AtomicBoolean processing = isEventProcessing.get(channelTopic);
90+
Guild guild = event.getGuild();
91+
List<CompletableFuture<?>> tasks = new ArrayList<>();
92+
93+
if (patterns.get(channelTopic) == null) {
94+
processing.set(false);
95+
return;
96+
}
97+
98+
long emptyChannelsCount = getEmptyChannelsCountFromTopic(guild, channelTopic);
99+
100+
if (emptyChannelsCount == 0) {
101+
long channelCount = getChannelCountFromTopic(guild, channelTopic);
102+
103+
tasks.add(createVoiceChannelFromTopic(guild, channelTopic, channelCount));
104+
} else if (emptyChannelsCount != 1) {
105+
tasks.addAll(removeDuplicateEmptyChannels(guild, channelTopic));
106+
tasks.addAll(renameTopicChannels(guild, channelTopic));
107+
}
108+
109+
if (!tasks.isEmpty()) {
110+
CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).thenCompose(v -> {
111+
List<CompletableFuture<?>> renameTasks = renameTopicChannels(guild, channelTopic);
112+
return CompletableFuture.allOf(renameTasks.toArray(CompletableFuture[]::new));
113+
}).handle((result, exception) -> {
114+
processEventFromQueue(channelTopic);
115+
return null;
116+
});
117+
return;
118+
}
119+
120+
processEventFromQueue(channelTopic);
121+
}
122+
123+
private static CompletableFuture<? extends StandardGuildChannel> createVoiceChannelFromTopic(
124+
Guild guild, String channelTopic, long topicChannelsCount) {
125+
Optional<VoiceChannel> voiceChannelOptional = getOriginalTopicChannel(guild, channelTopic);
126+
127+
if (voiceChannelOptional.isPresent()) {
128+
VoiceChannel originalChannel = voiceChannelOptional.orElseThrow();
129+
130+
return originalChannel.createCopy()
131+
.setName(getNumberedChannelTopic(channelTopic, topicChannelsCount + 1))
132+
.setPosition(originalChannel.getPositionRaw())
133+
.submit();
134+
}
135+
136+
return CompletableFuture.completedFuture(null);
137+
}
138+
139+
private static Optional<VoiceChannel> getOriginalTopicChannel(Guild guild,
140+
String channelTopic) {
141+
return guild.getVoiceChannels()
142+
.stream()
143+
.filter(channel -> channel.getName().equals(channelTopic))
144+
.findFirst();
145+
}
146+
147+
private List<CompletableFuture<Void>> removeDuplicateEmptyChannels(Guild guild,
148+
String channelTopic) {
149+
List<VoiceChannel> channelsToRemove = getVoiceChannelsFromTopic(guild, channelTopic)
150+
.filter(channel -> channel.getMembers().isEmpty())
151+
.toList();
152+
final List<CompletableFuture<Void>> tasks = new ArrayList<>();
153+
154+
channelsToRemove.subList(1, channelsToRemove.size())
155+
.forEach(channel -> tasks.add(channel.delete().submit()));
156+
157+
return tasks;
158+
}
159+
160+
private List<CompletableFuture<?>> renameTopicChannels(Guild guild, String channelTopic) {
161+
List<VoiceChannel> channels = getVoiceChannelsFromTopic(guild, channelTopic).toList();
162+
List<CompletableFuture<?>> tasks = new ArrayList<>();
163+
164+
IntStream.range(0, channels.size())
165+
.asLongStream()
166+
.mapToObj(number -> Pair.of(number + 1, channels.get((int) number)))
167+
.filter(pair -> pair.getLeft() != 1)
168+
.forEach(pair -> {
169+
long number = pair.getLeft();
170+
VoiceChannel voiceChannel = pair.getRight();
171+
String voiceChannelNameTopic = getChannelTopic(voiceChannel.getName());
172+
173+
tasks.add(voiceChannel.getManager()
174+
.setName(getNumberedChannelTopic(voiceChannelNameTopic, number))
175+
.submit());
176+
});
177+
178+
return tasks;
179+
}
180+
181+
private long getChannelCountFromTopic(Guild guild, String channelTopic) {
182+
return getVoiceChannelsFromTopic(guild, channelTopic).count();
183+
}
184+
185+
private Stream<VoiceChannel> getVoiceChannelsFromTopic(Guild guild, String channelTopic) {
186+
return guild.getVoiceChannels()
187+
.stream()
188+
.filter(channel -> patterns.get(channelTopic).test(getChannelTopic(channel.getName())));
189+
}
190+
191+
private long getEmptyChannelsCountFromTopic(Guild guild, String channelTopic) {
192+
return getVoiceChannelsFromTopic(guild, channelTopic)
193+
.map(channel -> channel.getMembers().size())
194+
.filter(number -> number == 0)
195+
.count();
196+
}
197+
198+
private static String getChannelTopic(String channelName) {
199+
Matcher matcher = channelTopicPattern.matcher(channelName);
200+
201+
if (matcher.find()) {
202+
return matcher.replaceAll("");
203+
}
204+
205+
return channelName;
206+
}
207+
208+
private static String getNumberedChannelTopic(String channelTopic, long id) {
209+
return String.format("%s %d", channelTopic, id);
13210
}
14211
}

0 commit comments

Comments
 (0)