Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
95d7ba4
Added framework for FreeCommand and ChannelStatus
borgrel Oct 19, 2021
9ff6df5
Added framework for FreeCommand and ChannelStatus
borgrel Oct 20, 2021
2b48717
added thread safety for 'busy'
borgrel Oct 22, 2021
48d9d3b
moved Channel Status functionality into its own class
borgrel Oct 26, 2021
6cce0af
Moved user responses to its own class
borgrel Nov 4, 2021
19b94f2
Added JavaDocs
borgrel Nov 4, 2021
22a7c64
Changed from user.getName() to getAsTag() (as per review)
borgrel Nov 7, 2021
e6ebf01
Changed UserStrings to TextBlocks
borgrel Nov 7, 2021
778a41d
@NotNull added, some grammer corrections
borgrel Nov 8, 2021
591b25a
JavaDoc revisions, added some extra syntax
borgrel Nov 9, 2021
9889b87
Made config unmodifiable more explicit.
borgrel Nov 9, 2021
330cfcb
Fixed accidental log4j2.xml commit
borgrel Nov 9, 2021
2fc84ef
Changed 'todo's to uppercase
borgrel Nov 9, 2021
16f42ee
improved javadocs
borgrel Nov 9, 2021
8a77276
removed exposed implementation details from javadoc
borgrel Nov 9, 2021
0c08faf
renamed channelsToMonitor to channelsToMonitorById
borgrel Nov 10, 2021
e2e52d2
made testing interval configurable
borgrel Nov 10, 2021
a8b754d
replaced accidental NPE's with IllegalArgumentException
borgrel Nov 10, 2021
b49e365
made class FreeCommand final
borgrel Nov 13, 2021
c329603
changed FREE_COMMAND constant to COMMAND_NAME
borgrel Nov 13, 2021
7a750a3
removed term private from javadocs
borgrel Nov 13, 2021
5558f99
changed method name shouldHandle to handleShouldBeProcessed
borgrel Nov 13, 2021
8cb41f9
added @NotNull to checkBusyStatusAllChannels
borgrel Nov 13, 2021
d44fe5e
added @NotNull to buildStatusMessage
borgrel Nov 13, 2021
a86c887
added @NotNull to getStatusMessage
borgrel Nov 13, 2021
c722140
added @NotNull to findExistingStatusMessage
borgrel Nov 13, 2021
e774dcf
improved code flow
borgrel Nov 13, 2021
88755b7
fixed formatting in javadocs
borgrel Nov 13, 2021
d1e0d0d
added //TODO with issue number
borgrel Nov 13, 2021
b2dc17c
changed to enum-util pattern
borgrel Nov 13, 2021
60f550e
changed postStatusInChannel to guildIdToStatusChannel
borgrel Nov 13, 2021
a49a946
removed delegation text from javadocs
borgrel Nov 13, 2021
1425e31
changed toDiscord method name to toDiscordContentRaw
borgrel Nov 13, 2021
58cf827
added ChannelStatusType enum
borgrel Nov 13, 2021
24c2285
added more detail to javadoc
borgrel Nov 13, 2021
8d1dbd7
added more detail to javadoc
borgrel Nov 13, 2021
4565b38
refactored onSlashCommand for improved readability
borgrel Nov 13, 2021
1c80abf
fixed import
borgrel Nov 13, 2021
9060f80
added FIXME comment
borgrel Nov 13, 2021
8e1e145
made package private
borgrel Nov 13, 2021
8ee097b
made package private
borgrel Nov 13, 2021
c798bbc
added @NotNull
borgrel Nov 13, 2021
70edab9
added TODO
borgrel Nov 13, 2021
d50b78e
improved javadocs
borgrel Nov 13, 2021
e57f392
improved javadocs
borgrel Nov 13, 2021
88318ff
changed method to modern chain
borgrel Nov 13, 2021
6063c9a
added space
borgrel Nov 13, 2021
9f88d70
changed variable name FREE_COLOR into MESSAGE_HIGHLIGHT_COLOR
borgrel Nov 17, 2021
ca3b73e
fixed javadocs
borgrel Nov 17, 2021
cdc6303
the extremely common adding @NotNull commit
borgrel Nov 17, 2021
2d7353f
the extremely common adding @NotNull commit
borgrel Nov 17, 2021
9ece875
changed visibility to package private
borgrel Nov 17, 2021
a8edfce
added synchronised
borgrel Nov 17, 2021
ed1104e
moved todo inside method
borgrel Nov 17, 2021
7708cd5
inverted if for better readability
borgrel Nov 17, 2021
cadb435
removed deprecated tag
borgrel Nov 17, 2021
0f2e6d7
changed variable name
borgrel Nov 17, 2021
4d1ce62
fixed comment
borgrel Nov 17, 2021
cb5d0c3
changed config to fail-fast
borgrel Nov 17, 2021
419897a
deleted unrelated class from branch
borgrel Nov 17, 2021
cfefb6e
made message retrieval limit configurable
borgrel Nov 17, 2021
0a72249
changed inactive to work when retrieveHistory fails
borgrel Nov 17, 2021
b2e78ed
streamlined displayStatus method
borgrel Nov 17, 2021
06bada9
cleaned up warnings
borgrel Nov 17, 2021
42b1b72
removed FIXME's
borgrel Nov 18, 2021
557e341
made classes package private
borgrel Nov 18, 2021
8514c53
refactored getMessageHistory
borgrel Nov 18, 2021
687e8e0
changed getLastMessageId from Optional<Long> to OptionalLong
borgrel Nov 18, 2021
fe6a3c3
added null check with IllegalStateException
borgrel Nov 18, 2021
4050911
cleanup
borgrel Nov 18, 2021
7d0a232
added javadocs
borgrel Nov 18, 2021
9de97d1
removed chaining for readability
borgrel Nov 18, 2021
08f1450
fixed error
borgrel Nov 18, 2021
e49d8d4
added comment
borgrel Nov 23, 2021
8fbb254
reordered methods for code factor
borgrel Nov 23, 2021
23a85cf
removed @NotNull
borgrel Nov 29, 2021
e031766
reformatted
borgrel Nov 30, 2021
97891b1
removed @NotNull
borgrel Nov 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
package org.togetherjava.tjbot.commands.free;

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.*;
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}.
*
* 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.
*/
final class ChannelMonitor {
// Map to store channel ID's, use Guild.getChannels() to guarantee order for display
private final Map<Long, ChannelStatus> channelsToMonitorById;
private final Map<Long, Long> guildIdToStatusChannel;

ChannelMonitor() {
guildIdToStatusChannel = new HashMap<>(); // JDA required to populate map
channelsToMonitorById = new HashMap<>();
}

/**
* Method for adding channels that need to be monitored.
*
* @param channelId the id of the channel to monitor
*/
public void addChannelToMonitor(final long channelId) {
channelsToMonitorById.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.
*
* This method also calls a method which updates the status of the channels in the
* {@link Guild}. So always add the status channel <strong>after</strong> 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(@NotNull final TextChannel channel) {
guildIdToStatusChannel.put(channel.getGuild().getIdLong(), channel.getIdLong());
updateStatusFor(channel.getGuild());
}

/**
* 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 whether the guild is configured in the free command system or not.
*/
public boolean isMonitoringGuild(final long guildId) {
return guildIdToStatusChannel.containsKey(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
* {@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.
*/
public boolean isMonitoringChannel(final long channelId) {
return channelsToMonitorById.containsKey(channelId);
}

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 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
* {@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
* recently than the configured duration.
* @throws IllegalArgumentException if the channel passed is not monitored. See
* {@link #addChannelToMonitor(long)}
*/
public boolean isChannelInactive(@NotNull final TextChannel channel) {
requiresIsMonitored(channel.getIdLong());

// TODO change the entire inactive test to work via rest-actions
return FreeUtil.getLastMessageId(channel)
// black magic to convert OptionalLong into Optional<Long> because OptionalLong does not
// have .map
.stream()
.boxed()
.findFirst()
.map(FreeUtil::timeFromId)
.map(createdTime -> createdTime.isBefore(FreeUtil.inactiveTimeLimit()))
.orElse(true); // if no channel history could be fetched assume channel is free
}

/**
* 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.
* @throws IllegalArgumentException if the channel passed is not monitored. See
* {@link #addChannelToMonitor(long)}
*/
public void setChannelBusy(final long channelId, final long userId) {
requiresIsMonitored(channelId).setBusy(userId);
}

/**
* 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
* {@link #addChannelToMonitor(long)}
*/
public void setChannelFree(final long channelId) {
requiresIsMonitored(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 @NotNull Stream<Long> guildIds() {
return guildIdToStatusChannel.keySet().stream();
}

/**
* 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
*/
public @NotNull Stream<Long> statusIds() {
return guildIdToStatusChannel.values().stream();
}

private @NotNull List<ChannelStatus> guildMonitoredChannelsList(@NotNull final Guild guild) {
return guild.getChannels()
.stream()
.map(GuildChannel::getIdLong)
.filter(channelsToMonitorById::containsKey)
.map(channelsToMonitorById::get)
.toList();
}

/**
* Creates the status message (specific to the guild specified) that shows which channels are
* busy/free.
* <p>
* 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
* 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<ChannelStatus> statusFor = guildMonitoredChannelsList(guild);

// update name so that current channel name is used
statusFor.forEach(channelStatus -> channelStatus.updateChannelName(guild));

// dynamically separate channels by channel categories
StringJoiner content = new StringJoiner("\n");
String categoryName = "";
for (ChannelStatus status : statusFor) {
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
// 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 + "__");
}
content.add(status.toDiscordContentRaw());
}

return content.toString();
}

/**
* 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)}.
* <p>
* 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.
*/
public void updateStatusFor(@NotNull Guild guild) {
// TODO add automation after Routine support (#235) is pushed
guildMonitoredChannelsList(guild).parallelStream()
.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);
}

/**
* 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.
* @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) {
if (!guildIdToStatusChannel.containsKey(guild.getIdLong())) {
throw new IllegalArgumentException(
"Guild %s is not configured in the free command system."
.formatted(guild.getName()));
}
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;
}

/**
* 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 human-readable text string that describes this class.
*/
@Override
public String toString() {
// This is called on boot as a debug level message by the logger
return "Monitoring Channels: %s%nDisplaying on Channels: %s"
.formatted(channelsToMonitorById, guildIdToStatusChannel);
}
}
Loading