Skip to content

Commit 34e42d5

Browse files
committed
feat: add ability to handle voice events
This commit aims to add functionality for developers to create their own voice receiver features that deal with events regarding Discord voice channels. It is implemented here since we need to handle such events for the purposes of dynamically creating and deleting voice channels on demand when guild members perform actions involving them, like connecting, disconnecting, or moving between them.
1 parent f8c91a9 commit 34e42d5

File tree

6 files changed

+251
-1
lines changed

6 files changed

+251
-1
lines changed

application/src/main/java/org/togetherjava/tjbot/config/Config.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public final class Config {
4646
private final RSSFeedsConfig rssFeedsConfig;
4747
private final String selectRolesChannelPattern;
4848
private final String memberCountCategoryPattern;
49+
private final List<String> dynamicVoiceChannelPatterns;
4950

5051
@SuppressWarnings("ConstructorWithTooManyParameters")
5152
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
@@ -94,7 +95,9 @@ private Config(@JsonProperty(value = "token", required = true) String token,
9495
required = true) FeatureBlacklistConfig featureBlacklistConfig,
9596
@JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig,
9697
@JsonProperty(value = "selectRolesChannelPattern",
97-
required = true) String selectRolesChannelPattern) {
98+
required = true) String selectRolesChannelPattern,
99+
@JsonProperty(value = "dynamicVoiceChannelPattern",
100+
required = true) List<String> dynamicVoiceChannelPatterns) {
98101
this.token = Objects.requireNonNull(token);
99102
this.githubApiKey = Objects.requireNonNull(githubApiKey);
100103
this.databasePath = Objects.requireNonNull(databasePath);
@@ -127,6 +130,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
127130
this.featureBlacklistConfig = Objects.requireNonNull(featureBlacklistConfig);
128131
this.rssFeedsConfig = Objects.requireNonNull(rssFeedsConfig);
129132
this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern);
133+
this.dynamicVoiceChannelPatterns = Objects.requireNonNull(dynamicVoiceChannelPatterns);
130134
}
131135

132136
/**
@@ -418,4 +422,14 @@ public String getMemberCountCategoryPattern() {
418422
public RSSFeedsConfig getRSSFeedsConfig() {
419423
return rssFeedsConfig;
420424
}
425+
426+
/**
427+
* Gets the list of voice channel patterns that are treated dynamically.
428+
*
429+
* @return the list of dynamic voice channel patterns
430+
*/
431+
public List<String> getDynamicVoiceChannelPatterns() {
432+
return this.dynamicVoiceChannelPatterns;
433+
}
434+
421435
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.togetherjava.tjbot.features.code.CodeMessageAutoDetection;
2121
import org.togetherjava.tjbot.features.code.CodeMessageHandler;
2222
import org.togetherjava.tjbot.features.code.CodeMessageManualDetection;
23+
import org.togetherjava.tjbot.features.dynamicvc.DynamicVoiceListener;
2324
import org.togetherjava.tjbot.features.filesharing.FileSharingMessageListener;
2425
import org.togetherjava.tjbot.features.github.GitHubCommand;
2526
import org.togetherjava.tjbot.features.github.GitHubReference;
@@ -146,6 +147,9 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
146147
features.add(new SlashCommandEducator());
147148
features.add(new PinnedNotificationRemover(config));
148149

