Skip to content

Fixing thread help system bugs #452

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
merged 2 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -16,18 +16,21 @@
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
import org.togetherjava.tjbot.config.Config;

import static org.togetherjava.tjbot.commands.help.HelpSystemHelper.TITLE_COMPACT_LENGTH_MAX;
import static org.togetherjava.tjbot.commands.help.HelpSystemHelper.TITLE_COMPACT_LENGTH_MIN;

/**
* Implements the {@code /ask} command, which is the main way of asking questions. The command can
* only be used in the staging channel.
*
* <p>
* Upon use, it will create a new thread for the question and invite all helpers interested in the
* given category to it. It will also introduce the user to the system and give a quick explanation
* message.
*
* <p>
* The other way to ask questions is by {@link ImplicitAskListener}.
*
* <p>
* Example usage:
*
*
* <pre>
* {@code
* /ask title: How to send emails? category: Frameworks
Expand Down Expand Up @@ -76,6 +79,10 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
return;
}

if (!handleIsValidTitle(title, event)) {
return;
}

TextChannel helpStagingChannel = event.getTextChannel();
helpStagingChannel.createThreadChannel("[%s] %s".formatted(category, title))
.flatMap(threadChannel -> handleEvent(event, threadChannel, event.getMember(), title,
Expand All @@ -96,6 +103,20 @@ private boolean handleIsStagingChannel(@NotNull IReplyCallback event) {
return false;
}

private boolean handleIsValidTitle(@NotNull CharSequence title, @NotNull IReplyCallback event) {
if (HelpSystemHelper.isTitleValid(title)) {
return true;
}

event.reply(
"Sorry, but the titel length (after removal of special characters) has to be between %d and %d."
.formatted(TITLE_COMPACT_LENGTH_MIN, TITLE_COMPACT_LENGTH_MAX))
.setEphemeral(true)
.queue();

return false;
}

private @NotNull RestAction<Message> handleEvent(@NotNull IReplyCallback event,
@NotNull ThreadChannel threadChannel, @NotNull Member author, @NotNull String title,
@NotNull String category) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.togetherjava.tjbot.commands.help;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.Role;
Expand All @@ -14,7 +16,10 @@
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
import org.togetherjava.tjbot.config.Config;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
* Implements the {@code /change-help-category} command, which is able to change the category of a
Expand All @@ -29,7 +34,11 @@
public final class ChangeHelpCategoryCommand extends SlashCommandAdapter {
private static final String CATEGORY_OPTION = "category";

private static final int COOLDOWN_DURATION_VALUE = 1;
private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.HOURS;

private final HelpSystemHelper helper;
private final Cache<Long, Instant> helpThreadIdToLastCategoryChange;

/**
* Creates a new instance.
Expand All @@ -49,6 +58,11 @@ public ChangeHelpCategoryCommand(@NotNull Config config, @NotNull HelpSystemHelp

getData().addOptions(category);

helpThreadIdToLastCategoryChange = Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterAccess(COOLDOWN_DURATION_VALUE, TimeUnit.of(COOLDOWN_DURATION_UNIT))
.build();

this.helper = helper;
}

Expand All @@ -66,6 +80,16 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
return;
}

if (isHelpThreadOnCooldown(helpThread)) {
event
.reply("Please wait a bit, this command can only be used once per %d %s."
.formatted(COOLDOWN_DURATION_VALUE, COOLDOWN_DURATION_UNIT))
.setEphemeral(true)
.queue();
return;
}
helpThreadIdToLastCategoryChange.put(helpThread.getIdLong(), Instant.now());

event.deferReply().queue();

helper.renameChannelToCategoryTitle(helpThread, category)
Expand Down Expand Up @@ -96,4 +120,13 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
return action.flatMap(any -> helpThread.sendMessage(headsUpWithoutRole)
.flatMap(message -> message.editMessage(headsUpWithRole)));
}

private boolean isHelpThreadOnCooldown(@NotNull ThreadChannel helpThread) {
return Optional
.ofNullable(helpThreadIdToLastCategoryChange.getIfPresent(helpThread.getIdLong()))
.map(lastCategoryChange -> lastCategoryChange.plus(COOLDOWN_DURATION_VALUE,
COOLDOWN_DURATION_UNIT))
.filter(Instant.now()::isBefore)
.isPresent();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.togetherjava.tjbot.commands.help;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.ThreadChannel;
Expand All @@ -8,14 +10,23 @@
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
import org.togetherjava.tjbot.commands.SlashCommandVisibility;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
* Implements the {@code /close} command to close question threads.
* <p>
* Can be used in active (non-archived) question threads. Will close, i.e. archive, the thread upon
* use. Meant to be used once a question has been resolved.
*/
public final class CloseCommand extends SlashCommandAdapter {
private static final int COOLDOWN_DURATION_VALUE = 1;
private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.HOURS;

private final HelpSystemHelper helper;
private final Cache<Long, Instant> helpThreadIdToLastClose;

/**
* Creates a new instance.
Expand All @@ -25,6 +36,11 @@ public final class CloseCommand extends SlashCommandAdapter {
public CloseCommand(@NotNull HelpSystemHelper helper) {
super("close", "Close this question thread", SlashCommandVisibility.GUILD);

helpThreadIdToLastClose = Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterAccess(COOLDOWN_DURATION_VALUE, TimeUnit.of(COOLDOWN_DURATION_UNIT))
.build();

this.helper = helper;
}

Expand All @@ -40,10 +56,28 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
return;
}

if (isHelpThreadOnCooldown(helpThread)) {
event
.reply("Please wait a bit, this command can only be used once per %d %s."
.formatted(COOLDOWN_DURATION_VALUE, COOLDOWN_DURATION_UNIT))
.setEphemeral(true)
.queue();
return;
}
helpThreadIdToLastClose.put(helpThread.getIdLong(), Instant.now());

MessageEmbed embed = new EmbedBuilder().setDescription("Closed the thread.")
.setColor(HelpSystemHelper.AMBIENT_COLOR)
.build();

event.replyEmbeds(embed).flatMap(any -> helpThread.getManager().setArchived(true)).queue();
}

private boolean isHelpThreadOnCooldown(@NotNull ThreadChannel helpThread) {
return Optional.ofNullable(helpThreadIdToLastClose.getIfPresent(helpThread.getIdLong()))
.map(lastCategoryChange -> lastCategoryChange.plus(COOLDOWN_DURATION_VALUE,
COOLDOWN_DURATION_UNIT))
.filter(Instant.now()::isBefore)
.isPresent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public final class HelpSystemHelper {
private static final Pattern EXTRACT_CATEGORY_TITLE_PATTERN =
Pattern.compile("(?:\\[(?<%s>.+)] )?(?<%s>.+)".formatted(CATEGORY_GROUP, TITLE_GROUP));

private static final Pattern TITLE_COMPACT_REMOVAL_PATTERN = Pattern.compile("\\W");
static final int TITLE_COMPACT_LENGTH_MIN = 2;
static final int TITLE_COMPACT_LENGTH_MAX = 80;

private final Predicate<String> isOverviewChannelName;
private final String overviewChannelPattern;
private final Predicate<String> isStagingChannelName;
Expand Down Expand Up @@ -175,4 +179,11 @@ boolean isStagingChannelName(@NotNull String channelName) {
String getStagingChannelPattern() {
return stagingChannelPattern;
}

static boolean isTitleValid(@NotNull CharSequence title) {
String titleCompact = TITLE_COMPACT_REMOVAL_PATTERN.matcher(title).replaceAll("");

return titleCompact.length() >= TITLE_COMPACT_LENGTH_MIN
&& titleCompact.length() <= TITLE_COMPACT_LENGTH_MAX;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
* <p>
* Listens to plain messages in the staging channel, picks them up and transfers them into a proper
* question thread.
*
* <p>
* The system can handle spam appropriately and will not create multiple threads for each message.
*
* <p>
* For example:
*
*
* <pre>
* {@code
* John sends: How to send emails?
Expand Down Expand Up @@ -128,17 +128,22 @@ private Optional<HelpThread> getLastHelpThreadIfOnCooldown(long userId) {
}

private static @NotNull String createTitle(@NotNull String message) {
String titleCandidate;
if (message.length() < TITLE_MAX_LENGTH) {
return message;
}
// Attempt to end at the last word before hitting the limit
// e.g. "[foo bar] baz" for a limit somewhere in between "baz"
int lastWordEnd = message.lastIndexOf(' ', TITLE_MAX_LENGTH);
if (lastWordEnd == -1) {
lastWordEnd = TITLE_MAX_LENGTH;
titleCandidate = message;
} else {
// Attempt to end at the last word before hitting the limit
// e.g. "[foo bar] baz" for a limit somewhere in between "baz"
int lastWordEnd = message.lastIndexOf(' ', TITLE_MAX_LENGTH);
if (lastWordEnd == -1) {
lastWordEnd = TITLE_MAX_LENGTH;
}

titleCandidate = message.substring(0, lastWordEnd);
}

return message.substring(0, lastWordEnd);
return HelpSystemHelper.isTitleValid(titleCandidate) ? titleCandidate : "Untitled";

}

private @NotNull RestAction<?> handleEvent(@NotNull ThreadChannel threadChannel,
Expand Down