-
-
Notifications
You must be signed in to change notification settings - Fork 101
Migrate free channel command #221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Zabuzard
merged 78 commits into
Together-Java:develop
from
borgrel:feature/free-channel-migration
Dec 7, 2021
Merged
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 9ff6df5
Added framework for FreeCommand and ChannelStatus
borgrel 2b48717
added thread safety for 'busy'
borgrel 48d9d3b
moved Channel Status functionality into its own class
borgrel 6cce0af
Moved user responses to its own class
borgrel 19b94f2
Added JavaDocs
borgrel 22a7c64
Changed from user.getName() to getAsTag() (as per review)
borgrel e6ebf01
Changed UserStrings to TextBlocks
borgrel 778a41d
@NotNull added, some grammer corrections
borgrel 591b25a
JavaDoc revisions, added some extra syntax
borgrel 9889b87
Made config unmodifiable more explicit.
borgrel 330cfcb
Fixed accidental log4j2.xml commit
borgrel 2fc84ef
Changed 'todo's to uppercase
borgrel 16f42ee
improved javadocs
borgrel 8a77276
removed exposed implementation details from javadoc
borgrel 0c08faf
renamed channelsToMonitor to channelsToMonitorById
borgrel e2e52d2
made testing interval configurable
borgrel a8b754d
replaced accidental NPE's with IllegalArgumentException
borgrel b49e365
made class FreeCommand final
borgrel c329603
changed FREE_COMMAND constant to COMMAND_NAME
borgrel 7a750a3
removed term private from javadocs
borgrel 5558f99
changed method name shouldHandle to handleShouldBeProcessed
borgrel 8cb41f9
added @NotNull to checkBusyStatusAllChannels
borgrel d44fe5e
added @NotNull to buildStatusMessage
borgrel a86c887
added @NotNull to getStatusMessage
borgrel c722140
added @NotNull to findExistingStatusMessage
borgrel e774dcf
improved code flow
borgrel 88755b7
fixed formatting in javadocs
borgrel d1e0d0d
added //TODO with issue number
borgrel b2dc17c
changed to enum-util pattern
borgrel 60f550e
changed postStatusInChannel to guildIdToStatusChannel
borgrel a49a946
removed delegation text from javadocs
borgrel 1425e31
changed toDiscord method name to toDiscordContentRaw
borgrel 58cf827
added ChannelStatusType enum
borgrel 24c2285
added more detail to javadoc
borgrel 8d1dbd7
added more detail to javadoc
borgrel 4565b38
refactored onSlashCommand for improved readability
borgrel 1c80abf
fixed import
borgrel 9060f80
added FIXME comment
borgrel 8e1e145
made package private
borgrel 8ee097b
made package private
borgrel c798bbc
added @NotNull
borgrel 70edab9
added TODO
borgrel d50b78e
improved javadocs
borgrel e57f392
improved javadocs
borgrel 88318ff
changed method to modern chain
borgrel 6063c9a
added space
borgrel 9f88d70
changed variable name FREE_COLOR into MESSAGE_HIGHLIGHT_COLOR
borgrel ca3b73e
fixed javadocs
borgrel cdc6303
the extremely common adding @NotNull commit
borgrel 2d7353f
the extremely common adding @NotNull commit
borgrel 9ece875
changed visibility to package private
borgrel a8edfce
added synchronised
borgrel ed1104e
moved todo inside method
borgrel 7708cd5
inverted if for better readability
borgrel cadb435
removed deprecated tag
borgrel 0f2e6d7
changed variable name
borgrel 4d1ce62
fixed comment
borgrel cb5d0c3
changed config to fail-fast
borgrel 419897a
deleted unrelated class from branch
borgrel cfefb6e
made message retrieval limit configurable
borgrel 0a72249
changed inactive to work when retrieveHistory fails
borgrel b2e78ed
streamlined displayStatus method
borgrel 06bada9
cleaned up warnings
borgrel 42b1b72
removed FIXME's
borgrel 557e341
made classes package private
borgrel 8514c53
refactored getMessageHistory
borgrel 687e8e0
changed getLastMessageId from Optional<Long> to OptionalLong
borgrel fe6a3c3
added null check with IllegalStateException
borgrel 4050911
cleanup
borgrel 7d0a232
added javadocs
borgrel 9de97d1
removed chaining for readability
borgrel 08f1450
fixed error
borgrel e49d8d4
added comment
borgrel 8fbb254
reordered methods for code factor
borgrel 23a85cf
removed @NotNull
borgrel e031766
reformatted
borgrel 97891b1
removed @NotNull
borgrel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
292 changes: 292 additions & 0 deletions
292
application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* 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) { | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
} | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* @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() | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.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); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.