Skip to content
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 @@ -111,6 +111,7 @@ public enum Features {
features.add(new AskCommand(config, helpSystemHelper));
features.add(new CloseCommand(helpSystemHelper));
features.add(new ChangeHelpCategoryCommand(config, helpSystemHelper));
features.add(new ChangeHelpTitleCommand(helpSystemHelper));

// Mixtures
features.add(new HelpThreadOverviewUpdater(config, helpSystemHelper));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
import org.togetherjava.tjbot.config.Config;

import java.util.Optional;

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

Expand Down Expand Up @@ -83,8 +85,14 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
return;
}

TextChannel helpStagingChannel = event.getTextChannel();
helpStagingChannel.createThreadChannel("[%s] %s".formatted(category, title))
Optional<TextChannel> maybeOverviewChannel =
helper.handleRequireOverviewChannelForAsk(event.getGuild(), event.getChannel());
if (maybeOverviewChannel.isEmpty()) {
return;
}
TextChannel overviewChannel = maybeOverviewChannel.orElseThrow();

overviewChannel.createThreadChannel("[%s] %s".formatted(category, title))
.flatMap(threadChannel -> handleEvent(event, threadChannel, event.getMember(), title,
category))
.queue(any -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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

Expand All @@ -34,8 +35,8 @@
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 static final int COOLDOWN_DURATION_VALUE = 30;
private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.MINUTES;

private final HelpSystemHelper helper;
private final Cache<Long, Instant> helpThreadIdToLastCategoryChange;
Expand Down Expand Up @@ -82,8 +83,9 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {

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))
.reply("Please wait a bit, this command can only be used once per %d %s.".formatted(
COOLDOWN_DURATION_VALUE,
COOLDOWN_DURATION_UNIT.toString().toLowerCase(Locale.US)))
.setEphemeral(true)
.queue();
return;
Expand All @@ -92,7 +94,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {

event.deferReply().queue();

helper.renameChannelToCategoryTitle(helpThread, category)
helper.renameChannelToCategory(helpThread, category)
.flatMap(any -> sendCategoryChangedMessage(helpThread.getGuild(), event.getHook(),
helpThread, category))
.queue();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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.ThreadChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import org.jetbrains.annotations.NotNull;
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
import org.togetherjava.tjbot.commands.SlashCommandVisibility;

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

/**
* Implements the {@code /change-help-title} command, which is able to change the title of a help
* thread.
* <p>
* This is to adjust a bad title in hindsight, for example if it was automatically created by
* {@link ImplicitAskListener}.
*/
public final class ChangeHelpTitleCommand extends SlashCommandAdapter {
private static final String TITLE_OPTION = "title";

private static final int COOLDOWN_DURATION_VALUE = 30;
private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.MINUTES;

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

/**
* Creates a new instance.
*
* @param helper the helper to use
*/
public ChangeHelpTitleCommand(@NotNull HelpSystemHelper helper) {
super("change-help-title", "changes the title of a help thread",
SlashCommandVisibility.GUILD);

getData().addOption(OptionType.STRING, TITLE_OPTION, "short and to the point", true);

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

this.helper = helper;
}

@Override
public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
String title = event.getOption(TITLE_OPTION).getAsString();

if (!helper.handleIsHelpThread(event)) {
return;
}

ThreadChannel helpThread = event.getThreadChannel();
if (helpThread.isArchived()) {
event.reply("This thread is already closed.").setEphemeral(true).queue();
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.toString().toLowerCase(Locale.US)))
.setEphemeral(true)
.queue();
return;
}
helpThreadIdToLastTitleChange.put(helpThread.getIdLong(), Instant.now());

helper.renameChannelToTitle(helpThread, title)
.flatMap(any -> event.reply("Changed the title to **%s**.".formatted(title)))
.queue();
}

