Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new UnquarantineCommand(actionsStore, config));
features.add(new WhoIsCommand());
features.add(new WolframAlphaCommand(config));
features.add(new AskCommand(config, helpSystemHelper));
features.add(new AskCommand(config, helpSystemHelper, database));
features.add(new ModMailCommand(jda, config));
features.add(new HelpThreadCommand(config, helpSystemHelper));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package org.togetherjava.tjbot.commands.help;

import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
Expand All @@ -19,12 +18,19 @@

import org.togetherjava.tjbot.commands.CommandVisibility;
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
import org.togetherjava.tjbot.commands.utils.MessageUtils;
import org.togetherjava.tjbot.config.Config;
import org.togetherjava.tjbot.db.Database;

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

import static org.togetherjava.tjbot.commands.help.HelpSystemHelper.TITLE_COMPACT_LENGTH_MAX;
import static org.togetherjava.tjbot.commands.help.HelpSystemHelper.TITLE_COMPACT_LENGTH_MIN;
import static org.togetherjava.tjbot.commands.help.HelpThreadCommand.*;
import static org.togetherjava.tjbot.db.generated.Tables.HELP_THREADS;

/**
* Implements the {@code /ask} command, which is the main way of asking questions. The command can
Expand All @@ -47,19 +53,27 @@
* </pre>
*/
public final class AskCommand extends SlashCommandAdapter {
private static final int COOLDOWN_DURATION_VALUE = 5;
private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.MINUTES;
private static final Logger logger = LoggerFactory.getLogger(AskCommand.class);
public static final String COMMAND_NAME = "ask";
private static final String TITLE_OPTION = "title";
private static final String CATEGORY_OPTION = "category";
private final Cache<Long, Instant> userToLastAsk = Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterAccess(COOLDOWN_DURATION_VALUE, TimeUnit.of(COOLDOWN_DURATION_UNIT))
.build();
private final HelpSystemHelper helper;
private final Database database;

/**
* Creates a new instance.
*
* @param config the config to use
* @param helper the helper to use
* @param database the database to get help threads from
*/
public AskCommand(Config config, HelpSystemHelper helper) {
public AskCommand(Config config, HelpSystemHelper helper, Database database) {
super("ask", "Ask a question - use this in the staging channel", CommandVisibility.GUILD);

OptionData title =
Expand All @@ -73,10 +87,16 @@ public AskCommand(Config config, HelpSystemHelper helper) {
getData().addOptions(title, category);

this.helper = helper;
this.database = database;
}

@Override
public void onSlashCommand(SlashCommandInteractionEvent event) {
if (isUserOnCooldown(event.getUser())) {
sendCooldownResponse(event);
return;
}

String title = event.getOption(TITLE_OPTION).getAsString();
String category = event.getOption(CATEGORY_OPTION).getAsString();

Expand All @@ -102,8 +122,44 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
overviewChannel.createThreadChannel(name.toChannelName())
.flatMap(threadChannel -> handleEvent(eventHook, threadChannel, author, title, category,
guild))
.queue(any -> {
}, e -> handleFailure(e, eventHook));
.queue(any -> userToLastAsk.put(event.getUser().getIdLong(), Instant.now()),
e -> handleFailure(e, eventHook));
}

private boolean isUserOnCooldown(User user) {
return Optional.ofNullable(userToLastAsk.getIfPresent(user.getIdLong()))
.map(lastAction -> lastAction.plus(COOLDOWN_DURATION_VALUE, COOLDOWN_DURATION_UNIT))
.filter(Instant.now()::isBefore)
.isPresent();
}

private void sendCooldownResponse(SlashCommandInteractionEvent event) {
User user = event.getUser();
Guild guild = event.getGuild();

RestAction<String> changeTitle = mentionHelpChangeCommand(guild, CHANGE_TITLE_SUBCOMMAND);
RestAction<String> changeCategory =
mentionHelpChangeCommand(guild, CHANGE_CATEGORY_SUBCOMMAND);
long lastThreadByAuthorId = database
.read(context -> context.selectFrom(HELP_THREADS)
.where(HELP_THREADS.AUTHOR_ID.eq(user.getIdLong()))
.orderBy(HELP_THREADS.CREATED_AT.desc())
.limit(1)
.fetchOne())
.getChannelId();

String message =
"""
Sorry, you can only create a single help thread every %s %s. Please use your existing thread %s instead.
If you made a typo or similar, you can adjust the title using the command %s and the category with %s 👌""";

RestAction.allOf(changeCategory, changeTitle)
.map(commandMentions -> message.formatted(COOLDOWN_DURATION_VALUE,
COOLDOWN_DURATION_UNIT.name().toLowerCase(),
MessageUtils.mentionChannelById(lastThreadByAuthorId), commandMentions.get(0),
commandMentions.get(1)))
.flatMap(text -> event.reply(text).setEphemeral(true))
.queue();
}

private boolean handleIsValidTitle(CharSequence title, IReplyCallback event) {
Expand Down Expand Up @@ -173,4 +229,9 @@ private static void handleFailure(Throwable exception, InteractionHook eventHook

logger.error("Attempted to create a help thread, but failed", exception);
}

private static RestAction<String> mentionHelpChangeCommand(Guild guild, String subcommand) {
return MessageUtils.mentionGuildSlashCommand(guild, COMMAND_NAME, CHANGE_SUBCOMMAND_GROUP,
subcommand);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
public final class HelpThreadCommand extends SlashCommandAdapter {
private static final int COOLDOWN_DURATION_VALUE = 30;
private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.MINUTES;
private static final String CHANGE_CATEGORY_SUBCOMMAND = "category";
public static final String CHANGE_CATEGORY_SUBCOMMAND = "category";
private static final String CHANGE_CATEGORY_OPTION = "category";
public static final String CHANGE_TITLE_SUBCOMMAND = "title";
private static final String CHANGE_TITLE_OPTION = "title";
private static final String CHANGE_TITLE_SUBCOMMAND = "title";
public static final String CHANGE_SUBCOMMAND_GROUP = "change";
public static final String COMMAND_NAME = "help-thread";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,16 @@ public static String abbreviate(String text, int maxLength) {
return text.substring(0, maxLength - ABBREVIATION.length()) + ABBREVIATION;
}

/**
* Mentions a guild channel by its id. If the given channelId is unknown the formatted text will
* say `#deleted-channel` in discord.
*
* @param channelId the ID of the channel to mention
* @return the channel as formatted string which Discord interprets as clickable mention
*/
public static String mentionChannelById(long channelId) {
// Clone of JDAs Channel#getAsMention, but unfortunately channel instances can not be
// created out of just an ID, unlike User#fromId
return "<#%d>".formatted(channelId);
}
}