Skip to content

Commit a1039f9

Browse files
committed
feat(dynamic-vcs): add VoiceReceiver and config changes
1 parent 817e044 commit a1039f9

File tree

5 files changed

+239
-2
lines changed

5 files changed

+239
-2
lines changed

application/config.json.template

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,10 @@
115115
"fallbackChannelPattern": "java-news-and-changes",
116116
"pollIntervalInMinutes": 10
117117
},
118-
"memberCountCategoryPattern": "Info"
118+
"memberCountCategoryPattern": "Info",
119+
"dynamicVoiceChannelPatterns": [
120+
"Gaming",
121+
"Support/Studying Room",
122+
"Chit Chat"
123+
]
119124
}

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

Lines changed: 15 additions & 1 deletion
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 = "dynamicVoiceChannelPatterns",
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
}
Lines changed: 69 additions & 0 deletions
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 events from. Called by the core system once during the startup in order to register
27+
* 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+
}
Lines changed: 52 additions & 0 deletions
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+
}

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

Lines changed: 97 additions & 0 deletions
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)