Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.togetherjava.tjbot.features;

import net.dv8tion.jda.api.events.guild.voice.GuildVoiceDeafenEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMuteEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceStreamEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceVideoEvent;

import java.util.regex.Pattern;

/**
* Receives incoming Discord guild events from voice channels matching a given pattern.
* <p>
* All voice receivers have to implement this interface. For convenience, there is a
* {@link VoiceReceiverAdapter} available that implemented most methods already. A new receiver can
* then be registered by adding it to {@link Features}.
* <p>
* <p>
* After registration, the system will notify a receiver whenever a new event was sent or an
* existing event was updated in any channel matching the {@link #getChannelNamePattern()} the bot
* is added to.
*/
public interface VoiceReceiver extends Feature {
/**
* Retrieves the pattern matching the names of channels of which this receiver is interested in
* receiving events from. Called by the core system once during the startup in order to register
* the receiver accordingly.
* <p>
* Changes on the pattern returned by this method afterwards will not be picked up.
*
* @return the pattern matching the names of relevant channels
*/
Pattern getChannelNamePattern();

/**
* Triggered by the core system whenever a member joined, left or moved voice channels.
*
* @param event the event that triggered this
*/
void onVoiceUpdate(GuildVoiceUpdateEvent event);

/**
* Triggered by the core system whenever a member toggled their camera in a voice channel.
*
* @param event the event that triggered this
*/
void onVideoToggle(GuildVoiceVideoEvent event);

/**
* Triggered by the core system whenever a member started or stopped a stream.
*
* @param event the event that triggered this
*/
void onStreamToggle(GuildVoiceStreamEvent event);

/**
* Triggered by the core system whenever a member toggled their mute status.
*
* @param event the event that triggered this
*/
void onMuteToggle(GuildVoiceMuteEvent event);

/**
* Triggered by the core system whenever a member toggled their deafened status.
*
* @param event the event that triggered this
*/
void onDeafenToggle(GuildVoiceDeafenEvent event);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.togetherjava.tjbot.features;

import net.dv8tion.jda.api.events.guild.voice.GuildVoiceDeafenEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMuteEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceStreamEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceVideoEvent;

import java.util.regex.Pattern;

public class VoiceReceiverAdapter implements VoiceReceiver {

private final Pattern channelNamePattern;

protected VoiceReceiverAdapter() {
this(Pattern.compile(".*"));
}

protected VoiceReceiverAdapter(Pattern channelNamePattern) {
this.channelNamePattern = channelNamePattern;
}

@Override
public Pattern getChannelNamePattern() {
return channelNamePattern;
}

@Override
public void onVoiceUpdate(GuildVoiceUpdateEvent event) {
// Adapter does not react by default, subclasses may change this behavior
}

@Override
public void onVideoToggle(GuildVoiceVideoEvent event) {
// Adapter does not react by default, subclasses may change this behavior
}

@Override
public void onStreamToggle(GuildVoiceStreamEvent event) {
// Adapter does not react by default, subclasses may change this behavior
}

@Override
public void onMuteToggle(GuildVoiceMuteEvent event) {
// Adapter does not react by default, subclasses may change this behavior
}

@Override
public void onDeafenToggle(GuildVoiceDeafenEvent event) {
// Adapter does not react by default, subclasses may change this behavior
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.channel.Channel;
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceDeafenEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMuteEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceStreamEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceVideoEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
Expand All @@ -16,6 +22,8 @@
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -32,6 +40,7 @@
import org.togetherjava.tjbot.features.UserContextCommand;
import org.togetherjava.tjbot.features.UserInteractionType;
import org.togetherjava.tjbot.features.UserInteractor;
import org.togetherjava.tjbot.features.VoiceReceiver;
import org.togetherjava.tjbot.features.componentids.ComponentId;
import org.togetherjava.tjbot.features.componentids.ComponentIdParser;
import org.togetherjava.tjbot.features.componentids.ComponentIdStore;
Expand Down Expand Up @@ -75,6 +84,7 @@ public final class BotCore extends ListenerAdapter implements CommandProvider {
private final ComponentIdParser componentIdParser;
private final ComponentIdStore componentIdStore;
private final Map<Pattern, MessageReceiver> channelNameToMessageReceiver = new HashMap<>();
private final Map<Pattern, VoiceReceiver> channelNameToVoiceReceiver = new HashMap<>();

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

// Voice receivers
features.stream()
.filter(VoiceReceiver.class::isInstance)
.map(VoiceReceiver.class::cast)
.forEach(voiceReceiver -> channelNameToVoiceReceiver
.put(voiceReceiver.getChannelNamePattern(), voiceReceiver));

// Event receivers
features.stream()
.filter(EventReceiver.class::isInstance)
Expand Down Expand Up @@ -238,6 +255,96 @@ public void onMessageDelete(final MessageDeleteEvent event) {
}
}

/**
* Calculates the correct voice channel to act upon.
*
* <p>
* If there is a <code>channelJoined</code> and a <code>channelLeft</code>, then the
* <code>channelJoined</code> is prioritized and returned. Otherwise, it returns
* <code>channelLeft</code>.
*
* <p>
* This is an essential method due to the need of updating both channel categories that a member
* utilizes. For example, take the scenario of a user browsing through voice channels:
*
* <pre>
* - User joins General -> channelJoined = General | channelLeft = null
* - User switches to Gaming -> channelJoined = Gaming | channelLeft = General
* - User leaves Discord -> channelJoined = null | channelLeft = Gaming
* </pre>
*
* <p>
* This way, we make sure that all relevant voice channels are updated.
*
* @param channelJoined the channel that the member has connected to, if any
* @param channelLeft the channel that the member left from, if any
* @return the join channel if not null, otherwise the leave channel, otherwise an empty
* optional
*/
private Optional<Channel> selectPreferredAudioChannel(@Nullable AudioChannelUnion channelJoined,
@Nullable AudioChannelUnion channelLeft) {
if (channelJoined != null) {
return Optional.of(channelJoined);
}

return Optional.ofNullable(channelLeft);
}

@Override
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
selectPreferredAudioChannel(event.getChannelJoined(), event.getChannelLeft())
.ifPresent(channel -> getVoiceReceiversSubscribedTo(channel)
.forEach(voiceReceiver -> voiceReceiver.onVoiceUpdate(event)));
}

@Override
public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();

if (channel == null) {
return;
}

getVoiceReceiversSubscribedTo(channel)
.forEach(voiceReceiver -> voiceReceiver.onVideoToggle(event));
}

@Override
public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();

if (channel == null) {
return;
}

getVoiceReceiversSubscribedTo(channel)
.forEach(voiceReceiver -> voiceReceiver.onStreamToggle(event));
}

@Override
public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();

if (channel == null) {
return;
}

getVoiceReceiversSubscribedTo(channel)
.forEach(voiceReceiver -> voiceReceiver.onMuteToggle(event));
}

@Override
public void onGuildVoiceDeafen(@NotNull GuildVoiceDeafenEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();

if (channel == null) {
return;
}

getVoiceReceiversSubscribedTo(channel)
.forEach(voiceReceiver -> voiceReceiver.onDeafenToggle(event));
}

private Stream<MessageReceiver> getMessageReceiversSubscribedTo(Channel channel) {
String channelName = channel.getName();
return channelNameToMessageReceiver.entrySet()
Expand All @@ -248,6 +355,16 @@ private Stream<MessageReceiver> getMessageReceiversSubscribedTo(Channel channel)
.map(Map.Entry::getValue);
}

private Stream<VoiceReceiver> getVoiceReceiversSubscribedTo(Channel channel) {
String channelName = channel.getName();
return channelNameToVoiceReceiver.entrySet()
.stream()
.filter(patternAndReceiver -> patternAndReceiver.getKey()
.matcher(channelName)
.matches())
.map(Map.Entry::getValue);
}

@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
String name = event.getName();
Expand Down
Loading