150+
// Voice receivers
151+
features.add(new DynamicVoiceListener());
152+
149153
// Event receivers
150154
features.add(new RejoinModerationRoleListener(actionsStore, config));
151155
features.add(new GuildLeaveCloseThreadListener(config));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.togetherjava.tjbot.features;
2+
3+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceDeafenEvent;
4+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMuteEvent;
5+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceStreamEvent;
6+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
7+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceVideoEvent;
8+
9+
import java.util.regex.Pattern;
10+
11+
/**
12+
* Receives incoming Discord guild events from voice channels matching a given pattern.
13+
* <p>
14+
* All voice receivers have to implement this interface. For convenience, there is a
15+
* {@link VoiceReceiverAdapter} available that implemented most methods already. A new receiver can
16+
* then be registered by adding it to {@link Features}.
17+
* <p>
18+
* <p>
19+
* After registration, the system will notify a receiver whenever a new event was sent or an
20+
* existing event was updated in any channel matching the {@link #getChannelNamePattern()} the bot
21+
* is added to.
22+
*/
23+
public interface VoiceReceiver extends Feature {
24+
/**
25+
* Retrieves the pattern matching the names of channels of which this receiver is interested in
26+
* receiving sent messages from. Called by the core system once during the startup in order to
27+
* register the receiver accordingly.
28+
* <p>
29+
* Changes on the pattern returned by this method afterwards will not be picked up.
30+
*
31+
* @return the pattern matching the names of relevant channels
32+
*/
33+
Pattern getChannelNamePattern();
34+
35+
/**
36+
* Triggered by the core system whenever a member joined, left or moved voice channels.
37+
*
38+
* @param event the event that triggered this
39+
*/
40+
void onVoiceUpdate(GuildVoiceUpdateEvent event);
41+
42+
/**
43+
* Triggered by the core system whenever a member toggled their camera in a voice channel.
44+
*
45+
* @param event the event that triggered this
46+
*/
47+
void onVideoToggle(GuildVoiceVideoEvent event);
48+
49+
/**
50+
* Triggered by the core system whenever a member started or stopped a stream.
51+
*
52+
* @param event the event that triggered this
53+
*/
54+
void onStreamToggle(GuildVoiceStreamEvent event);
55+
56+
/**
57+
* Triggered by the core system whenever a member toggled their mute status.
58+
*
59+
* @param event the event that triggered this
60+
*/
61+
void onMuteToggle(GuildVoiceMuteEvent event);
62+
63+
/**
64+
* Triggered by the core system whenever a member toggled their deafened status.
65+
*
66+
* @param event the event that triggered this
67+
*/
68+
void onDeafenToggle(GuildVoiceDeafenEvent event);
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.togetherjava.tjbot.features;
2+
3+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceDeafenEvent;
4+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMuteEvent;
5+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceStreamEvent;
6+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
7+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceVideoEvent;
8+
9+
import java.util.regex.Pattern;
10+
11+
public class VoiceReceiverAdapter implements VoiceReceiver {
12+
13+
private final Pattern channelNamePattern;
14+
15+
protected VoiceReceiverAdapter() {
16+
this(Pattern.compile(".*"));
17+
}
18+
19+
protected VoiceReceiverAdapter(Pattern channelNamePattern) {
20+
this.channelNamePattern = channelNamePattern;
21+
}
22+
23+
@Override
24+
public Pattern getChannelNamePattern() {
25+
return channelNamePattern;
26+
}
27+
28+
@Override
29+
public void onVoiceUpdate(GuildVoiceUpdateEvent event) {
30+
// Adapter does not react by default, subclasses may change this behavior
31+
}
32+
33+
@Override
34+
public void onVideoToggle(GuildVoiceVideoEvent event) {
35+
// Adapter does not react by default, subclasses may change this behavior
36+
}
37+
38+
@Override
39+
public void onStreamToggle(GuildVoiceStreamEvent event) {
40+
// Adapter does not react by default, subclasses may change this behavior
41+
}
42+
43+
@Override
44+
public void onMuteToggle(GuildVoiceMuteEvent event) {
45+
// Adapter does not react by default, subclasses may change this behavior
46+
}
47+
48+
@Override
49+
public void onDeafenToggle(GuildVoiceDeafenEvent event) {
50+
// Adapter does not react by default, subclasses may change this behavior
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.togetherjava.tjbot.features.dynamicvc;
2+
3+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
import org.togetherjava.tjbot.features.VoiceReceiverAdapter;
7+
8+
public class DynamicVoiceListener extends VoiceReceiverAdapter {
9+
10+
@Override
11+
public void onVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
12+
// TODO: Complete
13+
}
14+
}

application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java

+97
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
import net.dv8tion.jda.api.JDA;
44
import net.dv8tion.jda.api.entities.channel.Channel;
5+
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
6+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceDeafenEvent;
7+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMuteEvent;
8+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceStreamEvent;
9+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
10+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceVideoEvent;
511
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
612
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
713
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
@@ -16,6 +22,8 @@
1622
import net.dv8tion.jda.api.hooks.ListenerAdapter;
1723
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
1824
import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
25+
import org.jetbrains.annotations.NotNull;
26+
import org.jetbrains.annotations.Nullable;
1927
import org.jetbrains.annotations.Unmodifiable;
2028
import org.slf4j.Logger;
2129
import org.slf4j.LoggerFactory;
@@ -32,6 +40,7 @@
3240
import org.togetherjava.tjbot.features.UserContextCommand;
3341
import org.togetherjava.tjbot.features.UserInteractionType;
3442
import org.togetherjava.tjbot.features.UserInteractor;
43+
import org.togetherjava.tjbot.features.VoiceReceiver;
3544
import org.togetherjava.tjbot.features.componentids.ComponentId;
3645
import org.togetherjava.tjbot.features.componentids.ComponentIdParser;
3746
import org.togetherjava.tjbot.features.componentids.ComponentIdStore;
@@ -75,6 +84,7 @@ public final class BotCore extends ListenerAdapter implements CommandProvider {
7584
private final ComponentIdParser componentIdParser;
7685
private final ComponentIdStore componentIdStore;
7786
private final Map<Pattern, MessageReceiver> channelNameToMessageReceiver = new HashMap<>();
87+
private final Map<Pattern, VoiceReceiver> channelNameToVoiceReceiver = new HashMap<>();
7888

7989
/**
8090
* Creates a new command system which uses the given database to allow commands to persist data.
@@ -96,6 +106,13 @@ public BotCore(JDA jda, Database database, Config config) {
96106
.forEach(messageReceiver -> channelNameToMessageReceiver
97107
.put(messageReceiver.getChannelNamePattern(), messageReceiver));
98108

109+
// Voice receivers
110+
features.stream()
111+
.filter(VoiceReceiver.class::isInstance)
112+
.map(VoiceReceiver.class::cast)
113+
.forEach(voiceReceiver -> channelNameToVoiceReceiver
114+
.put(voiceReceiver.getChannelNamePattern(), voiceReceiver));
115+
99116
// Event receivers
100117
features.stream()
101118
.filter(EventReceiver.class::isInstance)
@@ -238,6 +255,76 @@ public void onMessageDelete(final MessageDeleteEvent event) {
238255
}
239256
}
240257

258+
/**
259+
* @param joinChannel the join channel
260+
* @param leftChannel the leave channel
261+
* @return the join channel if not null, otherwise the leave channel, otherwise an empty
262+
* optional
263+
*/
264+
private Optional<Channel> calculateSubscribeTarget(@Nullable AudioChannelUnion joinChannel,
265+
@Nullable AudioChannelUnion leftChannel) {
266+
if (joinChannel != null) {
267+
return Optional.of(joinChannel);
268+
}
269+
270+
return Optional.ofNullable(leftChannel);
271+
}
272+
273+
@Override
274+
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
275+
calculateSubscribeTarget(event.getChannelJoined(), event.getChannelLeft())
276+
.ifPresent(channel -> getVoiceReceiversSubscribedTo(channel)
277+
.forEach(voiceReceiver -> voiceReceiver.onVoiceUpdate(event)));
278+
}
279+
280+
@Override
281+
public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) {
282+
AudioChannelUnion channel = event.getVoiceState().getChannel();
283+
284+
if (channel == null) {
285+
return;
286+
}
287+
288+
getVoiceReceiversSubscribedTo(channel)
289+
.forEach(voiceReceiver -> voiceReceiver.onVideoToggle(event));
290+
}
291+
292+
@Override
293+
public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) {
294+
AudioChannelUnion channel = event.getVoiceState().getChannel();
295+
296+
if (channel == null) {
297+
return;
298+
}
299+
300+
getVoiceReceiversSubscribedTo(channel)
301+
.forEach(voiceReceiver -> voiceReceiver.onStreamToggle(event));
302+
}
303+
304+
@Override
305+
public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) {
306+
AudioChannelUnion channel = event.getVoiceState().getChannel();
307+
308+
if (channel == null) {
309+
return;
310+
}
311+
312+
getVoiceReceiversSubscribedTo(channel)
313+
.forEach(voiceReceiver -> voiceReceiver.onMuteToggle(event));
314+
}
315+
316+
@Override
317+
public void onGuildVoiceDeafen(@NotNull GuildVoiceDeafenEvent event) {
318+
AudioChannelUnion channel = event.getVoiceState().getChannel();
319+
320+
if (channel == null) {
321+
return;
322+
}
323+
324+
getVoiceReceiversSubscribedTo(channel)
325+
.forEach(voiceReceiver -> voiceReceiver.onDeafenToggle(event));
326+
}
327+
241328
private Stream<MessageReceiver> getMessageReceiversSubscribedTo(Channel channel) {
242329
String channelName = channel.getName();
243330
return channelNameToMessageReceiver.entrySet()
@@ -248,6 +335,16 @@ private Stream<MessageReceiver> getMessageReceiversSubscribedTo(Channel channel)
248335
.map(Map.Entry::getValue);
249336
}
250337

338+
private Stream<VoiceReceiver> getVoiceReceiversSubscribedTo(Channel channel) {
339+
String channelName = channel.getName();
340+
return channelNameToVoiceReceiver.entrySet()
341+
.stream()
342+
.filter(patternAndReceiver -> patternAndReceiver.getKey()
343+
.matcher(channelName)
344+
.matches())
345+
.map(Map.Entry::getValue);
346+
}
347+
251348
@Override
252349
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
253350
String name = event.getName();

0 commit comments

Comments
 (0)