private boolean isHelpThreadOnCooldown(@NotNull ThreadChannel helpThread) {
return Optional
.ofNullable(helpThreadIdToLastTitleChange.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 @@ -12,6 +12,7 @@

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

Expand All @@ -22,8 +23,8 @@
* 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 static final int COOLDOWN_DURATION_VALUE = 30;
private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.MINUTES;

private final HelpSystemHelper helper;
private final Cache<Long, Instant> helpThreadIdToLastClose;
Expand Down Expand Up @@ -58,8 +59,9 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {

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))
.reply("Please wait a bit, this command can only be used once per %d %s.".formatted(
COOLDOWN_DURATION_VALUE,
COOLDOWN_DURATION_UNIT.toString().toLowerCase(Locale.US)))
.setEphemeral(true)
.queue();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public final class HelpSystemHelper {
private static final String CODE_SYNTAX_EXAMPLE_PATH = "codeSyntaxExample.png";
private static final String CATEGORY_GROUP = "category";
private static final String TITLE_GROUP = "title";
private static final Pattern EXTRACT_CATEGORY_TITLE_PATTERN =
Pattern.compile("(?:\\[(?<%s>.+)] )?(?<%s>.+)".formatted(CATEGORY_GROUP, TITLE_GROUP));
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;
Expand Down Expand Up @@ -110,7 +110,7 @@ boolean handleIsHelpThread(@NotNull IReplyCallback event) {
if (event.getChannelType() == ChannelType.GUILD_PUBLIC_THREAD) {
ThreadChannel thread = event.getThreadChannel();

if (isStagingChannelName.test(thread.getParentChannel().getName())) {
if (isOverviewChannelName.test(thread.getParentChannel().getName())) {
return true;
}
}
Expand All @@ -136,30 +136,24 @@ Optional<Role> handleFindRoleForCategory(@NotNull String category, @NotNull Guil

@NotNull
Optional<String> getCategoryOfChannel(@NotNull Channel channel) {
Matcher matcher = EXTRACT_CATEGORY_TITLE_PATTERN.matcher(channel.getName());
if (!matcher.find()) {
return Optional.empty();
}

return Optional.ofNullable(matcher.group(CATEGORY_GROUP));
return Optional.ofNullable(HelpThreadName.ofChannelName(channel.getName()).category);
}

@NotNull
RestAction<Void> renameChannelToCategoryTitle(@NotNull GuildChannel channel,
RestAction<Void> renameChannelToCategory(@NotNull GuildChannel channel,
@NotNull String category) {
String currentTitle = channel.getName();
Matcher matcher = EXTRACT_CATEGORY_TITLE_PATTERN.matcher(currentTitle);
HelpThreadName currentName = HelpThreadName.ofChannelName(channel.getName());
HelpThreadName changedName = new HelpThreadName(category, currentName.title);

if (!matcher.matches()) {
throw new AssertionError("Pattern must match any thread name");
}
boolean hasCategoryInTitle = matcher.group(CATEGORY_GROUP) != null;
String titleWithoutCategory =
hasCategoryInTitle ? matcher.group(TITLE_GROUP) : currentTitle;
return channel.getManager().setName(changedName.toChannelName());
}

String titleWithCategory = "[%s] %s".formatted(category, titleWithoutCategory);
@NotNull
RestAction<Void> renameChannelToTitle(@NotNull GuildChannel channel, @NotNull String title) {
HelpThreadName currentName = HelpThreadName.ofChannelName(channel.getName());
HelpThreadName changedName = new HelpThreadName(currentName.category, title);

return channel.getManager().setName(titleWithCategory);
return channel.getManager().setName(changedName.toChannelName());
}

boolean isOverviewChannelName(@NotNull String channelName) {
Expand All @@ -186,4 +180,46 @@ static boolean isTitleValid(@NotNull CharSequence title) {
return titleCompact.length() >= TITLE_COMPACT_LENGTH_MIN
&& titleCompact.length() <= TITLE_COMPACT_LENGTH_MAX;
}

@NotNull
Optional<TextChannel> handleRequireOverviewChannelForAsk(@NotNull Guild guild,
@NotNull MessageChannel respondTo) {
Predicate<String> isChannelName = this::isOverviewChannelName;
String channelPattern = this.getOverviewChannelPattern();

Optional<TextChannel> maybeChannel = guild.getTextChannelCache()
.stream()
.filter(channel -> isChannelName.test(channel.getName()))
.findAny();

if (maybeChannel.isEmpty()) {
logger.warn(
"Attempted to create a help thread, did not find the overview channel matching the configured pattern '{}' for guild '{}'",
channelPattern, guild.getName());

respondTo.sendMessage(
"Sorry, I was unable to locate the overview channel. The server seems wrongly configured, please contact a moderator.")
.queue();
return Optional.empty();
}

return maybeChannel;
}

private record HelpThreadName(@Nullable String category, @NotNull String title) {
static @NotNull HelpThreadName ofChannelName(@NotNull CharSequence channelName) {
Matcher matcher = EXTRACT_CATEGORY_TITLE_PATTERN.matcher(channelName);

if (!matcher.matches()) {
throw new AssertionError("Pattern must match any thread name");
}

return new HelpThreadName(matcher.group(CATEGORY_GROUP), matcher.group(TITLE_GROUP));
}

@NotNull
String toChannelName() {
return category == null ? title : "[%s] %s".formatted(category, title);
}
}
}
Loading