diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java index 2d6af69d74..2b67bc6c85 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java @@ -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. - * + *
* 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. - * + *
* The other way to ask questions is by {@link ImplicitAskListener}. - * + *
* Example usage: - * + * *
* {@code
* /ask title: How to send emails? category: Frameworks
@@ -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,
@@ -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 handleEvent(@NotNull IReplyCallback event,
@NotNull ThreadChannel threadChannel, @NotNull Member author, @NotNull String title,
@NotNull String category) {
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java
index 67739b4ac7..6522473a27 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java
@@ -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;
@@ -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
@@ -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 helpThreadIdToLastCategoryChange;
/**
* Creates a new instance.
@@ -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;
}
@@ -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)
@@ -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();
+ }
}
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java
index 00efae8fb2..857015aa83 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java
@@ -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;
@@ -8,6 +10,11 @@
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.
*
@@ -15,7 +22,11 @@
* 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 helpThreadIdToLastClose;
/**
* Creates a new instance.
@@ -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;
}
@@ -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();
+ }
}
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java
index 8b835ed7ec..41fa901cc3 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java
@@ -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 isOverviewChannelName;
private final String overviewChannelPattern;
private final Predicate isStagingChannelName;
@@ -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;
+ }
}
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java
index 4a63e73581..d0b124b608 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java
@@ -27,11 +27,11 @@
*
* Listens to plain messages in the staging channel, picks them up and transfers them into a proper
* question thread.
- *
+ *
* The system can handle spam appropriately and will not create multiple threads for each message.
- *
+ *
* For example:
- *
+ *
*
* {@code
* John sends: How to send emails?
@@ -128,17 +128,22 @@ private Optional 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,