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
Expand Up @@ -6,6 +6,7 @@
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;

import java.util.List;

Expand Down Expand Up @@ -92,6 +93,11 @@ public class ModerationConfig extends GuildConfigItem {
* Invite links AutoMod should exclude.
*/
private List<String> automodInviteExcludes = List.of();

/**
* The ID of the voice channel template that lets users create their own voice channels.
*/
private long customVoiceChannelId;

/**
* Text that is sent to users when they're banned.
Expand Down Expand Up @@ -149,4 +155,8 @@ public Role getExpertRole() {
public TextChannel getNotificationThreadChannel() {
return this.getGuild().getTextChannelById(this.notificationThreadChannelId);
}

public VoiceChannel getCustomVoiceChannel() {
return this.getGuild().getVoiceChannelById(customVoiceChannelId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package net.discordjug.javabot.systems.custom_vc;

import java.util.function.BiConsumer;

import lombok.RequiredArgsConstructor;
import net.discordjug.javabot.annotations.AutoDetectableComponentHandler;
import net.discordjug.javabot.util.Responses;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.requests.restaction.PermissionOverrideAction;
import xyz.dynxsty.dih4jda.interactions.components.ButtonHandler;
import xyz.dynxsty.dih4jda.util.ComponentIdBuilder;

/**
* Handles buttons for managing custom voice channels.
*/
@AutoDetectableComponentHandler(CustomVCButtonHandler.COMPONENT_ID)
@RequiredArgsConstructor
public class CustomVCButtonHandler implements ButtonHandler {
static final String COMPONENT_ID = "custom-vc";

private static final String MAKE_PRIVATE_ID = "make-private";
private static final String MAKE_PUBLIC_ID = "make-public";

private final CustomVCRepository repository;

public Button createMakePrivateButton() {
return Button.primary(ComponentIdBuilder.build(COMPONENT_ID, MAKE_PRIVATE_ID), "make VC private");
}

public Button createMakePublicButton() {
return Button.primary(ComponentIdBuilder.build(COMPONENT_ID, MAKE_PUBLIC_ID), "make VC public");
}

@Override
public void handleButton(ButtonInteractionEvent event, Button button) {
if(!repository.isCustomVoiceChannel(event.getChannelIdLong()) ||
repository.getOwnerId(event.getChannelIdLong()) != event.getMember().getIdLong()) {
Responses.error(event, "Only the VC owner can use this.").queue();
return;
}
String[] id = ComponentIdBuilder.split(button.getId());
switch (id[1]) {
case MAKE_PRIVATE_ID -> changeVisibility(event, createMakePublicButton(), "This voice channel is now private.",
PermissionOverrideAction::setDenied);

case MAKE_PUBLIC_ID -> changeVisibility(event, createMakePrivateButton(), "This voice channel is now public.",
PermissionOverrideAction::setAllowed);
default -> Responses.error(event, "Unknown button").queue();
}
}

private void changeVisibility(ButtonInteractionEvent event, Button newButton, String messageContent,
BiConsumer<PermissionOverrideAction, Permission> permissionModifier) {
PermissionOverrideAction permissionOverrideAction = event
.getGuildChannel()
.asVoiceChannel()
.upsertPermissionOverride(event.getGuild().getPublicRole());
permissionModifier.accept(permissionOverrideAction, Permission.VIEW_CHANNEL);
permissionOverrideAction.queue();
event
.editButton(newButton)
.flatMap(edited -> {
return event
.getHook()
.setEphemeral(true)
.sendMessage(messageContent);
})
.queue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package net.discordjug.javabot.systems.custom_vc;

import java.util.List;

import lombok.RequiredArgsConstructor;
import net.discordjug.javabot.data.config.BotConfig;
import net.discordjug.javabot.util.ExceptionLogger;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
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 net.dv8tion.jda.api.events.session.ReadyEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.requests.restaction.ChannelAction;

/**
* Automatically creates a temporary voice channel when a user joins the {@link ModerationConfig#getCustomVoiceChannel() custom voice channel template} and deletes that voice channel when there are no users left.
*/
@RequiredArgsConstructor
public class CustomVCListener extends ListenerAdapter {
private final BotConfig botConfig;
private final CustomVCRepository repository;
private final CustomVCButtonHandler buttonHandler;

@Override
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
AudioChannelUnion vcJoined = event.getChannelJoined();
long customVoiceChannelId = botConfig.get(event.getGuild()).getModerationConfig().getCustomVoiceChannelId();
if (vcJoined != null && vcJoined.getIdLong() == customVoiceChannelId) {
createCustomVC(event, vcJoined);
}
AudioChannelUnion vcLeft = event.getChannelLeft();
if (vcLeft != null && repository.isCustomVoiceChannel(vcLeft.getIdLong()) && vcLeft.getMembers().isEmpty()) {
vcLeft.delete().queue();
}
}

private void createCustomVC(GuildVoiceUpdateEvent event, AudioChannelUnion vcJoined) {
ChannelAction<? extends StandardGuildChannel> copy = vcJoined.createCopy();
copy
.setName("custom-" + event.getMember().getId())
.addMemberPermissionOverride(
event.getMember().getIdLong(),
List.of(Permission.MANAGE_CHANNEL, Permission.VIEW_CHANNEL),
List.of())
.queue(newChannel -> {
repository.addCustomVoiceChannel(newChannel.getIdLong(), event.getMember().getIdLong());
if (!(newChannel instanceof VoiceChannel newVC)) {
ExceptionLogger.capture(new IllegalStateException("expected VoiceChannel to be created, got " + newChannel.getClass().getCanonicalName()));
newChannel.delete().queue();
return;
}
event.getGuild().moveVoiceMember(event.getMember(), newVC).queue();
newVC.sendMessageEmbeds(new EmbedBuilder()
.setTitle("Your personal Voice Channel")
.setDescription("""
This is your personal, temporary voice channel.
You can configure this channel using the button below, the `/vc-control` command or the Discord settings.
This channel will be deleted as soon as all people leave this channel.
""")
.build())
.addContent(event.getMember().getAsMention())
.addActionRow(buttonHandler.createMakePrivateButton())
.queue();
});
}

@Override
public void onReady(ReadyEvent event) {
for (long channelId : repository.getAllCustomVoiceChannels()) {
VoiceChannel vc = event.getJDA().getVoiceChannelById(channelId);
if (vc == null) {
repository.removeCustomVoiceChannel(channelId);
} else if (vc.getMembers().isEmpty()) {
vc.delete().queue();
repository.removeCustomVoiceChannel(channelId);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package net.discordjug.javabot.systems.custom_vc;

import java.util.List;

import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

/**
* Stores currently active custom voice channels and the owners of these channels.
*/
@Repository
@RequiredArgsConstructor
public class CustomVCRepository {

private final JdbcTemplate jdbcTemplate;

/**
* Stores a new custom voice channel.
* @param id the channel ID
* @param ownerId the ID of the owner of the voice channel
*/
public void addCustomVoiceChannel(long id, long ownerId) {
jdbcTemplate.update("INSERT INTO custom_vc (channel_id, owner_id) VALUES (?, ?)",
id, ownerId);
}

/**
* Removes a custom voice channel.
* @param id the channel ID
*/
public void removeCustomVoiceChannel(long id) {
jdbcTemplate.update("DELETE FROM custom_vc WHERE channel_id = ?", id);
}

/**
* Checks whether a channel is a custom voice channel.
* @param id the channel ID
* @return {@code true} if the channel is a custom voice channel, else {@code false}
*/
public boolean isCustomVoiceChannel(long id) {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM custom_vc WHERE channel_id = ?", (rs, rowId) -> rs.getInt(1), id) > 0;
}

/**
* Gets the owner of a custom voice channel.
* @param voiceChannelId the ID of the voice channel
* @return the ID of the owner of the custom voice channel
*/
public long getOwnerId(long voiceChannelId) {
return jdbcTemplate.queryForObject("SELECT owner_id FROM custom_vc WHERE channel_id = ?",
(rs, rowId) -> rs.getLong(1),
voiceChannelId);
}

/**
* Gets all custom voice channels of all guilds.
* @return a {@link List} of all custom voice channel IDs
*/
public List<Long> getAllCustomVoiceChannels() {
return jdbcTemplate.query("SELECT channel_id FROM custom_vc", (rs, row) -> rs.getLong(1));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.discordjug.javabot.systems.custom_vc.commands;

import net.discordjug.javabot.data.config.BotConfig;
import net.discordjug.javabot.systems.custom_vc.CustomVCRepository;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;

/**
* Adds a member to a custom voice channel.
*/
public class CustomVCAddMemberSubcommand extends CustomVCChangeMembersSubcommand {

public CustomVCAddMemberSubcommand(CustomVCRepository dataStorage,
BotConfig botConfig) {
super(new SubcommandData("add-member", "adds a member to the voice channel"), dataStorage, botConfig);
}

@Override
protected void apply(VoiceChannel vc, Member member, SlashCommandInteractionEvent event) {
vc.upsertPermissionOverride(member)
.setAllowed(Permission.VIEW_CHANNEL)
.queue();
event
.reply("Successfully added " + member.getAsMention() + " to " + vc.getAsMention() + ". They can now join the voice channel.")
.setEphemeral(true)
.queue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package net.discordjug.javabot.systems.custom_vc.commands;

import net.discordjug.javabot.data.config.BotConfig;
import net.discordjug.javabot.systems.custom_vc.CustomVCRepository;
import net.discordjug.javabot.util.Responses;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;

/**
* Abstract/base subcommand for changing the allowed members in custom voice channels.
*/
abstract class CustomVCChangeMembersSubcommand extends SlashCommand.Subcommand {

protected final BotConfig botConfig;
private final CustomVCRepository repository;

/**
* The constructor of this class, which sets the corresponding {@link net.dv8tion.jda.api.interactions.commands.build.SubcommandData}.
* @param subcommandData the configuration (name and description) of the subcommand
* @param repository The repository storing information about custom voice channels
* @param botConfig the main configuration of the bot
*/
protected CustomVCChangeMembersSubcommand(SubcommandData subcommandData, CustomVCRepository repository, BotConfig botConfig) {
this.botConfig = botConfig;
this.repository = repository;
setCommandData(
subcommandData
.addOption(OptionType.USER, "member", "The member in question")
);
}

@Override
public void execute(SlashCommandInteractionEvent event) {
if (botConfig.get(event.getGuild()).getModerationConfig().getCustomVoiceChannelId() == 0) {
Responses.error(event, "This feature is disabled.").queue();
return;
}
VoiceChannel vc = getCustomVoiceChannel(event);
if (vc == null) {
Responses.error(event, "This command be used in custom voice channels only. You can create a custom voice channel by joining " + botConfig.get(event.getGuild()).getModerationConfig().getCustomVoiceChannel().getAsMention() + ".")
.queue();
return;
}
if (repository.getOwnerId(vc.getIdLong()) != event.getMember().getIdLong()) {
Responses.error(event, "Only the owner of the custom voice channel can use this command. You can create your own custom voice channel by joining " + botConfig.get(event.getGuild()).getModerationConfig().getCustomVoiceChannel().getAsMention() + ".")
.queue();
return;
}
Member member = event.getOption("member", null, OptionMapping::getAsMember);
if (member == null) {
Responses.replyMissingArguments(event).queue();
return;
}

apply(vc, member, event);
}

protected abstract void apply(VoiceChannel vc, Member member, SlashCommandInteractionEvent event);

private VoiceChannel getCustomVoiceChannel(SlashCommandInteractionEvent event) {
if (repository.isCustomVoiceChannel(event.getChannelIdLong()) && event.getChannel().getType() == ChannelType.VOICE) {
return event.getChannel().asVoiceChannel();
}
GuildVoiceState voiceState = event.getMember().getVoiceState();
if (voiceState == null) {
return null;
}
AudioChannelUnion channel = voiceState.getChannel();
if (channel == null || channel.getType() != ChannelType.VOICE) {
return null;
}
if (repository.isCustomVoiceChannel(voiceState.getChannel().getIdLong())) {
return voiceState.getChannel().asVoiceChannel();
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.discordjug.javabot.systems.custom_vc.commands;

import net.dv8tion.jda.api.interactions.commands.build.Commands;
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;

/**
* Command for managing custom voice channels.
*/
public class CustomVCControlCommand extends SlashCommand {
/**
* The constructor of this class, which sets the corresponding {@link net.dv8tion.jda.api.interactions.commands.build.SlashCommandData}.
* @param addMemberSubcommand /vc-control add-member
* @param removeMemberSubcommand /vc-control remove-member
*/
public CustomVCControlCommand(CustomVCAddMemberSubcommand addMemberSubcommand, CustomVCRemoveMemberSubcommand removeMemberSubcommand) {
setCommandData(Commands.slash("vc-control", "Manages custom voice channels")
.setGuildOnly(true)
);
addSubcommands(addMemberSubcommand, removeMemberSubcommand);
}
}
Loading