Skip to content

Commit d6fabac

Browse files
authored
Merge pull request #495 from danthe1st/custom-vc
add custom voice channels
2 parents 919c061 + 81dd542 commit d6fabac

File tree

9 files changed

+422
-0
lines changed

9 files changed

+422
-0
lines changed

src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import net.dv8tion.jda.api.entities.Role;
77
import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel;
88
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
9+
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
910

1011
import java.util.List;
1112

@@ -92,6 +93,11 @@ public class ModerationConfig extends GuildConfigItem {
9293
* Invite links AutoMod should exclude.
9394
*/
9495
private List<String> automodInviteExcludes = List.of();
96+
97+
/**
98+
* The ID of the voice channel template that lets users create their own voice channels.
99+
*/
100+
private long customVoiceChannelId;
95101

96102
/**
97103
* Text that is sent to users when they're banned.
@@ -149,4 +155,8 @@ public Role getExpertRole() {
149155
public TextChannel getNotificationThreadChannel() {
150156
return this.getGuild().getTextChannelById(this.notificationThreadChannelId);
151157
}
158+
159+
public VoiceChannel getCustomVoiceChannel() {
160+
return this.getGuild().getVoiceChannelById(customVoiceChannelId);
161+
}
152162
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package net.discordjug.javabot.systems.custom_vc;
2+
3+
import java.util.function.BiConsumer;
4+
5+
import lombok.RequiredArgsConstructor;
6+
import net.discordjug.javabot.annotations.AutoDetectableComponentHandler;
7+
import net.discordjug.javabot.util.Responses;
8+
import net.dv8tion.jda.api.Permission;
9+
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
10+
import net.dv8tion.jda.api.interactions.components.buttons.Button;
11+
import net.dv8tion.jda.api.requests.restaction.PermissionOverrideAction;
12+
import xyz.dynxsty.dih4jda.interactions.components.ButtonHandler;
13+
import xyz.dynxsty.dih4jda.util.ComponentIdBuilder;
14+
15+
/**
16+
* Handles buttons for managing custom voice channels.
17+
*/
18+
@AutoDetectableComponentHandler(CustomVCButtonHandler.COMPONENT_ID)
19+
@RequiredArgsConstructor
20+
public class CustomVCButtonHandler implements ButtonHandler {
21+
static final String COMPONENT_ID = "custom-vc";
22+
23+
private static final String MAKE_PRIVATE_ID = "make-private";
24+
private static final String MAKE_PUBLIC_ID = "make-public";
25+
26+
private final CustomVCRepository repository;
27+
28+
public Button createMakePrivateButton() {
29+
return Button.primary(ComponentIdBuilder.build(COMPONENT_ID, MAKE_PRIVATE_ID), "make VC private");
30+
}
31+
32+
public Button createMakePublicButton() {
33+
return Button.primary(ComponentIdBuilder.build(COMPONENT_ID, MAKE_PUBLIC_ID), "make VC public");
34+
}
35+
36+
@Override
37+
public void handleButton(ButtonInteractionEvent event, Button button) {
38+
if(!repository.isCustomVoiceChannel(event.getChannelIdLong()) ||
39+
repository.getOwnerId(event.getChannelIdLong()) != event.getMember().getIdLong()) {
40+
Responses.error(event, "Only the VC owner can use this.").queue();
41+
return;
42+
}
43+
String[] id = ComponentIdBuilder.split(button.getId());
44+
switch (id[1]) {
45+
case MAKE_PRIVATE_ID -> changeVisibility(event, createMakePublicButton(), "This voice channel is now private.",
46+
PermissionOverrideAction::setDenied);
47+
48+
case MAKE_PUBLIC_ID -> changeVisibility(event, createMakePrivateButton(), "This voice channel is now public.",
49+
PermissionOverrideAction::setAllowed);
50+
default -> Responses.error(event, "Unknown button").queue();
51+
}
52+
}
53+
54+
private void changeVisibility(ButtonInteractionEvent event, Button newButton, String messageContent,
55+
BiConsumer<PermissionOverrideAction, Permission> permissionModifier) {
56+
PermissionOverrideAction permissionOverrideAction = event
57+
.getGuildChannel()
58+
.asVoiceChannel()
59+
.upsertPermissionOverride(event.getGuild().getPublicRole());
60+
permissionModifier.accept(permissionOverrideAction, Permission.VIEW_CHANNEL);
61+
permissionOverrideAction.queue();
62+
event
63+
.editButton(newButton)
64+
.flatMap(edited -> {
65+
return event
66+
.getHook()
67+
.setEphemeral(true)
68+
.sendMessage(messageContent);
69+
})
70+
.queue();
71+
}
72+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package net.discordjug.javabot.systems.custom_vc;
2+
3+
import java.util.List;
4+
5+
import lombok.RequiredArgsConstructor;
6+
import net.discordjug.javabot.data.config.BotConfig;
7+
import net.discordjug.javabot.util.ExceptionLogger;
8+
import net.dv8tion.jda.api.EmbedBuilder;
9+
import net.dv8tion.jda.api.Permission;
10+
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
11+
import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildChannel;
12+
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
13+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
14+
import net.dv8tion.jda.api.events.session.ReadyEvent;
15+
import net.dv8tion.jda.api.hooks.ListenerAdapter;
16+
import net.dv8tion.jda.api.requests.restaction.ChannelAction;
17+
18+
/**
19+
* 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.
20+
*/
21+
@RequiredArgsConstructor
22+
public class CustomVCListener extends ListenerAdapter {
23+
private final BotConfig botConfig;
24+
private final CustomVCRepository repository;
25+
private final CustomVCButtonHandler buttonHandler;
26+
27+
@Override
28+
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
29+
AudioChannelUnion vcJoined = event.getChannelJoined();
30+
long customVoiceChannelId = botConfig.get(event.getGuild()).getModerationConfig().getCustomVoiceChannelId();
31+
if (vcJoined != null && vcJoined.getIdLong() == customVoiceChannelId) {
32+
createCustomVC(event, vcJoined);
33+
}
34+
AudioChannelUnion vcLeft = event.getChannelLeft();
35+
if (vcLeft != null && repository.isCustomVoiceChannel(vcLeft.getIdLong()) && vcLeft.getMembers().isEmpty()) {
36+
vcLeft.delete().queue();
37+
}
38+
}
39+
40+
private void createCustomVC(GuildVoiceUpdateEvent event, AudioChannelUnion vcJoined) {
41+
ChannelAction<? extends StandardGuildChannel> copy = vcJoined.createCopy();
42+
copy
43+
.setName("custom-" + event.getMember().getId())
44+
.addMemberPermissionOverride(
45+
event.getMember().getIdLong(),
46+
List.of(Permission.MANAGE_CHANNEL, Permission.VIEW_CHANNEL),
47+
List.of())
48+
.queue(newChannel -> {
49+
repository.addCustomVoiceChannel(newChannel.getIdLong(), event.getMember().getIdLong());
50+
if (!(newChannel instanceof VoiceChannel newVC)) {
51+
ExceptionLogger.capture(new IllegalStateException("expected VoiceChannel to be created, got " + newChannel.getClass().getCanonicalName()));
52+
newChannel.delete().queue();
53+
return;
54+
}
55+
event.getGuild().moveVoiceMember(event.getMember(), newVC).queue();
56+
newVC.sendMessageEmbeds(new EmbedBuilder()
57+
.setTitle("Your personal Voice Channel")
58+
.setDescription("""
59+
This is your personal, temporary voice channel.
60+
You can configure this channel using the button below, the `/vc-control` command or the Discord settings.
61+
This channel will be deleted as soon as all people leave this channel.
62+
""")
63+
.build())
64+
.addContent(event.getMember().getAsMention())
65+
.addActionRow(buttonHandler.createMakePrivateButton())
66+
.queue();
67+
});
68+
}
69+
70+
@Override
71+
public void onReady(ReadyEvent event) {
72+
for (long channelId : repository.getAllCustomVoiceChannels()) {
73+
VoiceChannel vc = event.getJDA().getVoiceChannelById(channelId);
74+
if (vc == null) {
75+
repository.removeCustomVoiceChannel(channelId);
76+
} else if (vc.getMembers().isEmpty()) {
77+
vc.delete().queue();
78+
repository.removeCustomVoiceChannel(channelId);
79+
}
80+
}
81+
}
82+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package net.discordjug.javabot.systems.custom_vc;
2+
3+
import java.util.List;
4+
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.jdbc.core.JdbcTemplate;
7+
import org.springframework.stereotype.Repository;
8+
9+
/**
10+
* Stores currently active custom voice channels and the owners of these channels.
11+
*/
12+
@Repository
13+
@RequiredArgsConstructor
14+
public class CustomVCRepository {
15+
16+
private final JdbcTemplate jdbcTemplate;
17+
18+
/**
19+
* Stores a new custom voice channel.
20+
* @param id the channel ID
21+
* @param ownerId the ID of the owner of the voice channel
22+
*/
23+
public void addCustomVoiceChannel(long id, long ownerId) {
24+
jdbcTemplate.update("INSERT INTO custom_vc (channel_id, owner_id) VALUES (?, ?)",
25+
id, ownerId);
26+
}
27+
28+
/**
29+
* Removes a custom voice channel.
30+
* @param id the channel ID
31+
*/
32+
public void removeCustomVoiceChannel(long id) {
33+
jdbcTemplate.update("DELETE FROM custom_vc WHERE channel_id = ?", id);
34+
}
35+
36+
/**
37+
* Checks whether a channel is a custom voice channel.
38+
* @param id the channel ID
39+
* @return {@code true} if the channel is a custom voice channel, else {@code false}
40+
*/
41+
public boolean isCustomVoiceChannel(long id) {
42+
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM custom_vc WHERE channel_id = ?", (rs, rowId) -> rs.getInt(1), id) > 0;
43+
}
44+
45+
/**
46+
* Gets the owner of a custom voice channel.
47+
* @param voiceChannelId the ID of the voice channel
48+
* @return the ID of the owner of the custom voice channel
49+
*/
50+
public long getOwnerId(long voiceChannelId) {
51+
return jdbcTemplate.queryForObject("SELECT owner_id FROM custom_vc WHERE channel_id = ?",
52+
(rs, rowId) -> rs.getLong(1),
53+
voiceChannelId);
54+
}
55+
56+
/**
57+
* Gets all custom voice channels of all guilds.
58+
* @return a {@link List} of all custom voice channel IDs
59+
*/
60+
public List<Long> getAllCustomVoiceChannels() {
61+
return jdbcTemplate.query("SELECT channel_id FROM custom_vc", (rs, row) -> rs.getLong(1));
62+
}
63+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package net.discordjug.javabot.systems.custom_vc.commands;
2+
3+
import net.discordjug.javabot.data.config.BotConfig;
4+
import net.discordjug.javabot.systems.custom_vc.CustomVCRepository;
5+
import net.dv8tion.jda.api.Permission;
6+
import net.dv8tion.jda.api.entities.Member;
7+
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
8+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
9+
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
10+
11+
/**
12+
* Adds a member to a custom voice channel.
13+
*/
14+
public class CustomVCAddMemberSubcommand extends CustomVCChangeMembersSubcommand {
15+
16+
public CustomVCAddMemberSubcommand(CustomVCRepository dataStorage,
17+
BotConfig botConfig) {
18+
super(new SubcommandData("add-member", "adds a member to the voice channel"), dataStorage, botConfig);
19+
}
20+
21+
@Override
22+
protected void apply(VoiceChannel vc, Member member, SlashCommandInteractionEvent event) {
23+
vc.upsertPermissionOverride(member)
24+
.setAllowed(Permission.VIEW_CHANNEL)
25+
.queue();
26+
event
27+
.reply("Successfully added " + member.getAsMention() + " to " + vc.getAsMention() + ". They can now join the voice channel.")
28+
.setEphemeral(true)
29+
.queue();
30+
}
31+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package net.discordjug.javabot.systems.custom_vc.commands;
2+
3+
import net.discordjug.javabot.data.config.BotConfig;
4+
import net.discordjug.javabot.systems.custom_vc.CustomVCRepository;
5+
import net.discordjug.javabot.util.Responses;
6+
import net.dv8tion.jda.api.entities.GuildVoiceState;
7+
import net.dv8tion.jda.api.entities.Member;
8+
import net.dv8tion.jda.api.entities.channel.ChannelType;
9+
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
10+
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
11+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
12+
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
13+
import net.dv8tion.jda.api.interactions.commands.OptionType;
14+
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
15+
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
16+
17+
/**
18+
* Abstract/base subcommand for changing the allowed members in custom voice channels.
19+
*/
20+
abstract class CustomVCChangeMembersSubcommand extends SlashCommand.Subcommand {
21+
22+
protected final BotConfig botConfig;
23+
private final CustomVCRepository repository;
24+
25+
/**
26+
* The constructor of this class, which sets the corresponding {@link net.dv8tion.jda.api.interactions.commands.build.SubcommandData}.
27+
* @param subcommandData the configuration (name and description) of the subcommand
28+
* @param repository The repository storing information about custom voice channels
29+
* @param botConfig the main configuration of the bot
30+
*/
31+
protected CustomVCChangeMembersSubcommand(SubcommandData subcommandData, CustomVCRepository repository, BotConfig botConfig) {
32+
this.botConfig = botConfig;
33+
this.repository = repository;
34+
setCommandData(
35+
subcommandData
36+
.addOption(OptionType.USER, "member", "The member in question")
37+
);
38+
}
39+
40+
@Override
41+
public void execute(SlashCommandInteractionEvent event) {
42+
if (botConfig.get(event.getGuild()).getModerationConfig().getCustomVoiceChannelId() == 0) {
43+
Responses.error(event, "This feature is disabled.").queue();
44+
return;
45+
}
46+
VoiceChannel vc = getCustomVoiceChannel(event);
47+
if (vc == null) {
48+
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() + ".")
49+
.queue();
50+
return;
51+
}
52+
if (repository.getOwnerId(vc.getIdLong()) != event.getMember().getIdLong()) {
53+
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() + ".")
54+
.queue();
55+
return;
56+
}
57+
Member member = event.getOption("member", null, OptionMapping::getAsMember);
58+
if (member == null) {
59+
Responses.replyMissingArguments(event).queue();
60+
return;
61+
}
62+
63+
apply(vc, member, event);
64+
}
65+
66+
protected abstract void apply(VoiceChannel vc, Member member, SlashCommandInteractionEvent event);
67+
68+
private VoiceChannel getCustomVoiceChannel(SlashCommandInteractionEvent event) {
69+
if (repository.isCustomVoiceChannel(event.getChannelIdLong()) && event.getChannel().getType() == ChannelType.VOICE) {
70+
return event.getChannel().asVoiceChannel();
71+
}
72+
GuildVoiceState voiceState = event.getMember().getVoiceState();
73+
if (voiceState == null) {
74+
return null;
75+
}
76+
AudioChannelUnion channel = voiceState.getChannel();
77+
if (channel == null || channel.getType() != ChannelType.VOICE) {
78+
return null;
79+
}
80+
if (repository.isCustomVoiceChannel(voiceState.getChannel().getIdLong())) {
81+
return voiceState.getChannel().asVoiceChannel();
82+
}
83+
return null;
84+
}
85+
86+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package net.discordjug.javabot.systems.custom_vc.commands;
2+
3+
import net.dv8tion.jda.api.interactions.commands.build.Commands;
4+
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
5+
6+
/**
7+
* Command for managing custom voice channels.
8+
*/
9+
public class CustomVCControlCommand extends SlashCommand {
10+
/**
11+
* The constructor of this class, which sets the corresponding {@link net.dv8tion.jda.api.interactions.commands.build.SlashCommandData}.
12+
* @param addMemberSubcommand /vc-control add-member
13+
* @param removeMemberSubcommand /vc-control remove-member
14+
*/
15+
public CustomVCControlCommand(CustomVCAddMemberSubcommand addMemberSubcommand, CustomVCRemoveMemberSubcommand removeMemberSubcommand) {
16+
setCommandData(Commands.slash("vc-control", "Manages custom voice channels")
17+
.setGuildOnly(true)
18+
);
19+
addSubcommands(addMemberSubcommand, removeMemberSubcommand);
20+
}
21+
}

0 commit comments

Comments
 (0)