Skip to content

Commit 5bc588e

Browse files
authored
Add VoiceReceiver logic (#1369)
Signed-off-by: Chris Sdogkos <[email protected]>
1 parent aa12c13 commit 5bc588e

File tree

3 files changed

+238
-0
lines changed

3 files changed

+238
-0
lines changed
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: 117 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,96 @@ public void onMessageDelete(final MessageDeleteEvent event) {
238255
}
239256
}
240257

258+
/**
259+
* Calculates the correct voice channel to act upon.
260+
*
261+
* <p>
262+
* If there is a <code>channelJoined</code> and a <code>channelLeft</code>, then the
263+
* <code>channelJoined</code> is prioritized and returned. Otherwise, it returns
264+
* <code>channelLeft</code>.
265+
*
266+
* <p>
267+
* This is an essential method due to the need of updating both channel categories that a member
268+
* utilizes. For example, take the scenario of a user browsing through voice channels:
269+
*
270+
* <pre>
271+
* - User joins General -> channelJoined = General | channelLeft = null
272+
* - User switches to Gaming -> channelJoined = Gaming | channelLeft = General
273+
* - User leaves Discord -> channelJoined = null | channelLeft = Gaming
274+
* </pre>
275+
*
276+
* <p>
277+
* This way, we make sure that all relevant voice channels are updated.
278+
*
279+
* @param channelJoined the channel that the member has connected to, if any
280+
* @param channelLeft the channel that the member left from, if any
281+
* @return the join channel if not null, otherwise the leave channel, otherwise an empty
282+
* optional
283+
*/
284+
private Optional<Channel> selectPreferredAudioChannel(@Nullable AudioChannelUnion channelJoined,
285+
@Nullable AudioChannelUnion channelLeft) {
286+
if (channelJoined != null) {
287+
return Optional.of(channelJoined);
288+
}
289+
290+
return Optional.ofNullable(channelLeft);
291+
}
292+
293+
@Override
294+
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
295+
selectPreferredAudioChannel(event.getChannelJoined(), event.getChannelLeft())
296+
.ifPresent(channel -> getVoiceReceiversSubscribedTo(channel)
297+
.forEach(voiceReceiver -> voiceReceiver.onVoiceUpdate(event)));
298+
}
299+
300+
@Override
301+
public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) {
302+
AudioChannelUnion channel = event.getVoiceState().getChannel();
303+
304+
if (channel == null) {
305+
return;
306+
}
307+
308+
getVoiceReceiversSubscribedTo(channel)
309+
.forEach(voiceReceiver -> voiceReceiver.onVideoToggle(event));
310+
}
311+
312+
@Override
313+
public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) {
314+
AudioChannelUnion channel = event.getVoiceState().getChannel();
315+
316+
if (channel == null) {
317+
return;
318+
}
319+
320+
getVoiceReceiversSubscribedTo(channel)
321+
.forEach(voiceReceiver -> voiceReceiver.onStreamToggle(event));
322+
}
323+
324+
@Override
325+
public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) {
326+
AudioChannelUnion channel = event.getVoiceState().getChannel();
327+
328+
if (channel == null) {
329+
return;
330+
}
331+
332+
getVoiceReceiversSubscribedTo(channel)
333+
.forEach(voiceReceiver -> voiceReceiver.onMuteToggle(event));
334+
}
335+
336+
@Override
337+
public void onGuildVoiceDeafen(@NotNull GuildVoiceDeafenEvent event) {
338+
AudioChannelUnion channel = event.getVoiceState().getChannel();
339+
340+
if (channel == null) {
341+
return;
342+
}
343+
344+
getVoiceReceiversSubscribedTo(channel)
345+
.forEach(voiceReceiver -> voiceReceiver.onDeafenToggle(event));
346+
}
347+
241348
private Stream<MessageReceiver> getMessageReceiversSubscribedTo(Channel channel) {
242349
String channelName = channel.getName();
243350
return channelNameToMessageReceiver.entrySet()
@@ -248,6 +355,16 @@ private Stream<MessageReceiver> getMessageReceiversSubscribedTo(Channel channel)
248355
.map(Map.Entry::getValue);
249356
}
250357

358+
private Stream<VoiceReceiver> getVoiceReceiversSubscribedTo(Channel channel) {
359+
String channelName = channel.getName();
360+
return channelNameToVoiceReceiver.entrySet()
361+
.stream()
362+
.filter(patternAndReceiver -> patternAndReceiver.getKey()
363+
.matcher(channelName)
364+
.matches())
365+
.map(Map.Entry::getValue);
366+
}
367+
251368
@Override
252369
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
253370
String name = event.getName();

0 commit comments

Comments
 (0)