Skip to content

Commit

Permalink
feat: create dynamic voice channel system
Browse files Browse the repository at this point in the history
  • Loading branch information
christolis committed May 15, 2024
1 parent 34e42d5 commit 2f14b7d
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 3 deletions.
6 changes: 5 additions & 1 deletion application/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,9 @@
"fallbackChannelPattern": "java-news-and-changes",
"pollIntervalInMinutes": 10
},
"memberCountCategoryPattern": "Info"
"memberCountCategoryPattern": "Info",
"dynamicVoiceChannelPattern": [
"Gaming",
"Chit Chat"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new PinnedNotificationRemover(config));

// Voice receivers
features.add(new DynamicVoiceListener());
features.add(new DynamicVoiceListener(config));

// Event receivers
features.add(new RejoinModerationRoleListener(actionsStore, config));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,211 @@
package org.togetherjava.tjbot.features.dynamicvc;

import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildChannel;
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;

import org.togetherjava.tjbot.config.Config;
import org.togetherjava.tjbot.features.VoiceReceiverAdapter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class DynamicVoiceListener extends VoiceReceiverAdapter {

private final Map<String, Predicate<String>> patterns = new HashMap<>();
private static final Pattern channelTopicPattern = Pattern.compile("(\\s+\\d+)$");
private static final Map<String, Queue<GuildVoiceUpdateEvent>> eventQueues = new HashMap<>();
private static final Map<String, AtomicBoolean> isEventProcessing = new HashMap<>();

public DynamicVoiceListener(Config config) {
config.getDynamicVoiceChannelPatterns().forEach(pattern -> {
patterns.put(pattern, Pattern.compile(pattern).asMatchPredicate());
isEventProcessing.put(pattern, new AtomicBoolean(false));
eventQueues.put(pattern, new LinkedList<>());
});
}

@Override
public void onVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
// TODO: Complete
AudioChannelUnion joinChannel = event.getChannelJoined();
AudioChannelUnion leftChannel = event.getChannelLeft();

if (joinChannel != null) {
insertEventToQueue(event, getChannelTopic(joinChannel.getName()));
}

if (leftChannel != null) {
insertEventToQueue(event, getChannelTopic(leftChannel.getName()));
}
}

private void insertEventToQueue(GuildVoiceUpdateEvent event, String channelTopic) {
var eventQueue = eventQueues.get(channelTopic);

if (eventQueue == null) {
return;
}

eventQueue.add(event);

if (isEventProcessing.get(channelTopic).get()) {
return;
}

processEventFromQueue(channelTopic);
}

private void processEventFromQueue(String channelTopic) {
AtomicBoolean processing = isEventProcessing.get(channelTopic);
processing.set(false);
GuildVoiceUpdateEvent event = eventQueues.get(channelTopic).poll();

if (event == null) {
return;
}

processing.set(true);

handleTopicUpdate(event, channelTopic);
}

private void handleTopicUpdate(GuildVoiceUpdateEvent event, String channelTopic) {
AtomicBoolean processing = isEventProcessing.get(channelTopic);
Guild guild = event.getGuild();
List<CompletableFuture<?>> tasks = new ArrayList<>();

if (patterns.get(channelTopic) == null) {
processing.set(false);
return;
}

long emptyChannelsCount = getEmptyChannelsCountFromTopic(guild, channelTopic);

if (emptyChannelsCount == 0) {
long channelCount = getChannelCountFromTopic(guild, channelTopic);

tasks.add(createVoiceChannelFromTopic(guild, channelTopic, channelCount));
} else if (emptyChannelsCount != 1) {
tasks.addAll(removeDuplicateEmptyChannels(guild, channelTopic));
tasks.addAll(renameTopicChannels(guild, channelTopic));
}

if (!tasks.isEmpty()) {
CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).thenCompose(v -> {
List<CompletableFuture<?>> renameTasks = renameTopicChannels(guild, channelTopic);
return CompletableFuture.allOf(renameTasks.toArray(CompletableFuture[]::new));
}).handle((result, exception) -> {
processEventFromQueue(channelTopic);
return null;
});
return;
}

processEventFromQueue(channelTopic);
}

private static CompletableFuture<? extends StandardGuildChannel> createVoiceChannelFromTopic(
Guild guild, String channelTopic, long topicChannelsCount) {
Optional<VoiceChannel> voiceChannelOptional = getOriginalTopicChannel(guild, channelTopic);

if (voiceChannelOptional.isPresent()) {
VoiceChannel originalChannel = voiceChannelOptional.orElseThrow();

return originalChannel.createCopy()
.setName(getNumberedChannelTopic(channelTopic, topicChannelsCount + 1))
.setPosition(originalChannel.getPositionRaw())
.submit();
}

return CompletableFuture.completedFuture(null);
}

private static Optional<VoiceChannel> getOriginalTopicChannel(Guild guild,
String channelTopic) {
return guild.getVoiceChannels()
.stream()
.filter(channel -> channel.getName().equals(channelTopic))
.findFirst();
}

private List<CompletableFuture<Void>> removeDuplicateEmptyChannels(Guild guild,
String channelTopic) {
List<VoiceChannel> channelsToRemove = getVoiceChannelsFromTopic(guild, channelTopic)
.filter(channel -> channel.getMembers().isEmpty())
.toList();
final List<CompletableFuture<Void>> tasks = new ArrayList<>();

channelsToRemove.subList(1, channelsToRemove.size())
.forEach(channel -> tasks.add(channel.delete().submit()));

return tasks;
}

private List<CompletableFuture<?>> renameTopicChannels(Guild guild, String channelTopic) {
List<VoiceChannel> channels = getVoiceChannelsFromTopic(guild, channelTopic).toList();
List<CompletableFuture<?>> tasks = new ArrayList<>();

IntStream.range(0, channels.size())
.asLongStream()
.mapToObj(number -> Pair.of(number + 1, channels.get((int) number)))
.filter(pair -> pair.getLeft() != 1)
.forEach(pair -> {
long number = pair.getLeft();
VoiceChannel voiceChannel = pair.getRight();
String voiceChannelNameTopic = getChannelTopic(voiceChannel.getName());

tasks.add(voiceChannel.getManager()
.setName(getNumberedChannelTopic(voiceChannelNameTopic, number))
.submit());
});

return tasks;
}

private long getChannelCountFromTopic(Guild guild, String channelTopic) {
return getVoiceChannelsFromTopic(guild, channelTopic).count();
}

private Stream<VoiceChannel> getVoiceChannelsFromTopic(Guild guild, String channelTopic) {
return guild.getVoiceChannels()
.stream()
.filter(channel -> patterns.get(channelTopic).test(getChannelTopic(channel.getName())));
}

private long getEmptyChannelsCountFromTopic(Guild guild, String channelTopic) {
return getVoiceChannelsFromTopic(guild, channelTopic)
.map(channel -> channel.getMembers().size())
.filter(number -> number == 0)
.count();
}

private static String getChannelTopic(String channelName) {
Matcher matcher = channelTopicPattern.matcher(channelName);

if (matcher.find()) {
return matcher.replaceAll("");
}

return channelName;
}

private static String getNumberedChannelTopic(String channelTopic, long id) {
return String.format("%s %d", channelTopic, id);
}
}

0 comments on commit 2f14b7d

Please sign in to comment.