From 88282121301874ec2303eeb33c50fbdeefefee11 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sat, 25 Sep 2021 23:21:39 +0200 Subject: [PATCH 1/3] Added beginning of stateless help channel management. --- .../java/com/javadiscord/javabot/Bot.java | 7 ++- .../javabot/help/HelpChannelListener.java | 29 +++++++++++ .../javabot/help/HelpChannelManager.java | 38 ++++++++++++++ .../javabot/help/HelpChannelUpdater.java | 49 +++++++++++++++++++ .../properties/config/GuildConfig.java | 3 ++ .../properties/config/guild/HelpConfig.java | 38 ++++++++++++++ 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/javadiscord/javabot/help/HelpChannelListener.java create mode 100644 src/main/java/com/javadiscord/javabot/help/HelpChannelManager.java create mode 100644 src/main/java/com/javadiscord/javabot/help/HelpChannelUpdater.java create mode 100644 src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java diff --git a/src/main/java/com/javadiscord/javabot/Bot.java b/src/main/java/com/javadiscord/javabot/Bot.java index b09cf599c..e74005be2 100644 --- a/src/main/java/com/javadiscord/javabot/Bot.java +++ b/src/main/java/com/javadiscord/javabot/Bot.java @@ -2,6 +2,8 @@ import com.javadiscord.javabot.data.H2DataSource; import com.javadiscord.javabot.events.*; +import com.javadiscord.javabot.help.HelpChannelListener; +import com.javadiscord.javabot.help.HelpChannelUpdater; import com.javadiscord.javabot.properties.config.BotConfig; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; @@ -16,6 +18,7 @@ import java.util.TimeZone; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; /** * The main class where the bot is initialized. @@ -74,6 +77,7 @@ public static void main(String[] args) throws Exception { .addEventListeners(slashCommands) .build(); addEventListeners(jda); + asyncPool.scheduleAtFixedRate(new HelpChannelUpdater(jda), 1, 1, TimeUnit.SECONDS); } /** @@ -92,7 +96,8 @@ private static void addEventListeners(JDA jda) { new AutoMod(), new SubmissionListener(), new StarboardListener(), - new InteractionListener() + new InteractionListener(), + new HelpChannelListener() ); } } diff --git a/src/main/java/com/javadiscord/javabot/help/HelpChannelListener.java b/src/main/java/com/javadiscord/javabot/help/HelpChannelListener.java new file mode 100644 index 000000000..1e29f71b2 --- /dev/null +++ b/src/main/java/com/javadiscord/javabot/help/HelpChannelListener.java @@ -0,0 +1,29 @@ +package com.javadiscord.javabot.help; + +import com.javadiscord.javabot.Bot; +import net.dv8tion.jda.api.entities.Category; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +/** + * This listener is responsible for handling messages that are sent in one or + * more designated help channels. + */ +public class HelpChannelListener extends ListenerAdapter { + @Override + public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) { + if (event.getAuthor().isBot() || event.getAuthor().isSystem()) return; + + var config = Bot.config.get(event.getGuild()).getHelp(); + TextChannel channel = event.getChannel(); + Category category = channel.getParent(); + if (category == null) return; + if (channel.getName().startsWith(config.getOpenChannelPrefix())) { + String rawChannelName = channel.getName().substring(config.getOpenChannelPrefix().length()); + channel.getManager().setName(config.getReservedChannelPrefix() + rawChannelName).queue(); + channel.getManager().setPosition(category.getTextChannels().size()).queue(); + } + } +} diff --git a/src/main/java/com/javadiscord/javabot/help/HelpChannelManager.java b/src/main/java/com/javadiscord/javabot/help/HelpChannelManager.java new file mode 100644 index 000000000..62cfda79d --- /dev/null +++ b/src/main/java/com/javadiscord/javabot/help/HelpChannelManager.java @@ -0,0 +1,38 @@ +package com.javadiscord.javabot.help; + +import com.javadiscord.javabot.properties.config.guild.HelpConfig; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.TextChannel; + +import java.util.List; + +public class HelpChannelManager { + /** + * Opens a text channel so that it is ready for a new question. + * @param channel The channel to open. + * @param config The configuration properties. + */ + public void open(TextChannel channel, HelpConfig config) { + String rawName = channel.getName().substring(config.getReservedChannelPrefix().length()); + channel.getManager().setName(config.getOpenChannelPrefix() + rawName).queue(); + channel.getManager().setPosition(0).queue(); + removeAllMessages(channel); + } + + /** + * Utility method to remove all messages from a channel. + * @param channel The channel to remove messages from. + */ + private void removeAllMessages(TextChannel channel) { + List messages; + do { + messages = channel.getHistory().retrievePast(50).complete(); + if (messages.isEmpty()) break; + if (messages.size() == 1) { + channel.deleteMessageById(messages.get(0).getIdLong()).complete(); + } else { + channel.deleteMessages(messages).complete(); + } + } while (!messages.isEmpty()); + } +} diff --git a/src/main/java/com/javadiscord/javabot/help/HelpChannelUpdater.java b/src/main/java/com/javadiscord/javabot/help/HelpChannelUpdater.java new file mode 100644 index 000000000..8224d00fc --- /dev/null +++ b/src/main/java/com/javadiscord/javabot/help/HelpChannelUpdater.java @@ -0,0 +1,49 @@ +package com.javadiscord.javabot.help; + +import com.javadiscord.javabot.Bot; +import com.javadiscord.javabot.properties.config.guild.HelpConfig; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.User; + +import java.time.OffsetDateTime; + +public class HelpChannelUpdater implements Runnable { + private final JDA jda; + private final HelpChannelManager channelManager; + + public HelpChannelUpdater(JDA jda) { + this.jda = jda; + this.channelManager = new HelpChannelManager(); + } + + + @Override + public void run() { + for (var guild : this.jda.getGuilds()) { + var config = Bot.config.get(guild).getHelp(); + var channels = guild.getTextChannels(); + for (var channel : channels) { + if (channel.getName().startsWith(config.getReservedChannelPrefix())) { + this.checkReservedChannel(channel, config); + } + } + } + } + + private void checkReservedChannel(TextChannel channel, HelpConfig config) { + channel.getHistoryFromBeginning(1).queue(history -> { + if (history.isEmpty()) { + // Revert to open channel. + this.channelManager.open(channel, config); + } else { + Message firstMsg = history.getRetrievedHistory().get(0); + var channelBecomesInactiveAt = firstMsg.getTimeCreated().plusSeconds(config.getInactivityTimeoutSeconds()); + if (OffsetDateTime.now().isAfter(channelBecomesInactiveAt)) { + User user = firstMsg.getAuthor(); + } + } + }); + } +} diff --git a/src/main/java/com/javadiscord/javabot/properties/config/GuildConfig.java b/src/main/java/com/javadiscord/javabot/properties/config/GuildConfig.java index 057f776a5..dcfbe58ef 100644 --- a/src/main/java/com/javadiscord/javabot/properties/config/GuildConfig.java +++ b/src/main/java/com/javadiscord/javabot/properties/config/GuildConfig.java @@ -27,6 +27,7 @@ public class GuildConfig { private transient Path file; private SlashCommandConfig slashCommand; + private HelpConfig help; private ModerationConfig moderation; private QOTWConfig qotw; private WelcomeConfig welcome; @@ -38,6 +39,7 @@ public GuildConfig(Guild guild, Path file) { this.file = file; // Initialize all config items. this.slashCommand = new SlashCommandConfig(); + this.help = new HelpConfig(); this.moderation = new ModerationConfig(); this.qotw = new QOTWConfig(); this.welcome = new WelcomeConfig(); @@ -50,6 +52,7 @@ public GuildConfig(Guild guild, Path file) { private void setGuild(Guild guild) { this.guild = guild; this.slashCommand.setGuildConfig(this); + this.help.setGuildConfig(this); this.moderation.setGuildConfig(this); this.qotw.setGuildConfig(this); this.welcome.setGuildConfig(this); diff --git a/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java b/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java new file mode 100644 index 000000000..64f5892e2 --- /dev/null +++ b/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java @@ -0,0 +1,38 @@ +package com.javadiscord.javabot.properties.config.guild; + +import com.javadiscord.javabot.properties.config.GuildConfigItem; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class HelpConfig extends GuildConfigItem { + /** + * The string which is prefixed to any open help channel, where users are + * free to ask a question. + */ + private String openChannelPrefix = "\uD83D\uDFE2"; + + /** + * The string which is prefixed to any reserved help channel, where a user + * has already asked a question and is in the process of getting an answer. + */ + private String reservedChannelPrefix = "⛔"; + + /** + * The string which is prefixed to any inactive reserved help channel, where + * a user has asked a question but the channel has been inactive for a set + * amount of time. + */ + private String inactiveChannelPrefix = "\uD83D\uDFE0"; + + /** + * The number of seconds of inactivity before a channel is considered inactive. + */ + private int inactivityTimeoutSeconds = 1_800; + + /** + * The number of seconds to wait between each help channel update check. + */ + private long updateIntervalSeconds = 60; +} From 1bb2758e08d92ecf2f46ee4eaf738951dfa3af15 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sun, 26 Sep 2021 11:36:09 +0200 Subject: [PATCH 2/3] Added naming strategies and complete implementation. --- .../java/com/javadiscord/javabot/Bot.java | 3 - .../javabot/events/InteractionListener.java | 35 +++++- .../javadiscord/javabot/events/Startup.java | 5 + .../javabot/help/AlphabetNamingStrategy.java | 20 +++ .../javabot/help/AnimalNamingStrategy.java | 25 ++++ .../javabot/help/ChannelNamingStrategy.java | 13 ++ .../javabot/help/HelpChannelListener.java | 10 +- .../javabot/help/HelpChannelManager.java | 83 ++++++++---- .../javabot/help/HelpChannelUpdater.java | 119 ++++++++++++++---- .../properties/config/GuildConfig.java | 8 ++ .../properties/config/guild/HelpConfig.java | 45 ++++++- 11 files changed, 312 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/javadiscord/javabot/help/AlphabetNamingStrategy.java create mode 100644 src/main/java/com/javadiscord/javabot/help/AnimalNamingStrategy.java create mode 100644 src/main/java/com/javadiscord/javabot/help/ChannelNamingStrategy.java diff --git a/src/main/java/com/javadiscord/javabot/Bot.java b/src/main/java/com/javadiscord/javabot/Bot.java index e74005be2..2b5693aa9 100644 --- a/src/main/java/com/javadiscord/javabot/Bot.java +++ b/src/main/java/com/javadiscord/javabot/Bot.java @@ -3,7 +3,6 @@ import com.javadiscord.javabot.data.H2DataSource; import com.javadiscord.javabot.events.*; import com.javadiscord.javabot.help.HelpChannelListener; -import com.javadiscord.javabot.help.HelpChannelUpdater; import com.javadiscord.javabot.properties.config.BotConfig; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; @@ -18,7 +17,6 @@ import java.util.TimeZone; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; /** * The main class where the bot is initialized. @@ -77,7 +75,6 @@ public static void main(String[] args) throws Exception { .addEventListeners(slashCommands) .build(); addEventListeners(jda); - asyncPool.scheduleAtFixedRate(new HelpChannelUpdater(jda), 1, 1, TimeUnit.SECONDS); } /** diff --git a/src/main/java/com/javadiscord/javabot/events/InteractionListener.java b/src/main/java/com/javadiscord/javabot/events/InteractionListener.java index 7ef04465f..619c24e4f 100644 --- a/src/main/java/com/javadiscord/javabot/events/InteractionListener.java +++ b/src/main/java/com/javadiscord/javabot/events/InteractionListener.java @@ -2,11 +2,12 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.javadiscord.javabot.Bot; +import com.javadiscord.javabot.help.HelpChannelManager; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.Role; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.bson.Document; @@ -15,6 +16,7 @@ import static com.javadiscord.javabot.events.Startup.preferredGuild; import static com.mongodb.client.model.Filters.eq; +@Slf4j public class InteractionListener extends ListenerAdapter { // TODO: add Context-Menu Commands (once they're available in JDA) @@ -31,6 +33,7 @@ public void onButtonClick(ButtonClickEvent event) { case "dm-submission" -> this.handleDmSubmission(database, guild, event); case "submission" -> this.handleSubmission(database, guild, event); case "reaction-role" -> this.handleReactionRoles(event); + case "help-channel" -> this.handleHelpChannel(event, id[1]); } } @@ -81,4 +84,30 @@ private void handleReactionRoles(ButtonClickEvent event) { event.getHook().sendMessage("Added Role: " + role.getAsMention()).setEphemeral(true).queue(); } } + + private void handleHelpChannel(ButtonClickEvent event, String action) { + var config = Bot.config.get(event.getGuild()).getHelp(); + var channelManager = new HelpChannelManager(config); + TextChannel channel = event.getTextChannel(); + User owner = channelManager.getReservedChannelOwner(channel); + if (owner == null) { + return; // This channel will be pruned automatically. + } + + if ( + event.getUser().equals(owner) || + (event.getMember() != null && event.getMember().getRoles().contains(Bot.config.get(event.getGuild()).getModeration().getStaffRole())) + ) { + if (action.equals("done")) { + log.info("Removing reserved channel {} because it was marked as done.", channel.getAsMention()); + channel.delete().queue(); + } else if (action.equals("not-done")) { + if (event.getMessage() != null) { + log.info("Removing timeout check message in {} because it was marked as not-done.", channel.getAsMention()); + event.getMessage().delete().queue(); + channel.sendMessage("Okay, we'll keep this channel reserved for you, and check again in **" + config.getInactivityTimeoutMinutes() + "** minutes.").queue(); + } + } + } + } } diff --git a/src/main/java/com/javadiscord/javabot/events/Startup.java b/src/main/java/com/javadiscord/javabot/events/Startup.java index aa944e4df..3eb2569fc 100644 --- a/src/main/java/com/javadiscord/javabot/events/Startup.java +++ b/src/main/java/com/javadiscord/javabot/events/Startup.java @@ -5,6 +5,7 @@ import ch.qos.logback.classic.LoggerContext; import com.javadiscord.javabot.Bot; import com.javadiscord.javabot.commands.other.Version; +import com.javadiscord.javabot.help.HelpChannelUpdater; import com.javadiscord.javabot.other.Database; import com.javadiscord.javabot.other.Misc; import com.mongodb.MongoClient; @@ -92,6 +93,10 @@ public void onReady(ReadyEvent event) { new Database().deleteOpenSubmissions(guild); new StarboardListener().updateAllSBM(guild); Bot.slashCommands.registerSlashCommands(guild); + + // Schedule the help channel updater to run periodically for each guild. + var helpConfig = Bot.config.get(guild).getHelp(); + Bot.asyncPool.scheduleAtFixedRate(new HelpChannelUpdater(event.getJDA(), helpConfig), 5, helpConfig.getUpdateIntervalSeconds(), TimeUnit.SECONDS); } diff --git a/src/main/java/com/javadiscord/javabot/help/AlphabetNamingStrategy.java b/src/main/java/com/javadiscord/javabot/help/AlphabetNamingStrategy.java new file mode 100644 index 000000000..f153b8cde --- /dev/null +++ b/src/main/java/com/javadiscord/javabot/help/AlphabetNamingStrategy.java @@ -0,0 +1,20 @@ +package com.javadiscord.javabot.help; + +import com.javadiscord.javabot.properties.config.guild.HelpConfig; +import net.dv8tion.jda.api.entities.TextChannel; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Naming strategy that names help channels with a random letter. Note that the + * chance for duplicates is quite high! + */ +public class AlphabetNamingStrategy implements ChannelNamingStrategy { + private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + @Override + public String getName(List channels, HelpConfig config) { + return config.getOpenChannelPrefix() + "help-" + ALPHABET.charAt(ThreadLocalRandom.current().nextInt(ALPHABET.length())); + } +} diff --git a/src/main/java/com/javadiscord/javabot/help/AnimalNamingStrategy.java b/src/main/java/com/javadiscord/javabot/help/AnimalNamingStrategy.java new file mode 100644 index 000000000..2751eda23 --- /dev/null +++ b/src/main/java/com/javadiscord/javabot/help/AnimalNamingStrategy.java @@ -0,0 +1,25 @@ +package com.javadiscord.javabot.help; + +import com.javadiscord.javabot.properties.config.guild.HelpConfig; +import net.dv8tion.jda.api.entities.TextChannel; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * A naming strategy that names channels with a random animal name. + */ +public class AnimalNamingStrategy implements ChannelNamingStrategy { + private static final String[] ANIMALS = { + "bear", "tiger", "lion", "snake", "cheetah", "panther", "bat", "mosquito", "opossum", "raccoon", "beaver", + "walrus", "seal", "dolphin", "shark", "narwhal", "orca", "whale", "squid", "tuna", "nautilus", "jellyfish", + "seagull", "eagle", "hawk", "flamingo", "spoonbill", "puffin", "condor", "albatross", "parrot", "parakeet", + "rabbit", "sloth", "deer", "boar", "ferret", "dog", "cat", "marmoset", "mole", "lizard", "kangaroo" + }; + + @Override + public String getName(List channels, HelpConfig config) { + String name = ANIMALS[ThreadLocalRandom.current().nextInt(ANIMALS.length)]; + return config.getOpenChannelPrefix() + "help-" + name; + } +} diff --git a/src/main/java/com/javadiscord/javabot/help/ChannelNamingStrategy.java b/src/main/java/com/javadiscord/javabot/help/ChannelNamingStrategy.java new file mode 100644 index 000000000..4dc7f879a --- /dev/null +++ b/src/main/java/com/javadiscord/javabot/help/ChannelNamingStrategy.java @@ -0,0 +1,13 @@ +package com.javadiscord.javabot.help; + +import com.javadiscord.javabot.properties.config.guild.HelpConfig; +import net.dv8tion.jda.api.entities.TextChannel; + +import java.util.List; + +/** + * A strategy to use to generate names for new help channels as they're needed. + */ +public interface ChannelNamingStrategy { + String getName(List channels, HelpConfig config); +} diff --git a/src/main/java/com/javadiscord/javabot/help/HelpChannelListener.java b/src/main/java/com/javadiscord/javabot/help/HelpChannelListener.java index 1e29f71b2..406e05944 100644 --- a/src/main/java/com/javadiscord/javabot/help/HelpChannelListener.java +++ b/src/main/java/com/javadiscord/javabot/help/HelpChannelListener.java @@ -12,6 +12,7 @@ * more designated help channels. */ public class HelpChannelListener extends ListenerAdapter { + @Override public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) { if (event.getAuthor().isBot() || event.getAuthor().isSystem()) return; @@ -19,11 +20,12 @@ public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) { var config = Bot.config.get(event.getGuild()).getHelp(); TextChannel channel = event.getChannel(); Category category = channel.getParent(); - if (category == null) return; + if (category == null || !category.equals(config.getHelpChannelCategory())) return; + var channelManager = new HelpChannelManager(config); + + // If a message was sent in an open text channel, reserve it. if (channel.getName().startsWith(config.getOpenChannelPrefix())) { - String rawChannelName = channel.getName().substring(config.getOpenChannelPrefix().length()); - channel.getManager().setName(config.getReservedChannelPrefix() + rawChannelName).queue(); - channel.getManager().setPosition(category.getTextChannels().size()).queue(); + channelManager.reserve(channel, event.getAuthor()); } } } diff --git a/src/main/java/com/javadiscord/javabot/help/HelpChannelManager.java b/src/main/java/com/javadiscord/javabot/help/HelpChannelManager.java index 62cfda79d..bd48e0d82 100644 --- a/src/main/java/com/javadiscord/javabot/help/HelpChannelManager.java +++ b/src/main/java/com/javadiscord/javabot/help/HelpChannelManager.java @@ -1,38 +1,79 @@ package com.javadiscord.javabot.help; import com.javadiscord.javabot.properties.config.guild.HelpConfig; -import net.dv8tion.jda.api.entities.Message; +import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.User; -import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +/** + * This manager is responsible for all the main interactions that affect the + * help system's channels. + */ +@Slf4j public class HelpChannelManager { + private final HelpConfig config; + + public HelpChannelManager(HelpConfig config) { + this.config = config; + } + + public boolean isOpen(TextChannel channel) { + return channel.getName().startsWith(config.getOpenChannelPrefix()); + } + + public boolean isReserved(TextChannel channel) { + return channel.getName().startsWith(config.getReservedChannelPrefix()); + } + /** * Opens a text channel so that it is ready for a new question. - * @param channel The channel to open. - * @param config The configuration properties. */ - public void open(TextChannel channel, HelpConfig config) { - String rawName = channel.getName().substring(config.getReservedChannelPrefix().length()); - channel.getManager().setName(config.getOpenChannelPrefix() + rawName).queue(); - channel.getManager().setPosition(0).queue(); - removeAllMessages(channel); + public void openNew() { + var category = config.getHelpChannelCategory(); + if (category == null) throw new IllegalStateException("Missing help channel category. Cannot open a new help channel."); + String name = this.config.getChannelNamingStrategy().getName(category.getTextChannels(), config); + category.createTextChannel(name).queue(channel -> { + channel.getManager().setPosition(0).setTopic("Ask a question here!").queue(); + log.info("Created new help channel {}.", channel.getAsMention()); + }); + } + + /** + * Reserves a text channel for a user. + * @param channel The channel to reserve. + * @param reservingUser The user who is reserving the channel. + */ + public void reserve(TextChannel channel, User reservingUser) { + String rawChannelName = channel.getName().substring(config.getOpenChannelPrefix().length()); + channel.getManager() + .setName(config.getReservedChannelPrefix() + rawChannelName) + .setPosition(Objects.requireNonNull(channel.getParent()).getTextChannels().size()) + .setTopic(String.format( + "Reserved for %s\n(_id=%s_)", + reservingUser.getAsTag(), + reservingUser.getId() + )).queue(); + log.info("Reserved channel {} for {}.", channel.getAsMention(), reservingUser.getAsTag()); + openNew(); // Open a new channel immediately, to keep things balanced. } /** - * Utility method to remove all messages from a channel. - * @param channel The channel to remove messages from. + * Gets the owner of a reserved channel. + * @param channel The channel to get the owner of. + * @return The user who reserved the channel, or null. */ - private void removeAllMessages(TextChannel channel) { - List messages; - do { - messages = channel.getHistory().retrievePast(50).complete(); - if (messages.isEmpty()) break; - if (messages.size() == 1) { - channel.deleteMessageById(messages.get(0).getIdLong()).complete(); - } else { - channel.deleteMessages(messages).complete(); + public User getReservedChannelOwner(TextChannel channel) { + var pattern = Pattern.compile("\\(_id=(\\d+)_\\)"); + if (channel.getTopic() != null) { + var matcher = pattern.matcher(channel.getTopic()); + if (matcher.find()) { + String id = matcher.group(1); + return channel.getJDA().retrieveUserById(id).complete(); } - } while (!messages.isEmpty()); + } + return null; } } diff --git a/src/main/java/com/javadiscord/javabot/help/HelpChannelUpdater.java b/src/main/java/com/javadiscord/javabot/help/HelpChannelUpdater.java index 8224d00fc..0c379fbd2 100644 --- a/src/main/java/com/javadiscord/javabot/help/HelpChannelUpdater.java +++ b/src/main/java/com/javadiscord/javabot/help/HelpChannelUpdater.java @@ -1,49 +1,126 @@ package com.javadiscord.javabot.help; -import com.javadiscord.javabot.Bot; import com.javadiscord.javabot.properties.config.guild.HelpConfig; +import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; +import net.dv8tion.jda.internal.interactions.ButtonImpl; import java.time.OffsetDateTime; +import java.util.concurrent.TimeUnit; +/** + * Task that updates all help channels in a particular guild. + */ +@Slf4j public class HelpChannelUpdater implements Runnable { private final JDA jda; + private final HelpConfig config; private final HelpChannelManager channelManager; - public HelpChannelUpdater(JDA jda) { + public HelpChannelUpdater(JDA jda, HelpConfig config) { this.jda = jda; - this.channelManager = new HelpChannelManager(); + this.config = config; + this.channelManager = new HelpChannelManager(config); } @Override public void run() { - for (var guild : this.jda.getGuilds()) { - var config = Bot.config.get(guild).getHelp(); - var channels = guild.getTextChannels(); - for (var channel : channels) { - if (channel.getName().startsWith(config.getReservedChannelPrefix())) { - this.checkReservedChannel(channel, config); - } + var category = config.getHelpChannelCategory(); + if (category == null) throw new IllegalStateException("Missing required help channel category. Cannot update help channels."); + var channels = category.getTextChannels(); + int openChannelCount = 0; + for (var channel : channels) { + if (channelManager.isReserved(channel)) { + this.checkReservedChannel(channel); + } else if (channelManager.isOpen(channel) && this.checkOpenChannel(channel)) { + openChannelCount++; } } + while (openChannelCount < config.getPreferredOpenChannelCount()) { + channelManager.openNew(); + openChannelCount++; + } } - private void checkReservedChannel(TextChannel channel, HelpConfig config) { - channel.getHistoryFromBeginning(1).queue(history -> { - if (history.isEmpty()) { - // Revert to open channel. - this.channelManager.open(channel, config); - } else { - Message firstMsg = history.getRetrievedHistory().get(0); - var channelBecomesInactiveAt = firstMsg.getTimeCreated().plusSeconds(config.getInactivityTimeoutSeconds()); - if (OffsetDateTime.now().isAfter(channelBecomesInactiveAt)) { - User user = firstMsg.getAuthor(); - } + /** + * Performs checks on a reserved channel. This will do several things: + *
    + *
  • + * If the most recent message in the channel is old enough, it will + * send an activity check message in the channel, asking the user to + * confirm whether they're still using it. + *
  • + *
  • + * If the most recent message is an activity check message, and it + * has stuck around long enough without any response, then the + * channel will be removed. + *
  • + *
  • + * If for some reason we can't retrieve the owner of the channel, + * like if they left the server, the channel will be removed. + *
  • + *
+ * @param channel The channel to check. + */ + private void checkReservedChannel(TextChannel channel) { + User owner = this.channelManager.getReservedChannelOwner(channel); + if (owner == null) { + log.info("Removing reserved channel {} because no owner could be found.", channel.getAsMention()); + channel.delete().queue(); + return; + } + channel.getHistory().retrievePast(1).queue(messages -> { + Message mostRecentMessage = messages.isEmpty() ? null : messages.get(0); + if (mostRecentMessage == null) { + log.info("Removing reserved channel {} because no recent messages could be found.", channel.getAsMention()); + channel.delete().queue(); + return; + } + + // Check if the most recent message is a channel inactivity check, and check that it's old enough to surpass the remove timeout. + if ( + mostRecentMessage.getAuthor().equals(this.jda.getSelfUser()) && + mostRecentMessage.getContentRaw().contains("Are you finished with this channel?") && + mostRecentMessage.getTimeCreated().plusMinutes(config.getRemoveTimeoutMinutes()).isBefore(OffsetDateTime.now()) + ) { + log.info("Removing reserved channel {} because of inactivity for {} minutes following inactive check.", channel.getAsMention(), config.getRemoveTimeoutMinutes()); + channel.sendMessage(String.format( + "%s, this channel will be closed in 30 seconds due to prolonged inactivity. If your question still isn't answered, please ask again in an open channel.", + owner.getAsMention() + )).queue(); + channel.delete().queueAfter(30, TimeUnit.SECONDS); + return; + } + + // The most recent message is not an activity check, so check if it's old enough to warrant sending an activity check. + if (mostRecentMessage.getTimeCreated().plusMinutes(config.getInactivityTimeoutMinutes()).isBefore(OffsetDateTime.now())) { + log.info("Sending inactivity check to {} because of no activity after {} minutes.", channel.getAsMention(), config.getInactivityTimeoutMinutes()); + channel.sendMessage(String.format( + "Hey %s, it looks like this channel is inactive. Are you finished with this channel?\n\n> _If no response is received after %d minutes, this channel will be removed._", + owner.getAsMention(), + config.getRemoveTimeoutMinutes() + )) + .setActionRow( + new ButtonImpl("help-channel:done", "Yes, I'm done here!", ButtonStyle.SUCCESS, false, null), + new ButtonImpl("help-channel:not-done", "No, I'm still using it.", ButtonStyle.DANGER, false, null) + ) + .queue(); } }); } + + private boolean checkOpenChannel(TextChannel channel) { + boolean isEmpty = channel.getHistoryFromBeginning(1).complete().isEmpty(); + if (!isEmpty) { + log.info("Removing non-empty open channel {}.", channel.getAsMention()); + channel.delete().complete(); + return false; + } + return true; + } } diff --git a/src/main/java/com/javadiscord/javabot/properties/config/GuildConfig.java b/src/main/java/com/javadiscord/javabot/properties/config/GuildConfig.java index dcfbe58ef..c994f3b67 100644 --- a/src/main/java/com/javadiscord/javabot/properties/config/GuildConfig.java +++ b/src/main/java/com/javadiscord/javabot/properties/config/GuildConfig.java @@ -51,13 +51,21 @@ public GuildConfig(Guild guild, Path file) { private void setGuild(Guild guild) { this.guild = guild; + if (this.slashCommand == null) this.slashCommand = new SlashCommandConfig(); this.slashCommand.setGuildConfig(this); + if (this.help == null) this.help = new HelpConfig(); this.help.setGuildConfig(this); + if (this.moderation == null) this.moderation = new ModerationConfig(); this.moderation.setGuildConfig(this); + if (this.qotw == null) this.qotw = new QOTWConfig(); this.qotw.setGuildConfig(this); + if (this.welcome == null) this.welcome = new WelcomeConfig(); this.welcome.setGuildConfig(this); + if (this.stats == null) this.stats = new StatsConfig(); this.stats.setGuildConfig(this); + if (this.starBoard == null) this.starBoard = new StarBoardConfig(); this.starBoard.setGuildConfig(this); + if (this.jam == null) this.jam = new JamConfig(); this.jam.setGuildConfig(this); } diff --git a/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java b/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java index 64f5892e2..167911126 100644 --- a/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java +++ b/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java @@ -1,12 +1,34 @@ package com.javadiscord.javabot.properties.config.guild; +import com.javadiscord.javabot.help.AlphabetNamingStrategy; +import com.javadiscord.javabot.help.AnimalNamingStrategy; +import com.javadiscord.javabot.help.ChannelNamingStrategy; import com.javadiscord.javabot.properties.config.GuildConfigItem; import lombok.Data; import lombok.EqualsAndHashCode; +import net.dv8tion.jda.api.entities.Category; +/** + * Configuration for the guild's help system. + */ @Data @EqualsAndHashCode(callSuper = true) public class HelpConfig extends GuildConfigItem { + /** + * The id of the channel category that all help channels are in. + */ + private long categoryId; + + /** + * The strategy to use when naming help channels. + */ + private String channelNamingStrategy = "animal"; + + /** + * The number of open help channels to maintain. + */ + private int preferredOpenChannelCount = 3; + /** * The string which is prefixed to any open help channel, where users are * free to ask a question. @@ -27,12 +49,31 @@ public class HelpConfig extends GuildConfigItem { private String inactiveChannelPrefix = "\uD83D\uDFE0"; /** - * The number of seconds of inactivity before a channel is considered inactive. + * The number of minutes of inactivity before a channel is considered inactive. */ - private int inactivityTimeoutSeconds = 1_800; + private int inactivityTimeoutMinutes = 30; + + /** + * The number of minutes of inactivity before a previously inactive channel + * is removed. This is measured from the time at which the bot determined + * the channel to be inactive. + */ + private int removeTimeoutMinutes = 60; /** * The number of seconds to wait between each help channel update check. */ private long updateIntervalSeconds = 60; + + public Category getHelpChannelCategory() { + return getGuild().getCategoryById(this.categoryId); + } + + public ChannelNamingStrategy getChannelNamingStrategy() { + return switch (this.channelNamingStrategy) { + case "alphabet" -> new AlphabetNamingStrategy(); + case "animal" -> new AnimalNamingStrategy(); + default -> throw new IllegalArgumentException("Invalid channel naming strategy."); + }; + } } From df306883cf63efa8ee2bfa99bfe29ee132ef22c0 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Mon, 27 Sep 2021 08:10:11 +0200 Subject: [PATCH 3/3] Changed default config value for reserved channel prefix to raw unicode string. --- .../javadiscord/javabot/properties/config/guild/HelpConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java b/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java index 167911126..a08f3de8d 100644 --- a/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java +++ b/src/main/java/com/javadiscord/javabot/properties/config/guild/HelpConfig.java @@ -39,7 +39,7 @@ public class HelpConfig extends GuildConfigItem { * The string which is prefixed to any reserved help channel, where a user * has already asked a question and is in the process of getting an answer. */ - private String reservedChannelPrefix = "⛔"; + private String reservedChannelPrefix = "\u26D4"; /** * The string which is prefixed to any inactive reserved help channel, where