From 95d7ba43fc8d0cad1377f7d228625659988be304 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 19 Oct 2021 11:14:22 +0200 Subject: [PATCH 01/78] Added framework for FreeCommand and ChannelStatus Added Util for ephemeral error messages Modified Config to load FreeChannel config Improved Javadocs Added test Status message Rebased onto development --- .../tjbot/commands/free/ChannelStatus.java | 70 ++++++++ .../tjbot/commands/free/FreeCommand.java | 166 ++++++++++++++++++ .../tjbot/commands/free/StatusMessage.java | 42 +++++ .../tjbot/commands/free/Util.java | 18 ++ .../org/togetherjava/tjbot/config/Config.java | 17 +- .../tjbot/config/FreeCommandConfig.java | 46 +++++ 6 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java new file mode 100644 index 0000000000..81f3b68b60 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -0,0 +1,70 @@ +package org.togetherjava.tjbot.commands.free; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class ChannelStatus { + public static final boolean FREE = false; + public static final boolean BUSY = true; + + private static final String FREE_STATUS = ":green_circle:"; + private static final String BUSY_STATUS = ":red_circle:"; + + private final long channelID; + private boolean isBusy; + private String name; + + protected ChannelStatus(long id) { + channelID = id; + isBusy = true; + name = Long.toString(id); + } + + public boolean isBusy() { + return isBusy; + } + + public long getChannelID() { + return channelID; + } + + public @NotNull String getName() { + return name; + } + + public void setName(String name) { + this.name = Objects.requireNonNull(name); + } + + // todo make threadsafe? + protected void busy(boolean isBusy) { + this.isBusy = isBusy; + } + + + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ChannelStatus that = (ChannelStatus) o; + return channelID == that.channelID; + } + + @Override + public String toString() { + return "ChannelStatus{ %s is %s }".formatted(name, isBusy ? "busy" : "not busy"); + } + + public String toDiscord() { + return "%s\t%s".formatted(isBusy ? BUSY_STATUS : FREE_STATUS, name); + } + + @Override + public int hashCode() { + return Objects.hash(channelID); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java new file mode 100644 index 0000000000..c10a7157b7 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -0,0 +1,166 @@ +package org.togetherjava.tjbot.commands.free; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.GuildChannel; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.hooks.EventListener; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.config.FreeCommandConfig; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +// todo check if channel is valid before processing onSlashCommand (can SlashCommandVisibility be +// narrower than GUILD?) +// todo monitor all channels when list is empty? monitor none? (defaulting to all for testing) +// todo (use other emojis? use images?) +// todo add command to add/remove/status channels to monitor +// todo test if message is a reply and don't mark as busy if it is +// todo add button query to confirm that message is new question not additional info for existing +// discussion before marking as busy +// todo add scheduled tasks to check last message every 15mins and mark as free if 1hr (2hrs?) has +// passed + +/** + * Implementation of the free command. It is used to monitor a predefined list of channels and show + * users which ones are available for use and which are not. + *

+ * When a user posts a message in a channel that is being monitored that channel is automatically + * marked as busy until they post {@code /free} to notify the bot and other users that the channel + * is now available or after a preconfigured period of time has passed without any traffic. + *

+ * If any user posts a message that directly 'replies' to an existing message, in a monitored + * channel that is currently marked as free, the free status will remain. + *

+ * If a user starts typing in a channel where 2 or more users have posted multiple messages each, + * less than a configured time ago, they will receive an ephemeral message warning them that the + * channel is currently in use and that they should post in a free channel if they are trying to ask + * a question. + *

+ * A summary of the current status of those channels is displayed in a predefined channel. This + * channel may be one of the monitored channels however it is recommended that a different channel + * is used. + */ +public class FreeCommand extends SlashCommandAdapter implements EventListener { + private static final Logger logger = LoggerFactory.getLogger(FreeCommand.class); + private static final String FREE_COMMAND = "free"; + + // Map to store channel ID's, use Guild.getChannels() to guarantee order for display + private final Map channelsById; + private final Map guildToStatus; + + private boolean isRegistered; + + + /** + * Creates an instance of FreeCommand. + *

+ * This fetches configuration information from a json configuration file (see + * {@link FreeCommandConfig}) for further details. + */ + public FreeCommand() { + super(FREE_COMMAND, "marks this channel as free for another user to ask a question", + SlashCommandVisibility.GUILD); + + Collection config = Config.getInstance().getFreeCommandConfig(); + guildToStatus = config.stream() + .map(FreeCommandConfig::getStatusChannel) + // todo change Function.identity to JDA.getChannel.getGuild when viable. + .collect(Collectors.toMap(Function.identity(), Function.identity())); + channelsById = config.stream() + .map(FreeCommandConfig::getMonitoredChannels) + .flatMap(Collection::stream) + .map(ChannelStatus::new) + .collect(Collectors.toMap(ChannelStatus::getChannelID, Function.identity())); + + logger.debug("Config loaded: Display on {} and monitor {}", guildToStatus, channelsById); + isRegistered = false; + } + + /** + * When triggered with {@code /free} this will mark a help channel as not busy (free for another + * person to use). + *

+ * If this is called on from a channel that was not configured for monitoring (see + * {@link FreeCommandConfig}) the user will receive an ephemeral message stating such. + * + * @param event the event that triggered this + */ + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + logger.debug("/free used by {} on channel {}", event.getUser().getName(), + event.getChannel().getName()); + if (!isRegistered) { + event.getJDA().addEventListener(this); + } + long id = event.getChannel().getIdLong(); + ChannelStatus status = channelsById.get(id); + if (status != null) { + if (status.isBusy()) { + status.busy(ChannelStatus.FREE); + event.reply("This channel is now free.").queue(); + } else { + Util.sendErrorMessage(event, "This channel is already free, no changes made"); + } + } else { + Util.sendErrorMessage(event, + "This channel is not being monitored for free/busy status"); + } + drawStatus(event.getTextChannel()); + } + + public String drawStatus(TextChannel channel) { + final Guild guild = channel.getGuild(); + + List statusFor = guild.getChannels() + .stream() + .filter(channelInGuild -> channelsById.containsKey(channelInGuild.getIdLong())) + .toList(); + + String message = statusFor.stream() + .map(GuildChannel::getIdLong) + .map(channelsById::get) + .map(channelStatus -> { + // update name so that current name is used + channelStatus + .setName(guild.getGuildChannelById(channelStatus.getChannelID()).getName()); + return channelStatus; + }) + .map(ChannelStatus::toDiscord) + .collect(Collectors.joining("\n")); + + channel.getLatestMessageIdLong(); + channel.sendMessage(message).queue(); + + // Not implemented yet + return message; + } + + @Override + public void onEvent(@NotNull GenericEvent event) { + if (event instanceof GuildMessageReceivedEvent castEvent) { + if (castEvent.isWebhookMessage() || castEvent.getAuthor().isBot()) { + return; + } + ChannelStatus status = channelsById.get(castEvent.getChannel().getIdLong()); + if (status == null || status.isBusy()) { + logger.debug("Channel status is currently busy, skipping message {}", status); + return; + } + status.busy(ChannelStatus.BUSY); + castEvent.getMessage().reply("The channel was free, please ask your question").queue(); + + } + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java new file mode 100644 index 0000000000..d4c683d95c --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java @@ -0,0 +1,42 @@ +package org.togetherjava.tjbot.commands.free; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.TextChannel; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class StatusMessage { + private final Guild guild; + private final TextChannel postingChannel; + private final Message existingStatus; + private final List channelStatuses; + + + private StatusMessage(@NotNull Guild guild, @NotNull TextChannel postingChannel, + @NotNull List channelStatuses, Message existingStatus) { + this.guild = guild; + this.postingChannel = postingChannel; + this.channelStatuses = channelStatuses; + this.existingStatus = existingStatus; + } + + private static class StatusMessageBuilder { + private List channelStatuses; + private TextChannel postingChannel; + + private StatusMessageBuilder(@NotNull List channelStatuses) { + this.channelStatuses = channelStatuses; + } + + public StatusMessageBuilder channelToPostIn(TextChannel channel) { + postingChannel = channel; + return this; + } + } + + public static StatusMessageBuilder builder(@NotNull List channelStatuses) { + return new StatusMessageBuilder(channelStatuses); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java new file mode 100644 index 0000000000..4b2a3b85e4 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java @@ -0,0 +1,18 @@ +package org.togetherjava.tjbot.commands.free; + +import net.dv8tion.jda.api.interactions.Interaction; + +public class Util { + // private constructor to prevent this class getting instantiated + private Util() {} + + /** + * Helper method to easily send ephemeral messages to users. + * + * @param interaction The event or hook that this message is responding to + * @param message The text to be display for the user to read. + */ + public static void sendErrorMessage(Interaction interaction, String message) { + interaction.reply(message).setEphemeral(true).queue(); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index bcfd9d9c05..861c60bbe3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -6,6 +6,9 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Objects; /** @@ -29,6 +32,8 @@ public final class Config { private final String softModerationRolePattern; private final String tagManageRolePattern; + private final List freeCommand; + @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) private Config(@JsonProperty("token") String token, @@ -39,7 +44,8 @@ private Config(@JsonProperty("token") String token, @JsonProperty("mutedRolePattern") String mutedRolePattern, @JsonProperty("heavyModerationRolePattern") String heavyModerationRolePattern, @JsonProperty("softModerationRolePattern") String softModerationRolePattern, - @JsonProperty("tagManageRolePattern") String tagManageRolePattern) { + @JsonProperty("tagManageRolePattern") String tagManageRolePattern, + @JsonProperty("freeCommand") FreeCommandConfig[] freeCommand) { this.token = token; this.databasePath = databasePath; this.projectWebsite = projectWebsite; @@ -49,6 +55,7 @@ private Config(@JsonProperty("token") String token, this.heavyModerationRolePattern = heavyModerationRolePattern; this.softModerationRolePattern = softModerationRolePattern; this.tagManageRolePattern = tagManageRolePattern; + this.freeCommand = Arrays.stream(freeCommand).toList(); } /** @@ -159,4 +166,12 @@ public String getSoftModerationRolePattern() { public String getTagManageRolePattern() { return tagManageRolePattern; } + + /** + * + * @return an + */ + public Collection getFreeCommandConfig() { + return freeCommand; + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java new file mode 100644 index 0000000000..835c620e04 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -0,0 +1,46 @@ +package org.togetherjava.tjbot.config; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@SuppressWarnings("ClassCanBeRecord") +@JsonRootName(value = "freeCommand") +public final class FreeCommandConfig { + private final long statusChannel; + private final List monitoredChannels; + + /** + * //todo add java docs, include required json format to configure this + * + * @param statusChannel + * @param monitoredChannels + */ + @JsonCreator + private FreeCommandConfig(@JsonProperty("statusChannel") long statusChannel, + @JsonProperty("monitoredChannels") long[] monitoredChannels) { + this.statusChannel = statusChannel; + this.monitoredChannels = Arrays.stream(monitoredChannels).boxed().toList(); + } + + /** + * Gets the channelID where the status message will be displayed. + * + * @return the channelID for the status message + */ + public long getStatusChannel() { + return statusChannel; + } + + /** + * + * @return + */ + public Collection getMonitoredChannels() { + return monitoredChannels; + } +} From 9ff6df5e5891ec9019327a879ef02f309aded03d Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 20 Oct 2021 12:11:33 +0200 Subject: [PATCH 02/78] Added framework for FreeCommand and ChannelStatus Added Util for ephemeral error messages Modified Config to load FreeChannel config Improved Javadocs Added test Status message Rebased onto development Cleaned code --- .../togetherjava/tjbot/commands/Commands.java | 2 + .../tjbot/commands/free/FreeCommand.java | 145 +++++++++++++----- .../tjbot/commands/free/Util.java | 4 +- .../org/togetherjava/tjbot/config/Config.java | 11 +- .../tjbot/config/FreeCommandConfig.java | 32 ++-- 5 files changed, 139 insertions(+), 55 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java index de704a1f7a..0d6b32526e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java @@ -4,6 +4,7 @@ import org.togetherjava.tjbot.commands.basic.DatabaseCommand; import org.togetherjava.tjbot.commands.basic.PingCommand; import org.togetherjava.tjbot.commands.basic.VcActivityCommand; +import org.togetherjava.tjbot.commands.free.FreeCommand; import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; import org.togetherjava.tjbot.commands.moderation.BanCommand; import org.togetherjava.tjbot.commands.moderation.KickCommand; @@ -55,6 +56,7 @@ public enum Commands { commands.add(new KickCommand()); commands.add(new BanCommand()); commands.add(new UnbanCommand()); + commands.add(new FreeCommand()); return commands; } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index c10a7157b7..94d32a6678 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -2,8 +2,10 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.GuildChannel; +import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; import net.dv8tion.jda.api.hooks.EventListener; @@ -15,15 +17,13 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.FreeCommandConfig; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -// todo check if channel is valid before processing onSlashCommand (can SlashCommandVisibility be +// todo (can SlashCommandVisibility be // narrower than GUILD?) -// todo monitor all channels when list is empty? monitor none? (defaulting to all for testing) +// todo monitor all channels when list is empty? monitor none? // todo (use other emojis? use images?) // todo add command to add/remove/status channels to monitor // todo test if message is a reply and don't mark as busy if it is @@ -58,9 +58,10 @@ public class FreeCommand extends SlashCommandAdapter implements EventListener { // Map to store channel ID's, use Guild.getChannels() to guarantee order for display private final Map channelsById; - private final Map guildToStatus; + private final Map guildToStatusChannel; + private final Map channelToStatusMessage; - private boolean isRegistered; + private boolean isReady; /** @@ -74,18 +75,49 @@ public FreeCommand() { SlashCommandVisibility.GUILD); Collection config = Config.getInstance().getFreeCommandConfig(); - guildToStatus = config.stream() - .map(FreeCommandConfig::getStatusChannel) - // todo change Function.identity to JDA.getChannel.getGuild when viable. - .collect(Collectors.toMap(Function.identity(), Function.identity())); + guildToStatusChannel = new HashMap<>(); // JDA required to fetch guildID + channelToStatusMessage = new HashMap<>(); // empty .... used to track status messages channelsById = config.stream() .map(FreeCommandConfig::getMonitoredChannels) .flatMap(Collection::stream) .map(ChannelStatus::new) .collect(Collectors.toMap(ChannelStatus::getChannelID, Function.identity())); - logger.debug("Config loaded: Display on {} and monitor {}", guildToStatus, channelsById); - isRegistered = false; + isReady = false; + } + + @Override + public void onReady(@NotNull final ReadyEvent event) { + // todo remove this when onGuildMessageRecieved has another access point + event.getJDA().addEventListener(this); + + Collection config = Config.getInstance().getFreeCommandConfig(); + config.stream() + .map(FreeCommandConfig::getStatusChannel) + .distinct() // not necessary? validates user input, since input is from file + // should this test if already present first? + .forEach(id -> guildToStatusChannel + .put(event.getJDA().getGuildChannelById(id).getGuild().getIdLong(), id)); + + logger.debug("Config loaded:\nDisplay statuses on {}\nand monitor channels {}", + guildToStatusChannel, channelsById); + + // not collecting to map because onReady is not run during construction + guildToStatusChannel.values() + .stream() + .map(event.getJDA()::getTextChannelById) + .map(this::getStatusMessageIn) + .flatMap(Optional::stream) + .forEach(message -> channelToStatusMessage.put(message.getChannel().getIdLong(), + message.getIdLong())); + + + guildToStatusChannel.values() + .stream() + .map(event.getJDA()::getTextChannelById) + .forEach(this::drawStatus); + + isReady = true; } /** @@ -98,52 +130,71 @@ public FreeCommand() { * @param event the event that triggered this */ @Override - public void onSlashCommand(@NotNull SlashCommandEvent event) { + public void onSlashCommand(@NotNull final SlashCommandEvent event) { logger.debug("/free used by {} on channel {}", event.getUser().getName(), event.getChannel().getName()); - if (!isRegistered) { - event.getJDA().addEventListener(this); - } + if (!shouldHandle(event)) + return; + long id = event.getChannel().getIdLong(); + // do not need to test if key is present, shouldHandle(event) already does. ChannelStatus status = channelsById.get(id); - if (status != null) { - if (status.isBusy()) { - status.busy(ChannelStatus.FREE); - event.reply("This channel is now free.").queue(); - } else { - Util.sendErrorMessage(event, "This channel is already free, no changes made"); - } + if (status.isBusy()) { + status.busy(ChannelStatus.FREE); + drawStatus(getStatusChannelFor(event.getGuild())); + event.reply("This channel is now free.").queue(); } else { + Util.sendErrorMessage(event, "This channel is already free, no changes made"); + } + } + + private boolean shouldHandle(@NotNull final SlashCommandEvent event) { + if (!isReady) { + logger.debug( + "Slash command requested by {} in {}(channel: {}) before command is ready.", + event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); + Util.sendErrorMessage(event, "Command not ready please try again in a minute"); + return false; + } + if (!guildToStatusChannel.containsKey(event.getGuild().getIdLong())) { + logger.error( + "Slash command used by {} in {}(channel: {}) when guild is not registed in Free Command", + event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); + Util.sendErrorMessage(event, + "This guild (%s) is not configured to use the '/free' command, please add entries in the config, restart the bot and try again." + .formatted(event.getGuild().getName())); + return false; + } + if (!channelsById.containsKey(event.getChannel().getIdLong())) { + logger.debug("'/free called in unregistered channel {}({})", event.getGuild().getName(), + event.getChannel().getName()); Util.sendErrorMessage(event, "This channel is not being monitored for free/busy status"); + return false; } - drawStatus(event.getTextChannel()); + + return true; } public String drawStatus(TextChannel channel) { final Guild guild = channel.getGuild(); - List statusFor = guild.getChannels() + List statusFor = guild.getChannels() .stream() - .filter(channelInGuild -> channelsById.containsKey(channelInGuild.getIdLong())) - .toList(); - - String message = statusFor.stream() .map(GuildChannel::getIdLong) + .filter(channelsById::containsKey) .map(channelsById::get) - .map(channelStatus -> { - // update name so that current name is used - channelStatus - .setName(guild.getGuildChannelById(channelStatus.getChannelID()).getName()); - return channelStatus; - }) - .map(ChannelStatus::toDiscord) - .collect(Collectors.joining("\n")); + .toList(); + + // update name so that current channel name is used + statusFor.forEach(channelStatus -> channelStatus + .setName(guild.getGuildChannelById(channelStatus.getChannelID()).getName())); + String message = + statusFor.stream().map(ChannelStatus::toDiscord).collect(Collectors.joining("\n")); channel.getLatestMessageIdLong(); channel.sendMessage(message).queue(); - // Not implemented yet return message; } @@ -155,12 +206,24 @@ public void onEvent(@NotNull GenericEvent event) { } ChannelStatus status = channelsById.get(castEvent.getChannel().getIdLong()); if (status == null || status.isBusy()) { - logger.debug("Channel status is currently busy, skipping message {}", status); + logger.debug("Channel status is currently busy, ignoring message received. {} ", + status); return; } status.busy(ChannelStatus.BUSY); + drawStatus(getStatusChannelFor(castEvent.getGuild())); castEvent.getMessage().reply("The channel was free, please ask your question").queue(); - } } + + private TextChannel getStatusChannelFor(@NotNull final Guild guild) { + // todo add error checking for invalid keys ?? + return guild.getTextChannelById(guildToStatusChannel.get(guild.getIdLong())); + } + + private Optional getStatusMessageIn(TextChannel channel) { + channel.getHistory().retrievePast(100).queue(); + + return Optional.empty(); + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java index 4b2a3b85e4..bc4970bf9a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java @@ -1,6 +1,7 @@ package org.togetherjava.tjbot.commands.free; import net.dv8tion.jda.api.interactions.Interaction; +import org.jetbrains.annotations.NotNull; public class Util { // private constructor to prevent this class getting instantiated @@ -12,7 +13,8 @@ private Util() {} * @param interaction The event or hook that this message is responding to * @param message The text to be display for the user to read. */ - public static void sendErrorMessage(Interaction interaction, String message) { + public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull String message) { interaction.reply(message).setEphemeral(true).queue(); } + } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 861c60bbe3..706c453245 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -33,7 +33,7 @@ public final class Config { private final String tagManageRolePattern; private final List freeCommand; - + @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) private Config(@JsonProperty("token") String token, @@ -140,7 +140,7 @@ public String getDiscordGuildInvite() { /** * Gets the REGEX pattern used to identify roles that are allowed to use heavy moderation * commands, such as banning, based on role names. - * + * * @return the REGEX pattern */ public String getHeavyModerationRolePattern() { @@ -150,7 +150,7 @@ public String getHeavyModerationRolePattern() { /** * Gets the REGEX pattern used to identify roles that are allowed to use soft moderation * commands, such as kicking, muting or message deletion, based on role names. - * + * * @return the REGEX pattern */ public String getSoftModerationRolePattern() { @@ -168,8 +168,11 @@ public String getTagManageRolePattern() { } /** + * Gets a List of channel id's required to configure the free command system see + * {@link FreeCommandConfig} * - * @return an + * @return a List of instances of FreeCommandConfig, each of the instances are separated by + * guild. */ public Collection getFreeCommandConfig() { return freeCommand; diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index 835c620e04..0ee1e85813 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -8,18 +8,30 @@ import java.util.Collection; import java.util.List; +/** + * Config instance for the Free Command System see + * {@link org.togetherjava.tjbot.commands.free.FreeCommand} + * + * The Json looks as follows: + * + *

+ * "freeCommand": [
+ *   {
+ *       "statusChannel": long_number,
+ *       "monitoredChannels": [long_number, long_number]
+ *   }]
+ * 
+ * + * Additional Guilds may add their settings by adding additional {"statusChannel....... } + * + * The long channel ID can be found by right-clicking on the channel and selecting 'Copy ID' + */ @SuppressWarnings("ClassCanBeRecord") @JsonRootName(value = "freeCommand") public final class FreeCommandConfig { private final long statusChannel; private final List monitoredChannels; - /** - * //todo add java docs, include required json format to configure this - * - * @param statusChannel - * @param monitoredChannels - */ @JsonCreator private FreeCommandConfig(@JsonProperty("statusChannel") long statusChannel, @JsonProperty("monitoredChannels") long[] monitoredChannels) { @@ -28,17 +40,19 @@ private FreeCommandConfig(@JsonProperty("statusChannel") long statusChannel, } /** - * Gets the channelID where the status message will be displayed. + * Retrieves the channelID where the status message will be displayed. * - * @return the channelID for the status message + * @return the Channel ID where the Status Message is expected to be displayed */ public long getStatusChannel() { return statusChannel; } /** + * Retrieves a List of the channels on this guild that the guild wants monitored by the + * free/busy command system * - * @return + * @return an Unmodifiable List of Channel ID's */ public Collection getMonitoredChannels() { return monitoredChannels; From 2b48717c1e411054ec0bdd615f2b1e1c8294322d Mon Sep 17 00:00:00 2001 From: borgrel Date: Fri, 22 Oct 2021 16:36:23 +0200 Subject: [PATCH 03/78] added thread safety for 'busy' added javadocs --- .../tjbot/commands/free/ChannelStatus.java | 85 +++++++++- .../tjbot/commands/free/FreeCommand.java | 146 +++++++++++++++--- .../tjbot/commands/free/StatusMessage.java | 5 + 3 files changed, 206 insertions(+), 30 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 81f3b68b60..fc3dfabcb1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -1,10 +1,15 @@ package org.togetherjava.tjbot.commands.free; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.TextChannel; import org.jetbrains.annotations.NotNull; import java.util.Objects; -public class ChannelStatus { +/** + * Class that tracks the current free/busy status of a channel that requires monitoring. + */ +public final class ChannelStatus { public static final boolean FREE = false; public static final boolean BUSY = true; @@ -12,38 +17,90 @@ public class ChannelStatus { private static final String BUSY_STATUS = ":red_circle:"; private final long channelID; - private boolean isBusy; + private volatile boolean isBusy; private String name; + /** + * Creates an instance of a Channel Status. + *

+ * This does not validate the id as that requires the JDA. Any ChannelStatus that gets created + * with an invalid id *should* be ignored and won't be invoked. (Since the Channel statuses are + * selected by retrieval of channels id's via the guild id, before retrieving the relevant + * ChannelStatuses). + * + * @param id the long id of the {@link net.dv8tion.jda.api.entities.TextChannel} to monitor. + */ protected ChannelStatus(long id) { channelID = id; isBusy = true; name = Long.toString(id); } + /** + * Retrieves whether the channel is currently busy/free. + *

+ * This value is volatile but is not thread safe in any other way. While statuses change + * frequently, each individual status instance *should* only be modified from a single source, + * since it represents only a single channel and modification will only be triggered by activity + * in that one channel. + * + * @return the current stored status related to the channel id. + */ public boolean isBusy() { return isBusy; } + /** + * Retrieves the id for the {@link net.dv8tion.jda.api.entities.TextChannel} that this instance + * represents. There is no guarantee that the id is valid according to the {@link JDA}. + * + * @return the {@link net.dv8tion.jda.api.entities.TextChannel} id. + */ public long getChannelID() { return channelID; } + /** + * Retrieves the locally stored name of the {@link net.dv8tion.jda.api.entities.TextChannel} + * this represents. This value is initialised to the channel id and as such is never null. The + * name should first be set by retrieving the name the {@link JDA} currently uses, before + * calling this. + *

+ * The recommended value to use is {@link TextChannel#getAsMention()}. + * + * @return The currently stored name of the channel, originally the long id as string. + */ public @NotNull String getName() { return name; } + /** + * Method used to keep the channel name up to date with the {@link JDA}. This method is not + * called automatically. Manually update before using the value. + *

+ * The recommended value to use is {@link TextChannel#getAsMention()} + *

+ * This method is called in multithreaded context, however the value is not expected to change + * regularly and will not break anything if its invalid so no has not been made thread safe. + * + * @param name the value to set the channel name to. + */ public void setName(String name) { this.name = Objects.requireNonNull(name); } - // todo make threadsafe? protected void busy(boolean isBusy) { this.isBusy = isBusy; } - - + // todo should I overload equals with equals(long) so that a Set may be used instead of a Map + /** + * The identity of this object of solely based on the id value. Compares the long id's and + * determines if they are equal. + * + * @param o the other object to test against + * @return whether the objects have the same id or not. + */ @Override public boolean equals(Object o) { if (this == o) @@ -54,15 +111,31 @@ public boolean equals(Object o) { return channelID == that.channelID; } + /** + * A String representation of the instance, gives the name and the current status. + * + * @return a String representation of the instance. + */ @Override public String toString() { return "ChannelStatus{ %s is %s }".formatted(name, isBusy ? "busy" : "not busy"); } + /** + * A {@link #toString()} method spetially formatted for Discord ({@link JDA}. Uses emojis by + * string representation. + * + * @return a String representation of ChannelStatus, formatted for Discord + */ public String toDiscord() { - return "%s\t%s".formatted(isBusy ? BUSY_STATUS : FREE_STATUS, name); + return "%s %s".formatted(isBusy ? BUSY_STATUS : FREE_STATUS, name); } + /** + * The hash that represents the instance. It is based only on the id value. + * + * @return the instance's hash. + */ @Override public int hashCode() { return Objects.hash(channelID); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 94d32a6678..46a4dd7bd3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -1,9 +1,6 @@ package org.togetherjava.tjbot.commands.free; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.GuildChannel; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.GenericEvent; import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; @@ -14,15 +11,17 @@ import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.commands.utils.MessageUtils; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.FreeCommandConfig; +import java.awt.*; import java.util.*; +import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; -// todo (can SlashCommandVisibility be -// narrower than GUILD?) +// todo (can SlashCommandVisibility be narrower than GUILD?) // todo monitor all channels when list is empty? monitor none? // todo (use other emojis? use images?) // todo add command to add/remove/status channels to monitor @@ -54,7 +53,9 @@ */ public class FreeCommand extends SlashCommandAdapter implements EventListener { private static final Logger logger = LoggerFactory.getLogger(FreeCommand.class); + private static final String STATUS_TITLE = "**__CHANNEL STATUS__**\n\n"; private static final String FREE_COMMAND = "free"; + private static final Color FREE_COLOR = Color.decode("#CCCC00"); // Map to store channel ID's, use Guild.getChannels() to guarantee order for display private final Map channelsById; @@ -86,6 +87,19 @@ public FreeCommand() { isReady = false; } + /** + * Reaction to the 'onReady' event. This method binds the configurables to the + * {@link net.dv8tion.jda.api.JDA} instance. Including fetching the names of the channels this + * command monitors (as per the JDA) + *

+ * It also updates the Status messages in their relevant channels, so that the message is up to + * date. + *

+ * This also registers a new listener on the {@link net.dv8tion.jda.api.JDA}, this should be + * removed when the code base supports additional functionality + * + * @param event the event this method reacts to + */ @Override public void onReady(@NotNull final ReadyEvent event) { // todo remove this when onGuildMessageRecieved has another access point @@ -115,7 +129,7 @@ public void onReady(@NotNull final ReadyEvent event) { guildToStatusChannel.values() .stream() .map(event.getJDA()::getTextChannelById) - .forEach(this::drawStatus); + .forEach(this::displayStatus); isReady = true; } @@ -141,13 +155,21 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { ChannelStatus status = channelsById.get(id); if (status.isBusy()) { status.busy(ChannelStatus.FREE); - drawStatus(getStatusChannelFor(event.getGuild())); + displayStatus(getStatusChannelFor(event.getGuild())); event.reply("This channel is now free.").queue(); } else { Util.sendErrorMessage(event, "This channel is already free, no changes made"); } } + /** + * Private method to test event to see if it should be processed. + * + * Will respond to users describing the problem if the event should not be processed. + * + * @param event the event to test for validity. + * @return true if the event should be processed false otherwise. + */ private boolean shouldHandle(@NotNull final SlashCommandEvent event) { if (!isReady) { logger.debug( @@ -158,7 +180,7 @@ private boolean shouldHandle(@NotNull final SlashCommandEvent event) { } if (!guildToStatusChannel.containsKey(event.getGuild().getIdLong())) { logger.error( - "Slash command used by {} in {}(channel: {}) when guild is not registed in Free Command", + "Slash command used by {} in {}(channel: {}) when guild is not registered in Free Command", event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); Util.sendErrorMessage(event, "This guild (%s) is not configured to use the '/free' command, please add entries in the config, restart the bot and try again." @@ -176,9 +198,44 @@ private boolean shouldHandle(@NotNull final SlashCommandEvent event) { return true; } - public String drawStatus(TextChannel channel) { + /** + * Builds the message that will be displayed for users. + *

+ * This method dynamically builds the status message as per the current values on the guild, + * including the channel categories. This method will detect any changes made on the guild and + * represent those changes in the status message. + * + * @param channel the text channel the status message will be posted in. + */ + public void displayStatus(@NotNull TextChannel channel) { final Guild guild = channel.getGuild(); + String messageTxt = buildStatusMessage(guild); + MessageEmbed embed = MessageUtils.generateEmbed(STATUS_TITLE, messageTxt, + channel.getJDA().getSelfUser(), FREE_COLOR); + + long latestMessageId = channel.getLatestMessageIdLong(); + Optional statusMessage = getStatusMessageIn(channel); + channel.sendMessage("Message selected is: " + statusMessage).queue(); + if (statusMessage.isPresent()) { + Message message = statusMessage.get(); + if (message.getIdLong() != latestMessageId) { + message.delete(); + } else { + message.editMessageEmbeds(embed); + } + } + + channel.sendMessageEmbeds(embed).queue(); + } + + public String buildStatusMessage(@NotNull Guild guild) { + if (!guildToStatusChannel.containsKey(guild.getIdLong())) { + throw new IllegalArgumentException( + "The guild '%s(%s)' is not registered in the free command system" + .formatted(guild.getName(), guild.getIdLong())); + } + List statusFor = guild.getChannels() .stream() .map(GuildChannel::getIdLong) @@ -188,31 +245,46 @@ public String drawStatus(TextChannel channel) { // update name so that current channel name is used statusFor.forEach(channelStatus -> channelStatus - .setName(guild.getGuildChannelById(channelStatus.getChannelID()).getName())); - String message = - statusFor.stream().map(ChannelStatus::toDiscord).collect(Collectors.joining("\n")); + .setName(guild.getGuildChannelById(channelStatus.getChannelID()).getAsMention())); - channel.getLatestMessageIdLong(); - channel.sendMessage(message).queue(); + // dynamically separate channels by channel categories + StringBuilder sb = new StringBuilder(); + String categoryName = ""; + for (ChannelStatus status : statusFor) { + Category category = guild.getGuildChannelById(status.getChannelID()).getParent(); + if (category != null && !category.getName().equals(categoryName)) { + categoryName = category.getName(); + sb.append("\n__").append(categoryName).append("__\n"); + } + sb.append(status.toDiscord()).append("\n"); + } - return message; + return sb.toString(); } + /** + * Method for responding to 'onGuildMessageRecieved' this will need to be replaced by a more + * appropriate method when the bot has more functionality. + *

+ * Marks channels as busy when a user posts a message in a monitored channel that is currently free. + * + * @param event the generic event that includes the 'onGuildMessageReceived'. + */ @Override public void onEvent(@NotNull GenericEvent event) { - if (event instanceof GuildMessageReceivedEvent castEvent) { - if (castEvent.isWebhookMessage() || castEvent.getAuthor().isBot()) { + if (event instanceof GuildMessageReceivedEvent guildEvent) { + if (guildEvent.isWebhookMessage() || guildEvent.getAuthor().isBot()) { return; } - ChannelStatus status = channelsById.get(castEvent.getChannel().getIdLong()); + ChannelStatus status = channelsById.get(guildEvent.getChannel().getIdLong()); if (status == null || status.isBusy()) { logger.debug("Channel status is currently busy, ignoring message received. {} ", status); return; } status.busy(ChannelStatus.BUSY); - drawStatus(getStatusChannelFor(castEvent.getGuild())); - castEvent.getMessage().reply("The channel was free, please ask your question").queue(); + displayStatus(getStatusChannelFor(guildEvent.getGuild())); + guildEvent.getMessage().reply("The channel was free, please ask your question").queue(); } } @@ -221,9 +293,35 @@ private TextChannel getStatusChannelFor(@NotNull final Guild guild) { return guild.getTextChannelById(guildToStatusChannel.get(guild.getIdLong())); } - private Optional getStatusMessageIn(TextChannel channel) { - channel.getHistory().retrievePast(100).queue(); + private Optional getStatusMessageIn(@NotNull TextChannel channel) { + if (channelToStatusMessage.containsKey(channel.getIdLong())) { + Long id = channelToStatusMessage.get(channel.getIdLong()); + if (id == null) { + return Optional.empty(); + } + Message message = channel.getHistory().getMessageById(id); + if (message == null) { + return Optional.empty(); + } + return Optional.of(message); + } + return findExistingStatusMessage(channel); + } + + private Optional findExistingStatusMessage(@NotNull TextChannel channel) { + // will only run when bots starts, afterwards its stored in a map + Optional result = channel.getHistory() + .retrievePast(100) + .map(history -> history.stream() + .filter(message -> !message.getEmbeds().isEmpty()) + .filter(message -> message.getAuthor().equals(channel.getJDA().getSelfUser())) + .filter(message -> message.getEmbeds().get(0).getTitle().equals(STATUS_TITLE)) + .findFirst()) + .complete(); - return Optional.empty(); + Message mapping = result.orElse(null); + channelToStatusMessage.put(channel.getIdLong(), + mapping == null ? null : mapping.getIdLong()); + return result; } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java index d4c683d95c..4f57d3f4c1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java @@ -7,6 +7,11 @@ import java.util.List; +/** + * A start at building a class for status messages. Most functionality that should be here is + * currently in {@link FreeCommand} + */ +@SuppressWarnings("c") public class StatusMessage { private final Guild guild; private final TextChannel postingChannel; From 48d9d3b9ccef7dc55398738f724211d7b7bc20f1 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 26 Oct 2021 22:50:45 +0200 Subject: [PATCH 04/78] moved Channel Status functionality into its own class --- .../tjbot/commands/free/ChannelMonitor.java | 113 +++++++++++++++ .../tjbot/commands/free/ChannelStatus.java | 33 +++-- .../tjbot/commands/free/FreeCommand.java | 136 ++++++++---------- .../tjbot/commands/free/StatusMessage.java | 47 ------ .../tjbot/commands/free/package-info.java | 5 + .../tjbot/commands/system/JDAScheduler.java | 34 +++++ 6 files changed, 235 insertions(+), 133 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java delete mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/package-info.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/system/JDAScheduler.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java new file mode 100644 index 0000000000..d5e350ea33 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -0,0 +1,113 @@ +package org.togetherjava.tjbot.commands.free; + +import net.dv8tion.jda.api.entities.*; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + + +/** + * A class responsible for monitoring the status of channels and reporting on their busy/free status + * for use by {@link FreeCommand} + */ +public class ChannelMonitor { + // Map to store channel ID's, use Guild.getChannels() to guarantee order for display + private final Map channelsToMonitor; + private final Map postStatusInChannel; + + + + ChannelMonitor() { + postStatusInChannel = new HashMap<>(); // JDA required to fetch guildID + channelsToMonitor = new HashMap<>(); + } + + /** + * Method for adding channels that need to be monitored. + * + * @param channelId the id of the channel to monitor + */ + public void addChannelToMonitor(long channelId) { + channelsToMonitor.put(channelId, new ChannelStatus(channelId)); + } + + /** + * Method for adding the channel that the status will be printed in. Even though the method only + * stores the long id it requires, the method requires the actual {@link TextChannel} to be + * passed because it needs to verify it as well as store the guild id. + * + * @param channel the channel the status message must be displayed in + */ + public void addChannelForStatus(TextChannel channel) { + postStatusInChannel.put(channel.getGuild().getIdLong(), channel.getIdLong()); + updateChannelStatusFor(channel.getGuild()); + } + + public boolean isMonitoringGuild(long guildId) { + return postStatusInChannel.containsKey(guildId); + } + + public boolean isChannelBusy(long channelId) { + return channelsToMonitor.get(channelId).isBusy(); + } + + public Stream guildIds() { + return postStatusInChannel.keySet().stream(); + } + + public Stream statusIds() { + return postStatusInChannel.values().stream(); + } + + public boolean isMonitoringChannel(long channelId) { + return channelsToMonitor.containsKey(channelId); + } + + public void setChannelBusy(long channelId, long userId) { + channelsToMonitor.get(channelId).setBusy(userId); + } + + public String statusMessage(@NotNull Guild guild) { + List statusFor = guild.getChannels() + .stream() + .map(GuildChannel::getIdLong) + .filter(channelsToMonitor::containsKey) + .map(channelsToMonitor::get) + .toList(); + + // update name so that current channel name is used + statusFor.forEach(channelStatus -> channelStatus + .setName(guild.getGuildChannelById(channelStatus.getChannelId()).getAsMention())); + + // dynamically separate channels by channel categories + StringBuilder sb = new StringBuilder(); + String categoryName = ""; + for (ChannelStatus status : statusFor) { + Category category = guild.getGuildChannelById(status.getChannelId()).getParent(); + if (category != null && !category.getName().equals(categoryName)) { + categoryName = category.getName(); + sb.append("\n__").append(categoryName).append("__\n"); + } + sb.append(status.toDiscord()).append("\n"); + } + + return sb.toString(); + } + + private void updateChannelStatusFor(@NotNull Guild guild) { + //need to complete code here + } + + public TextChannel getStatusChannelFor(@NotNull final Guild guild) { + // todo add error checking for invalid keys ?? + return guild.getTextChannelById(postStatusInChannel.get(guild.getIdLong())); + } + + public String toString() { + return "Monitoring Channels: %s%nDisplaying on Channels: %s".formatted(channelsToMonitor, + postStatusInChannel); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index fc3dfabcb1..2c1e2312b5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -16,7 +16,8 @@ public final class ChannelStatus { private static final String FREE_STATUS = ":green_circle:"; private static final String BUSY_STATUS = ":red_circle:"; - private final long channelID; + private final long channelId; + private long userId; private volatile boolean isBusy; private String name; @@ -30,9 +31,9 @@ public final class ChannelStatus { * * @param id the long id of the {@link net.dv8tion.jda.api.entities.TextChannel} to monitor. */ - protected ChannelStatus(long id) { - channelID = id; - isBusy = true; + ChannelStatus(long id) { + channelId = id; + isBusy = false; name = Long.toString(id); } @@ -56,8 +57,8 @@ public boolean isBusy() { * * @return the {@link net.dv8tion.jda.api.entities.TextChannel} id. */ - public long getChannelID() { - return channelID; + public long getChannelId() { + return channelId; } /** @@ -89,8 +90,20 @@ public void setName(String name) { this.name = Objects.requireNonNull(name); } - protected void busy(boolean isBusy) { - this.isBusy = isBusy; + public void setBusy(long userId) { + if (isBusy == FREE) { + this.isBusy = BUSY; + this.userId = userId; + } + } + public boolean isAsker(long userId) { + return this.userId == userId; + } + + public void setFree() { + if (isBusy == BUSY) { + isBusy = FREE; + } } // todo should I overload equals with equals(long) so that a Set may be used instead of a Map @@ -108,7 +121,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; ChannelStatus that = (ChannelStatus) o; - return channelID == that.channelID; + return channelId == that.channelId; } /** @@ -138,6 +151,6 @@ public String toDiscord() { */ @Override public int hashCode() { - return Objects.hash(channelID); + return Objects.hash(channelId); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 46a4dd7bd3..4881041cb3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -17,9 +17,8 @@ import java.awt.*; import java.util.*; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; // todo (can SlashCommandVisibility be narrower than GUILD?) // todo monitor all channels when list is empty? monitor none? @@ -53,13 +52,16 @@ */ public class FreeCommand extends SlashCommandAdapter implements EventListener { private static final Logger logger = LoggerFactory.getLogger(FreeCommand.class); + private static final String STATUS_TITLE = "**__CHANNEL STATUS__**\n\n"; private static final String FREE_COMMAND = "free"; private static final Color FREE_COLOR = Color.decode("#CCCC00"); + private static final ScheduledExecutorService EXECUTOR_SERVICE = + Executors.newSingleThreadScheduledExecutor(); + // Map to store channel ID's, use Guild.getChannels() to guarantee order for display - private final Map channelsById; - private final Map guildToStatusChannel; + private ChannelMonitor channelMonitor; private final Map channelToStatusMessage; private boolean isReady; @@ -75,14 +77,15 @@ public FreeCommand() { super(FREE_COMMAND, "marks this channel as free for another user to ask a question", SlashCommandVisibility.GUILD); - Collection config = Config.getInstance().getFreeCommandConfig(); - guildToStatusChannel = new HashMap<>(); // JDA required to fetch guildID - channelToStatusMessage = new HashMap<>(); // empty .... used to track status messages - channelsById = config.stream() + channelToStatusMessage = new HashMap<>(); + + channelMonitor = new ChannelMonitor(); + Config.getInstance() + .getFreeCommandConfig() + .stream() .map(FreeCommandConfig::getMonitoredChannels) .flatMap(Collection::stream) - .map(ChannelStatus::new) - .collect(Collectors.toMap(ChannelStatus::getChannelID, Function.identity())); + .forEach(channelMonitor::addChannelToMonitor); isReady = false; } @@ -90,10 +93,10 @@ public FreeCommand() { /** * Reaction to the 'onReady' event. This method binds the configurables to the * {@link net.dv8tion.jda.api.JDA} instance. Including fetching the names of the channels this - * command monitors (as per the JDA) + * command monitors. *

- * It also updates the Status messages in their relevant channels, so that the message is up to - * date. + * It also updates the Status messages in their relevant channels, so that the message is + * up-to-date. *

* This also registers a new listener on the {@link net.dv8tion.jda.api.JDA}, this should be * removed when the code base supports additional functionality @@ -105,29 +108,30 @@ public void onReady(@NotNull final ReadyEvent event) { // todo remove this when onGuildMessageRecieved has another access point event.getJDA().addEventListener(this); - Collection config = Config.getInstance().getFreeCommandConfig(); - config.stream() + Config.getInstance() + .getFreeCommandConfig() + .stream() .map(FreeCommandConfig::getStatusChannel) - .distinct() // not necessary? validates user input, since input is from file - // should this test if already present first? - .forEach(id -> guildToStatusChannel - .put(event.getJDA().getGuildChannelById(id).getGuild().getIdLong(), id)); + .distinct() // not necessary? (validates user input, since input is from file) + .map(event.getJDA()::getTextChannelById) + .filter(Objects::nonNull) // not necessary? this will hide errors in the config file + .forEach(channelMonitor::addChannelForStatus); - logger.debug("Config loaded:\nDisplay statuses on {}\nand monitor channels {}", - guildToStatusChannel, channelsById); + logger.debug("Config loaded:\n{}", channelMonitor); - // not collecting to map because onReady is not run during construction - guildToStatusChannel.values() - .stream() + + // not currently working, attempts to find the existing status message. (for all guilds) + channelMonitor.statusIds() .map(event.getJDA()::getTextChannelById) + .filter(Objects::nonNull) // not necessary? this will hide errors in the config file .map(this::getStatusMessageIn) .flatMap(Optional::stream) .forEach(message -> channelToStatusMessage.put(message.getChannel().getIdLong(), message.getIdLong())); + // todo check current channel status - guildToStatusChannel.values() - .stream() + channelMonitor.statusIds() .map(event.getJDA()::getTextChannelById) .forEach(this::displayStatus); @@ -152,10 +156,9 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { long id = event.getChannel().getIdLong(); // do not need to test if key is present, shouldHandle(event) already does. - ChannelStatus status = channelsById.get(id); - if (status.isBusy()) { - status.busy(ChannelStatus.FREE); - displayStatus(getStatusChannelFor(event.getGuild())); + if (channelMonitor.isChannelBusy(id)) { + channelMonitor.setChannelBusy(id, event.getUser().getIdLong()); + displayStatus(channelMonitor.getStatusChannelFor(event.getGuild())); event.reply("This channel is now free.").queue(); } else { Util.sendErrorMessage(event, "This channel is already free, no changes made"); @@ -178,17 +181,17 @@ private boolean shouldHandle(@NotNull final SlashCommandEvent event) { Util.sendErrorMessage(event, "Command not ready please try again in a minute"); return false; } - if (!guildToStatusChannel.containsKey(event.getGuild().getIdLong())) { + if (!channelMonitor.isMonitoringGuild(event.getGuild().getIdLong())) { logger.error( - "Slash command used by {} in {}(channel: {}) when guild is not registered in Free Command", + "Slash command used by {} in {}(channel: {}) when guild is not configured for Free Command", event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); Util.sendErrorMessage(event, "This guild (%s) is not configured to use the '/free' command, please add entries in the config, restart the bot and try again." .formatted(event.getGuild().getName())); return false; } - if (!channelsById.containsKey(event.getChannel().getIdLong())) { - logger.debug("'/free called in unregistered channel {}({})", event.getGuild().getName(), + if (!channelMonitor.isMonitoringChannel(event.getChannel().getIdLong())) { + logger.debug("'/free called in unconfigured channel {}({})", event.getGuild().getName(), event.getChannel().getName()); Util.sendErrorMessage(event, "This channel is not being monitored for free/busy status"); @@ -220,9 +223,9 @@ public void displayStatus(@NotNull TextChannel channel) { if (statusMessage.isPresent()) { Message message = statusMessage.get(); if (message.getIdLong() != latestMessageId) { - message.delete(); + message.delete().queue(); } else { - message.editMessageEmbeds(embed); + message.editMessageEmbeds(embed).queue(); } } @@ -230,43 +233,21 @@ public void displayStatus(@NotNull TextChannel channel) { } public String buildStatusMessage(@NotNull Guild guild) { - if (!guildToStatusChannel.containsKey(guild.getIdLong())) { + if (!channelMonitor.isMonitoringGuild(guild.getIdLong())) { throw new IllegalArgumentException( - "The guild '%s(%s)' is not registered in the free command system" + "The guild '%s(%s)' is not configured in the free command system" .formatted(guild.getName(), guild.getIdLong())); } - List statusFor = guild.getChannels() - .stream() - .map(GuildChannel::getIdLong) - .filter(channelsById::containsKey) - .map(channelsById::get) - .toList(); - - // update name so that current channel name is used - statusFor.forEach(channelStatus -> channelStatus - .setName(guild.getGuildChannelById(channelStatus.getChannelID()).getAsMention())); - - // dynamically separate channels by channel categories - StringBuilder sb = new StringBuilder(); - String categoryName = ""; - for (ChannelStatus status : statusFor) { - Category category = guild.getGuildChannelById(status.getChannelID()).getParent(); - if (category != null && !category.getName().equals(categoryName)) { - categoryName = category.getName(); - sb.append("\n__").append(categoryName).append("__\n"); - } - sb.append(status.toDiscord()).append("\n"); - } - - return sb.toString(); + return channelMonitor.statusMessage(guild); } /** * Method for responding to 'onGuildMessageRecieved' this will need to be replaced by a more * appropriate method when the bot has more functionality. *

- * Marks channels as busy when a user posts a message in a monitored channel that is currently free. + * Marks channels as busy when a user posts a message in a monitored channel that is currently + * free. * * @param event the generic event that includes the 'onGuildMessageReceived'. */ @@ -276,23 +257,26 @@ public void onEvent(@NotNull GenericEvent event) { if (guildEvent.isWebhookMessage() || guildEvent.getAuthor().isBot()) { return; } - ChannelStatus status = channelsById.get(guildEvent.getChannel().getIdLong()); - if (status == null || status.isBusy()) { - logger.debug("Channel status is currently busy, ignoring message received. {} ", - status); + if (channelMonitor.isMonitoringChannel( + ((GuildMessageReceivedEvent) event).getChannel().getIdLong())) { + logger.debug( + "Channel is not being monitored, ignoring message received in {} from {}", + guildEvent.getChannel().getName(), guildEvent.getAuthor()); return; } - status.busy(ChannelStatus.BUSY); - displayStatus(getStatusChannelFor(guildEvent.getGuild())); + if (channelMonitor.isChannelBusy(guildEvent.getChannel().getIdLong())) { + logger.debug( + "Channel status is currently busy, ignoring message received in {} from {}", + guildEvent.getChannel().getName(), guildEvent.getAuthor()); + return; + } + channelMonitor.setChannelBusy(guildEvent.getChannel().getIdLong(), + guildEvent.getAuthor().getIdLong()); + displayStatus(channelMonitor.getStatusChannelFor(guildEvent.getGuild())); guildEvent.getMessage().reply("The channel was free, please ask your question").queue(); } } - private TextChannel getStatusChannelFor(@NotNull final Guild guild) { - // todo add error checking for invalid keys ?? - return guild.getTextChannelById(guildToStatusChannel.get(guild.getIdLong())); - } - private Optional getStatusMessageIn(@NotNull TextChannel channel) { if (channelToStatusMessage.containsKey(channel.getIdLong())) { Long id = channelToStatusMessage.get(channel.getIdLong()); @@ -315,7 +299,7 @@ private Optional findExistingStatusMessage(@NotNull TextChannel channel .map(history -> history.stream() .filter(message -> !message.getEmbeds().isEmpty()) .filter(message -> message.getAuthor().equals(channel.getJDA().getSelfUser())) - .filter(message -> message.getEmbeds().get(0).getTitle().equals(STATUS_TITLE)) + .filter(message -> STATUS_TITLE.equals(message.getEmbeds().get(0).getTitle())) .findFirst()) .complete(); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java deleted file mode 100644 index 4f57d3f4c1..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/StatusMessage.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.togetherjava.tjbot.commands.free; - -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.TextChannel; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * A start at building a class for status messages. Most functionality that should be here is - * currently in {@link FreeCommand} - */ -@SuppressWarnings("c") -public class StatusMessage { - private final Guild guild; - private final TextChannel postingChannel; - private final Message existingStatus; - private final List channelStatuses; - - - private StatusMessage(@NotNull Guild guild, @NotNull TextChannel postingChannel, - @NotNull List channelStatuses, Message existingStatus) { - this.guild = guild; - this.postingChannel = postingChannel; - this.channelStatuses = channelStatuses; - this.existingStatus = existingStatus; - } - - private static class StatusMessageBuilder { - private List channelStatuses; - private TextChannel postingChannel; - - private StatusMessageBuilder(@NotNull List channelStatuses) { - this.channelStatuses = channelStatuses; - } - - public StatusMessageBuilder channelToPostIn(TextChannel channel) { - postingChannel = channel; - return this; - } - } - - public static StatusMessageBuilder builder(@NotNull List channelStatuses) { - return new StatusMessageBuilder(channelStatuses); - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/package-info.java new file mode 100644 index 0000000000..5fcca7cd69 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/package-info.java @@ -0,0 +1,5 @@ +/** + * This packages offers all the functionality for the free command system. Marking channels as + * free/busy and displaying a status message representing such + */ +package org.togetherjava.tjbot.commands.free; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/JDAScheduler.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/JDAScheduler.java new file mode 100644 index 0000000000..1c134d9a6b --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/JDAScheduler.java @@ -0,0 +1,34 @@ +package org.togetherjava.tjbot.commands.system; + +import net.dv8tion.jda.api.JDA; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.concurrent.*; + +public class JDAScheduler { + private static JDAScheduler instance; + + private final JDA jda; + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private JDAScheduler(JDA jda) { + this.jda = jda; + } + + public static void create(@NotNull JDA jda) { + Objects.requireNonNull(jda, "Cannot create a JDAScheduler without an instance of JDA"); + if (instance != null) { + throw new IllegalStateException("Can only create one instance of JDAScheduler"); + } + + instance = new JDAScheduler(jda); + } + + public static ScheduledFuture scheduleTask(Callable task, long delay, TimeUnit timeUnit) { + Objects.requireNonNull(instance, + "Cannot schedule a task before the scheduler has been created"); + + return instance.scheduler.schedule(task, delay, timeUnit); + } +} From 6cce0afaf1fbd00dfd87c52d51bc3ebbba459812 Mon Sep 17 00:00:00 2001 From: borgrel Date: Thu, 4 Nov 2021 13:47:46 +0200 Subject: [PATCH 05/78] Moved user responses to its own class Added status message editing Added on boot channel inactive check --- .../tjbot/commands/free/ChannelMonitor.java | 76 +++++++--- .../tjbot/commands/free/ChannelStatus.java | 39 ++++-- .../tjbot/commands/free/FreeCommand.java | 131 ++++++++++-------- .../tjbot/commands/free/UserStrings.java | 35 +++++ .../tjbot/commands/free/Util.java | 7 + application/src/main/resources/log4j2.xml | 3 +- 6 files changed, 205 insertions(+), 86 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index d5e350ea33..dcfba43d89 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -30,7 +30,7 @@ public class ChannelMonitor { * * @param channelId the id of the channel to monitor */ - public void addChannelToMonitor(long channelId) { + public void addChannelToMonitor(final long channelId) { channelsToMonitor.put(channelId, new ChannelStatus(channelId)); } @@ -39,48 +39,73 @@ public void addChannelToMonitor(long channelId) { * stores the long id it requires, the method requires the actual {@link TextChannel} to be * passed because it needs to verify it as well as store the guild id. * + * This method also calls a method which updates the status of the channels in the {@link Guild} + * . So always add the status channel AFTER you have added all monitored channels for the guild, + * see {@link #addChannelToMonitor(long)}. + * * @param channel the channel the status message must be displayed in */ - public void addChannelForStatus(TextChannel channel) { + public void addChannelForStatus(@NotNull final TextChannel channel) { postStatusInChannel.put(channel.getGuild().getIdLong(), channel.getIdLong()); - updateChannelStatusFor(channel.getGuild()); + updateStatusFor(channel.getGuild()); } - public boolean isMonitoringGuild(long guildId) { + public boolean isMonitoringGuild(final long guildId) { return postStatusInChannel.containsKey(guildId); } - public boolean isChannelBusy(long channelId) { + public boolean isMonitoringChannel(final long channelId) { + return channelsToMonitor.containsKey(channelId); + } + + public boolean isChannelBusy(final long channelId) { return channelsToMonitor.get(channelId).isBusy(); } - public Stream guildIds() { - return postStatusInChannel.keySet().stream(); + public boolean isChannelInactive(@NotNull final TextChannel channel) { + if (!channelsToMonitor.containsKey(channel.getIdLong())) { + throw new IllegalArgumentException( + "Channel requested %s is not monitored by free channel" + .formatted(channel.getName())); + } + return channel.getHistory() + .retrievePast(1) + .map(messages -> messages.get(0)) + .map(Message::getTimeCreated) + .map(createdTime -> createdTime.isBefore(Util.anHourAgo())) + .complete(); } - public Stream statusIds() { - return postStatusInChannel.values().stream(); + public void setChannelBusy(final long channelId, final long userId) { + channelsToMonitor.get(channelId).setBusy(userId); } - public boolean isMonitoringChannel(long channelId) { - return channelsToMonitor.containsKey(channelId); + public void setChannelFree(final long channelId) { + channelsToMonitor.get(channelId).setFree(); } - public void setChannelBusy(long channelId, long userId) { - channelsToMonitor.get(channelId).setBusy(userId); + public Stream guildIds() { + return postStatusInChannel.keySet().stream(); + } + + public Stream statusIds() { + return postStatusInChannel.values().stream(); } - public String statusMessage(@NotNull Guild guild) { - List statusFor = guild.getChannels() + private @NotNull List guildMonitoredChannelsList(@NotNull final Guild guild) { + return guild.getChannels() .stream() .map(GuildChannel::getIdLong) .filter(channelsToMonitor::containsKey) .map(channelsToMonitor::get) .toList(); + } + + public String statusMessage(@NotNull final Guild guild) { + List statusFor = guildMonitoredChannelsList(guild); // update name so that current channel name is used - statusFor.forEach(channelStatus -> channelStatus - .setName(guild.getGuildChannelById(channelStatus.getChannelId()).getAsMention())); + statusFor.forEach(channelStatus -> channelStatus.updateChannelName(guild)); // dynamically separate channels by channel categories StringBuilder sb = new StringBuilder(); @@ -89,6 +114,7 @@ public String statusMessage(@NotNull Guild guild) { Category category = guild.getGuildChannelById(status.getChannelId()).getParent(); if (category != null && !category.getName().equals(categoryName)) { categoryName = category.getName(); + // append the category name on a new line with markup for underlining sb.append("\n__").append(categoryName).append("__\n"); } sb.append(status.toDiscord()).append("\n"); @@ -97,15 +123,27 @@ public String statusMessage(@NotNull Guild guild) { return sb.toString(); } - private void updateChannelStatusFor(@NotNull Guild guild) { - //need to complete code here + public void updateStatusFor(@NotNull Guild guild) { + List statusFor = guildMonitoredChannelsList(guild); + + statusFor.stream() + .parallel() + .filter(ChannelStatus::isBusy) + .map(ChannelStatus::getChannelId) + .map(guild::getTextChannelById) + .filter(this::isChannelInactive) + .map(TextChannel::getIdLong) + .forEach(this::setChannelFree); + } + public TextChannel getStatusChannelFor(@NotNull final Guild guild) { // todo add error checking for invalid keys ?? return guild.getTextChannelById(postStatusInChannel.get(guild.getIdLong())); } + @Override public String toString() { return "Monitoring Channels: %s%nDisplaying on Channels: %s".formatted(channelsToMonitor, postStatusInChannel); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 2c1e2312b5..63171500ee 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -1,6 +1,8 @@ package org.togetherjava.tjbot.commands.free; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.GuildChannel; import net.dv8tion.jda.api.entities.TextChannel; import org.jetbrains.annotations.NotNull; @@ -33,7 +35,7 @@ public final class ChannelStatus { */ ChannelStatus(long id) { channelId = id; - isBusy = false; + isBusy = BUSY; name = Long.toString(id); } @@ -75,6 +77,11 @@ public long getChannelId() { return name; } + + private void setName(@NotNull String name) { + this.name = Objects.requireNonNull(name); + } + /** * Method used to keep the channel name up to date with the {@link JDA}. This method is not * called automatically. Manually update before using the value. @@ -83,11 +90,22 @@ public long getChannelId() { *

* This method is called in multithreaded context, however the value is not expected to change * regularly and will not break anything if its invalid so no has not been made thread safe. - * - * @param name the value to set the channel name to. + * + * @param guild the {@link Guild} that the channel belongs to, to retrieve its name from. */ - public void setName(String name) { - this.name = Objects.requireNonNull(name); + public void updateChannelName(@NotNull Guild guild) { + GuildChannel channel = guild.getGuildChannelById(channelId); + if (channel == null) { + throw new IllegalArgumentException( + "The guild passed in '%s' is not related to the channel this status is for: %s" + .formatted(guild.getName(), this)); + } + if (channel instanceof TextChannel textChannel) { + setName(textChannel.getAsMention()); + } else + throw new IllegalStateException( + "This channel status was created with the id for a non-text-channel and status cannot be monitored: '%s'" + .formatted(channelId)); } public void setBusy(long userId) { @@ -96,6 +114,7 @@ public void setBusy(long userId) { this.userId = userId; } } + public boolean isAsker(long userId) { return this.userId == userId; } @@ -120,8 +139,9 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - ChannelStatus that = (ChannelStatus) o; - return channelId == that.channelId; + + ChannelStatus status = (ChannelStatus) o; + return channelId == status.channelId; } /** @@ -135,8 +155,9 @@ public String toString() { } /** - * A {@link #toString()} method spetially formatted for Discord ({@link JDA}. Uses emojis by - * string representation. + * A {@link #toString()} method specially formatted for Discord ({@link JDA}. Uses emojis by + * string representation, that discord will automatically convert into images. Using this string + * outside of discord will display unexpected results. * * @return a String representation of ChannelStatus, formatted for Discord */ diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 4881041cb3..53bfdea5e7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -1,6 +1,10 @@ package org.togetherjava.tjbot.commands.free; -import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.events.GenericEvent; import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; @@ -17,8 +21,6 @@ import java.awt.*; import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; // todo (can SlashCommandVisibility be narrower than GUILD?) // todo monitor all channels when list is empty? monitor none? @@ -57,9 +59,6 @@ public class FreeCommand extends SlashCommandAdapter implements EventListener { private static final String FREE_COMMAND = "free"; private static final Color FREE_COLOR = Color.decode("#CCCC00"); - private static final ScheduledExecutorService EXECUTOR_SERVICE = - Executors.newSingleThreadScheduledExecutor(); - // Map to store channel ID's, use Guild.getChannels() to guarantee order for display private ChannelMonitor channelMonitor; private final Map channelToStatusMessage; @@ -78,14 +77,7 @@ public FreeCommand() { SlashCommandVisibility.GUILD); channelToStatusMessage = new HashMap<>(); - channelMonitor = new ChannelMonitor(); - Config.getInstance() - .getFreeCommandConfig() - .stream() - .map(FreeCommandConfig::getMonitoredChannels) - .flatMap(Collection::stream) - .forEach(channelMonitor::addChannelToMonitor); isReady = false; } @@ -105,34 +97,20 @@ public FreeCommand() { */ @Override public void onReady(@NotNull final ReadyEvent event) { + final JDA jda = event.getJDA(); // todo remove this when onGuildMessageRecieved has another access point - event.getJDA().addEventListener(this); - - Config.getInstance() - .getFreeCommandConfig() - .stream() - .map(FreeCommandConfig::getStatusChannel) - .distinct() // not necessary? (validates user input, since input is from file) - .map(event.getJDA()::getTextChannelById) - .filter(Objects::nonNull) // not necessary? this will hide errors in the config file - .forEach(channelMonitor::addChannelForStatus); + jda.addEventListener(this); + initChannelsToMonitor(); + initStatusMessageChannels(jda); logger.debug("Config loaded:\n{}", channelMonitor); - - // not currently working, attempts to find the existing status message. (for all guilds) - channelMonitor.statusIds() - .map(event.getJDA()::getTextChannelById) - .filter(Objects::nonNull) // not necessary? this will hide errors in the config file - .map(this::getStatusMessageIn) - .flatMap(Optional::stream) - .forEach(message -> channelToStatusMessage.put(message.getChannel().getIdLong(), - message.getIdLong())); - - // todo check current channel status + checkBusyStatusAllChannels(jda); + initStatusMessages(jda); channelMonitor.statusIds() .map(event.getJDA()::getTextChannelById) + .filter(Objects::nonNull) .forEach(this::displayStatus); isReady = true; @@ -151,17 +129,20 @@ public void onReady(@NotNull final ReadyEvent event) { public void onSlashCommand(@NotNull final SlashCommandEvent event) { logger.debug("/free used by {} on channel {}", event.getUser().getName(), event.getChannel().getName()); - if (!shouldHandle(event)) + if (!shouldHandle(event)) { return; + } long id = event.getChannel().getIdLong(); // do not need to test if key is present, shouldHandle(event) already does. if (channelMonitor.isChannelBusy(id)) { - channelMonitor.setChannelBusy(id, event.getUser().getIdLong()); + // todo check if /free called by original author, if not put message asking if he + // approves + channelMonitor.setChannelFree(id); displayStatus(channelMonitor.getStatusChannelFor(event.getGuild())); - event.reply("This channel is now free.").queue(); + event.reply(UserStrings.MARK_AS_FREE.message()).queue(); } else { - Util.sendErrorMessage(event, "This channel is already free, no changes made"); + Util.sendErrorMessage(event, UserStrings.ALREADY_FREE_ERROR.message()); } } @@ -178,7 +159,7 @@ private boolean shouldHandle(@NotNull final SlashCommandEvent event) { logger.debug( "Slash command requested by {} in {}(channel: {}) before command is ready.", event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); - Util.sendErrorMessage(event, "Command not ready please try again in a minute"); + Util.sendErrorMessage(event, UserStrings.NOT_READY_ERROR.message()); return false; } if (!channelMonitor.isMonitoringGuild(event.getGuild().getIdLong())) { @@ -186,15 +167,13 @@ private boolean shouldHandle(@NotNull final SlashCommandEvent event) { "Slash command used by {} in {}(channel: {}) when guild is not configured for Free Command", event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); Util.sendErrorMessage(event, - "This guild (%s) is not configured to use the '/free' command, please add entries in the config, restart the bot and try again." - .formatted(event.getGuild().getName())); + UserStrings.NOT_CONFIGURED_ERROR.formatted(event.getGuild().getName())); return false; } if (!channelMonitor.isMonitoringChannel(event.getChannel().getIdLong())) { - logger.debug("'/free called in unconfigured channel {}({})", event.getGuild().getName(), - event.getChannel().getName()); - Util.sendErrorMessage(event, - "This channel is not being monitored for free/busy status"); + logger.debug("'/free called in un-configured channel {}({})", + event.getGuild().getName(), event.getChannel().getName()); + Util.sendErrorMessage(event, UserStrings.NOT_MONITORED_ERROR.message()); return false; } @@ -219,17 +198,25 @@ public void displayStatus(@NotNull TextChannel channel) { long latestMessageId = channel.getLatestMessageIdLong(); Optional statusMessage = getStatusMessageIn(channel); - channel.sendMessage("Message selected is: " + statusMessage).queue(); if (statusMessage.isPresent()) { Message message = statusMessage.get(); if (message.getIdLong() != latestMessageId) { message.delete().queue(); + channel.sendMessageEmbeds(embed) + .queue(message1 -> channelToStatusMessage.put(channel.getIdLong(), + message1.getIdLong())); } else { message.editMessageEmbeds(embed).queue(); } + } else { + channel.sendMessageEmbeds(embed) + .queue(message1 -> channelToStatusMessage.put(channel.getIdLong(), + message1.getIdLong())); } + } - channel.sendMessageEmbeds(embed).queue(); + private void checkBusyStatusAllChannels(JDA jda) { + channelMonitor.guildIds().map(jda::getGuildById).forEach(channelMonitor::updateStatusFor); } public String buildStatusMessage(@NotNull Guild guild) { @@ -243,7 +230,7 @@ public String buildStatusMessage(@NotNull Guild guild) { } /** - * Method for responding to 'onGuildMessageRecieved' this will need to be replaced by a more + * Method for responding to 'onGuildMessageReceived' this will need to be replaced by a more * appropriate method when the bot has more functionality. *

* Marks channels as busy when a user posts a message in a monitored channel that is currently @@ -257,8 +244,7 @@ public void onEvent(@NotNull GenericEvent event) { if (guildEvent.isWebhookMessage() || guildEvent.getAuthor().isBot()) { return; } - if (channelMonitor.isMonitoringChannel( - ((GuildMessageReceivedEvent) event).getChannel().getIdLong())) { + if (!channelMonitor.isMonitoringChannel(guildEvent.getChannel().getIdLong())) { logger.debug( "Channel is not being monitored, ignoring message received in {} from {}", guildEvent.getChannel().getName(), guildEvent.getAuthor()); @@ -273,7 +259,7 @@ public void onEvent(@NotNull GenericEvent event) { channelMonitor.setChannelBusy(guildEvent.getChannel().getIdLong(), guildEvent.getAuthor().getIdLong()); displayStatus(channelMonitor.getStatusChannelFor(guildEvent.getGuild())); - guildEvent.getMessage().reply("The channel was free, please ask your question").queue(); + guildEvent.getMessage().reply(UserStrings.NEW_QUESTION.message()).queue(); } } @@ -283,7 +269,7 @@ private Optional getStatusMessageIn(@NotNull TextChannel channel) { if (id == null) { return Optional.empty(); } - Message message = channel.getHistory().getMessageById(id); + Message message = channel.getHistoryAround(id, 1).complete().getMessageById(id); if (message == null) { return Optional.empty(); } @@ -299,13 +285,46 @@ private Optional findExistingStatusMessage(@NotNull TextChannel channel .map(history -> history.stream() .filter(message -> !message.getEmbeds().isEmpty()) .filter(message -> message.getAuthor().equals(channel.getJDA().getSelfUser())) - .filter(message -> STATUS_TITLE.equals(message.getEmbeds().get(0).getTitle())) + // .filter(message -> STATUS_TITLE.equals(message.getEmbeds().get(0).getTitle())) .findFirst()) .complete(); - Message mapping = result.orElse(null); - channelToStatusMessage.put(channel.getIdLong(), - mapping == null ? null : mapping.getIdLong()); + if (result.isPresent()) { + channelToStatusMessage.put(channel.getIdLong(), result.get().getIdLong()); + } else { + channelToStatusMessage.put(channel.getIdLong(), null); + } return result; } + + private void initChannelsToMonitor() { + Config.getInstance() + .getFreeCommandConfig() + .stream() + .map(FreeCommandConfig::getMonitoredChannels) + .flatMap(Collection::stream) + .forEach(channelMonitor::addChannelToMonitor); + } + + private void initStatusMessageChannels(@NotNull final JDA jda) { + Config.getInstance() + .getFreeCommandConfig() + .stream() + .map(FreeCommandConfig::getStatusChannel) + .distinct() // not necessary? (validates user input, since input is from file) + .map(jda::getTextChannelById) + .filter(Objects::nonNull) // not necessary? this will hide errors in the config file + .forEach(channelMonitor::addChannelForStatus); + } + + private void initStatusMessages(@NotNull final JDA jda) { + // not currently working, attempts to find the existing status message. (for all guilds) + channelMonitor.statusIds() + .map(jda::getTextChannelById) + .filter(Objects::nonNull) // not necessary? this will hide errors in the config file + .map(this::getStatusMessageIn) + .flatMap(Optional::stream) + .forEach(message -> channelToStatusMessage.put(message.getChannel().getIdLong(), + message.getIdLong())); + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java new file mode 100644 index 0000000000..92823a3389 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -0,0 +1,35 @@ +package org.togetherjava.tjbot.commands.free; + +import javax.annotation.Nullable; + +public enum UserStrings { + NEW_QUESTION(""" + Thank you for asking a question in an available channel. + When a helper who can answer this question reads it they will help you. + Please be patient. + + __Please do not post your question in other channels__ + """), + MARK_AS_FREE("This channel is now free for a question to be asked."), + ALREADY_FREE_ERROR("This channel is already free, no changes made"), + NOT_READY_ERROR("Command not ready please try again in a minute"), + NOT_MONITORED_ERROR("This channel is not being monitored for free/busy status." + + " If you believe this channel should be part of the free/busy status system," + + " please discuss it with a moderator"), + NOT_CONFIGURED_ERROR("This guild (%s) is not configured to use the '/free' command," + + " please add entries in the config, restart the bot and try again."); + + private final String message; + + UserStrings(String message) { + this.message = message; + } + + public String message() { + return message; + } + + public String formatted(@Nullable Object... args) { + return message.formatted(args); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java index bc4970bf9a..1e95aaee99 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java @@ -3,6 +3,9 @@ import net.dv8tion.jda.api.interactions.Interaction; import org.jetbrains.annotations.NotNull; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; + public class Util { // private constructor to prevent this class getting instantiated private Util() {} @@ -17,4 +20,8 @@ public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull S interaction.reply(message).setEphemeral(true).queue(); } + public static OffsetDateTime anHourAgo() { + return OffsetDateTime.now().minus(1, ChronoUnit.HOURS); + } + } diff --git a/application/src/main/resources/log4j2.xml b/application/src/main/resources/log4j2.xml index d82af3842e..9bf489aa18 100644 --- a/application/src/main/resources/log4j2.xml +++ b/application/src/main/resources/log4j2.xml @@ -21,10 +21,9 @@ - + - From 19b94f26c799eafda799c396d66b7d374b8c5690 Mon Sep 17 00:00:00 2001 From: borgrel Date: Thu, 4 Nov 2021 22:47:04 +0200 Subject: [PATCH 06/78] Added JavaDocs --- .../tjbot/commands/free/ChannelMonitor.java | 98 ++++++++++++++++++- .../tjbot/commands/free/ChannelStatus.java | 54 +++++++--- .../tjbot/commands/free/FreeCommand.java | 4 +- .../tjbot/commands/free/UserStrings.java | 26 ++++- 4 files changed, 162 insertions(+), 20 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index dcfba43d89..5dadff9a66 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -50,24 +50,53 @@ public void addChannelForStatus(@NotNull final TextChannel channel) { updateStatusFor(channel.getGuild()); } + /** + * This method tests whether a guild id is configured for monitoring in the free command system. + * + * @param guildId the id of the guild to test. + * @return {@code true} if the guild is configured in the system, {@code false} otherwise. + */ public boolean isMonitoringGuild(final long guildId) { return postStatusInChannel.containsKey(guildId); } + /** + * This method tests whether a channel id is configured for monitoring in the free command + * system. + * + * @param channelId the id of the channel to test. + * @return {@code true} if the channel is configured in the system, {@code false} otherwise. + */ public boolean isMonitoringChannel(final long channelId) { return channelsToMonitor.containsKey(channelId); } + /** + * This is a delegation method to pass operations to {@link ChannelStatus}. see + * {@link ChannelStatus#isBusy()} for details. + * + * @param channelId the id for the channel to test. + * @return {@code true} if the channel is 'busy', false if the channel is 'free'. + */ public boolean isChannelBusy(final long channelId) { return channelsToMonitor.get(channelId).isBusy(); } + /** + * This method tests if a channel is currently active by fetching the latest message and testing + * if it was posted more than an hour ago. + * + * @param channel the channel to test. + * @return {@code true} if the channel is inactive, false if it has received messages more + * recently than an hour ago. + */ public boolean isChannelInactive(@NotNull final TextChannel channel) { if (!channelsToMonitor.containsKey(channel.getIdLong())) { throw new IllegalArgumentException( "Channel requested %s is not monitored by free channel" .formatted(channel.getName())); } + // todo change the entire inactive test to work via restactions return channel.getHistory() .retrievePast(1) .map(messages -> messages.get(0)) @@ -76,18 +105,43 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { .complete(); } + /** + * This is a delegation method to pass operations to {@link ChannelStatus}. see + * {@link ChannelStatus#setBusy(long)} for details. + * + * @param channelId the id for the channel status to modify. + * @param userId the id of the user changing the status to busy. + */ public void setChannelBusy(final long channelId, final long userId) { channelsToMonitor.get(channelId).setBusy(userId); } + /** + * This is a delegation method to pass operations to {@link ChannelStatus}. see + * {@link ChannelStatus#setFree()} for details. + * + * @param channelId the id for the channel status to modify. + */ public void setChannelFree(final long channelId) { channelsToMonitor.get(channelId).setFree(); } + /** + * This method provides a stream of the id's for guilds that are currently being monitored. This + * is streamed purely as a simple method of encapsulation. + * + * @return a stream of guild id's + */ public Stream guildIds() { return postStatusInChannel.keySet().stream(); } + /** + * This method provides a stream of the id's for channels that status's are displayed in. This + * is streamed purely as a simple method of encapsulation. + * + * @return a stream of channel id's + */ public Stream statusIds() { return postStatusInChannel.values().stream(); } @@ -101,6 +155,22 @@ public Stream statusIds() { .toList(); } + /** + * Creates the status message (specific to the guild specified) that shows which channels are + * busy/free. + *

+ * It gets the list of all channels in the guild and filters out all channels not being + * monitored (to get the correct channel order) it then updates the names of the channels in + * case they were changed on the guild. + *

+ * If then iterates through all channels and checks which category they are in so that it can + * add the categories to the output too. + * + * @param guild the guild the message is intended for. + * @return a string representing the busy/free status of channels in this guild. The String + * includes emojis and other discord specific markup. Attempting to display this + * somewhere other than discord will lead to unexpected results. + */ public String statusMessage(@NotNull final Guild guild) { List statusFor = guildMonitoredChannelsList(guild); @@ -115,6 +185,9 @@ public String statusMessage(@NotNull final Guild guild) { if (category != null && !category.getName().equals(categoryName)) { categoryName = category.getName(); // append the category name on a new line with markup for underlining + // fixme possible bug when not all channels are part of categories, may mistakenly + // include uncategoried channels inside previous category. will an uncategoried + // channel return an empty string or null? javadocs dont say. sb.append("\n__").append(categoryName).append("__\n"); } sb.append(status.toDiscord()).append("\n"); @@ -123,6 +196,16 @@ public String statusMessage(@NotNull final Guild guild) { return sb.toString(); } + /** + * This method checks all channels in a guild that is currently being monitored and are + * currently busy and determines if the last time it was updated is more than an hour ago. If so + * it changes the channel's status to free. + *

+ * This method is run automatically during startup and should be run on a 15minute schedule. The + * scheduled execution is not currently implemented + * + * @param guild the guild for which to test the channel statuses of. + */ public void updateStatusFor(@NotNull Guild guild) { List statusFor = guildMonitoredChannelsList(guild); @@ -134,15 +217,26 @@ public void updateStatusFor(@NotNull Guild guild) { .filter(this::isChannelInactive) .map(TextChannel::getIdLong) .forEach(this::setChannelFree); - } - + /** + * This method returns the {@link TextChannel} that has been configured as the output of the + * status messages about busy/free for the specified guild. + * + * @param guild the {@link Guild} for which to retrieve the TextChannel for. + * @return the TextChannel where status messages are output in the specified guild. + */ public TextChannel getStatusChannelFor(@NotNull final Guild guild) { // todo add error checking for invalid keys ?? return guild.getTextChannelById(postStatusInChannel.get(guild.getIdLong())); } + /** + * The toString method for this class, it prints out a list of the currently monitored channels + * and the channels the status are printed in. This is called on boot by as a debug level logger + * + * @return the string to print. + */ @Override public String toString() { return "Monitoring Channels: %s%nDisplaying on Channels: %s".formatted(channelsToMonitor, diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 63171500ee..4cd039e2d7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -33,7 +33,7 @@ public final class ChannelStatus { * * @param id the long id of the {@link net.dv8tion.jda.api.entities.TextChannel} to monitor. */ - ChannelStatus(long id) { + ChannelStatus(final long id) { channelId = id; isBusy = BUSY; name = Long.toString(id); @@ -53,6 +53,18 @@ public boolean isBusy() { return isBusy; } + /** + * Method to test if an id is the same as the id of the help requester who most recently posted + * a question. + * + * @param userId the id to test + * @return {@code true} if the id value passed in is the same as the value of the user who most + * recently changed the status to 'busy'. {@code false} otherwise. + */ + public boolean isAsker(final long userId) { + return this.userId == userId; + } + /** * Retrieves the id for the {@link net.dv8tion.jda.api.entities.TextChannel} that this instance * represents. There is no guarantee that the id is valid according to the {@link JDA}. @@ -77,8 +89,7 @@ public long getChannelId() { return name; } - - private void setName(@NotNull String name) { + private void setName(@NotNull final String name) { this.name = Objects.requireNonNull(name); } @@ -89,11 +100,12 @@ private void setName(@NotNull String name) { * The recommended value to use is {@link TextChannel#getAsMention()} *

* This method is called in multithreaded context, however the value is not expected to change - * regularly and will not break anything if its invalid so no has not been made thread safe. + * regularly and will not break anything if it is incorrect for a read or two, so it has not + * been made thread safe. * * @param guild the {@link Guild} that the channel belongs to, to retrieve its name from. */ - public void updateChannelName(@NotNull Guild guild) { + public void updateChannelName(@NotNull final Guild guild) { GuildChannel channel = guild.getGuildChannelById(channelId); if (channel == null) { throw new IllegalArgumentException( @@ -108,17 +120,33 @@ public void updateChannelName(@NotNull Guild guild) { .formatted(channelId)); } - public void setBusy(long userId) { + /** + * Method to set the channel status to busy, a user id is passed in to keep track of the current + * user requesting help. This id will be used to confirm that the author is satisfied with the + * channel being marked as free. + *

+ * This functionality is not yet implemented so the id can be anything atm. Also note that on + * reboot the bot does not currently search for the author so the first time its marked as free + * there will be no confirmation. + * + * @param userId the id of the user who changed the status to 'busy' + */ + public void setBusy(final long userId) { if (isBusy == FREE) { this.isBusy = BUSY; this.userId = userId; } } - public boolean isAsker(long userId) { - return this.userId == userId; - } - + /** + * Method to set the channel status to free, the user id of the previous help requester is not + * overwritten by this method. So until another user changes the status to busy the old value + * will remain. + *

+ * The value will be 0 until the first time that the status is changed from free to busy. + *

+ * This functionality is not yet implemented so the id can be anything atm. + */ public void setFree() { if (isBusy == BUSY) { isBusy = FREE; @@ -134,7 +162,7 @@ public void setFree() { * @return whether the objects have the same id or not. */ @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) @@ -150,7 +178,7 @@ public boolean equals(Object o) { * @return a String representation of the instance. */ @Override - public String toString() { + public @NotNull String toString() { return "ChannelStatus{ %s is %s }".formatted(name, isBusy ? "busy" : "not busy"); } @@ -161,7 +189,7 @@ public String toString() { * * @return a String representation of ChannelStatus, formatted for Discord */ - public String toDiscord() { + public @NotNull String toDiscord() { return "%s %s".formatted(isBusy ? BUSY_STATUS : FREE_STATUS, name); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 53bfdea5e7..84177fa767 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -25,7 +25,7 @@ // todo (can SlashCommandVisibility be narrower than GUILD?) // todo monitor all channels when list is empty? monitor none? // todo (use other emojis? use images?) -// todo add command to add/remove/status channels to monitor +// todo add command to add/remove/status channels to monitor? // todo test if message is a reply and don't mark as busy if it is // todo add button query to confirm that message is new question not additional info for existing // discussion before marking as busy @@ -110,7 +110,7 @@ public void onReady(@NotNull final ReadyEvent event) { channelMonitor.statusIds() .map(event.getJDA()::getTextChannelById) - .filter(Objects::nonNull) + .filter(Objects::nonNull) .forEach(this::displayStatus); isReady = true; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java index 92823a3389..4828420de4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -1,7 +1,13 @@ package org.togetherjava.tjbot.commands.free; +import org.jetbrains.annotations.NotNull; + import javax.annotation.Nullable; +/** + * Class containing all the strings sent to users during their interaction with the free command + * system. This does not include the logged strings or the exception strings. + */ public enum UserStrings { NEW_QUESTION(""" Thank you for asking a question in an available channel. @@ -21,15 +27,29 @@ public enum UserStrings { private final String message; - UserStrings(String message) { + UserStrings(@NotNull String message) { this.message = message; } - public String message() { + /** + * Method to fetch the string that will be sent to a user in reaction to any event triggered by + * the free command system for that user. + * + * @return the string to send to a user to give them the specified response. + */ + public @NotNull String message() { return message; } - public String formatted(@Nullable Object... args) { + /** + * Method to fetch the string that will be sent to a user in reaction to any event triggered by + * the free command system for that user. This can be used to add tagged values in the same way + * as {@link String#format(String, Object...)} + * + * @param args the replacement values for the specified tags. + * @return the string to send to a user to give them the specified response. + */ + public @NotNull String formatted(@Nullable Object... args) { return message.formatted(args); } } From 22a7c64da2c8c8e9237596c9e02e2a1d4643167c Mon Sep 17 00:00:00 2001 From: borgrel Date: Sun, 7 Nov 2021 10:41:41 +0200 Subject: [PATCH 07/78] Changed from user.getName() to getAsTag() (as per review) --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 84177fa767..43b01372f0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -127,7 +127,7 @@ public void onReady(@NotNull final ReadyEvent event) { */ @Override public void onSlashCommand(@NotNull final SlashCommandEvent event) { - logger.debug("/free used by {} on channel {}", event.getUser().getName(), + logger.debug("/free used by {} on channel {}", event.getUser().getAsTag(), event.getChannel().getName()); if (!shouldHandle(event)) { return; From e6ebf01caa07a6e1a0589fbff4077dbe74aab8f5 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sun, 7 Nov 2021 13:08:22 +0200 Subject: [PATCH 08/78] Changed UserStrings to TextBlocks --- .../tjbot/commands/free/UserStrings.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java index 4828420de4..00366f7483 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -16,14 +16,21 @@ public enum UserStrings { __Please do not post your question in other channels__ """), - MARK_AS_FREE("This channel is now free for a question to be asked."), - ALREADY_FREE_ERROR("This channel is already free, no changes made"), - NOT_READY_ERROR("Command not ready please try again in a minute"), - NOT_MONITORED_ERROR("This channel is not being monitored for free/busy status." - + " If you believe this channel should be part of the free/busy status system," - + " please discuss it with a moderator"), - NOT_CONFIGURED_ERROR("This guild (%s) is not configured to use the '/free' command," - + " please add entries in the config, restart the bot and try again."); + MARK_AS_FREE(""" + This channel is now free for a question to be asked. + """), + ALREADY_FREE_ERROR(""" + This channel is already free, no changes made + """), + NOT_READY_ERROR(""" + Command not ready please try again in a minute + """), + NOT_MONITORED_ERROR(""" + This channel is not being monitored for free/busy status. If you believe this channel should be part of the free/busy status system, please discuss it with a moderator + """), + NOT_CONFIGURED_ERROR(""" + This guild (%s) is not configured to use the '/free' command, please add entries in the config, restart the bot and try again. + """); private final String message; From 778a41dac42e24baf28cef5ee988b404817ae458 Mon Sep 17 00:00:00 2001 From: borgrel Date: Mon, 8 Nov 2021 22:32:51 +0200 Subject: [PATCH 09/78] @NotNull added, some grammer corrections --- .../org/togetherjava/tjbot/commands/free/UserStrings.java | 8 ++++---- .../java/org/togetherjava/tjbot/commands/free/Util.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java index 00366f7483..3ffcd1ae4e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -17,13 +17,13 @@ public enum UserStrings { __Please do not post your question in other channels__ """), MARK_AS_FREE(""" - This channel is now free for a question to be asked. + This channel is now available for a question to be asked. """), ALREADY_FREE_ERROR(""" - This channel is already free, no changes made + This channel is already free, no changes made. """), NOT_READY_ERROR(""" - Command not ready please try again in a minute + Command not ready please try again in a minute. """), NOT_MONITORED_ERROR(""" This channel is not being monitored for free/busy status. If you believe this channel should be part of the free/busy status system, please discuss it with a moderator @@ -59,4 +59,4 @@ This guild (%s) is not configured to use the '/free' command, please add entries public @NotNull String formatted(@Nullable Object... args) { return message.formatted(args); } -} +} \ No newline at end of file diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java index 1e95aaee99..c62aab174a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java @@ -20,7 +20,7 @@ public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull S interaction.reply(message).setEphemeral(true).queue(); } - public static OffsetDateTime anHourAgo() { + public static @NotNull OffsetDateTime anHourAgo() { return OffsetDateTime.now().minus(1, ChronoUnit.HOURS); } From 591b25ac1a12bdea70190e211349e2466e330b85 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 9 Nov 2021 14:57:57 +0200 Subject: [PATCH 10/78] JavaDoc revisions, added some extra syntax --- .../tjbot/commands/free/ChannelMonitor.java | 10 +++++----- .../togetherjava/tjbot/commands/free/UserStrings.java | 9 +++++---- .../togetherjava/tjbot/config/FreeCommandConfig.java | 6 ++++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 5dadff9a66..6e18dd91b5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -11,9 +11,9 @@ /** * A class responsible for monitoring the status of channels and reporting on their busy/free status - * for use by {@link FreeCommand} + * for use by {@link FreeCommand}. */ -public class ChannelMonitor { +public final class ChannelMonitor { // Map to store channel ID's, use Guild.getChannels() to guarantee order for display private final Map channelsToMonitor; private final Map postStatusInChannel; @@ -39,9 +39,9 @@ public void addChannelToMonitor(final long channelId) { * stores the long id it requires, the method requires the actual {@link TextChannel} to be * passed because it needs to verify it as well as store the guild id. * - * This method also calls a method which updates the status of the channels in the {@link Guild} - * . So always add the status channel AFTER you have added all monitored channels for the guild, - * see {@link #addChannelToMonitor(long)}. + * This method also calls a method which updates the status of the channels in the + * {@link Guild}. So always add the status channel after you have added all + * monitored channels for the guild, see {@link #addChannelToMonitor(long)}. * * @param channel the channel the status message must be displayed in */ diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java index 3ffcd1ae4e..252dfed93d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -25,11 +25,12 @@ public enum UserStrings { NOT_READY_ERROR(""" Command not ready please try again in a minute. """), - NOT_MONITORED_ERROR(""" - This channel is not being monitored for free/busy status. If you believe this channel should be part of the free/busy status system, please discuss it with a moderator - """), + NOT_MONITORED_ERROR("This channel is not being monitored for free/busy status. If you" + + "believe this channel should be part of the free/busy status system, please discuss it" + + "with a moderator"), NOT_CONFIGURED_ERROR(""" - This guild (%s) is not configured to use the '/free' command, please add entries in the config, restart the bot and try again. + This guild (%s) is not configured to use the '/free' command. + Please add entries in the config, restart the bot and try again. """); private final String message; diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index 0ee1e85813..a769258418 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -3,9 +3,11 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonRootName; +import org.jetbrains.annotations.NotNull; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; /** @@ -54,7 +56,7 @@ public long getStatusChannel() { * * @return an Unmodifiable List of Channel ID's */ - public Collection getMonitoredChannels() { - return monitoredChannels; + public @NotNull Collection getMonitoredChannels() { + return Collections.unmodifiableCollection(monitoredChannels); } } From 9889b8713f92430cf61bf4699d3e0d8220f94772 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 9 Nov 2021 15:31:31 +0200 Subject: [PATCH 11/78] Made config unmodifiable more explicit. Fixed javadocs --- .../tjbot/commands/free/ChannelMonitor.java | 15 +++++---------- .../tjbot/commands/free/UserStrings.java | 2 +- .../org/togetherjava/tjbot/config/Config.java | 8 ++++---- .../tjbot/config/FreeCommandConfig.java | 9 ++++----- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 6e18dd91b5..ab95163cf7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -18,8 +18,6 @@ public final class ChannelMonitor { private final Map channelsToMonitor; private final Map postStatusInChannel; - - ChannelMonitor() { postStatusInChannel = new HashMap<>(); // JDA required to fetch guildID channelsToMonitor = new HashMap<>(); @@ -132,7 +130,7 @@ public void setChannelFree(final long channelId) { * * @return a stream of guild id's */ - public Stream guildIds() { + public @NotNull Stream guildIds() { return postStatusInChannel.keySet().stream(); } @@ -142,7 +140,7 @@ public Stream guildIds() { * * @return a stream of channel id's */ - public Stream statusIds() { + public @NotNull Stream statusIds() { return postStatusInChannel.values().stream(); } @@ -159,12 +157,9 @@ public Stream statusIds() { * Creates the status message (specific to the guild specified) that shows which channels are * busy/free. *

- * It gets the list of all channels in the guild and filters out all channels not being - * monitored (to get the correct channel order) it then updates the names of the channels in - * case they were changed on the guild. - *

- * If then iterates through all channels and checks which category they are in so that it can - * add the categories to the output too. + * It first updates the channel names, order and grouping(categories) according to + * {@link net.dv8tion.jda.api.JDA} for the monitored channels. So that the output is always + * consistent with remote changes. * * @param guild the guild the message is intended for. * @return a string representing the busy/free status of channels in this guild. The String diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java index 252dfed93d..1ccca9d1bc 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -60,4 +60,4 @@ This guild (%s) is not configured to use the '/free' command. public @NotNull String formatted(@Nullable Object... args) { return message.formatted(args); } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 706c453245..35af42d53b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -6,8 +6,8 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -45,7 +45,7 @@ private Config(@JsonProperty("token") String token, @JsonProperty("heavyModerationRolePattern") String heavyModerationRolePattern, @JsonProperty("softModerationRolePattern") String softModerationRolePattern, @JsonProperty("tagManageRolePattern") String tagManageRolePattern, - @JsonProperty("freeCommand") FreeCommandConfig[] freeCommand) { + @JsonProperty("freeCommand") List freeCommand) { this.token = token; this.databasePath = databasePath; this.projectWebsite = projectWebsite; @@ -55,7 +55,7 @@ private Config(@JsonProperty("token") String token, this.heavyModerationRolePattern = heavyModerationRolePattern; this.softModerationRolePattern = softModerationRolePattern; this.tagManageRolePattern = tagManageRolePattern; - this.freeCommand = Arrays.stream(freeCommand).toList(); + this.freeCommand = Collections.unmodifiableList(freeCommand); } /** @@ -175,6 +175,6 @@ public String getTagManageRolePattern() { * guild. */ public Collection getFreeCommandConfig() { - return freeCommand; + return freeCommand; // already unmodifiable } } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index a769258418..e926c24581 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonRootName; import org.jetbrains.annotations.NotNull; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -34,11 +33,11 @@ public final class FreeCommandConfig { private final long statusChannel; private final List monitoredChannels; - @JsonCreator + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) private FreeCommandConfig(@JsonProperty("statusChannel") long statusChannel, - @JsonProperty("monitoredChannels") long[] monitoredChannels) { + @JsonProperty("monitoredChannels") List monitoredChannels) { this.statusChannel = statusChannel; - this.monitoredChannels = Arrays.stream(monitoredChannels).boxed().toList(); + this.monitoredChannels = Collections.unmodifiableList(monitoredChannels); } /** @@ -57,6 +56,6 @@ public long getStatusChannel() { * @return an Unmodifiable List of Channel ID's */ public @NotNull Collection getMonitoredChannels() { - return Collections.unmodifiableCollection(monitoredChannels); + return monitoredChannels; // already unmodifiable } } From 330cfcba3e27b862c2783e74f38cf44483240756 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 9 Nov 2021 20:54:05 +0200 Subject: [PATCH 12/78] Fixed accidental log4j2.xml commit Changed classname Util to FreeUtil --- .../togetherjava/tjbot/commands/free/ChannelMonitor.java | 2 +- .../org/togetherjava/tjbot/commands/free/FreeCommand.java | 8 ++++---- .../tjbot/commands/free/{Util.java => FreeUtil.java} | 8 ++++---- .../org/togetherjava/tjbot/config/FreeCommandConfig.java | 3 +++ application/src/main/resources/log4j2.xml | 3 ++- 5 files changed, 14 insertions(+), 10 deletions(-) rename application/src/main/java/org/togetherjava/tjbot/commands/free/{Util.java => FreeUtil.java} (76%) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index ab95163cf7..91237576d3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -99,7 +99,7 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { .retrievePast(1) .map(messages -> messages.get(0)) .map(Message::getTimeCreated) - .map(createdTime -> createdTime.isBefore(Util.anHourAgo())) + .map(createdTime -> createdTime.isBefore(FreeUtil.anHourAgo())) .complete(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 43b01372f0..66f7551f9b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -142,7 +142,7 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { displayStatus(channelMonitor.getStatusChannelFor(event.getGuild())); event.reply(UserStrings.MARK_AS_FREE.message()).queue(); } else { - Util.sendErrorMessage(event, UserStrings.ALREADY_FREE_ERROR.message()); + FreeUtil.sendErrorMessage(event, UserStrings.ALREADY_FREE_ERROR.message()); } } @@ -159,21 +159,21 @@ private boolean shouldHandle(@NotNull final SlashCommandEvent event) { logger.debug( "Slash command requested by {} in {}(channel: {}) before command is ready.", event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); - Util.sendErrorMessage(event, UserStrings.NOT_READY_ERROR.message()); + FreeUtil.sendErrorMessage(event, UserStrings.NOT_READY_ERROR.message()); return false; } if (!channelMonitor.isMonitoringGuild(event.getGuild().getIdLong())) { logger.error( "Slash command used by {} in {}(channel: {}) when guild is not configured for Free Command", event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); - Util.sendErrorMessage(event, + FreeUtil.sendErrorMessage(event, UserStrings.NOT_CONFIGURED_ERROR.formatted(event.getGuild().getName())); return false; } if (!channelMonitor.isMonitoringChannel(event.getChannel().getIdLong())) { logger.debug("'/free called in un-configured channel {}({})", event.getGuild().getName(), event.getChannel().getName()); - Util.sendErrorMessage(event, UserStrings.NOT_MONITORED_ERROR.message()); + FreeUtil.sendErrorMessage(event, UserStrings.NOT_MONITORED_ERROR.message()); return false; } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java similarity index 76% rename from application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java rename to application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index c62aab174a..6b330a4645 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/Util.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -2,13 +2,13 @@ import net.dv8tion.jda.api.interactions.Interaction; import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.config.FreeCommandConfig; import java.time.OffsetDateTime; -import java.time.temporal.ChronoUnit; -public class Util { +public class FreeUtil { // private constructor to prevent this class getting instantiated - private Util() {} + private FreeUtil() {} /** * Helper method to easily send ephemeral messages to users. @@ -21,7 +21,7 @@ public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull S } public static @NotNull OffsetDateTime anHourAgo() { - return OffsetDateTime.now().minus(1, ChronoUnit.HOURS); + return OffsetDateTime.now().minus(FreeCommandConfig.INACTIVE_DURATION, FreeCommandConfig.INACTIVE_UNIT); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index e926c24581..b73797c6b9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonRootName; import org.jetbrains.annotations.NotNull; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -30,6 +31,8 @@ @SuppressWarnings("ClassCanBeRecord") @JsonRootName(value = "freeCommand") public final class FreeCommandConfig { + public static final long INACTIVE_DURATION = 1; + public static final ChronoUnit INACTIVE_UNIT = ChronoUnit.HOURS; private final long statusChannel; private final List monitoredChannels; diff --git a/application/src/main/resources/log4j2.xml b/application/src/main/resources/log4j2.xml index 9bf489aa18..d82af3842e 100644 --- a/application/src/main/resources/log4j2.xml +++ b/application/src/main/resources/log4j2.xml @@ -21,9 +21,10 @@ - + + From 2fc84efd2d5bfd2da1458b9d31ac2ad75c545550 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 9 Nov 2021 21:44:30 +0200 Subject: [PATCH 13/78] Changed 'todo's to uppercase changed method name 'anHourAgo' to 'inactiveTimeLimit' added javadocs --- .../tjbot/commands/free/ChannelMonitor.java | 28 ++++++++++++++----- .../tjbot/commands/free/ChannelStatus.java | 2 +- .../tjbot/commands/free/FreeCommand.java | 18 ++++++------ .../tjbot/commands/free/FreeUtil.java | 5 ++-- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 91237576d3..11f86dbf95 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -12,6 +12,17 @@ /** * A class responsible for monitoring the status of channels and reporting on their busy/free status * for use by {@link FreeCommand}. + * + * Channels for monitoring are added via {@link #addChannelToMonitor(long)} however the monitoring + * will not be accessible/visible until a channel in the same {@link Guild} is registered for the + * output via {@link #addChannelForStatus(TextChannel)}. This will all happen automatically for any + * channels listed in {@link org.togetherjava.tjbot.config.FreeCommandConfig}. + * + * When a status channel is added for a guild, all monitored channels for that guild are tested and + * an {@link IllegalStateException} is thrown if any of them are not {@link TextChannel}s. + * + * After successful configuration, any changes in busy/free status will automatically be displayed + * in the configured {@code Status Channel} for that guild. */ public final class ChannelMonitor { // Map to store channel ID's, use Guild.getChannels() to guarantee order for display @@ -19,7 +30,7 @@ public final class ChannelMonitor { private final Map postStatusInChannel; ChannelMonitor() { - postStatusInChannel = new HashMap<>(); // JDA required to fetch guildID + postStatusInChannel = new HashMap<>(); // JDA required to populate map channelsToMonitor = new HashMap<>(); } @@ -50,9 +61,11 @@ public void addChannelForStatus(@NotNull final TextChannel channel) { /** * This method tests whether a guild id is configured for monitoring in the free command system. + * To add a guild for monitoring see {@link org.togetherjava.tjbot.config.FreeCommandConfig} or + * {@link #addChannelForStatus(TextChannel)}. * * @param guildId the id of the guild to test. - * @return {@code true} if the guild is configured in the system, {@code false} otherwise. + * @return whether the guild is configured in the free command system or not. */ public boolean isMonitoringGuild(final long guildId) { return postStatusInChannel.containsKey(guildId); @@ -60,7 +73,8 @@ public boolean isMonitoringGuild(final long guildId) { /** * This method tests whether a channel id is configured for monitoring in the free command - * system. + * system. To add a channel for monitoring see {@link org.togetherjava.tjbot.config.FreeCommandConfig} or + * {@link #addChannelToMonitor(long)}. * * @param channelId the id of the channel to test. * @return {@code true} if the channel is configured in the system, {@code false} otherwise. @@ -94,12 +108,12 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { "Channel requested %s is not monitored by free channel" .formatted(channel.getName())); } - // todo change the entire inactive test to work via restactions + // TODO change the entire inactive test to work via restactions return channel.getHistory() .retrievePast(1) .map(messages -> messages.get(0)) .map(Message::getTimeCreated) - .map(createdTime -> createdTime.isBefore(FreeUtil.anHourAgo())) + .map(createdTime -> createdTime.isBefore(FreeUtil.inactiveTimeLimit())) .complete(); } @@ -180,7 +194,7 @@ public String statusMessage(@NotNull final Guild guild) { if (category != null && !category.getName().equals(categoryName)) { categoryName = category.getName(); // append the category name on a new line with markup for underlining - // fixme possible bug when not all channels are part of categories, may mistakenly + // FIXME possible bug when not all channels are part of categories, may mistakenly // include uncategoried channels inside previous category. will an uncategoried // channel return an empty string or null? javadocs dont say. sb.append("\n__").append(categoryName).append("__\n"); @@ -222,7 +236,7 @@ public void updateStatusFor(@NotNull Guild guild) { * @return the TextChannel where status messages are output in the specified guild. */ public TextChannel getStatusChannelFor(@NotNull final Guild guild) { - // todo add error checking for invalid keys ?? + // TODO add error checking for invalid keys ?? return guild.getTextChannelById(postStatusInChannel.get(guild.getIdLong())); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 4cd039e2d7..4a3f40ae5f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -153,7 +153,7 @@ public void setFree() { } } - // todo should I overload equals with equals(long) so that a Set may be used instead of a Map + // TODO should I overload equals with equals(long) so that a Set may be used instead of a Map /** * The identity of this object of solely based on the id value. Compares the long id's and * determines if they are equal. diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 66f7551f9b..bf2b73fe21 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -22,14 +22,14 @@ import java.awt.*; import java.util.*; -// todo (can SlashCommandVisibility be narrower than GUILD?) -// todo monitor all channels when list is empty? monitor none? -// todo (use other emojis? use images?) -// todo add command to add/remove/status channels to monitor? -// todo test if message is a reply and don't mark as busy if it is -// todo add button query to confirm that message is new question not additional info for existing +// TODO (can SlashCommandVisibility be narrower than GUILD?) +// TODO monitor all channels when list is empty? monitor none? +// TODO (use other emojis? use images?) +// TODO add command to add/remove/status channels to monitor? +// TODO test if message is a reply and don't mark as busy if it is +// TODO add button query to confirm that message is new question not additional info for existing // discussion before marking as busy -// todo add scheduled tasks to check last message every 15mins and mark as free if 1hr (2hrs?) has +// TODO add scheduled tasks to check last message every 15mins and mark as free if 1hr (2hrs?) has // passed /** @@ -98,7 +98,7 @@ public FreeCommand() { @Override public void onReady(@NotNull final ReadyEvent event) { final JDA jda = event.getJDA(); - // todo remove this when onGuildMessageRecieved has another access point + // TODO remove this when onGuildMessageRecieved has another access point jda.addEventListener(this); initChannelsToMonitor(); @@ -136,7 +136,7 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { long id = event.getChannel().getIdLong(); // do not need to test if key is present, shouldHandle(event) already does. if (channelMonitor.isChannelBusy(id)) { - // todo check if /free called by original author, if not put message asking if he + // TODO check if /free called by original author, if not put message asking if he // approves channelMonitor.setChannelFree(id); displayStatus(channelMonitor.getStatusChannelFor(event.getGuild())); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index 6b330a4645..ad4f15ede8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -20,8 +20,9 @@ public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull S interaction.reply(message).setEphemeral(true).queue(); } - public static @NotNull OffsetDateTime anHourAgo() { - return OffsetDateTime.now().minus(FreeCommandConfig.INACTIVE_DURATION, FreeCommandConfig.INACTIVE_UNIT); + public static @NotNull OffsetDateTime inactiveTimeLimit() { + return OffsetDateTime.now() + .minus(FreeCommandConfig.INACTIVE_DURATION, FreeCommandConfig.INACTIVE_UNIT); } } From 16f42ee1fae4bbac60d0e5b25df9d83f6b02b736 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 9 Nov 2021 23:07:42 +0200 Subject: [PATCH 14/78] improved javadocs removed obsolete 'requireNonNull' call --- .../tjbot/commands/free/ChannelMonitor.java | 22 ++++++++++--------- .../tjbot/commands/free/ChannelStatus.java | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 11f86dbf95..a3f5f65bf8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -73,7 +73,8 @@ public boolean isMonitoringGuild(final long guildId) { /** * This method tests whether a channel id is configured for monitoring in the free command - * system. To add a channel for monitoring see {@link org.togetherjava.tjbot.config.FreeCommandConfig} or + * system. To add a channel for monitoring see + * {@link org.togetherjava.tjbot.config.FreeCommandConfig} or * {@link #addChannelToMonitor(long)}. * * @param channelId the id of the channel to test. @@ -96,7 +97,10 @@ public boolean isChannelBusy(final long channelId) { /** * This method tests if a channel is currently active by fetching the latest message and testing - * if it was posted more than an hour ago. + * if it was posted more recently than the configured time limit, see + * {@link FreeUtil#inactiveTimeLimit()} and + * {@link org.togetherjava.tjbot.config.FreeCommandConfig#INACTIVE_DURATION}, + * {@link org.togetherjava.tjbot.config.FreeCommandConfig#INACTIVE_UNIT}. * * @param channel the channel to test. * @return {@code true} if the channel is inactive, false if it has received messages more @@ -216,10 +220,7 @@ public String statusMessage(@NotNull final Guild guild) { * @param guild the guild for which to test the channel statuses of. */ public void updateStatusFor(@NotNull Guild guild) { - List statusFor = guildMonitoredChannelsList(guild); - - statusFor.stream() - .parallel() + guildMonitoredChannelsList(guild).parallelStream() .filter(ChannelStatus::isBusy) .map(ChannelStatus::getChannelId) .map(guild::getTextChannelById) @@ -235,19 +236,20 @@ public void updateStatusFor(@NotNull Guild guild) { * @param guild the {@link Guild} for which to retrieve the TextChannel for. * @return the TextChannel where status messages are output in the specified guild. */ - public TextChannel getStatusChannelFor(@NotNull final Guild guild) { + public @NotNull TextChannel getStatusChannelFor(@NotNull final Guild guild) { // TODO add error checking for invalid keys ?? return guild.getTextChannelById(postStatusInChannel.get(guild.getIdLong())); } /** - * The toString method for this class, it prints out a list of the currently monitored channels - * and the channels the status are printed in. This is called on boot by as a debug level logger + * The toString method for this class, it generates a human-readable text string of the + * currently monitored channels and the channels the status are printed in. * - * @return the string to print. + * @return the human-readable text string that describes this class. */ @Override public String toString() { + // This is called on boot by as a debug level logger return "Monitoring Channels: %s%nDisplaying on Channels: %s".formatted(channelsToMonitor, postStatusInChannel); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 4a3f40ae5f..fabdce3c78 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -90,7 +90,7 @@ public long getChannelId() { } private void setName(@NotNull final String name) { - this.name = Objects.requireNonNull(name); + this.name = name; } /** From 8a772762693de13febbe687d78d4760f47b3ff4f Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 9 Nov 2021 23:57:20 +0200 Subject: [PATCH 15/78] removed exposed implementation details from javadoc --- .../org/togetherjava/tjbot/commands/free/ChannelStatus.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index fabdce3c78..8758196cd7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -83,7 +83,7 @@ public long getChannelId() { *

* The recommended value to use is {@link TextChannel#getAsMention()}. * - * @return The currently stored name of the channel, originally the long id as string. + * @return The currently stored name of the channel. */ public @NotNull String getName() { return name; @@ -155,7 +155,7 @@ public void setFree() { // TODO should I overload equals with equals(long) so that a Set may be used instead of a Map /** - * The identity of this object of solely based on the id value. Compares the long id's and + * The identity of this object is solely based on the id value. Compares the long id's and * determines if they are equal. * * @param o the other object to test against From 0c08fafc587686cfcd064408f9c9ff2653178e90 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 10 Nov 2021 11:37:19 +0200 Subject: [PATCH 16/78] renamed channelsToMonitor to channelsToMonitorById --- .../tjbot/commands/free/ChannelMonitor.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index a3f5f65bf8..a6e69f8535 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -26,12 +26,12 @@ */ public final class ChannelMonitor { // Map to store channel ID's, use Guild.getChannels() to guarantee order for display - private final Map channelsToMonitor; + private final Map channelsToMonitorById; private final Map postStatusInChannel; ChannelMonitor() { postStatusInChannel = new HashMap<>(); // JDA required to populate map - channelsToMonitor = new HashMap<>(); + channelsToMonitorById = new HashMap<>(); } /** @@ -40,7 +40,7 @@ public final class ChannelMonitor { * @param channelId the id of the channel to monitor */ public void addChannelToMonitor(final long channelId) { - channelsToMonitor.put(channelId, new ChannelStatus(channelId)); + channelsToMonitorById.put(channelId, new ChannelStatus(channelId)); } /** @@ -81,7 +81,7 @@ public boolean isMonitoringGuild(final long guildId) { * @return {@code true} if the channel is configured in the system, {@code false} otherwise. */ public boolean isMonitoringChannel(final long channelId) { - return channelsToMonitor.containsKey(channelId); + return channelsToMonitorById.containsKey(channelId); } /** @@ -92,7 +92,7 @@ public boolean isMonitoringChannel(final long channelId) { * @return {@code true} if the channel is 'busy', false if the channel is 'free'. */ public boolean isChannelBusy(final long channelId) { - return channelsToMonitor.get(channelId).isBusy(); + return channelsToMonitorById.get(channelId).isBusy(); } /** @@ -107,7 +107,7 @@ public boolean isChannelBusy(final long channelId) { * recently than an hour ago. */ public boolean isChannelInactive(@NotNull final TextChannel channel) { - if (!channelsToMonitor.containsKey(channel.getIdLong())) { + if (!channelsToMonitorById.containsKey(channel.getIdLong())) { throw new IllegalArgumentException( "Channel requested %s is not monitored by free channel" .formatted(channel.getName())); @@ -129,7 +129,7 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { * @param userId the id of the user changing the status to busy. */ public void setChannelBusy(final long channelId, final long userId) { - channelsToMonitor.get(channelId).setBusy(userId); + channelsToMonitorById.get(channelId).setBusy(userId); } /** @@ -139,7 +139,7 @@ public void setChannelBusy(final long channelId, final long userId) { * @param channelId the id for the channel status to modify. */ public void setChannelFree(final long channelId) { - channelsToMonitor.get(channelId).setFree(); + channelsToMonitorById.get(channelId).setFree(); } /** @@ -166,8 +166,8 @@ public void setChannelFree(final long channelId) { return guild.getChannels() .stream() .map(GuildChannel::getIdLong) - .filter(channelsToMonitor::containsKey) - .map(channelsToMonitor::get) + .filter(channelsToMonitorById::containsKey) + .map(channelsToMonitorById::get) .toList(); } @@ -250,7 +250,7 @@ public void updateStatusFor(@NotNull Guild guild) { @Override public String toString() { // This is called on boot by as a debug level logger - return "Monitoring Channels: %s%nDisplaying on Channels: %s".formatted(channelsToMonitor, + return "Monitoring Channels: %s%nDisplaying on Channels: %s".formatted(channelsToMonitorById, postStatusInChannel); } } From e2e52d284398943076104ce8b01e2e86dc135aab Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 10 Nov 2021 15:35:23 +0200 Subject: [PATCH 17/78] made testing interval configurable updated javadocs to reflect it --- .../togetherjava/tjbot/commands/free/ChannelMonitor.java | 9 +++++---- .../org/togetherjava/tjbot/config/FreeCommandConfig.java | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index a6e69f8535..e1d1e16fe0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -214,8 +214,9 @@ public String statusMessage(@NotNull final Guild guild) { * currently busy and determines if the last time it was updated is more than an hour ago. If so * it changes the channel's status to free. *

- * This method is run automatically during startup and should be run on a 15minute schedule. The - * scheduled execution is not currently implemented + * This method is run automatically during startup and should be run on a set schedule, as + * defined in {@link org.togetherjava.tjbot.config.FreeCommandConfig}. The scheduled execution + * is not currently implemented * * @param guild the guild for which to test the channel statuses of. */ @@ -250,7 +251,7 @@ public void updateStatusFor(@NotNull Guild guild) { @Override public String toString() { // This is called on boot by as a debug level logger - return "Monitoring Channels: %s%nDisplaying on Channels: %s".formatted(channelsToMonitorById, - postStatusInChannel); + return "Monitoring Channels: %s%nDisplaying on Channels: %s" + .formatted(channelsToMonitorById, postStatusInChannel); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index b73797c6b9..f920643b2e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -33,6 +33,9 @@ public final class FreeCommandConfig { public static final long INACTIVE_DURATION = 1; public static final ChronoUnit INACTIVE_UNIT = ChronoUnit.HOURS; + public static final long INACTIVE_TEST_INTERVAL = 15; + public static final ChronoUnit INACTIVE_TEST_UNIT = ChronoUnit.MINUTES; + private final long statusChannel; private final List monitoredChannels; From a8b754dad60343d95598eeddde6d11e1f89cb453 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 10 Nov 2021 15:54:23 +0200 Subject: [PATCH 18/78] replaced accidental NPE's with IllegalArgumentException --- .../tjbot/commands/free/ChannelMonitor.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index e1d1e16fe0..4e4ec32117 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -95,6 +95,14 @@ public boolean isChannelBusy(final long channelId) { return channelsToMonitorById.get(channelId).isBusy(); } + private ChannelStatus requiresIsMonitored(final long channelId) { + if (!channelsToMonitorById.containsKey(channelId)) { + throw new IllegalArgumentException( + "Channel with id: %s is not monitored by free channel".formatted(channelId)); + } + return channelsToMonitorById.get(channelId); + } + /** * This method tests if a channel is currently active by fetching the latest message and testing * if it was posted more recently than the configured time limit, see @@ -105,13 +113,12 @@ public boolean isChannelBusy(final long channelId) { * @param channel the channel to test. * @return {@code true} if the channel is inactive, false if it has received messages more * recently than an hour ago. + * @throws IllegalArgumentException if the channel passed is not monitored. See + * {@link #addChannelToMonitor(long)} */ public boolean isChannelInactive(@NotNull final TextChannel channel) { - if (!channelsToMonitorById.containsKey(channel.getIdLong())) { - throw new IllegalArgumentException( - "Channel requested %s is not monitored by free channel" - .formatted(channel.getName())); - } + requiresIsMonitored(channel.getIdLong()); + // TODO change the entire inactive test to work via restactions return channel.getHistory() .retrievePast(1) @@ -127,9 +134,11 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { * * @param channelId the id for the channel status to modify. * @param userId the id of the user changing the status to busy. + * @throws IllegalArgumentException if the channel passed is not monitored. See + * {@link #addChannelToMonitor(long)} */ public void setChannelBusy(final long channelId, final long userId) { - channelsToMonitorById.get(channelId).setBusy(userId); + requiresIsMonitored(channelId).setBusy(userId); } /** @@ -137,9 +146,11 @@ public void setChannelBusy(final long channelId, final long userId) { * {@link ChannelStatus#setFree()} for details. * * @param channelId the id for the channel status to modify. + * @throws IllegalArgumentException if the channel passed is not monitored. See + * {@link #addChannelToMonitor(long)} */ public void setChannelFree(final long channelId) { - channelsToMonitorById.get(channelId).setFree(); + requiresIsMonitored(channelId).setFree(); } /** From b49e3654b362f87054d785db11c8479d6dcadb25 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 12:25:25 +0200 Subject: [PATCH 19/78] made class FreeCommand final --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index bf2b73fe21..112cc8a92e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -52,7 +52,7 @@ * channel may be one of the monitored channels however it is recommended that a different channel * is used. */ -public class FreeCommand extends SlashCommandAdapter implements EventListener { +public final class FreeCommand extends SlashCommandAdapter implements EventListener { private static final Logger logger = LoggerFactory.getLogger(FreeCommand.class); private static final String STATUS_TITLE = "**__CHANNEL STATUS__**\n\n"; From c32960361c4173a9b0c84a59bd9c9af55d4269e0 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 12:27:14 +0200 Subject: [PATCH 20/78] changed FREE_COMMAND constant to COMMAND_NAME --- .../org/togetherjava/tjbot/commands/free/FreeCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 112cc8a92e..4481b55f6a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -56,7 +56,7 @@ public final class FreeCommand extends SlashCommandAdapter implements EventListe private static final Logger logger = LoggerFactory.getLogger(FreeCommand.class); private static final String STATUS_TITLE = "**__CHANNEL STATUS__**\n\n"; - private static final String FREE_COMMAND = "free"; + private static final String COMMAND_NAME = "free"; private static final Color FREE_COLOR = Color.decode("#CCCC00"); // Map to store channel ID's, use Guild.getChannels() to guarantee order for display @@ -73,7 +73,7 @@ public final class FreeCommand extends SlashCommandAdapter implements EventListe * {@link FreeCommandConfig}) for further details. */ public FreeCommand() { - super(FREE_COMMAND, "marks this channel as free for another user to ask a question", + super(COMMAND_NAME, "marks this channel as free for another user to ask a question", SlashCommandVisibility.GUILD); channelToStatusMessage = new HashMap<>(); From 7a750a34bb995120c3ca335bb4c22b9b38f730df Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 12:33:43 +0200 Subject: [PATCH 21/78] removed term private from javadocs --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 4481b55f6a..6546942e5b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -147,7 +147,7 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { } /** - * Private method to test event to see if it should be processed. + * Method to test event to see if it should be processed. * * Will respond to users describing the problem if the event should not be processed. * From 5558f99951fa6894e1986484014097e1bc9893a3 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 12:37:46 +0200 Subject: [PATCH 22/78] changed method name shouldHandle to handleShouldBeProcessed --- .../org/togetherjava/tjbot/commands/free/FreeCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 6546942e5b..fbeb7929a1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -129,7 +129,7 @@ public void onReady(@NotNull final ReadyEvent event) { public void onSlashCommand(@NotNull final SlashCommandEvent event) { logger.debug("/free used by {} on channel {}", event.getUser().getAsTag(), event.getChannel().getName()); - if (!shouldHandle(event)) { + if (!handleShouldBeProcessed(event)) { return; } @@ -154,7 +154,7 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { * @param event the event to test for validity. * @return true if the event should be processed false otherwise. */ - private boolean shouldHandle(@NotNull final SlashCommandEvent event) { + private boolean handleShouldBeProcessed(@NotNull final SlashCommandEvent event) { if (!isReady) { logger.debug( "Slash command requested by {} in {}(channel: {}) before command is ready.", From 8cb41f938bc2019c26da7d8f6784d4d9c9342c2d Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 12:53:01 +0200 Subject: [PATCH 23/78] added @NotNull to checkBusyStatusAllChannels --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index fbeb7929a1..9feecca9e8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -215,7 +215,7 @@ public void displayStatus(@NotNull TextChannel channel) { } } - private void checkBusyStatusAllChannels(JDA jda) { + private void checkBusyStatusAllChannels(@NotNull JDA jda) { channelMonitor.guildIds().map(jda::getGuildById).forEach(channelMonitor::updateStatusFor); } From d44fe5e4ae649ef960f96c7779b8756693dac7a6 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 12:56:01 +0200 Subject: [PATCH 24/78] added @NotNull to buildStatusMessage --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 9feecca9e8..e88cdd5186 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -219,7 +219,7 @@ private void checkBusyStatusAllChannels(@NotNull JDA jda) { channelMonitor.guildIds().map(jda::getGuildById).forEach(channelMonitor::updateStatusFor); } - public String buildStatusMessage(@NotNull Guild guild) { + public @NotNull String buildStatusMessage(@NotNull Guild guild) { if (!channelMonitor.isMonitoringGuild(guild.getIdLong())) { throw new IllegalArgumentException( "The guild '%s(%s)' is not configured in the free command system" From a86c8874bde4cd47c1193d2abe38b307cd5a6a56 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 13:01:11 +0200 Subject: [PATCH 25/78] added @NotNull to getStatusMessage --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index e88cdd5186..d2efafc67b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -263,7 +263,7 @@ public void onEvent(@NotNull GenericEvent event) { } } - private Optional getStatusMessageIn(@NotNull TextChannel channel) { + private @NotNull Optional getStatusMessageIn(@NotNull TextChannel channel) { if (channelToStatusMessage.containsKey(channel.getIdLong())) { Long id = channelToStatusMessage.get(channel.getIdLong()); if (id == null) { From c7221409981a05009c8cd39b9461d70b502e6a7d Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 13:03:20 +0200 Subject: [PATCH 26/78] added @NotNull to findExistingStatusMessage --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index d2efafc67b..2db2406cd4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -278,7 +278,7 @@ public void onEvent(@NotNull GenericEvent event) { return findExistingStatusMessage(channel); } - private Optional findExistingStatusMessage(@NotNull TextChannel channel) { + private @NotNull Optional findExistingStatusMessage(@NotNull TextChannel channel) { // will only run when bots starts, afterwards its stored in a map Optional result = channel.getHistory() .retrievePast(100) From e774dcf027a665cd15b96902bfa64875ceeeaf6a Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 13:08:17 +0200 Subject: [PATCH 27/78] improved code flow --- .../org/togetherjava/tjbot/commands/free/FreeCommand.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 2db2406cd4..21be818799 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -289,11 +289,8 @@ public void onEvent(@NotNull GenericEvent event) { .findFirst()) .complete(); - if (result.isPresent()) { - channelToStatusMessage.put(channel.getIdLong(), result.get().getIdLong()); - } else { - channelToStatusMessage.put(channel.getIdLong(), null); - } + channelToStatusMessage.put(channel.getIdLong(), + result.map(Message::getIdLong).orElse(null)); return result; } From 88755b7764c579189e6e5478667263a41ac0ad5c Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 13:25:09 +0200 Subject: [PATCH 28/78] fixed formatting in javadocs --- .../java/org/togetherjava/tjbot/config/FreeCommandConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index f920643b2e..641724f188 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -24,7 +24,7 @@ * }] * * - * Additional Guilds may add their settings by adding additional {"statusChannel....... } + * Additional Guilds may add their settings by adding additional {@code {"statusChannel":... } } * * The long channel ID can be found by right-clicking on the channel and selecting 'Copy ID' */ From d1e0d0dd9ea119ca4984636fca55d634e4f182a3 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 13:30:39 +0200 Subject: [PATCH 29/78] added //TODO with issue number --- .../java/org/togetherjava/tjbot/config/FreeCommandConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index 641724f188..995194578f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -31,6 +31,7 @@ @SuppressWarnings("ClassCanBeRecord") @JsonRootName(value = "freeCommand") public final class FreeCommandConfig { + // TODO make constants configurable via config file once config templating (#234) is pushed public static final long INACTIVE_DURATION = 1; public static final ChronoUnit INACTIVE_UNIT = ChronoUnit.HOURS; public static final long INACTIVE_TEST_INTERVAL = 15; From b2dc17cddbfe3b09b85eb0ebe64a7107b1b1799e Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 13:50:25 +0200 Subject: [PATCH 30/78] changed to enum-util pattern --- .../java/org/togetherjava/tjbot/commands/free/FreeUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index ad4f15ede8..989ee4aca4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -6,7 +6,8 @@ import java.time.OffsetDateTime; -public class FreeUtil { +public enum FreeUtil { + ; // private constructor to prevent this class getting instantiated private FreeUtil() {} From 60f550e98843a51e73398bc34b40fb8a638b6446 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 13:51:18 +0200 Subject: [PATCH 31/78] changed postStatusInChannel to guildIdToStatusChannel --- .../tjbot/commands/free/ChannelMonitor.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 4e4ec32117..ca7ef6b6d7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -27,10 +27,10 @@ public final class ChannelMonitor { // Map to store channel ID's, use Guild.getChannels() to guarantee order for display private final Map channelsToMonitorById; - private final Map postStatusInChannel; + private final Map guildIdToStatusChannel; ChannelMonitor() { - postStatusInChannel = new HashMap<>(); // JDA required to populate map + guildIdToStatusChannel = new HashMap<>(); // JDA required to populate map channelsToMonitorById = new HashMap<>(); } @@ -55,7 +55,7 @@ public void addChannelToMonitor(final long channelId) { * @param channel the channel the status message must be displayed in */ public void addChannelForStatus(@NotNull final TextChannel channel) { - postStatusInChannel.put(channel.getGuild().getIdLong(), channel.getIdLong()); + guildIdToStatusChannel.put(channel.getGuild().getIdLong(), channel.getIdLong()); updateStatusFor(channel.getGuild()); } @@ -68,7 +68,7 @@ public void addChannelForStatus(@NotNull final TextChannel channel) { * @return whether the guild is configured in the free command system or not. */ public boolean isMonitoringGuild(final long guildId) { - return postStatusInChannel.containsKey(guildId); + return guildIdToStatusChannel.containsKey(guildId); } /** @@ -160,7 +160,7 @@ public void setChannelFree(final long channelId) { * @return a stream of guild id's */ public @NotNull Stream guildIds() { - return postStatusInChannel.keySet().stream(); + return guildIdToStatusChannel.keySet().stream(); } /** @@ -170,7 +170,7 @@ public void setChannelFree(final long channelId) { * @return a stream of channel id's */ public @NotNull Stream statusIds() { - return postStatusInChannel.values().stream(); + return guildIdToStatusChannel.values().stream(); } private @NotNull List guildMonitoredChannelsList(@NotNull final Guild guild) { @@ -250,7 +250,7 @@ public void updateStatusFor(@NotNull Guild guild) { */ public @NotNull TextChannel getStatusChannelFor(@NotNull final Guild guild) { // TODO add error checking for invalid keys ?? - return guild.getTextChannelById(postStatusInChannel.get(guild.getIdLong())); + return guild.getTextChannelById(guildIdToStatusChannel.get(guild.getIdLong())); } /** @@ -263,6 +263,6 @@ public void updateStatusFor(@NotNull Guild guild) { public String toString() { // This is called on boot by as a debug level logger return "Monitoring Channels: %s%nDisplaying on Channels: %s" - .formatted(channelsToMonitorById, postStatusInChannel); + .formatted(channelsToMonitorById, guildIdToStatusChannel); } } From a49a9462f73b0649cff1ee1b9780d661ad6d7f57 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 14:24:40 +0200 Subject: [PATCH 32/78] removed delegation text from javadocs added @throws to javadocs --- .../tjbot/commands/free/ChannelMonitor.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index ca7ef6b6d7..6159d9bfd4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -84,17 +84,6 @@ public boolean isMonitoringChannel(final long channelId) { return channelsToMonitorById.containsKey(channelId); } - /** - * This is a delegation method to pass operations to {@link ChannelStatus}. see - * {@link ChannelStatus#isBusy()} for details. - * - * @param channelId the id for the channel to test. - * @return {@code true} if the channel is 'busy', false if the channel is 'free'. - */ - public boolean isChannelBusy(final long channelId) { - return channelsToMonitorById.get(channelId).isBusy(); - } - private ChannelStatus requiresIsMonitored(final long channelId) { if (!channelsToMonitorById.containsKey(channelId)) { throw new IllegalArgumentException( @@ -103,6 +92,18 @@ private ChannelStatus requiresIsMonitored(final long channelId) { return channelsToMonitorById.get(channelId); } + /** + * This method tests if channel status to busy, see {@link ChannelStatus#isBusy()} for details. + * + * @param channelId the id for the channel to test. + * @return {@code true} if the channel is 'busy', false if the channel is 'free'. + * @throws IllegalArgumentException if the channel passed is not monitored. See + * {@link #addChannelToMonitor(long)} + */ + public boolean isChannelBusy(final long channelId) { + return requiresIsMonitored(channelId).isBusy(); + } + /** * This method tests if a channel is currently active by fetching the latest message and testing * if it was posted more recently than the configured time limit, see @@ -112,7 +113,7 @@ private ChannelStatus requiresIsMonitored(final long channelId) { * * @param channel the channel to test. * @return {@code true} if the channel is inactive, false if it has received messages more - * recently than an hour ago. + * recently than the configured duration. * @throws IllegalArgumentException if the channel passed is not monitored. See * {@link #addChannelToMonitor(long)} */ @@ -129,8 +130,8 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { } /** - * This is a delegation method to pass operations to {@link ChannelStatus}. see - * {@link ChannelStatus#setBusy(long)} for details. + * This method sets the channel's status to 'busy' see {@link ChannelStatus#setBusy(long)} for + * details. * * @param channelId the id for the channel status to modify. * @param userId the id of the user changing the status to busy. @@ -142,8 +143,8 @@ public void setChannelBusy(final long channelId, final long userId) { } /** - * This is a delegation method to pass operations to {@link ChannelStatus}. see - * {@link ChannelStatus#setFree()} for details. + * This method sets the channel's status to 'free', see {@link ChannelStatus#setFree()} for + * details. * * @param channelId the id for the channel status to modify. * @throws IllegalArgumentException if the channel passed is not monitored. See @@ -221,9 +222,10 @@ public String statusMessage(@NotNull final Guild guild) { } /** - * This method checks all channels in a guild that is currently being monitored and are - * currently busy and determines if the last time it was updated is more than an hour ago. If so - * it changes the channel's status to free. + * This method checks all channels in a guild that are currently being monitored and are busy + * and determines if the last time it was updated is more recent than the configured time see + * {@link org.togetherjava.tjbot.config.FreeCommandConfig#INACTIVE_UNIT}. If so it changes the + * channel's status to free, see {@link ChannelMonitor#isChannelInactive(TextChannel)}. *

* This method is run automatically during startup and should be run on a set schedule, as * defined in {@link org.togetherjava.tjbot.config.FreeCommandConfig}. The scheduled execution From 1425e31f0653e49ebdf98df15670fa72c9398a31 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 14:36:42 +0200 Subject: [PATCH 33/78] changed toDiscord method name to toDiscordContentRaw changed StringBuilder to StringJoiner --- .../togetherjava/tjbot/commands/free/ChannelMonitor.java | 9 +++++---- .../togetherjava/tjbot/commands/free/ChannelStatus.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 6159d9bfd4..6a735f1823 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.StringJoiner; import java.util.stream.Stream; @@ -203,7 +204,7 @@ public String statusMessage(@NotNull final Guild guild) { statusFor.forEach(channelStatus -> channelStatus.updateChannelName(guild)); // dynamically separate channels by channel categories - StringBuilder sb = new StringBuilder(); + StringJoiner content = new StringJoiner("\n"); String categoryName = ""; for (ChannelStatus status : statusFor) { Category category = guild.getGuildChannelById(status.getChannelId()).getParent(); @@ -213,12 +214,12 @@ public String statusMessage(@NotNull final Guild guild) { // FIXME possible bug when not all channels are part of categories, may mistakenly // include uncategoried channels inside previous category. will an uncategoried // channel return an empty string or null? javadocs dont say. - sb.append("\n__").append(categoryName).append("__\n"); + content.add("\n__" + categoryName + "__"); } - sb.append(status.toDiscord()).append("\n"); + content.add(status.toDiscordContentRaw()); } - return sb.toString(); + return content.toString(); } /** diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 8758196cd7..9dfead646f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -189,7 +189,7 @@ public boolean equals(final Object o) { * * @return a String representation of ChannelStatus, formatted for Discord */ - public @NotNull String toDiscord() { + public @NotNull String toDiscordContentRaw() { return "%s %s".formatted(isBusy ? BUSY_STATUS : FREE_STATUS, name); } From 58cf827d06ebc9ddbab09a16d17422717b60aed0 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 16:01:45 +0200 Subject: [PATCH 34/78] added ChannelStatusType enum --- .../tjbot/commands/free/ChannelMonitor.java | 8 ++++- .../tjbot/commands/free/ChannelStatus.java | 25 +++++++--------- .../commands/free/ChannelStatusType.java | 30 +++++++++++++++++++ 3 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 6a735f1823..7fabde80a8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -250,9 +250,15 @@ public void updateStatusFor(@NotNull Guild guild) { * * @param guild the {@link Guild} for which to retrieve the TextChannel for. * @return the TextChannel where status messages are output in the specified guild. + * @throws IllegalArgumentException if the guild passed has not configured in the free command + * system, see {@link #addChannelForStatus(TextChannel)} */ public @NotNull TextChannel getStatusChannelFor(@NotNull final Guild guild) { - // TODO add error checking for invalid keys ?? + if (!guildIdToStatusChannel.containsKey(guild.getIdLong())) { + throw new IllegalArgumentException( + "Guild %s is not configured in the free command system." + .formatted(guild.getName())); + } return guild.getTextChannelById(guildIdToStatusChannel.get(guild.getIdLong())); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 9dfead646f..304082190b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -12,15 +12,10 @@ * Class that tracks the current free/busy status of a channel that requires monitoring. */ public final class ChannelStatus { - public static final boolean FREE = false; - public static final boolean BUSY = true; - - private static final String FREE_STATUS = ":green_circle:"; - private static final String BUSY_STATUS = ":red_circle:"; private final long channelId; - private long userId; - private volatile boolean isBusy; + private volatile long userId; + private volatile ChannelStatusType status; private String name; /** @@ -35,7 +30,7 @@ public final class ChannelStatus { */ ChannelStatus(final long id) { channelId = id; - isBusy = BUSY; + status = ChannelStatusType.BUSY; name = Long.toString(id); } @@ -50,7 +45,7 @@ public final class ChannelStatus { * @return the current stored status related to the channel id. */ public boolean isBusy() { - return isBusy; + return status.isBusy(); } /** @@ -132,8 +127,8 @@ public void updateChannelName(@NotNull final Guild guild) { * @param userId the id of the user who changed the status to 'busy' */ public void setBusy(final long userId) { - if (isBusy == FREE) { - this.isBusy = BUSY; + if (status.isFree()) { + status = ChannelStatusType.BUSY; this.userId = userId; } } @@ -148,8 +143,8 @@ public void setBusy(final long userId) { * This functionality is not yet implemented so the id can be anything atm. */ public void setFree() { - if (isBusy == BUSY) { - isBusy = FREE; + if (status.isBusy()) { + status = ChannelStatusType.FREE; } } @@ -179,7 +174,7 @@ public boolean equals(final Object o) { */ @Override public @NotNull String toString() { - return "ChannelStatus{ %s is %s }".formatted(name, isBusy ? "busy" : "not busy"); + return "ChannelStatus{ %s is %s }".formatted(name, status.description()); } /** @@ -190,7 +185,7 @@ public boolean equals(final Object o) { * @return a String representation of ChannelStatus, formatted for Discord */ public @NotNull String toDiscordContentRaw() { - return "%s %s".formatted(isBusy ? BUSY_STATUS : FREE_STATUS, name); + return "%s %s".formatted(status.toDiscordContentRaw(), name); } /** diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java new file mode 100644 index 0000000000..58d4e39acb --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java @@ -0,0 +1,30 @@ +package org.togetherjava.tjbot.commands.free; + +public enum ChannelStatusType { + FREE("free", ":green_circle:"), + BUSY("busy", ":red_circle:"); + + private final String description; + private final String emoji; + + ChannelStatusType(String description, String emoji) { + this.description = description; + this.emoji = emoji; + } + + public boolean isFree() { + return this == FREE; + } + + public boolean isBusy() { + return this == BUSY; + } + + public String description() { + return description; + } + + public String toDiscordContentRaw() { + return emoji; + } +} From 24c22859201865df6adc8d7ba522dd4c52788d54 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 20:39:58 +0200 Subject: [PATCH 35/78] added more detail to javadoc --- .../org/togetherjava/tjbot/commands/free/FreeCommand.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 21be818799..987ec630a1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -148,8 +148,11 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { /** * Method to test event to see if it should be processed. - * + *

* Will respond to users describing the problem if the event should not be processed. + *

+ * This checks if the command system is ready to process events, if the event was triggered in a + * monitored guild and in a monitored channel. * * @param event the event to test for validity. * @return true if the event should be processed false otherwise. From 8d1dbd7785b3a6aed3e5ebf67d29dd15cccd7754 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 20:41:28 +0200 Subject: [PATCH 36/78] added more detail to javadoc --- .../org/togetherjava/tjbot/commands/free/ChannelStatus.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 304082190b..4f8ec02979 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -95,8 +95,9 @@ private void setName(@NotNull final String name) { * The recommended value to use is {@link TextChannel#getAsMention()} *

* This method is called in multithreaded context, however the value is not expected to change - * regularly and will not break anything if it is incorrect for a read or two, so it has not - * been made thread safe. + * regularly and will not break anything if it is incorrect for a read or two, and it should be + * updated before use, which will happen in the using thread. So it has not been made thread + * safe. * * @param guild the {@link Guild} that the channel belongs to, to retrieve its name from. */ From 4565b3895453f86f384c58d99453f1ee89406493 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 20:45:39 +0200 Subject: [PATCH 37/78] refactored onSlashCommand for improved readability --- .../tjbot/commands/free/FreeCommand.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 987ec630a1..1da101e87e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -135,15 +135,14 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { long id = event.getChannel().getIdLong(); // do not need to test if key is present, shouldHandle(event) already does. - if (channelMonitor.isChannelBusy(id)) { - // TODO check if /free called by original author, if not put message asking if he - // approves - channelMonitor.setChannelFree(id); - displayStatus(channelMonitor.getStatusChannelFor(event.getGuild())); - event.reply(UserStrings.MARK_AS_FREE.message()).queue(); - } else { + if (!channelMonitor.isChannelBusy(id)) { FreeUtil.sendErrorMessage(event, UserStrings.ALREADY_FREE_ERROR.message()); + return; } + // TODO check if /free called by original author, if not put message asking if he approves + channelMonitor.setChannelFree(id); + displayStatus(channelMonitor.getStatusChannelFor(event.getGuild())); + event.reply(UserStrings.MARK_AS_FREE.message()).queue(); } /** From 1c80abf219449e1eaa831baced90f63a1c3e7c7d Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 21:06:14 +0200 Subject: [PATCH 38/78] fixed import --- .../java/org/togetherjava/tjbot/commands/free/UserStrings.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java index 1ccca9d1bc..d1976cd909 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -1,8 +1,7 @@ package org.togetherjava.tjbot.commands.free; import org.jetbrains.annotations.NotNull; - -import javax.annotation.Nullable; +import org.jetbrains.annotations.Nullable; /** * Class containing all the strings sent to users during their interaction with the free command From 9060f80c235ed099d21c84b105316b0413a1d31f Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 21:06:53 +0200 Subject: [PATCH 39/78] added FIXME comment --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 1da101e87e..1565d284ea 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -287,6 +287,7 @@ public void onEvent(@NotNull GenericEvent event) { .map(history -> history.stream() .filter(message -> !message.getEmbeds().isEmpty()) .filter(message -> message.getAuthor().equals(channel.getJDA().getSelfUser())) + // FIXME the equals is not working, i believe its because there is no getTitleRaw() // .filter(message -> STATUS_TITLE.equals(message.getEmbeds().get(0).getTitle())) .findFirst()) .complete(); @@ -317,7 +318,7 @@ private void initStatusMessageChannels(@NotNull final JDA jda) { } private void initStatusMessages(@NotNull final JDA jda) { - // not currently working, attempts to find the existing status message. (for all guilds) + // Attempts to find the existing status message, for all guilds. On launch. channelMonitor.statusIds() .map(jda::getTextChannelById) .filter(Objects::nonNull) // not necessary? this will hide errors in the config file From 8e1e1451fe15585a502d742da33cdc608e1340cd Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 21:16:32 +0200 Subject: [PATCH 40/78] made package private --- .../java/org/togetherjava/tjbot/commands/free/FreeUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index 989ee4aca4..4afaf005cc 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -6,7 +6,7 @@ import java.time.OffsetDateTime; -public enum FreeUtil { +enum FreeUtil { ; // private constructor to prevent this class getting instantiated private FreeUtil() {} From 8ee097b03dbade7d3fc9350f166926d91eca52aa Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 21:17:07 +0200 Subject: [PATCH 41/78] made package private --- .../java/org/togetherjava/tjbot/commands/free/UserStrings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java index d1976cd909..0a62023a50 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -7,7 +7,7 @@ * Class containing all the strings sent to users during their interaction with the free command * system. This does not include the logged strings or the exception strings. */ -public enum UserStrings { +enum UserStrings { NEW_QUESTION(""" Thank you for asking a question in an available channel. When a helper who can answer this question reads it they will help you. From c798bbcd492465b024377611b3bb2e985806a050 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 21:17:43 +0200 Subject: [PATCH 42/78] added @NotNull --- .../src/main/java/org/togetherjava/tjbot/config/Config.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 35af42d53b..35c077dc13 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.file.Path; @@ -174,7 +175,7 @@ public String getTagManageRolePattern() { * @return a List of instances of FreeCommandConfig, each of the instances are separated by * guild. */ - public Collection getFreeCommandConfig() { + public @NotNull Collection getFreeCommandConfig() { return freeCommand; // already unmodifiable } } From 70edab99c76517467d66f3e9e396b00b30a4cd28 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 21:21:39 +0200 Subject: [PATCH 43/78] added TODO --- .../org/togetherjava/tjbot/commands/free/ChannelMonitor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 7fabde80a8..28716155d9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -235,6 +235,7 @@ public String statusMessage(@NotNull final Guild guild) { * @param guild the guild for which to test the channel statuses of. */ public void updateStatusFor(@NotNull Guild guild) { + // TODO add automation after Routine support (#235) is pushed guildMonitoredChannelsList(guild).parallelStream() .filter(ChannelStatus::isBusy) .map(ChannelStatus::getChannelId) From d50b78e89902c863556485dfc9425bd725f2f7f8 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 21:34:51 +0200 Subject: [PATCH 44/78] improved javadocs --- .../org/togetherjava/tjbot/commands/free/ChannelStatus.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 4f8ec02979..47c520512b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -100,6 +100,12 @@ private void setName(@NotNull final String name) { * safe. * * @param guild the {@link Guild} that the channel belongs to, to retrieve its name from. + * @throws IllegalArgumentException if the guild has not been added, see + * {@link ChannelMonitor#addChannelForStatus(TextChannel)} + * @throws IllegalStateException if a channel was added, see + * {@link ChannelMonitor#addChannelToMonitor(long)}, that is not a {@link TextChannel}. + * Since addChannelToMonitor does not access the {@link JDA} the entry can only be + * validated before use instead of on addition. */ public void updateChannelName(@NotNull final Guild guild) { GuildChannel channel = guild.getGuildChannelById(channelId); From e57f392342c49a1ca4f2303f6ff552ba0d086343 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 23:36:17 +0200 Subject: [PATCH 45/78] improved javadocs --- .../java/org/togetherjava/tjbot/commands/free/FreeUtil.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index 4afaf005cc..e60fb9e0ec 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -6,10 +6,11 @@ import java.time.OffsetDateTime; +/** + * A class containing helper methods required by the commands.free package + */ enum FreeUtil { ; - // private constructor to prevent this class getting instantiated - private FreeUtil() {} /** * Helper method to easily send ephemeral messages to users. @@ -25,5 +26,4 @@ public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull S return OffsetDateTime.now() .minus(FreeCommandConfig.INACTIVE_DURATION, FreeCommandConfig.INACTIVE_UNIT); } - } From 88318ffe01a5e9f090e67b1972ea6bb65b12d3be Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 23:37:59 +0200 Subject: [PATCH 46/78] changed method to modern chain --- .../tjbot/commands/free/FreeCommand.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 1565d284ea..eaef9ef70a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -10,6 +10,7 @@ import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; import net.dv8tion.jda.api.hooks.EventListener; +import net.dv8tion.jda.api.requests.RestAction; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -198,6 +199,7 @@ public void displayStatus(@NotNull TextChannel channel) { MessageEmbed embed = MessageUtils.generateEmbed(STATUS_TITLE, messageTxt, channel.getJDA().getSelfUser(), FREE_COLOR); + // FIXME need an if call before getLatestMessageId long latestMessageId = channel.getLatestMessageIdLong(); Optional statusMessage = getStatusMessageIn(channel); if (statusMessage.isPresent()) { @@ -266,18 +268,12 @@ public void onEvent(@NotNull GenericEvent event) { } private @NotNull Optional getStatusMessageIn(@NotNull TextChannel channel) { - if (channelToStatusMessage.containsKey(channel.getIdLong())) { - Long id = channelToStatusMessage.get(channel.getIdLong()); - if (id == null) { - return Optional.empty(); - } - Message message = channel.getHistoryAround(id, 1).complete().getMessageById(id); - if (message == null) { - return Optional.empty(); - } - return Optional.of(message); + if (!channelToStatusMessage.containsKey(channel.getIdLong())) { + return findExistingStatusMessage(channel); } - return findExistingStatusMessage(channel); + return Optional.ofNullable(channelToStatusMessage.get(channel.getIdLong())) + .map(channel::retrieveMessageById) + .map(RestAction::complete); } private @NotNull Optional findExistingStatusMessage(@NotNull TextChannel channel) { From 6063c9a12969ba35f298c4b15477a81a0c677849 Mon Sep 17 00:00:00 2001 From: borgrel Date: Sat, 13 Nov 2021 23:38:30 +0200 Subject: [PATCH 47/78] added space --- .../java/org/togetherjava/tjbot/config/FreeCommandConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index 995194578f..22ebf4ca8a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -24,7 +24,7 @@ * }] * * - * Additional Guilds may add their settings by adding additional {@code {"statusChannel":... } } + * Additional Guilds may add their settings by adding additional {@code {"statusChannel": ... } } * * The long channel ID can be found by right-clicking on the channel and selecting 'Copy ID' */ From 9f88d709125b180baf31fce9e4f5b5f6d0ac4c69 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 10:00:52 +0200 Subject: [PATCH 48/78] changed variable name FREE_COLOR into MESSAGE_HIGHLIGHT_COLOR --- .../org/togetherjava/tjbot/commands/free/FreeCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index eaef9ef70a..ae0032a410 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -58,7 +58,7 @@ public final class FreeCommand extends SlashCommandAdapter implements EventListe private static final String STATUS_TITLE = "**__CHANNEL STATUS__**\n\n"; private static final String COMMAND_NAME = "free"; - private static final Color FREE_COLOR = Color.decode("#CCCC00"); + private static final Color MESSAGE_HIGHLIGHT_COLOR = Color.decode("#CCCC00"); // Map to store channel ID's, use Guild.getChannels() to guarantee order for display private ChannelMonitor channelMonitor; @@ -197,7 +197,7 @@ public void displayStatus(@NotNull TextChannel channel) { String messageTxt = buildStatusMessage(guild); MessageEmbed embed = MessageUtils.generateEmbed(STATUS_TITLE, messageTxt, - channel.getJDA().getSelfUser(), FREE_COLOR); + channel.getJDA().getSelfUser(), MESSAGE_HIGHLIGHT_COLOR); // FIXME need an if call before getLatestMessageId long latestMessageId = channel.getLatestMessageIdLong(); From ca3b73eb554e00e53506e3e5b5850bd6970f81c9 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 10:31:21 +0200 Subject: [PATCH 49/78] fixed javadocs --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- .../java/org/togetherjava/tjbot/commands/free/FreeUtil.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index ae0032a410..d3f32d24ab 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -203,7 +203,7 @@ public void displayStatus(@NotNull TextChannel channel) { long latestMessageId = channel.getLatestMessageIdLong(); Optional statusMessage = getStatusMessageIn(channel); if (statusMessage.isPresent()) { - Message message = statusMessage.get(); + Message message = statusMessage.orElseThrow(); if (message.getIdLong() != latestMessageId) { message.delete().queue(); channel.sendMessageEmbeds(embed) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index e60fb9e0ec..f9cd46bb8e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -7,7 +7,7 @@ import java.time.OffsetDateTime; /** - * A class containing helper methods required by the commands.free package + * A class containing helper methods required by the free package command. */ enum FreeUtil { ; From cdc6303f552530c9bf918071ae806f0fc01b78f6 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 10:33:35 +0200 Subject: [PATCH 50/78] the extremely common adding @NotNull commit --- .../togetherjava/tjbot/commands/free/ChannelStatusType.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java index 58d4e39acb..887499cd73 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java @@ -1,5 +1,7 @@ package org.togetherjava.tjbot.commands.free; +import org.jetbrains.annotations.NotNull; + public enum ChannelStatusType { FREE("free", ":green_circle:"), BUSY("busy", ":red_circle:"); @@ -20,11 +22,11 @@ public boolean isBusy() { return this == BUSY; } - public String description() { + public @NotNull String description() { return description; } - public String toDiscordContentRaw() { + public @NotNull String toDiscordContentRaw() { return emoji; } } From 2d7353fea42f7ca51e64cd3cb7ffe487e3a36fb6 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 10:35:52 +0200 Subject: [PATCH 51/78] the extremely common adding @NotNull commit --- .../org/togetherjava/tjbot/commands/free/ChannelStatusType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java index 887499cd73..f90850b33d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java @@ -9,7 +9,7 @@ public enum ChannelStatusType { private final String description; private final String emoji; - ChannelStatusType(String description, String emoji) { + ChannelStatusType(@NotNull String description, @NotNull String emoji) { this.description = description; this.emoji = emoji; } From 9ece875f11f7d01a30332ab9175b7575e9b456d0 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 10:36:38 +0200 Subject: [PATCH 52/78] changed visibility to package private --- .../org/togetherjava/tjbot/commands/free/ChannelStatusType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java index f90850b33d..8ebb09f4e5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatusType.java @@ -2,7 +2,7 @@ import org.jetbrains.annotations.NotNull; -public enum ChannelStatusType { +enum ChannelStatusType { FREE("free", ":green_circle:"), BUSY("busy", ":red_circle:"); From a8edfce00c5c05b49b7a86b118e926ad2eb8f292 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 10:43:03 +0200 Subject: [PATCH 53/78] added synchronised --- .../org/togetherjava/tjbot/commands/free/ChannelStatus.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 47c520512b..dc08860a76 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -133,7 +133,7 @@ public void updateChannelName(@NotNull final Guild guild) { * * @param userId the id of the user who changed the status to 'busy' */ - public void setBusy(final long userId) { + public synchronized void setBusy(final long userId) { if (status.isFree()) { status = ChannelStatusType.BUSY; this.userId = userId; @@ -149,10 +149,8 @@ public void setBusy(final long userId) { *

* This functionality is not yet implemented so the id can be anything atm. */ - public void setFree() { - if (status.isBusy()) { + public synchronized void setFree() { status = ChannelStatusType.FREE; - } } // TODO should I overload equals with equals(long) so that a Set may be used instead of a Map From ed1104e17e82f4e5946d74264d05360868463886 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 10:48:39 +0200 Subject: [PATCH 54/78] moved todo inside method --- .../org/togetherjava/tjbot/commands/free/ChannelStatus.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index dc08860a76..274bd7e6c8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -153,7 +153,6 @@ public synchronized void setFree() { status = ChannelStatusType.FREE; } - // TODO should I overload equals with equals(long) so that a Set may be used instead of a Map /** * The identity of this object is solely based on the id value. Compares the long id's and * determines if they are equal. @@ -163,6 +162,7 @@ public synchronized void setFree() { */ @Override public boolean equals(final Object o) { + // TODO should I overload equals with equals(long) so that a Set may be used instead of a Map if (this == o) return true; if (o == null || getClass() != o.getClass()) From 7708cd5a16ac93f069373466d149d72060651b20 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 13:51:20 +0200 Subject: [PATCH 55/78] inverted if for better readability --- .../tjbot/commands/free/ChannelStatus.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 274bd7e6c8..7dfd594d4c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -114,12 +114,12 @@ public void updateChannelName(@NotNull final Guild guild) { "The guild passed in '%s' is not related to the channel this status is for: %s" .formatted(guild.getName(), this)); } - if (channel instanceof TextChannel textChannel) { + if (!(channel instanceof TextChannel textChannel)) { + throw new IllegalStateException("This channel status was created with the id for a" + + "non-text-channel and status cannot be monitored: '%s'".formatted(channelId)); + } else { setName(textChannel.getAsMention()); - } else - throw new IllegalStateException( - "This channel status was created with the id for a non-text-channel and status cannot be monitored: '%s'" - .formatted(channelId)); + } } /** @@ -150,7 +150,7 @@ public synchronized void setBusy(final long userId) { * This functionality is not yet implemented so the id can be anything atm. */ public synchronized void setFree() { - status = ChannelStatusType.FREE; + status = ChannelStatusType.FREE; } /** @@ -162,7 +162,8 @@ public synchronized void setFree() { */ @Override public boolean equals(final Object o) { - // TODO should I overload equals with equals(long) so that a Set may be used instead of a Map + // TODO should I overload equals with equals(long) so that a Set may be used instead of a + // Map if (this == o) return true; if (o == null || getClass() != o.getClass()) From cadb435c22ed3f4fe993294e8756c79a74cad19e Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 13:58:25 +0200 Subject: [PATCH 56/78] removed deprecated tag --- .../java/org/togetherjava/tjbot/config/FreeCommandConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index 22ebf4ca8a..43eddd49b4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -29,7 +29,7 @@ * The long channel ID can be found by right-clicking on the channel and selecting 'Copy ID' */ @SuppressWarnings("ClassCanBeRecord") -@JsonRootName(value = "freeCommand") +@JsonRootName("freeCommand") public final class FreeCommandConfig { // TODO make constants configurable via config file once config templating (#234) is pushed public static final long INACTIVE_DURATION = 1; From 0f2e6d7e18b1f93706908584b70e61a728fb4c85 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 14:24:21 +0200 Subject: [PATCH 57/78] changed variable name --- .../tjbot/commands/free/FreeCommand.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index d3f32d24ab..4d7318acd3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -62,7 +62,7 @@ public final class FreeCommand extends SlashCommandAdapter implements EventListe // Map to store channel ID's, use Guild.getChannels() to guarantee order for display private ChannelMonitor channelMonitor; - private final Map channelToStatusMessage; + private final Map channelIdToMessageIdForStatus; private boolean isReady; @@ -77,7 +77,7 @@ public FreeCommand() { super(COMMAND_NAME, "marks this channel as free for another user to ask a question", SlashCommandVisibility.GUILD); - channelToStatusMessage = new HashMap<>(); + channelIdToMessageIdForStatus = new HashMap<>(); channelMonitor = new ChannelMonitor(); isReady = false; @@ -207,14 +207,14 @@ public void displayStatus(@NotNull TextChannel channel) { if (message.getIdLong() != latestMessageId) { message.delete().queue(); channel.sendMessageEmbeds(embed) - .queue(message1 -> channelToStatusMessage.put(channel.getIdLong(), + .queue(message1 -> channelIdToMessageIdForStatus.put(channel.getIdLong(), message1.getIdLong())); } else { message.editMessageEmbeds(embed).queue(); } } else { channel.sendMessageEmbeds(embed) - .queue(message1 -> channelToStatusMessage.put(channel.getIdLong(), + .queue(message1 -> channelIdToMessageIdForStatus.put(channel.getIdLong(), message1.getIdLong())); } } @@ -268,10 +268,10 @@ public void onEvent(@NotNull GenericEvent event) { } private @NotNull Optional getStatusMessageIn(@NotNull TextChannel channel) { - if (!channelToStatusMessage.containsKey(channel.getIdLong())) { + if (!channelIdToMessageIdForStatus.containsKey(channel.getIdLong())) { return findExistingStatusMessage(channel); } - return Optional.ofNullable(channelToStatusMessage.get(channel.getIdLong())) + return Optional.ofNullable(channelIdToMessageIdForStatus.get(channel.getIdLong())) .map(channel::retrieveMessageById) .map(RestAction::complete); } @@ -288,7 +288,7 @@ public void onEvent(@NotNull GenericEvent event) { .findFirst()) .complete(); - channelToStatusMessage.put(channel.getIdLong(), + channelIdToMessageIdForStatus.put(channel.getIdLong(), result.map(Message::getIdLong).orElse(null)); return result; } @@ -320,7 +320,7 @@ private void initStatusMessages(@NotNull final JDA jda) { .filter(Objects::nonNull) // not necessary? this will hide errors in the config file .map(this::getStatusMessageIn) .flatMap(Optional::stream) - .forEach(message -> channelToStatusMessage.put(message.getChannel().getIdLong(), + .forEach(message -> channelIdToMessageIdForStatus.put(message.getChannel().getIdLong(), message.getIdLong())); } } From 4d1ce62892ecd0d00d50d9253fb6943bed33ee9c Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 15:32:55 +0200 Subject: [PATCH 58/78] fixed comment --- .../org/togetherjava/tjbot/commands/free/ChannelMonitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 28716155d9..c063f0236d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -271,7 +271,7 @@ public void updateStatusFor(@NotNull Guild guild) { */ @Override public String toString() { - // This is called on boot by as a debug level logger + // This is called on boot as a debug level message by the logger return "Monitoring Channels: %s%nDisplaying on Channels: %s" .formatted(channelsToMonitorById, guildIdToStatusChannel); } From cb5d0c3ad03c0004e6be7a22e248e44316b5be4a Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 15:35:20 +0200 Subject: [PATCH 59/78] changed config to fail-fast added javadocs --- .../tjbot/commands/free/FreeCommand.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 4d7318acd3..04cfe6b959 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -223,6 +223,15 @@ private void checkBusyStatusAllChannels(@NotNull JDA jda) { channelMonitor.guildIds().map(jda::getGuildById).forEach(channelMonitor::updateStatusFor); } + /** + * Method for creating the message that shows the channel statuses for the specified guild. + * + * @param guild the guild that the message is required for. + * @return the message to display showing the channel statuses. Includes Discord specific + * formatting, trying to display elsewhere may have unpredictable results. + * @throws IllegalArgumentException if the guild passed in is not configured in the free command + * system, see {@link ChannelMonitor#addChannelForStatus(TextChannel)}. + */ public @NotNull String buildStatusMessage(@NotNull Guild guild) { if (!channelMonitor.isMonitoringGuild(guild.getIdLong())) { throw new IllegalArgumentException( @@ -307,9 +316,9 @@ private void initStatusMessageChannels(@NotNull final JDA jda) { .getFreeCommandConfig() .stream() .map(FreeCommandConfig::getStatusChannel) - .distinct() // not necessary? (validates user input, since input is from file) .map(jda::getTextChannelById) - .filter(Objects::nonNull) // not necessary? this will hide errors in the config file + // throws NPE if id from config does not match a text channel + .map(Objects::requireNonNull) .forEach(channelMonitor::addChannelForStatus); } @@ -317,7 +326,7 @@ private void initStatusMessages(@NotNull final JDA jda) { // Attempts to find the existing status message, for all guilds. On launch. channelMonitor.statusIds() .map(jda::getTextChannelById) - .filter(Objects::nonNull) // not necessary? this will hide errors in the config file + .filter(Objects::nonNull) // only here for sonarLint .map(this::getStatusMessageIn) .flatMap(Optional::stream) .forEach(message -> channelIdToMessageIdForStatus.put(message.getChannel().getIdLong(), From 419897ad43a9b4fbdface10fe724bf71ab101b03 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 15:44:28 +0200 Subject: [PATCH 60/78] deleted unrelated class from branch --- .../tjbot/commands/system/JDAScheduler.java | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/system/JDAScheduler.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/JDAScheduler.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/JDAScheduler.java deleted file mode 100644 index 1c134d9a6b..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/JDAScheduler.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.togetherjava.tjbot.commands.system; - -import net.dv8tion.jda.api.JDA; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; -import java.util.concurrent.*; - -public class JDAScheduler { - private static JDAScheduler instance; - - private final JDA jda; - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - - private JDAScheduler(JDA jda) { - this.jda = jda; - } - - public static void create(@NotNull JDA jda) { - Objects.requireNonNull(jda, "Cannot create a JDAScheduler without an instance of JDA"); - if (instance != null) { - throw new IllegalStateException("Can only create one instance of JDAScheduler"); - } - - instance = new JDAScheduler(jda); - } - - public static ScheduledFuture scheduleTask(Callable task, long delay, TimeUnit timeUnit) { - Objects.requireNonNull(instance, - "Cannot schedule a task before the scheduler has been created"); - - return instance.scheduler.schedule(task, delay, timeUnit); - } -} From cfefb6ec301a5ab2ccac3a8e7024de7a6183c30d Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 15:55:21 +0200 Subject: [PATCH 61/78] made message retrieval limit configurable --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 3 ++- .../java/org/togetherjava/tjbot/config/FreeCommandConfig.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 04cfe6b959..d1b8888a80 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -287,8 +287,9 @@ public void onEvent(@NotNull GenericEvent event) { private @NotNull Optional findExistingStatusMessage(@NotNull TextChannel channel) { // will only run when bots starts, afterwards its stored in a map + Optional result = channel.getHistory() - .retrievePast(100) + .retrievePast(FreeCommandConfig.MESSAGE_RETRIEVE_LIMIT) .map(history -> history.stream() .filter(message -> !message.getEmbeds().isEmpty()) .filter(message -> message.getAuthor().equals(channel.getJDA().getSelfUser())) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index 43eddd49b4..23356e8c03 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -36,6 +36,7 @@ public final class FreeCommandConfig { public static final ChronoUnit INACTIVE_UNIT = ChronoUnit.HOURS; public static final long INACTIVE_TEST_INTERVAL = 15; public static final ChronoUnit INACTIVE_TEST_UNIT = ChronoUnit.MINUTES; + public static final int MESSAGE_RETRIEVE_LIMIT = 10; private final long statusChannel; private final List monitoredChannels; From 0a72249080d9281b58294fe55aef9c92bf7c0bb6 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 19:46:14 +0200 Subject: [PATCH 62/78] changed inactive to work when retrieveHistory fails --- .../tjbot/commands/free/ChannelMonitor.java | 13 ++-- .../tjbot/commands/free/FreeUtil.java | 66 +++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index c063f0236d..5995c66fd9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -1,6 +1,9 @@ package org.togetherjava.tjbot.commands.free; -import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.Category; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.GuildChannel; +import net.dv8tion.jda.api.entities.TextChannel; import org.jetbrains.annotations.NotNull; import java.util.HashMap; @@ -122,12 +125,10 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { requiresIsMonitored(channel.getIdLong()); // TODO change the entire inactive test to work via restactions - return channel.getHistory() - .retrievePast(1) - .map(messages -> messages.get(0)) - .map(Message::getTimeCreated) + return FreeUtil.getLastMessageId(channel) + .map(FreeUtil::timeFromId) .map(createdTime -> createdTime.isBefore(FreeUtil.inactiveTimeLimit())) - .complete(); + .orElse(true); } /** diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index f9cd46bb8e..c59ed21e75 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -1,16 +1,24 @@ package org.togetherjava.tjbot.commands.free; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.interactions.Interaction; +import net.dv8tion.jda.api.utils.TimeUtil; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.FreeCommandConfig; import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; /** * A class containing helper methods required by the free package command. */ enum FreeUtil { ; + private static final Logger logger = LoggerFactory.getLogger(FreeUtil.class); /** * Helper method to easily send ephemeral messages to users. @@ -22,6 +30,64 @@ public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull S interaction.reply(message).setEphemeral(true).queue(); } + /** + * Method that provides the message history of a {@link TextChannel}. + *

+ *

+ * This method attempts to retrieve the message history, and logs any problems that occur in the + * attempt. + * + * @param channel the channel from which the history is required. + * @param limit the number of messages to retrieve. + * @return the requested message history or empty if unable to. + */ + public static @NotNull Optional> getChannelHistory(@NotNull TextChannel channel, + final int limit) { + return Optional + .ofNullable(channel.getHistory().retrievePast(limit).mapToResult().map(listResult -> { + if (listResult.isFailure()) { + logger.error("Failed to retrieve messages from %s because of:" + .formatted(channel.getAsMention()), listResult.getFailure()); + return null; + } else { + return listResult.get(); + } + }).complete()); + } + + /** + * Method that provides the id of the latest message in a {@link TextChannel}. + *

+ * This method tests for problems with retrieving the id like the latest message was deleted and + * the channel history being empty (or network trouble), etc. + * + * @param channel the channel from which the latest message is required. + * @return the id of the latest message or empty if it could not be retrieved. + */ + public static @NotNull Optional getLastMessageId(@NotNull TextChannel channel) { + return Optional + .ofNullable(channel.hasLatestMessage() ? channel.getLatestMessageIdLong() : null) + .or(() -> getChannelHistory(channel, 1) + .map(list -> !list.isEmpty() ? list.get(0).getIdLong() : null)); + } + + /** + * Method that returns the time data from a discord snowflake. + * + * @param id the snowflake containing the time desired + * @return the creation time of the entity the id represents + */ + public static @NotNull OffsetDateTime timeFromId(long id) { + return TimeUtil.getTimeCreated(id); + } + + /** + * Method that calculates a time value a specific duration before now. The duration is + * configured in {@link FreeCommandConfig#INACTIVE_UNIT} and + * {@link FreeCommandConfig#INACTIVE_DURATION}. + * + * @return the time value a set duration before now. + */ public static @NotNull OffsetDateTime inactiveTimeLimit() { return OffsetDateTime.now() .minus(FreeCommandConfig.INACTIVE_DURATION, FreeCommandConfig.INACTIVE_UNIT); From b2e78ed689e7039e974e12ffc220c375476335f5 Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 22:47:11 +0200 Subject: [PATCH 63/78] streamlined displayStatus method --- .../tjbot/commands/free/FreeCommand.java | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index d1b8888a80..418b48ac3b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -184,11 +184,15 @@ private boolean handleShouldBeProcessed(@NotNull final SlashCommandEvent event) } /** - * Builds the message that will be displayed for users. + * Displays the message that will be displayed for users. *

- * This method dynamically builds the status message as per the current values on the guild, - * including the channel categories. This method will detect any changes made on the guild and - * represent those changes in the status message. + * This method detects if any messages have been posted in the channel below the status message. + * If that is the case this will delete the existing status message and post another one so that + * it's the last message in the channel. + *

+ * If it cannot find an existing status message it will create a new one. + *

+ * Otherwise it will edit the existing message. * * @param channel the text channel the status message will be posted in. */ @@ -199,24 +203,22 @@ public void displayStatus(@NotNull TextChannel channel) { MessageEmbed embed = MessageUtils.generateEmbed(STATUS_TITLE, messageTxt, channel.getJDA().getSelfUser(), MESSAGE_HIGHLIGHT_COLOR); - // FIXME need an if call before getLatestMessageId - long latestMessageId = channel.getLatestMessageIdLong(); - Optional statusMessage = getStatusMessageIn(channel); - if (statusMessage.isPresent()) { - Message message = statusMessage.orElseThrow(); - if (message.getIdLong() != latestMessageId) { - message.delete().queue(); - channel.sendMessageEmbeds(embed) - .queue(message1 -> channelIdToMessageIdForStatus.put(channel.getIdLong(), - message1.getIdLong())); - } else { - message.editMessageEmbeds(embed).queue(); - } - } else { - channel.sendMessageEmbeds(embed) - .queue(message1 -> channelIdToMessageIdForStatus.put(channel.getIdLong(), - message1.getIdLong())); + getStatusMessageIn(channel).flatMap(this::deleteIfNotLatest) + .ifPresentOrElse(message -> message.editMessageEmbeds(embed).queue(), + () -> channel.sendMessageEmbeds(embed) + .queue(message -> channelIdToMessageIdForStatus.put(channel.getIdLong(), + message.getIdLong()))); + } + + private @NotNull Optional deleteIfNotLatest(@NotNull Message message) { + if (FreeUtil.getLastMessageId(message.getTextChannel()) + .filter(lastId -> message.getIdLong() != lastId) + .isPresent()) { + + message.delete().queue(); + return Optional.empty(); } + return Optional.of(message); } private void checkBusyStatusAllChannels(@NotNull JDA jda) { @@ -225,6 +227,10 @@ private void checkBusyStatusAllChannels(@NotNull JDA jda) { /** * Method for creating the message that shows the channel statuses for the specified guild. + *

+ * This method dynamically builds the status message as per the current values on the guild, + * including the channel categories. This method will detect any changes made on the guild and + * represent those changes in the status message. * * @param guild the guild that the message is required for. * @return the message to display showing the channel statuses. Includes Discord specific @@ -288,19 +294,18 @@ public void onEvent(@NotNull GenericEvent event) { private @NotNull Optional findExistingStatusMessage(@NotNull TextChannel channel) { // will only run when bots starts, afterwards its stored in a map - Optional result = channel.getHistory() - .retrievePast(FreeCommandConfig.MESSAGE_RETRIEVE_LIMIT) - .map(history -> history.stream() + Optional statusMessage = FreeUtil + .getChannelHistory(channel, FreeCommandConfig.MESSAGE_RETRIEVE_LIMIT) + .flatMap(history -> history.stream() .filter(message -> !message.getEmbeds().isEmpty()) .filter(message -> message.getAuthor().equals(channel.getJDA().getSelfUser())) // FIXME the equals is not working, i believe its because there is no getTitleRaw() // .filter(message -> STATUS_TITLE.equals(message.getEmbeds().get(0).getTitle())) - .findFirst()) - .complete(); + .findFirst()); channelIdToMessageIdForStatus.put(channel.getIdLong(), - result.map(Message::getIdLong).orElse(null)); - return result; + statusMessage.map(Message::getIdLong).orElse(null)); + return statusMessage; } private void initChannelsToMonitor() { From 06bada9d7730090baed6931895747b918b4d4a2c Mon Sep 17 00:00:00 2001 From: borgrel Date: Wed, 17 Nov 2021 23:27:25 +0200 Subject: [PATCH 64/78] cleaned up warnings --- .../tjbot/commands/free/ChannelMonitor.java | 30 ++++++++++++------- .../tjbot/commands/free/ChannelStatus.java | 6 ++-- .../tjbot/commands/free/FreeCommand.java | 20 +++++++++---- .../tjbot/config/FreeCommandConfig.java | 4 +-- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 5995c66fd9..316e174885 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -6,10 +6,7 @@ import net.dv8tion.jda.api.entities.TextChannel; import org.jetbrains.annotations.NotNull; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.StringJoiner; +import java.util.*; import java.util.stream.Stream; @@ -124,7 +121,7 @@ public boolean isChannelBusy(final long channelId) { public boolean isChannelInactive(@NotNull final TextChannel channel) { requiresIsMonitored(channel.getIdLong()); - // TODO change the entire inactive test to work via restactions + // TODO change the entire inactive test to work via rest-actions return FreeUtil.getLastMessageId(channel) .map(FreeUtil::timeFromId) .map(createdTime -> createdTime.isBefore(FreeUtil.inactiveTimeLimit())) @@ -167,8 +164,8 @@ public void setChannelFree(final long channelId) { } /** - * This method provides a stream of the id's for channels that status's are displayed in. This - * is streamed purely as a simple method of encapsulation. + * This method provides a stream of the id's for channels where statuses are displayed. This is + * streamed purely as a simple method of encapsulation. * * @return a stream of channel id's */ @@ -208,13 +205,18 @@ public String statusMessage(@NotNull final Guild guild) { StringJoiner content = new StringJoiner("\n"); String categoryName = ""; for (ChannelStatus status : statusFor) { - Category category = guild.getGuildChannelById(status.getChannelId()).getParent(); + TextChannel channel = guild.getTextChannelById(status.getChannelId()); + if (channel == null) { + // pointless ... added to remove warnings + continue; + } + Category category = channel.getParent(); if (category != null && !category.getName().equals(categoryName)) { categoryName = category.getName(); // append the category name on a new line with markup for underlining // FIXME possible bug when not all channels are part of categories, may mistakenly - // include uncategoried channels inside previous category. will an uncategoried - // channel return an empty string or null? javadocs dont say. + // include uncategorized channels inside previous category. will an uncategorized + // channel return an empty string or null? javadocs don't say. content.add("\n__" + categoryName + "__"); } content.add(status.toDiscordContentRaw()); @@ -241,6 +243,7 @@ public void updateStatusFor(@NotNull Guild guild) { .filter(ChannelStatus::isBusy) .map(ChannelStatus::getChannelId) .map(guild::getTextChannelById) + .filter(Objects::nonNull) // pointless, added for warnings .filter(this::isChannelInactive) .map(TextChannel::getIdLong) .forEach(this::setChannelFree); @@ -261,7 +264,12 @@ public void updateStatusFor(@NotNull Guild guild) { "Guild %s is not configured in the free command system." .formatted(guild.getName())); } - return guild.getTextChannelById(guildIdToStatusChannel.get(guild.getIdLong())); + long channelId = guildIdToStatusChannel.get(guild.getIdLong()); + TextChannel channel = guild.getTextChannelById(channelId); + if (channel == null) + throw new IllegalStateException("Status channel %d does not exist in guild %s" + .formatted(channelId, guild.getName())); + return channel; } /** diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 7dfd594d4c..157fe20467 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -44,7 +44,7 @@ public final class ChannelStatus { * * @return the current stored status related to the channel id. */ - public boolean isBusy() { + public synchronized boolean isBusy() { return status.isBusy(); } @@ -169,8 +169,8 @@ public boolean equals(final Object o) { if (o == null || getClass() != o.getClass()) return false; - ChannelStatus status = (ChannelStatus) o; - return channelId == status.channelId; + ChannelStatus channelStatus = (ChannelStatus) o; + return channelId == channelStatus.channelId; } /** diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 418b48ac3b..71913592b7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -30,7 +30,8 @@ // TODO test if message is a reply and don't mark as busy if it is // TODO add button query to confirm that message is new question not additional info for existing // discussion before marking as busy -// TODO add scheduled tasks to check last message every 15mins and mark as free if 1hr (2hrs?) has +// TODO add scheduled tasks to check last message every predefined duration and mark as free if +// applicable // passed /** @@ -61,7 +62,7 @@ public final class FreeCommand extends SlashCommandAdapter implements EventListe private static final Color MESSAGE_HIGHLIGHT_COLOR = Color.decode("#CCCC00"); // Map to store channel ID's, use Guild.getChannels() to guarantee order for display - private ChannelMonitor channelMonitor; + private final ChannelMonitor channelMonitor; private final Map channelIdToMessageIdForStatus; private boolean isReady; @@ -99,7 +100,7 @@ public FreeCommand() { @Override public void onReady(@NotNull final ReadyEvent event) { final JDA jda = event.getJDA(); - // TODO remove this when onGuildMessageRecieved has another access point + // TODO remove this when onGuildMessageReceived has another access point jda.addEventListener(this); initChannelsToMonitor(); @@ -142,6 +143,7 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { } // TODO check if /free called by original author, if not put message asking if he approves channelMonitor.setChannelFree(id); + // null impossible for event.getGuild (tested in handleShouldBeProcessed) ignore warning displayStatus(channelMonitor.getStatusChannelFor(event.getGuild())); event.reply(UserStrings.MARK_AS_FREE.message()).queue(); } @@ -165,6 +167,11 @@ private boolean handleShouldBeProcessed(@NotNull final SlashCommandEvent event) FreeUtil.sendErrorMessage(event, UserStrings.NOT_READY_ERROR.message()); return false; } + if (event.getGuild() == null) { + logger.error("This is an impossible event, because CommandSystem controls routing," + + "added to remove warnings. Guild for slash event is null"); + return false; + } if (!channelMonitor.isMonitoringGuild(event.getGuild().getIdLong())) { logger.error( "Slash command used by {} in {}(channel: {}) when guild is not configured for Free Command", @@ -222,7 +229,10 @@ public void displayStatus(@NotNull TextChannel channel) { } private void checkBusyStatusAllChannels(@NotNull JDA jda) { - channelMonitor.guildIds().map(jda::getGuildById).forEach(channelMonitor::updateStatusFor); + channelMonitor.guildIds() + .map(jda::getGuildById) + .filter(Objects::nonNull) + .forEach(channelMonitor::updateStatusFor); } /** @@ -292,7 +302,7 @@ public void onEvent(@NotNull GenericEvent event) { } private @NotNull Optional findExistingStatusMessage(@NotNull TextChannel channel) { - // will only run when bots starts, afterwards its stored in a map + // will only run when bot starts, afterwards its stored in a map Optional statusMessage = FreeUtil .getChannelHistory(channel, FreeCommandConfig.MESSAGE_RETRIEVE_LIMIT) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java index 23356e8c03..e1cc6bd01c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/FreeCommandConfig.java @@ -58,8 +58,8 @@ public long getStatusChannel() { } /** - * Retrieves a List of the channels on this guild that the guild wants monitored by the - * free/busy command system + * Retrieves a Collection of the channels that this guild wants to have registered for + * monitoring by the free/busy command system * * @return an Unmodifiable List of Channel ID's */ From 42b1b7222656827d5b01fdfc1de1058ac996c785 Mon Sep 17 00:00:00 2001 From: borgrel Date: Thu, 18 Nov 2021 10:46:08 +0200 Subject: [PATCH 65/78] removed FIXME's --- .../org/togetherjava/tjbot/commands/free/ChannelMonitor.java | 2 +- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 316e174885..858be8981c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -214,7 +214,7 @@ public String statusMessage(@NotNull final Guild guild) { if (category != null && !category.getName().equals(categoryName)) { categoryName = category.getName(); // append the category name on a new line with markup for underlining - // FIXME possible bug when not all channels are part of categories, may mistakenly + // TODO possible bug when not all channels are part of categories, may mistakenly // include uncategorized channels inside previous category. will an uncategorized // channel return an empty string or null? javadocs don't say. content.add("\n__" + categoryName + "__"); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 71913592b7..fbf1514aa6 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -309,7 +309,7 @@ public void onEvent(@NotNull GenericEvent event) { .flatMap(history -> history.stream() .filter(message -> !message.getEmbeds().isEmpty()) .filter(message -> message.getAuthor().equals(channel.getJDA().getSelfUser())) - // FIXME the equals is not working, i believe its because there is no getTitleRaw() + // TODO the equals is not working, i believe its because there is no getTitleRaw() // .filter(message -> STATUS_TITLE.equals(message.getEmbeds().get(0).getTitle())) .findFirst()); From 557e3412a500b97ce1884207bec2eae779cbe727 Mon Sep 17 00:00:00 2001 From: borgrel Date: Thu, 18 Nov 2021 10:47:47 +0200 Subject: [PATCH 66/78] made classes package private --- .../org/togetherjava/tjbot/commands/free/ChannelMonitor.java | 2 +- .../org/togetherjava/tjbot/commands/free/ChannelStatus.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 858be8981c..85825fe6e1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -25,7 +25,7 @@ * After successful configuration, any changes in busy/free status will automatically be displayed * in the configured {@code Status Channel} for that guild. */ -public final class ChannelMonitor { +final class ChannelMonitor { // Map to store channel ID's, use Guild.getChannels() to guarantee order for display private final Map channelsToMonitorById; private final Map guildIdToStatusChannel; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 157fe20467..3dda6a17c8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -11,7 +11,7 @@ /** * Class that tracks the current free/busy status of a channel that requires monitoring. */ -public final class ChannelStatus { +final class ChannelStatus { private final long channelId; private volatile long userId; From 8514c53b604e72d2cc4361a4404977656589ff4f Mon Sep 17 00:00:00 2001 From: borgrel Date: Thu, 18 Nov 2021 11:57:10 +0200 Subject: [PATCH 67/78] refactored getMessageHistory --- .../tjbot/commands/free/FreeUtil.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index c59ed21e75..be72bde7b5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -43,16 +43,14 @@ public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull S */ public static @NotNull Optional> getChannelHistory(@NotNull TextChannel channel, final int limit) { - return Optional - .ofNullable(channel.getHistory().retrievePast(limit).mapToResult().map(listResult -> { - if (listResult.isFailure()) { - logger.error("Failed to retrieve messages from %s because of:" - .formatted(channel.getAsMention()), listResult.getFailure()); - return null; - } else { - return listResult.get(); - } - }).complete()); + return channel.getHistory().retrievePast(limit).mapToResult().map(listResult -> { + if (listResult.isFailure()) { + logger.error("Failed to retrieve messages from %s because of:" + .formatted(channel.getAsMention()), listResult.getFailure()); + return Optional.>empty(); + } + return Optional.of(listResult.get()); + }).complete(); } /** From 687e8e0cb6b33eefcf81bb8667ff15187149534d Mon Sep 17 00:00:00 2001 From: borgrel Date: Thu, 18 Nov 2021 22:18:23 +0200 Subject: [PATCH 68/78] changed getLastMessageId from Optional to OptionalLong --- .../tjbot/commands/free/ChannelMonitor.java | 5 +++++ .../tjbot/commands/free/FreeCommand.java | 5 +++++ .../tjbot/commands/free/FreeUtil.java | 16 +++++++++++----- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 85825fe6e1..37f10dc71f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -123,6 +123,11 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { // TODO change the entire inactive test to work via rest-actions return FreeUtil.getLastMessageId(channel) + // black magic to convert OptionalLong into Optional because OptionalLong does not + // have .map + .stream() + .boxed() + .findFirst() .map(FreeUtil::timeFromId) .map(createdTime -> createdTime.isBefore(FreeUtil.inactiveTimeLimit())) .orElse(true); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index fbf1514aa6..4e234d1a55 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -219,6 +219,11 @@ public void displayStatus(@NotNull TextChannel channel) { private @NotNull Optional deleteIfNotLatest(@NotNull Message message) { if (FreeUtil.getLastMessageId(message.getTextChannel()) + // black magic to convert OptionalLong to Optional because OptionalLong does not + // have .filter + .stream() + .boxed() + .findFirst() .filter(lastId -> message.getIdLong() != lastId) .isPresent()) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java index be72bde7b5..99a002c257 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeUtil.java @@ -12,6 +12,7 @@ import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; +import java.util.OptionalLong; /** * A class containing helper methods required by the free package command. @@ -62,11 +63,16 @@ public static void sendErrorMessage(@NotNull Interaction interaction, @NotNull S * @param channel the channel from which the latest message is required. * @return the id of the latest message or empty if it could not be retrieved. */ - public static @NotNull Optional getLastMessageId(@NotNull TextChannel channel) { - return Optional - .ofNullable(channel.hasLatestMessage() ? channel.getLatestMessageIdLong() : null) - .or(() -> getChannelHistory(channel, 1) - .map(list -> !list.isEmpty() ? list.get(0).getIdLong() : null)); + public static @NotNull OptionalLong getLastMessageId(@NotNull TextChannel channel) { + if (channel.hasLatestMessage()) { + return OptionalLong.of(channel.getLatestMessageIdLong()); + } + // black magic to convert Optional into OptionalLong because Optional does not have + // .mapToLong + return getChannelHistory(channel, 1).stream() + .flatMap(List::stream) + .mapToLong(Message::getIdLong) + .findFirst(); } /** From fe6a3c32cab2ac3d02fbd32ccc30ed1eafb5c38a Mon Sep 17 00:00:00 2001 From: borgrel Date: Thu, 18 Nov 2021 22:54:42 +0200 Subject: [PATCH 69/78] added null check with IllegalStateException --- .../tjbot/commands/free/FreeCommand.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 4e234d1a55..2716aa009b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -337,12 +337,21 @@ private void initStatusMessageChannels(@NotNull final JDA jda) { .getFreeCommandConfig() .stream() .map(FreeCommandConfig::getStatusChannel) - .map(jda::getTextChannelById) - // throws NPE if id from config does not match a text channel - .map(Objects::requireNonNull) + // throws IllegalState if the id's don't match TextChannels + .map(id -> requiresTextChannel(jda, id)) .forEach(channelMonitor::addChannelForStatus); } + private @NotNull TextChannel requiresTextChannel(@NotNull JDA jda, long id) { + TextChannel channel = jda.getTextChannelById(id); + if (channel == null) { + throw new IllegalStateException( + "The id '%d' supplied in the config file, is not a valid id for a TextChannel" + .formatted(id)); + } + return channel; + } + private void initStatusMessages(@NotNull final JDA jda) { // Attempts to find the existing status message, for all guilds. On launch. channelMonitor.statusIds() From 40509118bc7c09e619c23cb49fd2186b4b0deed5 Mon Sep 17 00:00:00 2001 From: borgrel Date: Fri, 19 Nov 2021 00:13:55 +0200 Subject: [PATCH 70/78] cleanup --- .../tjbot/commands/free/FreeCommand.java | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 2716aa009b..8893583a17 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -21,7 +21,10 @@ import org.togetherjava.tjbot.config.FreeCommandConfig; import java.awt.*; -import java.util.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; // TODO (can SlashCommandVisibility be narrower than GUILD?) // TODO monitor all channels when list is empty? monitor none? @@ -32,7 +35,6 @@ // discussion before marking as busy // TODO add scheduled tasks to check last message every predefined duration and mark as free if // applicable -// passed /** * Implementation of the free command. It is used to monitor a predefined list of channels and show @@ -99,6 +101,7 @@ public FreeCommand() { */ @Override public void onReady(@NotNull final ReadyEvent event) { + @NotNull final JDA jda = event.getJDA(); // TODO remove this when onGuildMessageReceived has another access point jda.addEventListener(this); @@ -108,11 +111,9 @@ public void onReady(@NotNull final ReadyEvent event) { logger.debug("Config loaded:\n{}", channelMonitor); checkBusyStatusAllChannels(jda); - initStatusMessages(jda); channelMonitor.statusIds() - .map(event.getJDA()::getTextChannelById) - .filter(Objects::nonNull) + .map(id -> requiresTextChannel(jda, id)) .forEach(this::displayStatus); isReady = true; @@ -143,11 +144,20 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { } // TODO check if /free called by original author, if not put message asking if he approves channelMonitor.setChannelFree(id); - // null impossible for event.getGuild (tested in handleShouldBeProcessed) ignore warning - displayStatus(channelMonitor.getStatusChannelFor(event.getGuild())); + displayStatus(channelMonitor.getStatusChannelFor(requiresGuild(event))); event.reply(UserStrings.MARK_AS_FREE.message()).queue(); } + private @NotNull Guild requiresGuild(SlashCommandEvent event) { + Guild guild = event.getGuild(); + if (guild == null) { + throw new IllegalStateException( + "A global slash command '%s' somehow got routed to the free system which requires a guild" + .formatted(event.getCommandString())); + } + return guild; + } + /** * Method to test event to see if it should be processed. *

@@ -167,22 +177,20 @@ private boolean handleShouldBeProcessed(@NotNull final SlashCommandEvent event) FreeUtil.sendErrorMessage(event, UserStrings.NOT_READY_ERROR.message()); return false; } - if (event.getGuild() == null) { - logger.error("This is an impossible event, because CommandSystem controls routing," - + "added to remove warnings. Guild for slash event is null"); - return false; - } - if (!channelMonitor.isMonitoringGuild(event.getGuild().getIdLong())) { + // checks if guild is null and throws IllegalStateException if it is + @NotNull + Guild guild = requiresGuild(event); + if (!channelMonitor.isMonitoringGuild(guild.getIdLong())) { logger.error( "Slash command used by {} in {}(channel: {}) when guild is not configured for Free Command", - event.getUser().getIdLong(), event.getGuild(), event.getChannel().getName()); + event.getUser().getIdLong(), guild, event.getChannel().getName()); FreeUtil.sendErrorMessage(event, - UserStrings.NOT_CONFIGURED_ERROR.formatted(event.getGuild().getName())); + UserStrings.NOT_CONFIGURED_ERROR.formatted(guild.getName())); return false; } if (!channelMonitor.isMonitoringChannel(event.getChannel().getIdLong())) { - logger.debug("'/free called in un-configured channel {}({})", - event.getGuild().getName(), event.getChannel().getName()); + logger.debug("'/free called in un-configured channel {}({})", guild.getName(), + event.getChannel().getName()); FreeUtil.sendErrorMessage(event, UserStrings.NOT_MONITORED_ERROR.message()); return false; } @@ -235,11 +243,20 @@ public void displayStatus(@NotNull TextChannel channel) { private void checkBusyStatusAllChannels(@NotNull JDA jda) { channelMonitor.guildIds() - .map(jda::getGuildById) - .filter(Objects::nonNull) + .map(id -> requiresGuild(jda, id)) .forEach(channelMonitor::updateStatusFor); } + private @NotNull Guild requiresGuild(@NotNull JDA jda, long id) { + Guild guild = jda.getGuildById(id); + if (guild == null) { + throw new IllegalStateException( + "The guild with id '%d' has been deleted since free command system was configured." + .formatted(id)); + } + return guild; + } + /** * Method for creating the message that shows the channel statuses for the specified guild. *

@@ -337,7 +354,7 @@ private void initStatusMessageChannels(@NotNull final JDA jda) { .getFreeCommandConfig() .stream() .map(FreeCommandConfig::getStatusChannel) - // throws IllegalState if the id's don't match TextChannels + // throws IllegalStateException if the id's don't match TextChannels .map(id -> requiresTextChannel(jda, id)) .forEach(channelMonitor::addChannelForStatus); } @@ -351,15 +368,4 @@ private void initStatusMessageChannels(@NotNull final JDA jda) { } return channel; } - - private void initStatusMessages(@NotNull final JDA jda) { - // Attempts to find the existing status message, for all guilds. On launch. - channelMonitor.statusIds() - .map(jda::getTextChannelById) - .filter(Objects::nonNull) // only here for sonarLint - .map(this::getStatusMessageIn) - .flatMap(Optional::stream) - .forEach(message -> channelIdToMessageIdForStatus.put(message.getChannel().getIdLong(), - message.getIdLong())); - } } From 7d0a232bc1b676a8f45baebc49b806442256da88 Mon Sep 17 00:00:00 2001 From: borgrel Date: Fri, 19 Nov 2021 00:17:54 +0200 Subject: [PATCH 71/78] added javadocs --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 8893583a17..216b33f295 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -127,6 +127,7 @@ public void onReady(@NotNull final ReadyEvent event) { * {@link FreeCommandConfig}) the user will receive an ephemeral message stating such. * * @param event the event that triggered this + * @throws IllegalStateException if this method is called for a Global Slash Command */ @Override public void onSlashCommand(@NotNull final SlashCommandEvent event) { From 9de97d1805868378680e8284a37fa0c17b040836 Mon Sep 17 00:00:00 2001 From: borgrel Date: Fri, 19 Nov 2021 00:24:44 +0200 Subject: [PATCH 72/78] removed chaining for readability --- .../tjbot/commands/free/FreeCommand.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 216b33f295..1e286cbe38 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -21,10 +21,7 @@ import org.togetherjava.tjbot.config.FreeCommandConfig; import java.awt.*; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.util.*; // TODO (can SlashCommandVisibility be narrower than GUILD?) // TODO monitor all channels when list is empty? monitor none? @@ -227,18 +224,13 @@ public void displayStatus(@NotNull TextChannel channel) { } private @NotNull Optional deleteIfNotLatest(@NotNull Message message) { - if (FreeUtil.getLastMessageId(message.getTextChannel()) - // black magic to convert OptionalLong to Optional because OptionalLong does not - // have .filter - .stream() - .boxed() - .findFirst() - .filter(lastId -> message.getIdLong() != lastId) - .isPresent()) { + OptionalLong lastId = FreeUtil.getLastMessageId(message.getTextChannel()); + if (lastId.isPresent() && lastId.getAsLong() == message.getIdLong()) { message.delete().queue(); return Optional.empty(); } + return Optional.of(message); } From 08f145065beba69b031b4fde5858715dc8e4e514 Mon Sep 17 00:00:00 2001 From: borgrel Date: Fri, 19 Nov 2021 00:27:43 +0200 Subject: [PATCH 73/78] fixed error --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 1e286cbe38..f690daf2de 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -226,7 +226,7 @@ public void displayStatus(@NotNull TextChannel channel) { private @NotNull Optional deleteIfNotLatest(@NotNull Message message) { OptionalLong lastId = FreeUtil.getLastMessageId(message.getTextChannel()); - if (lastId.isPresent() && lastId.getAsLong() == message.getIdLong()) { + if (lastId.isPresent() && lastId.getAsLong() != message.getIdLong()) { message.delete().queue(); return Optional.empty(); } From e49d8d4bbd429d8f20e353fe4501126129764950 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 23 Nov 2021 16:43:11 +0200 Subject: [PATCH 74/78] added comment --- .../org/togetherjava/tjbot/commands/free/ChannelMonitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 37f10dc71f..9b600e79b6 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -130,7 +130,7 @@ public boolean isChannelInactive(@NotNull final TextChannel channel) { .findFirst() .map(FreeUtil::timeFromId) .map(createdTime -> createdTime.isBefore(FreeUtil.inactiveTimeLimit())) - .orElse(true); + .orElse(true); // if no channel history could be fetched assume channel is free } /** From 8fbb25409db9cac5d766e9341906eb0ac560d464 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 23 Nov 2021 17:10:50 +0200 Subject: [PATCH 75/78] reordered methods for code factor --- .../tjbot/commands/free/FreeCommand.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index f690daf2de..dcfefeec6e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -146,16 +146,6 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { event.reply(UserStrings.MARK_AS_FREE.message()).queue(); } - private @NotNull Guild requiresGuild(SlashCommandEvent event) { - Guild guild = event.getGuild(); - if (guild == null) { - throw new IllegalStateException( - "A global slash command '%s' somehow got routed to the free system which requires a guild" - .formatted(event.getCommandString())); - } - return guild; - } - /** * Method to test event to see if it should be processed. *

@@ -250,6 +240,16 @@ private void checkBusyStatusAllChannels(@NotNull JDA jda) { return guild; } + private @NotNull Guild requiresGuild(SlashCommandEvent event) { + Guild guild = event.getGuild(); + if (guild == null) { + throw new IllegalStateException( + "A global slash command '%s' somehow got routed to the free system which requires a guild" + .formatted(event.getCommandString())); + } + return guild; + } + /** * Method for creating the message that shows the channel statuses for the specified guild. *

From 23a85cf31eeddf191496a7df0cb637a982af175c Mon Sep 17 00:00:00 2001 From: borgrel Date: Mon, 29 Nov 2021 14:28:57 +0200 Subject: [PATCH 76/78] removed @NotNull --- .../org/togetherjava/tjbot/commands/free/FreeCommand.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index dcfefeec6e..6febd7778f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -98,7 +98,6 @@ public FreeCommand() { */ @Override public void onReady(@NotNull final ReadyEvent event) { - @NotNull final JDA jda = event.getJDA(); // TODO remove this when onGuildMessageReceived has another access point jda.addEventListener(this); @@ -166,8 +165,7 @@ private boolean handleShouldBeProcessed(@NotNull final SlashCommandEvent event) return false; } // checks if guild is null and throws IllegalStateException if it is - @NotNull - Guild guild = requiresGuild(event); + @NotNull Guild guild = requiresGuild(event); if (!channelMonitor.isMonitoringGuild(guild.getIdLong())) { logger.error( "Slash command used by {} in {}(channel: {}) when guild is not configured for Free Command", From e0317660231a71424bf3f28e5c9f84181b536b33 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 30 Nov 2021 12:45:15 +0200 Subject: [PATCH 77/78] reformatted --- .../org/togetherjava/tjbot/commands/free/FreeCommand.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 6febd7778f..4bfb3e535e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -165,7 +165,8 @@ private boolean handleShouldBeProcessed(@NotNull final SlashCommandEvent event) return false; } // checks if guild is null and throws IllegalStateException if it is - @NotNull Guild guild = requiresGuild(event); + @NotNull + Guild guild = requiresGuild(event); if (!channelMonitor.isMonitoringGuild(guild.getIdLong())) { logger.error( "Slash command used by {} in {}(channel: {}) when guild is not configured for Free Command", @@ -243,7 +244,7 @@ private void checkBusyStatusAllChannels(@NotNull JDA jda) { if (guild == null) { throw new IllegalStateException( "A global slash command '%s' somehow got routed to the free system which requires a guild" - .formatted(event.getCommandString())); + .formatted(event.getCommandString())); } return guild; } From 97891b19fd71ac40bf6626f6a52e0c15aa9239c5 Mon Sep 17 00:00:00 2001 From: borgrel Date: Tue, 30 Nov 2021 13:59:43 +0200 Subject: [PATCH 78/78] removed @NotNull --- .../java/org/togetherjava/tjbot/commands/free/FreeCommand.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 4bfb3e535e..006f789b99 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -165,7 +165,6 @@ private boolean handleShouldBeProcessed(@NotNull final SlashCommandEvent event) return false; } // checks if guild is null and throws IllegalStateException if it is - @NotNull Guild guild = requiresGuild(event); if (!channelMonitor.isMonitoringGuild(guild.getIdLong())) { logger.error(