diff --git a/TOS.md b/TOS.md index a4c82bc60b..19b3acd584 100644 --- a/TOS.md +++ b/TOS.md @@ -13,7 +13,7 @@ By inviting **TJ-Bot** and using its features (accessible via [Discord](https:// You acknowledge that you must only invite the **bot** to a Server owned by **Together Java**, and only with their explicit approval. -Further, you have the priviledge to build and host the **bot** based on the [source](https://github.com/Together-Java/TJ-Bot) yourself. You may use such a self-hosted **bot** freely on any Discord Server (Server) you share with it, you can invite it to any Server that you have "Manage Server" rights for and you acknowledge that this priviledge might get revoked for you, if you're subject of breaking the terms and/or policy of this **bot**, or the Terms of Service, Privacy Policy and/or Community Guidelines of Discord Inc. +Further, you have the priviledge to build and host the **bot** based on the [source](https://github.com/Together-Java/TJ-Bot) yourself. You may use such a self-hosted **bot** freely on any Discord Server (Server) you share with it, you can invite it to any Server that you have "Manage Server" rights for, and you acknowledge that this priviledge might get revoked for you, if you're subject of breaking the terms and/or policy of this **bot**, or the Terms of Service, Privacy Policy and/or Community Guidelines of Discord Inc. Through Inviting, the **bot** may collect specific data as described in its [Privacy Policy](#your-privacy). The intended usage of this data is for core functionalities of the **bot** such as command handling. diff --git a/application/build.gradle b/application/build.gradle index b004d0fae6..75ede0d319 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -45,11 +45,13 @@ dependencies { implementation project(':database') implementation project(':utils') - implementation 'net.dv8tion:JDA:5.0.0-alpha.9' + implementation 'net.dv8tion:JDA:5.0.0-alpha.20' - implementation 'org.apache.logging.log4j:log4j-core:2.18.0' + implementation 'org.apache.logging.log4j:log4j-core:2.19.0' runtimeOnly 'org.apache.logging.log4j:log4j-slf4j18-impl:2.18.0' + implementation 'club.minnced:discord-webhooks:0.8.2' + implementation 'org.jooq:jooq:3.17.2' implementation 'io.mikael:urlbuilder:2.0.9' @@ -63,11 +65,11 @@ dependencies { implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.13.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' - implementation 'com.github.freva:ascii-table:1.6.0' + implementation 'com.github.freva:ascii-table:1.8.0' implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:4.8.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' diff --git a/application/config.json.template b/application/config.json.template index 51faf35082..d7a3563a38 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -83,5 +83,7 @@ "wsc", "wsf", "wsh" - ] + ], + "logInfoChannelWebhook": "", + "logErrorChannelWebhook": "" } diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index fed19cc192..2d9490bf64 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -3,6 +3,7 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.exceptions.InvalidTokenException; import net.dv8tion.jda.api.requests.GatewayIntent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,8 +12,9 @@ import org.togetherjava.tjbot.commands.system.BotCore; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.logging.LogMarkers; +import org.togetherjava.tjbot.logging.discord.DiscordLogging; -import javax.security.auth.login.LoginException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -57,6 +59,7 @@ public static void main(final String[] args) { Thread.setDefaultUncaughtExceptionHandler(Application::onUncaughtException); Runtime.getRuntime().addShutdownHook(new Thread(Application::onShutdown)); + DiscordLogging.startDiscordLogging(config); runBot(config); } @@ -79,19 +82,20 @@ public static void runBot(Config config) { Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath()); JDA jda = JDABuilder.createDefault(config.getToken()) - .enableIntents(GatewayIntent.GUILD_MEMBERS) + .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT) .build(); + jda.awaitReady(); + BotCore core = new BotCore(jda, database, config); + CommandReloading.reloadCommands(jda, core); + core.scheduleRoutines(jda); + jda.addEventListener(core); - jda.awaitReady(); - // We fire the event manually, since the core might be added too late to receive the - // actual event fired from JDA - core.onReady(jda); logger.info("Bot is ready"); - } catch (LoginException e) { - logger.error("Failed to login", e); + } catch (InvalidTokenException e) { + logger.error(LogMarkers.SENSITIVE, "Failed to login", e); } catch (InterruptedException e) { logger.error("Interrupted while waiting for setup to complete", e); Thread.currentThread().interrupt(); diff --git a/application/src/main/java/org/togetherjava/tjbot/CommandReloading.java b/application/src/main/java/org/togetherjava/tjbot/CommandReloading.java new file mode 100644 index 0000000000..54108595ae --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/CommandReloading.java @@ -0,0 +1,101 @@ +package org.togetherjava.tjbot; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction; +import org.jetbrains.annotations.Contract; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.BotCommand; +import org.togetherjava.tjbot.commands.CommandVisibility; +import org.togetherjava.tjbot.commands.system.CommandProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * Offers utility functions for reloading all commands. + */ +public class CommandReloading { + private static final Logger logger = LoggerFactory.getLogger(CommandReloading.class); + + /** + * According to Discord + * docs, there can be a maximum of 110 commands, 100 slash, and 5 context each. + *

+ * Because this bot most of the time operates in only 1 server, chances of exceeding this limit + * is low. + */ + public static final int MAX_COMMAND_COUNT = 110; + + private CommandReloading() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Reloads all commands based on the given {@link CommandProvider}. + * + * @param jda the JDA to update commands on + * @param commandProvider the {@link CommandProvider} to grab commands from + */ + public static void reloadCommands(final JDA jda, final CommandProvider commandProvider) { + logger.info("Reloading commands..."); + List actions = + Collections.synchronizedList(new ArrayList<>(MAX_COMMAND_COUNT)); + + // Reload global commands + actions.add(updateCommandsIf(command -> CommandVisibility.GLOBAL == command.getVisibility(), + getGlobalUpdateAction(jda), commandProvider)); + + // Reload guild commands (potentially many guilds) + // NOTE Storing the guild actions in a list is potentially dangerous since the + // bot might theoretically be part of so many guilds that it exceeds the max size of + // list. However, correctly reducing RestActions in a stream is not trivial. + getGuildUpdateActions(jda) + .map(updateAction -> updateCommandsIf( + command -> CommandVisibility.GUILD == command.getVisibility(), updateAction, + commandProvider)) + .forEach(actions::add); + logger.debug("Reloading commands over {} action-upstreams", actions.size()); + + // Send message when all are done + RestAction.allOf(actions).queue(a -> logger.debug("Commands successfully reloaded!")); + } + + /** + * Updates all commands given by the command provider which pass the given filter by pushing + * through the given action upstream. + * + * @param commandFilter filter that matches commands that should be uploaded + * @param updateAction the upstream to update commands + * @return the given upstream for chaining + */ + @Contract("_, _, _ -> param2") + private static CommandListUpdateAction updateCommandsIf( + final Predicate commandFilter, + final CommandListUpdateAction updateAction, final CommandProvider commandProvider) { + commandProvider.getInteractors() + .stream() + .filter(BotCommand.class::isInstance) + .map(BotCommand.class::cast) + .filter(commandFilter) + .map(BotCommand::getData) + .forEach(updateAction::addCommands); + + return updateAction; + } + + private static CommandListUpdateAction getGlobalUpdateAction(final JDA jda) { + return jda.updateCommands(); + } + + private static Stream getGuildUpdateActions(final JDA jda) { + return jda.getGuildCache().stream().map(Guild::updateCommands); + } + +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/BotCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/BotCommand.java new file mode 100644 index 0000000000..cc923af08f --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/BotCommand.java @@ -0,0 +1,60 @@ +package org.togetherjava.tjbot.commands; + +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; + +import java.util.List; + +/** + * Represents a Discord command. + *

+ * All commands have to implement this interface. For convenience, there is a + * {@link BotCommandAdapter} available that implemented most methods already. A new command can then + * be registered by adding it to {@link Features}. + *

+ * Commands can either be visible globally in Discord or just to specific guilds. Some + * configurations can be made via {@link CommandData}, which is then to be returned by + * {@link #getData()} where the system will then pick it up from. + *

+ * After registration, the system will notify a command whenever one of its corresponding command + * method, buttons ({@link #onButtonClick(ButtonInteractionEvent, List)}) or menus + * ({@link #onSelectionMenu(SelectMenuInteractionEvent, List)}) have been triggered. + *

+ * Some example commands are available in {@link org.togetherjava.tjbot.commands.basic}. + */ +public interface BotCommand extends UserInteractor { + + /** + * Gets the type of this command. + *

+ * After registration of the command, the type must not change anymore. + * + * @return the type of the command + */ + Command.Type getType(); + + /** + * Gets the visibility of this command. + *

+ * After registration of the command, the visibility must not change anymore. + * + * @return the visibility of the command + */ + CommandVisibility getVisibility(); + + /** + * Gets the command data belonging to this command. + *

+ * See {@link net.dv8tion.jda.api.interactions.commands.build.Commands Commands} for details on + * how to create and configure instances of it. + *

+ * This method may be called multiple times, implementations must not create new data each time + * but instead configure it once beforehand. The core system will automatically call this method + * to register the command to Discord. + * + * @return the command data of this command + */ + CommandData getData(); +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/BotCommandAdapter.java b/application/src/main/java/org/togetherjava/tjbot/commands/BotCommandAdapter.java new file mode 100644 index 0000000000..3a5e5af1f3 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/BotCommandAdapter.java @@ -0,0 +1,137 @@ +package org.togetherjava.tjbot.commands; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import org.jetbrains.annotations.Contract; +import org.togetherjava.tjbot.commands.componentids.ComponentId; +import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; +import org.togetherjava.tjbot.commands.componentids.Lifespan; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Adapter implementation of a {@link BotCommand}. The minimal setup only requires implementation of + * their respective command method. A new command can then be registered by adding it to + * {@link Features}. + *

+ * Further, {@link #onButtonClick(ButtonInteractionEvent, List)} and + * {@link #onSelectionMenu(SelectMenuInteractionEvent, List)} can be overridden if desired. The + * default implementation is empty, the adapter will not react to such events. + *

+ *

+ * The adapter manages some getters for you, you've to create the {@link CommandData} yourself. See + * {@link #BotCommandAdapter(CommandData, CommandVisibility)}} for more info on that. Minimal + * modifications can be done on the {@link CommandData} returned by {@link #getData()}. + *

+ *

+ * If implementations want to add buttons or selection menus, it is highly advised to use component + * IDs generated by {@link #generateComponentId(String...)}, which will automatically create IDs + * that are valid per {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)}. + *

+ *

+ * Some example commands are available in {@link org.togetherjava.tjbot.commands.basic}. + * Registration of commands can be done in {@link Features}. + */ +public abstract class BotCommandAdapter implements BotCommand { + private final String name; + private final Command.Type type; + private final CommandVisibility visibility; + private final CommandData data; + private ComponentIdGenerator componentIdGenerator; + + /** + * Creates a new adapter with the given data. + * + * @param data the data for this command + * @param visibility the visibility of the command + */ + protected BotCommandAdapter(CommandData data, CommandVisibility visibility) { + this.data = data.setGuildOnly(visibility == CommandVisibility.GUILD); + this.visibility = Objects.requireNonNull(visibility, "The visibility shouldn't be null"); + + this.name = data.getName(); + this.type = data.getType(); + } + + @Override + public final String getName() { + return name; + } + + @Override + public Command.Type getType() { + return type; + } + + @Override + public final CommandVisibility getVisibility() { + return visibility; + } + + @Override + public CommandData getData() { + return data; + } + + @Override + @Contract(mutates = "this") + public final void acceptComponentIdGenerator(ComponentIdGenerator generator) { + componentIdGenerator = + Objects.requireNonNull(generator, "The given generator cannot be null"); + } + + @SuppressWarnings("NoopMethodInAbstractClass") + @Override + public void onButtonClick(ButtonInteractionEvent event, List args) { + // Adapter does not react by default, subclasses may change this behavior + } + + @SuppressWarnings("NoopMethodInAbstractClass") + @Override + public void onSelectionMenu(SelectMenuInteractionEvent event, List args) { + // Adapter does not react by default, subclasses may change this behavior + } + + /** + * Helper method to generate component IDs that are considered valid per + * {@link #acceptComponentIdGenerator(ComponentIdGenerator)}. + *

+ * They can be used to create buttons or selection menus and transport additional data + * throughout the event (e.g. the user id who created the button dialog). + *

+ * IDs generated by this method have a regular lifespan, meaning that they might get evicted and + * expire after not being used for a long time. Use + * {@link #generateComponentId(Lifespan, String...)} to set other lifespans, if desired. + * + * @param args the extra arguments that should be part of the ID + * @return the generated component ID + */ + @SuppressWarnings("OverloadedVarargsMethod") + protected final String generateComponentId(String... args) { + return generateComponentId(Lifespan.REGULAR, args); + } + + /** + * Helper method to generate component IDs that are considered valid per + * {@link #acceptComponentIdGenerator(ComponentIdGenerator)}. + *

+ * They can be used to create buttons or selection menus and transport additional data + * throughout the event (e.g. the user id who created the button dialog). + * + * @param lifespan the lifespan of the component id, controls when an id that was not used for a + * long time might be evicted and expire + * @param args the extra arguments that should be part of the ID + * @return the generated component ID + */ + @SuppressWarnings({"OverloadedVarargsMethod", "WeakerAccess"}) + protected final String generateComponentId(Lifespan lifespan, String... args) { + return componentIdGenerator + .generate(new ComponentId(UserInteractorPrefix.getPrefixedNameFromInstance(this), + Arrays.asList(args)), lifespan); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandVisibility.java b/application/src/main/java/org/togetherjava/tjbot/commands/CommandVisibility.java similarity index 89% rename from application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandVisibility.java rename to application/src/main/java/org/togetherjava/tjbot/commands/CommandVisibility.java index e0d184c764..f8a2284590 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandVisibility.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/CommandVisibility.java @@ -3,7 +3,7 @@ /** * Visibility of a slash command, i.e. in which context it can be used by users. */ -public enum SlashCommandVisibility { +public enum CommandVisibility { /** * The command can be used within the context of a guild. */ diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java index 7b71f93443..f05b626572 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -97,6 +97,10 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new RejoinModerationRoleListener(actionsStore, config)); features.add(new OnGuildLeaveCloseThreadListener(database)); + // Message context commands + + // User context commands + // Slash commands features.add(new LogLevelCommand()); features.add(new PingCommand()); @@ -121,9 +125,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new WhoIsCommand()); features.add(new WolframAlphaCommand(config)); features.add(new AskCommand(config, helpSystemHelper)); - features.add(new CloseCommand()); - features.add(new ChangeHelpCategoryCommand(config, helpSystemHelper)); - features.add(new ChangeHelpTitleCommand(helpSystemHelper)); + features.add(new HelpThreadCommand(config, helpSystemHelper)); // Mixtures features.add(new HelpThreadOverviewUpdater(config, helpSystemHelper)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/MessageContextCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/MessageContextCommand.java new file mode 100644 index 0000000000..912f264450 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/MessageContextCommand.java @@ -0,0 +1,61 @@ +package org.togetherjava.tjbot.commands; + +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.components.ComponentInteraction; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; +import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; + +import java.util.List; + +/** + * A message context-command is a command, accessible when right-clicking on a message. + * + *

+ * Represents a Discord message context-command. Mostly decorating + * {@link net.dv8tion.jda.api.interactions.commands.Command}. + *

+ * All message context-commands have to implement this interface. For convenience, there is a + * {@link BotCommandAdapter} available that implemented most methods already. A new command can then + * be registered by adding it to {@link Features}. + *

+ *

+ * Context commands can either be visible globally in Discord or just to specific guilds. Minor + * adjustments can be made via {@link CommandData}, which is then to be returned by + * {@link #getData()} where the system will then pick it up from. + *

+ * After registration, the system will notify a command whenever one of its corresponding message + * context-commands ({@link #onMessageContext(MessageContextInteractionEvent)}), buttons + * ({@link #onButtonClick(ButtonInteractionEvent, List)}) or menus + * ({@link #onSelectionMenu(SelectMenuInteractionEvent, List)}) have been triggered. + *

+ *

+ * Some example commands are available in {@link org.togetherjava.tjbot.commands.basic}. + */ +public interface MessageContextCommand extends BotCommand { + + /** + * Triggered by the core system when a message context-command corresponding to this + * implementation (based on {@link #getData()}) has been triggered. + *

+ * This method may be called multithreaded. In particular, there are no guarantees that it will + * be executed on the same thread repeatedly or on the same thread that other event methods have + * been called on. + *

+ * Details are available in the given event and the event also enables implementations to + * respond to it. + *

+ * Buttons or menus have to be created with a component ID (see + * {@link ComponentInteraction#getComponentId()}, + * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, String)}) + * in a very specific format, otherwise the core system will fail to identify the command that + * corresponded to the button or menu click event and is unable to route it back. + *

+ * See {@link #acceptComponentIdGenerator(ComponentIdGenerator)} for more info. + * + * @param event the event that triggered this + */ + void onMessageContext(MessageContextInteractionEvent event); +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java index 9618c7412f..ba72609bc2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java @@ -1,29 +1,32 @@ package org.togetherjava.tjbot.commands; -import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; import net.dv8tion.jda.api.interactions.commands.build.CommandData; import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; import net.dv8tion.jda.api.interactions.components.ComponentInteraction; import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; -import org.togetherjava.tjbot.commands.componentids.ComponentId; import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; -import org.togetherjava.tjbot.commands.componentids.Lifespan; import java.util.List; /** + * A slash-command is a command in Discord, with slash (/) as the prefix. These commands offer + * enhanced functionality and superior UX over text-commands. An example slash-command is the + * `/thread` command, this allows you to create threads using your keyboard. + * + *

* Represents a Discord slash-command. Mostly decorating - * {@link net.dv8tion.jda.api.interactions.commands.Command}. + * {@link net.dv8tion.jda.api.interactions.commands.Command Command}. *

* All slash commands have to implement this interface. For convenience, there is a * {@link SlashCommandAdapter} available that implemented most methods already. A new command can * then be registered by adding it to {@link Features}. *

- *

* Slash commands can either be visible globally in Discord or just to specific guilds. They can * have subcommands, options, menus and more. This can be configured via {@link CommandData}, which * is then to be returned by {@link #getData()} where the system will then pick it up from. @@ -33,32 +36,21 @@ * ({@link #onButtonClick(ButtonInteractionEvent, List)}) or menus * ({@link #onSelectionMenu(SelectMenuInteractionEvent, List)}) have been triggered. *

- *

* Some example commands are available in {@link org.togetherjava.tjbot.commands.basic}. */ -public interface SlashCommand extends UserInteractor { +public interface SlashCommand extends BotCommand { /** * Gets the description of the command. *

* Requirements for this are documented in {@link Commands#slash(String, String)}. *

- *

* After registration of the command, the description must not change anymore. * * @return the description of the command */ String getDescription(); - /** - * Gets the visibility of this command. - *

- * After registration of the command, the visibility must not change anymore. - * - * @return the visibility of the command - */ - SlashCommandVisibility getVisibility(); - /** * Gets the command data belonging to this command. *

@@ -67,7 +59,6 @@ public interface SlashCommand extends UserInteractor { *

* See {@link CommandData} for details on how to create and configure instances of it. *

- *

* This method may be called multiple times, implementations must not create new data each time * but instead configure it once beforehand. The core system will automatically call this method * to register the command to Discord. @@ -80,7 +71,7 @@ public interface SlashCommand extends UserInteractor { * Triggered by the core system when a slash command corresponding to this implementation (based * on {@link #getData()}) has been triggered. *

- * This method may be called multi-threaded. In particular, there are no guarantees that it will + * This method may be called multithreaded. In particular, there are no guarantees that it will * be executed on the same thread repeatedly or on the same thread that other event methods have * been called on. *

@@ -89,30 +80,38 @@ public interface SlashCommand extends UserInteractor { *

* Buttons or menus have to be created with a component ID (see * {@link ComponentInteraction#getComponentId()}, - * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, Emoji)}) + * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, String)}) * in a very specific format, otherwise the core system will fail to identify the command that * corresponded to the button or menu click event and is unable to route it back. *

- * The component ID has to be a UUID-string (see {@link java.util.UUID}), which is associated to - * a specific database entry, containing meta information about the command being executed. Such - * a database entry can be created and a UUID be obtained by using - * {@link ComponentIdGenerator#generate(ComponentId, Lifespan)}, as provided by the instance - * given to {@link #acceptComponentIdGenerator(ComponentIdGenerator)} during system setup. The - * required {@link ComponentId} instance accepts optional extra arguments, which, if provided, - * can be picked up during the corresponding event (see - * {@link #onButtonClick(ButtonInteractionEvent, List)}, - * {@link #onSelectionMenu(SelectMenuInteractionEvent, List)}). + * See {@link #acceptComponentIdGenerator(ComponentIdGenerator)} for more info on the ID's. + * + * @param event the event that triggered this + */ + void onSlashCommand(SlashCommandInteractionEvent event); + + /** + * Autocompletion is comparable, but not the same as slash-command choices. Choices allow you to + * set a static list of {@value OptionData#MAX_CHOICES} possible "choices" to the commmand. + * Autocomplete allows you to dynamically give the user a list of + * {@value OptionData#MAX_CHOICES} possible choices. These choices can be generated based on the + * input of the user, the functionality is comparable to Google's autocompletion when searching + * for something. The given choices are not enforced by Discord, the user can + * ignore auto-completion and send whatever they want. *

- * Alternatively, if {@link SlashCommandAdapter} has been extended, it also offers a handy - * {@link SlashCommandAdapter#generateComponentId(String...)} method to ease the flow. + * Triggered by the core system when a slash command's autocomplete corresponding to this + * implementation (based on {@link #getData()}) has been triggered. Don't forget to enable + * autocomplete using {@link OptionData#setAutoComplete(boolean)}! *

- * See Component-IDs on - * our Wiki for more details and examples of how to use component IDs. + * This method may be called multithreaded. In particular, there are no guarantees that it will + * be executed on the same thread repeatedly or on the same thread that other event methods have + * been called on. *

- * This method will be called in a multi-threaded context and the event may not be hold valid - * forever. + * Details are available in the given event and the event also enables implementations to + * respond to it.
+ * See {@link #acceptComponentIdGenerator(ComponentIdGenerator)} for more info on the ID's. * * @param event the event that triggered this */ - void onSlashCommand(SlashCommandInteractionEvent event); + void onAutoComplete(CommandAutoCompleteInteractionEvent event); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java index cc071fd767..9923a15ea4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java @@ -1,5 +1,6 @@ package org.togetherjava.tjbot.commands; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; @@ -11,13 +12,8 @@ import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; import org.jetbrains.annotations.Range; import org.jetbrains.annotations.Unmodifiable; -import org.togetherjava.tjbot.commands.componentids.ComponentId; -import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; -import org.togetherjava.tjbot.commands.componentids.Lifespan; -import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.function.Function; import java.util.stream.IntStream; @@ -30,18 +26,15 @@ * {@link #onSelectionMenu(SelectMenuInteractionEvent, List)} can be overridden if desired. The * default implementation is empty, the adapter will not react to such events. *

- *

* The adapter manages all command related data itself, which can be provided during construction - * (see {@link #SlashCommandAdapter(String, String, SlashCommandVisibility)}). In order to add - * options, subcommands or similar command configurations, use {@link #getData()} and mutate the - * returned data object (see {@link CommandData} for details on how to work with this class). - *

+ * (see {@link #SlashCommandAdapter(String, String, CommandVisibility)}). In order to add options, + * subcommands or similar command configurations, use {@link #getData()} and mutate the returned + * data object (see {@link CommandData} for details on how to work with this class). *

* If implementations want to add buttons or selection menus, it is highly advised to use component * IDs generated by {@link #generateComponentId(String...)}, which will automatically create IDs * that are valid per {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)}. *

- *

* Some example commands are available in {@link org.togetherjava.tjbot.commands.basic}. A minimal * setup would consist of a class like * @@ -63,12 +56,9 @@ *

* and registration of an instance of that class in {@link Features}. */ -public abstract class SlashCommandAdapter implements SlashCommand { - private final String name; +public abstract class SlashCommandAdapter extends BotCommandAdapter implements SlashCommand { private final String description; - private final SlashCommandVisibility visibility; private final SlashCommandData data; - private ComponentIdGenerator componentIdGenerator; /** * Creates a new adapter with the given data. @@ -79,18 +69,11 @@ public abstract class SlashCommandAdapter implements SlashCommand { * {@link SlashCommandData#setDescription(String)} * @param visibility the visibility of the command */ - protected SlashCommandAdapter(String name, String description, - SlashCommandVisibility visibility) { - this.name = name; + protected SlashCommandAdapter(String name, String description, CommandVisibility visibility) { + super(Commands.slash(name, description), visibility); this.description = description; - this.visibility = visibility; - - data = Commands.slash(name, description); - } - @Override - public final String getName() { - return name; + this.data = (SlashCommandData) super.getData(); } @Override @@ -98,70 +81,17 @@ public final String getDescription() { return description; } - @Override - public final SlashCommandVisibility getVisibility() { - return visibility; - } - @Override public final SlashCommandData getData() { return data; } - @Override - public final void acceptComponentIdGenerator(ComponentIdGenerator generator) { - componentIdGenerator = generator; - } - - @SuppressWarnings("NoopMethodInAbstractClass") - @Override - public void onButtonClick(ButtonInteractionEvent event, List args) { - // Adapter does not react by default, subclasses may change this behavior - } - @SuppressWarnings("NoopMethodInAbstractClass") @Override - public void onSelectionMenu(SelectMenuInteractionEvent event, List args) { + public void onAutoComplete(CommandAutoCompleteInteractionEvent event) { // Adapter does not react by default, subclasses may change this behavior } - /** - * Helper method to generate component IDs that are considered valid per - * {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)}. - *

- * They can be used to create buttons or selection menus and transport additional data - * throughout the event (e.g. the user id who created the button dialog). - *

- * IDs generated by this method have a regular lifespan, meaning that they might get evicted and - * expire after not being used for a long time. Use - * {@link #generateComponentId(Lifespan, String...)} to set other lifespans, if desired. - * - * @param args the extra arguments that should be part of the ID - * @return the generated component ID - */ - @SuppressWarnings("OverloadedVarargsMethod") - protected final String generateComponentId(String... args) { - return generateComponentId(Lifespan.REGULAR, args); - } - - /** - * Helper method to generate component IDs that are considered valid per - * {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)}. - *

- * They can be used to create buttons or selection menus and transport additional data - * throughout the event (e.g. the user id who created the button dialog). - * - * @param lifespan the lifespan of the component id, controls when an id that was not used for a - * long time might be evicted and expire - * @param args the extra arguments that should be part of the ID - * @return the generated component ID - */ - @SuppressWarnings({"OverloadedVarargsMethod", "WeakerAccess"}) - protected final String generateComponentId(Lifespan lifespan, String... args) { - return Objects.requireNonNull(componentIdGenerator) - .generate(new ComponentId(getName(), Arrays.asList(args)), lifespan); - } - /** * Copies the given option multiple times. *

diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/UserContextCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/UserContextCommand.java new file mode 100644 index 0000000000..cb1cab4127 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/UserContextCommand.java @@ -0,0 +1,62 @@ +package org.togetherjava.tjbot.commands; + +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.components.ComponentInteraction; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; +import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; + +import java.util.List; + +/** + * A user context-command is a command, accessible when right-clicking on a member in a guild. These + * commands aren't accessible within DM's. + * + *

+ * Represents a Discord user context-command. Mostly decorating + * {@link net.dv8tion.jda.api.interactions.commands.Command}. + *

+ * All user context-commands have to implement this interface. For convenience, there is a + * {@link BotCommandAdapter} available that implemented most methods already. A new command can then + * be registered by adding it to {@link Features}. + *

+ *

+ * Context commands can either be visible globally in Discord or just to specific guilds. Minor + * adjustments can be made via {@link CommandData}, which is then to be returned by + * {@link #getData()} where the system will then pick it up from. + *

+ * After registration, the system will notify a command whenever one of its corresponding user + * context-commands ({@link #onUserContext(UserContextInteractionEvent)}), buttons + * ({@link #onButtonClick(ButtonInteractionEvent, List)}) or menus + * ({@link #onSelectionMenu(SelectMenuInteractionEvent, List)}) have been triggered. + *

+ *

+ * Some example commands are available in {@link org.togetherjava.tjbot.commands.basic}. + */ +public interface UserContextCommand extends BotCommand { + + /** + * Triggered by the core system when a user context-command corresponding to this implementation + * (based on {@link #getData()}) has been triggered. + *

+ * This method may be called multithreaded. In particular, there are no guarantees that it will + * be executed on the same thread repeatedly or on the same thread that other event methods have + * been called on. + *

+ * Details are available in the given event and the event also enables implementations to + * respond to it. + *

+ * Buttons or menus have to be created with a component ID (see + * {@link ComponentInteraction#getComponentId()}, + * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, String)}) + * in a very specific format, otherwise the core system will fail to identify the command that + * corresponded to the button or menu click event and is unable to route it back. + *

+ * See {@link #acceptComponentIdGenerator(ComponentIdGenerator)} for more info. + * + * @param event the event that triggered this + */ + void onUserContext(UserContextInteractionEvent event); +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractor.java b/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractor.java index b46cb8069b..65311be4bc 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractor.java @@ -3,24 +3,26 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import org.togetherjava.tjbot.commands.componentids.ComponentId; import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; +import org.togetherjava.tjbot.commands.componentids.Lifespan; import java.util.List; /** - * Represents a feature that can interact with users. The most important implementation is - * {@link SlashCommand}. + * Represents a feature that can interact with users. The most used implementation is + * {@link SlashCommand}, {@link UserContextCommand} and {@link MessageContextCommand}. *

- * An interactor must have a unique name and can react to button clicks and selection menu actions. + * An interactor can react to button clicks and selection menu actions. This is done based on the + * given {@link #getName()}, because of this names have to be unique. But, names can be complicated + * if their type is different, all the types can be seen in {@link UserInteractorPrefix} */ public interface UserInteractor extends Feature { /** * Gets the name of the interactor. *

- * Requirements for this are documented in - * {@link net.dv8tion.jda.api.interactions.commands.build.Commands#slash(String, String)}. - *

+ * You cannot start the name with any of the prefixes found in {@link UserInteractorPrefix} *

* After registration of the interactor, the name must not change anymore. * @@ -32,15 +34,12 @@ public interface UserInteractor extends Feature { * Triggered by the core system when a button corresponding to this implementation (based on * {@link #getName()}) has been clicked. *

- * This method may be called multi-threaded. In particular, there are no guarantees that it will + * This method may be called multithreaded. In particular, there are no guarantees that it will * be executed on the same thread repeatedly or on the same thread that other event methods have * been called on. *

* Details are available in the given event and the event also enables implementations to * respond to it. - *

- * This method will be called in a multi-threaded context and the event may not be hold valid - * forever. * * @param event the event that triggered this * @param args the arguments transported with the button, see @@ -53,15 +52,12 @@ public interface UserInteractor extends Feature { * Triggered by the core system when a selection menu corresponding to this implementation * (based on {@link #getName()}) has been clicked. *

- * This method may be called multi-threaded. In particular, there are no guarantees that it will + * This method may be called multithreaded. In particular, there are no guarantees that it will * be executed on the same thread repeatedly or on the same thread that other event methods have * been called on. *

* Details are available in the given event and the event also enables implementations to * respond to it. - *

- * This method will be called in a multi-threaded context and the event may not be hold valid - * forever. * * @param event the event that triggered this * @param args the arguments transported with the selection menu, see @@ -71,11 +67,25 @@ public interface UserInteractor extends Feature { void onSelectionMenu(SelectMenuInteractionEvent event, List args); /** - * Triggered by the core system during its setup phase. It will provide the interactor a - * component id generator through this method, which can be used to generate component ids, as - * used for button or selection menus. See - * {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)} for details on how to use - * this. + * Triggered by the core system during its setup phase. It will provide the command a component + * id generator through this method, which can be used to generate component ids, as used for + * button or selection menus. + * + *

+ * The component ID has to be a UUID-string (see {@link java.util.UUID}), which is associated to + * a specific database entry, containing meta information about the command being executed. Such + * a database entry can be created and a UUID be obtained by using + * {@link ComponentIdGenerator#generate(ComponentId, Lifespan)}, as provided by the instance + * given to this method during system setup. The required {@link ComponentId} instance accepts + * optional extra arguments, which, if provided, can be picked up during the corresponding event + * (see {@link #onButtonClick(ButtonInteractionEvent, List)}, + * {@link #onSelectionMenu(SelectMenuInteractionEvent, List)}). + *

+ * Alternatively, if {@link BotCommandAdapter} has been extended, it also offers a handy + * {@link BotCommandAdapter#generateComponentId(String...)} method to ease the flow. + *

+ * See Component-IDs on + * our Wiki for more details and examples of how to use component IDs. * * @param generator the provided component id generator */ diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractorPrefix.java b/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractorPrefix.java new file mode 100644 index 0000000000..53c76a3b5c --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractorPrefix.java @@ -0,0 +1,118 @@ +package org.togetherjava.tjbot.commands; + +import org.jetbrains.annotations.Contract; + +/** + * Contains the prefixes for the UserInteractor's. + *

+ * This is used for separate interactors with the same name, by command type (and possibly more in + * the future). Our system doesn't allow multiple interactors with the same name, while having a + * slash-command, and a message-context-command with the same name can be useful. + */ +public enum UserInteractorPrefix { + // Implementations that are none of the following have no dedicated prefix. + + /** + * Prefix for slash commands. + */ + SLASH_COMMAND(SlashCommand.class, "s-"), + /** + * Prefix for message context commands. + */ + MESSAGE_CONTEXT_COMMAND(MessageContextCommand.class, "mc-"), + /** + * Prefix for user context commands. + */ + USER_CONTEXT_COMMAND(UserContextCommand.class, "uc-"); + + private final Class classType; + private final String prefix; + + UserInteractorPrefix(Class classType, final String prefix) { + this.classType = classType; + this.prefix = prefix; + } + + /** + * The prefix for the command + * + * @return the command's prefix + */ + public String getPrefix() { + return prefix; + } + + /** + * Returns the name, attached with the prefix in front of it. + * + * @param name the name + * @return the name, with the prefix in front of it. + */ + public String getPrefixedName(String name) { + return prefix + name; + } + + /** + * The class type that should receive the prefix + * + * @return a {@link Class} instance of the type + */ + public Class getClassType() { + return classType; + } + + /** + * Checks what enum value the given instance collaborates to. + *

+ * This returns the name of the interactor, and adds the designated prefix to the name. As + * example, a slash-command with the name "help" becomes "s-help". + * + * @param instance an instance to type check for a prefix + * @param the type of the instance + * @return the interactor's name, with its prefix + */ + public static String getPrefixedNameFromInstance(final T instance) { + String name = instance.getName(); + + for (UserInteractorPrefix value : values()) { + Class valueClassType = value.getClassType(); + + if (valueClassType.isInstance(instance)) { + return value.getPrefix() + name; + } + } + + return name; + } + + /** + * Checks what enum value the given instance collaborates to. + *

+ * This combines the given name, with the interactor's prefix. As example, a slash-command with + * the name "help" becomes "s-help". + * + * @param clazz the class to get the prefix from + * @param name the name of the instance + * @param the type of the instance + * @return the prefixed {@link String} + */ + public static String getPrefixedNameFromClass(final Class clazz, + final String name) { + + for (UserInteractorPrefix value : values()) { + Class valueClassType = value.getClassType(); + + if (valueClassType.isAssignableFrom(clazz)) { + return value.getPrefix() + name; + } + } + + return name; + } + + @Override + @Contract(pure = true) + public String toString() { + return "UserInteractorPrefix{" + "prefix='" + prefix + '\'' + '}'; + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/PingCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/PingCommand.java index fdaba47f5b..fd5afa121a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/PingCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/PingCommand.java @@ -1,8 +1,8 @@ package org.togetherjava.tjbot.commands.basic; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.togetherjava.tjbot.commands.CommandVisibility; import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; /** * Implementation of an example command to illustrate how to respond to a user. @@ -14,7 +14,7 @@ public final class PingCommand extends SlashCommandAdapter { * Creates an instance of the ping pong command. */ public PingCommand() { - super("ping", "Bot responds with 'Pong!'", SlashCommandVisibility.GUILD); + super("ping", "Bot responds with 'Pong!'", CommandVisibility.GUILD); } /** diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/RoleSelectCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/RoleSelectCommand.java index 6b0e1376c2..05a372eca1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/RoleSelectCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/RoleSelectCommand.java @@ -3,6 +3,7 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; @@ -15,11 +16,12 @@ import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.CommandVisibility; import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.commands.componentids.Lifespan; -import java.awt.Color; +import java.awt.*; +import java.util.List; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -29,7 +31,7 @@ *

* The command works in two stages. First, a user sets up a role selection dialog by using the * command: - * + * *

  * {@code
  * /role-select
@@ -40,7 +42,7 @@
  *   selectable-role2: @Droid
  * }
  * 
- * + * * Afterwards, users can pick their roles in a menu, upon which the command adjusts their roles. */ public final class RoleSelectCommand extends SlashCommandAdapter { @@ -61,7 +63,7 @@ public final class RoleSelectCommand extends SlashCommandAdapter { public RoleSelectCommand() { super("role-select", "Creates a dialog that lets users pick roles, system roles are ignored when selected.", - SlashCommandVisibility.GUILD); + CommandVisibility.GUILD); OptionData roleOption = new OptionData(OptionType.ROLE, ROLE_OPTION, "pick roles that users will then be able to select", true); @@ -182,9 +184,8 @@ private void sendRoleSelectionMenu(final CommandInteraction event, .map(RoleSelectCommand::mapToSelectOption) .forEach(menu::addOptions); - OptionMapping titleOption = event.getOption(TITLE_OPTION); - String title = titleOption == null ? "Select your roles:" : titleOption.getAsString(); - + String title = + event.getOption(TITLE_OPTION, "Select your roles:", OptionMapping::getAsString); MessageEmbed embed = createEmbed(title, event.getOption(DESCRIPTION_OPTION).getAsString()); event.replyEmbeds(embed).addActionRow(menu.build()).queue(); @@ -195,7 +196,7 @@ private static SelectOption mapToSelectOption(Role role) { SelectOption option = SelectOption.of(role.getName(), role.getId()); if (null != roleIcon && roleIcon.isEmoji()) { - option = option.withEmoji((Emoji.fromUnicode(roleIcon.getEmoji()))); + option = option.withEmoji(Emoji.fromUnicode(roleIcon.getEmoji())); } return option; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java index f6bc7965b7..3ca76e4ae3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java @@ -1,8 +1,9 @@ package org.togetherjava.tjbot.commands.basic; -import net.dv8tion.jda.api.entities.Emote; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.requests.ErrorResponse; @@ -22,8 +23,8 @@ public final class SuggestionsUpDownVoter extends MessageReceiverAdapter { private static final Logger logger = LoggerFactory.getLogger(SuggestionsUpDownVoter.class); private static final int TITLE_MAX_LENGTH = 60; - private static final String FALLBACK_UP_VOTE = "πŸ‘"; - private static final String FALLBACK_DOWN_VOTE = "πŸ‘Ž"; + private static final Emoji FALLBACK_UP_VOTE = Emoji.fromUnicode("πŸ‘"); + private static final Emoji FALLBACK_DOWN_VOTE = Emoji.fromUnicode("πŸ‘Ž"); private final SuggestionsConfig config; @@ -68,13 +69,13 @@ private static void createThread(Message message) { message.createThreadChannel(title).queue(); } - private static void reactWith(String emoteName, String fallbackUnicodeEmote, Guild guild, + private static void reactWith(String emojiName, Emoji fallbackEmoji, Guild guild, Message message) { - getEmoteByName(emoteName, guild).map(message::addReaction).orElseGet(() -> { + getEmojiByName(emojiName, guild).map(message::addReaction).orElseGet(() -> { logger.warn( - "Unable to vote on a suggestion with the configured emote ('{}'), using fallback instead.", - emoteName); - return message.addReaction(fallbackUnicodeEmote); + "Unable to vote on a suggestion with the configured emoji ('{}'), using fallback instead.", + emojiName); + return message.addReaction(fallbackEmoji); }).queue(ignored -> { }, exception -> { if (exception instanceof ErrorResponseException responseException @@ -88,7 +89,7 @@ private static void reactWith(String emoteName, String fallbackUnicodeEmote, Gui }); } - private static Optional getEmoteByName(String name, Guild guild) { - return guild.getEmotesByName(name, false).stream().findAny(); + private static Optional getEmojiByName(String name, Guild guild) { + return guild.getEmojisByName(name, false).stream().findAny(); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java index 33f97035b4..033616ab34 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java @@ -4,7 +4,7 @@ import net.dv8tion.jda.api.entities.GuildVoiceState; import net.dv8tion.jda.api.entities.Invite; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.VoiceChannel; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.commands.OptionMapping; @@ -15,8 +15,8 @@ import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.CommandVisibility; import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; import javax.annotation.Nullable; import java.util.List; @@ -121,7 +121,7 @@ public final class VcActivityCommand extends SlashCommandAdapter { public VcActivityCommand() { super("vc-activity", "Starts a VC activity (you need to be in an voice channel to run this command)", - SlashCommandVisibility.GUILD); + CommandVisibility.GUILD); SubcommandData applicationSubCommand = new SubcommandData(APPLICATION_SUBCOMMAND, "Choose an application from our list") diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java index dd8035e8d0..0a746965d8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java @@ -1,6 +1,5 @@ package org.togetherjava.tjbot.commands.componentids; -import net.dv8tion.jda.api.entities.Emoji; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.components.ComponentInteraction; import net.dv8tion.jda.api.interactions.components.buttons.Button; @@ -26,7 +25,7 @@ public interface ComponentIdGenerator { * interactions, such as button or selection menus. *

* See {@link ComponentInteraction#getComponentId()} and - * {@link Button#of(ButtonStyle, String, Emoji)} for details on where the generated ID can be + * {@link Button#of(ButtonStyle, String, String)} for details on where the generated ID can be * used. * * @param componentId the component ID payload to persist and generate a valid ID for diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java index 4e17409c6e..83173f0474 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java @@ -1,6 +1,5 @@ package org.togetherjava.tjbot.commands.componentids; -import net.dv8tion.jda.api.entities.Emoji; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.components.ComponentInteraction; import net.dv8tion.jda.api.interactions.components.buttons.Button; @@ -28,7 +27,7 @@ public interface ComponentIdParser { * interactions, such as button or selection menus. *

* See {@link ComponentInteraction#getComponentId()} and - * {@link Button#of(ButtonStyle, String, Emoji)} for details on where the ID was originally + * {@link Button#of(ButtonStyle, String, String)} for details on where the ID was originally * transported with. * * @param uuid the UUID to parse which represents the component ID diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java index cbbf106f50..7883c280ae 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java @@ -12,6 +12,7 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.ComponentIds; import org.togetherjava.tjbot.db.generated.tables.records.ComponentIdsRecord; +import org.togetherjava.tjbot.logging.LogMarkers; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -323,8 +324,8 @@ private void logDebugSizeStatistics() { ComponentIds.COMPONENT_IDS.LIFESPAN.eq(lifespan.name()))))); int recordsCount = lifespanToCount.values().stream().mapToInt(Integer::intValue).sum(); - logger.debug("The component id store consists of {} records ({})", recordsCount, - lifespanToCount); + logger.debug(LogMarkers.SENSITIVE, "The component id store consists of {} records ({})", + recordsCount, lifespanToCount); } @Override diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java index c36b37730d..9e145b6b2e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java @@ -2,10 +2,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import net.dv8tion.jda.api.entities.ChannelType; import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.ThreadChannel; import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.interactions.components.buttons.Button; import org.slf4j.Logger; @@ -114,7 +114,8 @@ private void processAttachments(MessageReceivedEvent event, List> tasks = new ArrayList<>(); for (Message.Attachment attachment : attachments) { - CompletableFuture task = attachment.retrieveInputStream() + CompletableFuture task = attachment.getProxy() + .download() .thenApply(this::readAttachment) .thenAccept( content -> nameToFile.put(getNameOf(attachment), new GistFile(content))); @@ -217,7 +218,7 @@ private boolean isHelpThread(MessageReceivedEvent event) { return false; } - ThreadChannel thread = event.getThreadChannel(); + ThreadChannel thread = event.getChannel().asThreadChannel(); String rootChannelName = thread.getParentChannel().getName(); return isStagingChannelName.test(rootChannelName) || isOverviewChannelName.test(rootChannelName); 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 451ba9d4bf..59ce0f3fff 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 @@ -1,6 +1,11 @@ package org.togetherjava.tjbot.commands.help; -import net.dv8tion.jda.api.entities.*; +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 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; import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.interactions.InteractionHook; @@ -11,8 +16,8 @@ import net.dv8tion.jda.api.requests.RestAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.CommandVisibility; import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; import java.util.Optional; @@ -54,8 +59,7 @@ public final class AskCommand extends SlashCommandAdapter { * @param helper the helper to use */ public AskCommand(Config config, HelpSystemHelper helper) { - super("ask", "Ask a question - use this in the staging channel", - SlashCommandVisibility.GUILD); + super("ask", "Ask a question - use this in the staging channel", CommandVisibility.GUILD); OptionData title = new OptionData(OptionType.STRING, TITLE_OPTION, "short and to the point", true); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java index 2623c1b13f..5d20b179e1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java @@ -4,7 +4,7 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; -import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.Routine; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java index 0ebe47f21e..768341fad0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java @@ -2,9 +2,9 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.GuildMessageChannel; import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.slf4j.Logger; 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 deleted file mode 100644 index fd884591f8..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java +++ /dev/null @@ -1,128 +0,0 @@ -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; -import net.dv8tion.jda.api.entities.ThreadChannel; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import net.dv8tion.jda.api.interactions.InteractionHook; -import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import net.dv8tion.jda.api.requests.RestAction; -import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; -import org.togetherjava.tjbot.config.Config; - -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-category} command, which is able to change the category of a - * help thread. - *

- * This is either used for threads that do not have categories yet (as created by - * {@link ImplicitAskListener}), or simply to adjust the category afterwards. - *

- * Changing the category will invite all helpers interested into the corresponding category to the - * question thread. - */ -public final class ChangeHelpCategoryCommand extends SlashCommandAdapter { - private static final String CATEGORY_OPTION = "category"; - - private static final int COOLDOWN_DURATION_VALUE = 30; - private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.MINUTES; - - private final HelpSystemHelper helper; - private final Cache helpThreadIdToLastCategoryChange; - - /** - * Creates a new instance. - * - * @param config the config to use - * @param helper the helper to use - */ - public ChangeHelpCategoryCommand(Config config, HelpSystemHelper helper) { - super("change-help-category", "changes the category of a help thread", - SlashCommandVisibility.GUILD); - - OptionData category = new OptionData(OptionType.STRING, CATEGORY_OPTION, - "select what describes the question the best", true); - config.getHelpSystem() - .getCategories() - .forEach(categoryText -> category.addChoice(categoryText, categoryText)); - - getData().addOptions(category); - - helpThreadIdToLastCategoryChange = Caffeine.newBuilder() - .maximumSize(1_000) - .expireAfterAccess(COOLDOWN_DURATION_VALUE, TimeUnit.of(COOLDOWN_DURATION_UNIT)) - .build(); - - this.helper = helper; - } - - @Override - public void onSlashCommand(SlashCommandInteractionEvent event) { - String category = event.getOption(CATEGORY_OPTION).getAsString(); - - 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; - } - helpThreadIdToLastCategoryChange.put(helpThread.getIdLong(), Instant.now()); - - event.deferReply().queue(); - - helper.renameChannelToCategory(helpThread, category) - .flatMap(any -> sendCategoryChangedMessage(helpThread.getGuild(), event.getHook(), - helpThread, category)) - .queue(); - } - - private RestAction sendCategoryChangedMessage(Guild guild, InteractionHook hook, - ThreadChannel helpThread, String category) { - String changedContent = "Changed the category to **%s**.".formatted(category); - var action = hook.editOriginal(changedContent); - - Optional helperRole = helper.handleFindRoleForCategory(category, guild); - if (helperRole.isEmpty()) { - return action; - } - - // We want to invite all members of a role, but without hard-pinging them. However, - // manually inviting them is cumbersome and can hit rate limits. - // Instead, we abuse the fact that a role-ping through an edit will not hard-ping users, - // but still invite them to a thread. - String headsUpPattern = "%splease have a look, thanks."; - String headsUpWithoutRole = headsUpPattern.formatted(""); - String headsUpWithRole = - headsUpPattern.formatted(helperRole.orElseThrow().getAsMention() + " "); - return action.flatMap(any -> helpThread.sendMessage(headsUpWithoutRole) - .flatMap(message -> message.editMessage(headsUpWithRole))); - } - - private boolean isHelpThreadOnCooldown(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/ChangeHelpTitleCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpTitleCommand.java deleted file mode 100644 index 8ea5288f25..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpTitleCommand.java +++ /dev/null @@ -1,108 +0,0 @@ -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.callbacks.IReplyCallback; -import net.dv8tion.jda.api.interactions.commands.OptionType; -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; - -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 /change-help-title} command, which is able to change the title of a help - * thread. - *

- * 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 helpThreadIdToLastTitleChange; - - /** - * Creates a new instance. - * - * @param helper the helper to use - */ - public ChangeHelpTitleCommand(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(SlashCommandInteractionEvent event) { - String title = event.getOption(TITLE_OPTION).getAsString(); - - if (!handleIsValidTitle(title, 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(ThreadChannel helpThread) { - return Optional - .ofNullable(helpThreadIdToLastTitleChange.getIfPresent(helpThread.getIdLong())) - .map(lastCategoryChange -> lastCategoryChange.plus(COOLDOWN_DURATION_VALUE, - COOLDOWN_DURATION_UNIT)) - .filter(Instant.now()::isBefore) - .isPresent(); - } - - private boolean handleIsValidTitle(CharSequence title, IReplyCallback event) { - if (HelpSystemHelper.isTitleValid(title)) { - return true; - } - - event.reply( - "Sorry, but the title 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; - } -} 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 deleted file mode 100644 index 3b0e348e7e..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java +++ /dev/null @@ -1,75 +0,0 @@ -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; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -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 /close} command to close question threads. - *

- * 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 = 30; - private static final ChronoUnit COOLDOWN_DURATION_UNIT = ChronoUnit.MINUTES; - - private final Cache helpThreadIdToLastClose; - - /** - * Creates a new instance. - */ - public CloseCommand() { - super("close", "Close this question thread", SlashCommandVisibility.GUILD); - - helpThreadIdToLastClose = Caffeine.newBuilder() - .maximumSize(1_000) - .expireAfterAccess(COOLDOWN_DURATION_VALUE, TimeUnit.of(COOLDOWN_DURATION_UNIT)) - .build(); - } - - @Override - public void onSlashCommand(SlashCommandInteractionEvent event) { - 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; - } - 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(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 23bf93705f..2831d23400 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 @@ -2,10 +2,17 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.MessageBuilder; import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.requests.RestAction; -import net.dv8tion.jda.api.requests.restaction.MessageAction; +import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; +import net.dv8tion.jda.api.utils.FileUpload; +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -108,11 +115,12 @@ RestAction sendExplanationMessage(MessageChannel threadChannel) { **provide details**, context, more code, examples and maybe some screenshots. \ With enough info, someone knows the answer for sure."""), HelpSystemHelper.embedWith( - "Don't forget to close your thread using the command **/close** when your question has been answered, thanks.")); + "Don't forget to close your thread using the command **/help-thread close** when your question has been answered, thanks.")); - MessageAction action = threadChannel.sendMessage(message); + MessageCreateAction action = threadChannel.sendMessage(message); if (useCodeSyntaxExampleImage) { - action = action.addFile(codeSyntaxExampleData, CODE_SYNTAX_EXAMPLE_PATH); + action = action + .addFiles(FileUpload.fromData(codeSyntaxExampleData, CODE_SYNTAX_EXAMPLE_PATH)); } return action.setEmbeds(embeds); } @@ -274,6 +282,13 @@ private void executeUncategorizedAdviceCheck(long threadChannelId, long authorId return new CompletedRestAction<>(jda, null); } + if (threadChannel.isArchived()) { + logger.debug( + "Channel for uncategorized advice check is archived already (thread {} by author {}).", + threadChannelId, authorId); + return new CompletedRestAction<>(jda, null); + } + Optional category = getCategoryOfChannel(threadChannel); if (category.isPresent()) { logger.debug( @@ -286,9 +301,11 @@ private void executeUncategorizedAdviceCheck(long threadChannelId, long authorId MessageEmbed embed = HelpSystemHelper.embedWith( """ Hey there πŸ‘‹ You have to select a category for your help thread, otherwise nobody can see your question. - Please use the `/change-help-category` slash-command and pick what fits best, thanks πŸ™‚ + Please use the `/help-thread change category` slash-command and pick what fits best, thanks πŸ™‚ """); - Message message = new MessageBuilder(author.getAsMention()).setEmbeds(embed).build(); + MessageCreateData message = new MessageCreateBuilder().setContent(author.getAsMention()) + .setEmbeds(embed) + .build(); return threadChannel.sendMessage(message); }).queue(); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java index 79553fcdee..75db5e8f1a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java @@ -1,7 +1,12 @@ package org.togetherjava.tjbot.commands.help; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.requests.RestAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java index 7185bf20a4..ef98c5f523 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java @@ -2,7 +2,11 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.utils.TimeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,7 +25,7 @@ public final class HelpThreadAutoArchiver implements Routine { private static final Logger logger = LoggerFactory.getLogger(HelpThreadAutoArchiver.class); private static final int SCHEDULE_MINUTES = 60; - private static final Duration ARCHIVE_AFTER_INACTIVITY_OF = Duration.ofHours(24); + private static final Duration ARCHIVE_AFTER_INACTIVITY_OF = Duration.ofHours(12); private final HelpSystemHelper helper; @@ -73,12 +77,12 @@ private void autoArchiveForThread(ThreadChannel threadChannel, Instant archiveAf if (shouldBeArchived(threadChannel, archiveAfterMoment)) { logger.debug("Auto archiving help thread {}", threadChannel.getId()); - MessageEmbed embed = new EmbedBuilder().setDescription( - """ - Closed the thread due to inactivity. + MessageEmbed embed = new EmbedBuilder().setDescription(""" + Closed the thread due to inactivity. - If your question was not resolved yet, feel free to create a new thread. \ - But try to improve the quality of your question to make it easier to help you πŸ‘""") + If your question was not resolved yet, feel free to just post a message \ + to reopen it, or create a new thread. But try to improve the quality of \ + your question to make it easier to help you πŸ‘""") .setColor(HelpSystemHelper.AMBIENT_COLOR) .build(); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadCommand.java new file mode 100644 index 0000000000..b37c503537 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadCommand.java @@ -0,0 +1,248 @@ +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.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.WebhookMessageEditAction; +import org.togetherjava.tjbot.commands.CommandVisibility; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.config.Config; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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 /help-thread} command, which are contains special command for help threads + * only + */ +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"; + private static final String CHANGE_CATEGORY_OPTION = "category"; + private static final String CHANGE_TITLE_OPTION = "title"; + private static final String CHANGE_TITLE_SUBCOMMAND = "title"; + + private final HelpSystemHelper helper; + private final Map nameToSubcommand; + private final Map> subcommandToCooldownCache; + private final Map> subcommandToEventHandler; + + /** + * Creates a new instance. + * + * @param config the config to use + * @param helper the helper to use + */ + public HelpThreadCommand(Config config, HelpSystemHelper helper) { + super("help-thread", "Help thread specific commands", CommandVisibility.GUILD); + + OptionData categoryChoices = + new OptionData(OptionType.STRING, CHANGE_CATEGORY_OPTION, "new category", true); + config.getHelpSystem() + .getCategories() + .forEach(categoryText -> categoryChoices.addChoice(categoryText, categoryText)); + + SubcommandData changeCategory = + Subcommand.CHANGE_CATEGORY.toSubcommandData().addOptions(categoryChoices); + + SubcommandData changeTitle = Subcommand.CHANGE_TITLE.toSubcommandData() + .addOption(OptionType.STRING, CHANGE_TITLE_OPTION, "new title", true); + + SubcommandGroupData changeCommands = + new SubcommandGroupData("change", "Change the details of this help thread") + .addSubcommands(changeCategory, changeTitle); + getData().addSubcommandGroups(changeCommands); + + getData().addSubcommands(Subcommand.CLOSE.toSubcommandData()); + + this.helper = helper; + + Supplier> createCooldownCache = () -> Caffeine.newBuilder() + .maximumSize(1_000) + .expireAfterAccess(COOLDOWN_DURATION_VALUE, TimeUnit.of(COOLDOWN_DURATION_UNIT)) + .build(); + nameToSubcommand = streamSubcommands() + .collect(Collectors.toMap(Subcommand::getCommandName, Function.identity())); + subcommandToCooldownCache = new EnumMap<>(streamSubcommands() + .filter(Subcommand::hasCooldown) + .collect(Collectors.toMap(Function.identity(), any -> createCooldownCache.get()))); + subcommandToEventHandler = new EnumMap<>( + Map.of(Subcommand.CHANGE_CATEGORY, this::changeCategory, Subcommand.CHANGE_TITLE, + this::changeTitle, Subcommand.CLOSE, this::closeThread)); + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + ThreadChannel helpThread = event.getChannel().asThreadChannel(); + + Subcommand invokedSubcommand = + Objects.requireNonNull(nameToSubcommand.get(event.getSubcommandName())); + + if (invokedSubcommand.hasCooldown() + && isHelpThreadOnCooldown(invokedSubcommand, helpThread)) { + sendCooldownMessage(event); + return; + } + + subcommandToEventHandler.get(invokedSubcommand).accept(event, helpThread); + } + + private boolean isHelpThreadOnCooldown(Subcommand subcommand, ThreadChannel helpThread) { + Cache helpThreadIdToLastAction = requireCooldownCache(subcommand); + return Optional.ofNullable(helpThreadIdToLastAction.getIfPresent(helpThread.getIdLong())) + .map(lastAction -> lastAction.plus(COOLDOWN_DURATION_VALUE, COOLDOWN_DURATION_UNIT)) + .filter(Instant.now()::isBefore) + .isPresent(); + } + + private Cache requireCooldownCache(Subcommand subcommand) { + if (!subcommand.hasCooldown()) { + throw new IllegalArgumentException( + "Must only be used with subcommands that do have cooldown, but " + subcommand + + " was given."); + } + + return subcommandToCooldownCache.get(subcommand); + } + + private void sendCooldownMessage(SlashCommandInteractionEvent event) { + 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(); + } + + private void changeCategory(SlashCommandInteractionEvent event, ThreadChannel helpThread) { + String category = event.getOption(CHANGE_CATEGORY_OPTION).getAsString(); + + event.deferReply().queue(); + refreshCooldownFor(Subcommand.CHANGE_CATEGORY, helpThread); + + helper.renameChannelToCategory(helpThread, category) + .flatMap(any -> sendCategoryChangedMessage(helpThread.getGuild(), event.getHook(), + helpThread, category)) + .queue(); + } + + private void refreshCooldownFor(Subcommand subcommand, ThreadChannel helpThread) { + Cache helpThreadIdToLastAction = requireCooldownCache(subcommand); + helpThreadIdToLastAction.put(helpThread.getIdLong(), Instant.now()); + } + + private RestAction sendCategoryChangedMessage(Guild guild, InteractionHook hook, + ThreadChannel helpThread, String category) { + String changedContent = "Changed the category to **%s**.".formatted(category); + WebhookMessageEditAction action = hook.editOriginal(changedContent); + + Optional helperRole = helper.handleFindRoleForCategory(category, guild); + if (helperRole.isEmpty()) { + return action; + } + + // We want to invite all members of a role, but without hard-pinging them. However, + // manually inviting them is cumbersome and can hit rate limits. + // Instead, we abuse the fact that a role-ping through an edit will not hard-ping users, + // but still invite them to a thread. + String headsUpPattern = "%s please have a look, thanks."; + String headsUpWithoutRole = headsUpPattern.formatted(""); + String headsUpWithRole = headsUpPattern.formatted(helperRole.orElseThrow().getAsMention()); + return action.flatMap(any -> helpThread.sendMessage(headsUpWithoutRole) + .flatMap(message -> message.editMessage(headsUpWithRole))); + } + + private void changeTitle(SlashCommandInteractionEvent event, ThreadChannel helpThread) { + String title = event.getOption(CHANGE_TITLE_OPTION).getAsString(); + + if (!HelpSystemHelper.isTitleValid(title)) { + event.reply( + "Sorry, but the title 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; + } + + refreshCooldownFor(Subcommand.CHANGE_TITLE, helpThread); + + helper.renameChannelToTitle(helpThread, title) + .flatMap(any -> event.reply("Changed the title to **%s**.".formatted(title))) + .queue(); + } + + private void closeThread(SlashCommandInteractionEvent event, ThreadChannel helpThread) { + refreshCooldownFor(Subcommand.CLOSE, helpThread); + + MessageEmbed embed = new EmbedBuilder().setDescription("Closed the thread.") + .setColor(HelpSystemHelper.AMBIENT_COLOR) + .build(); + + event.replyEmbeds(embed).flatMap(any -> helpThread.getManager().setArchived(true)).queue(); + } + + private static Stream streamSubcommands() { + return Arrays.stream(Subcommand.values()); + } + + enum Subcommand { + CHANGE_CATEGORY(CHANGE_CATEGORY_SUBCOMMAND, "Change the category of this help thread", + Cooldown.YES), + CHANGE_TITLE(CHANGE_TITLE_SUBCOMMAND, "Change the title of this help thread", Cooldown.YES), + CLOSE("close", "Close this help thread", Cooldown.YES); + + private final String commandName; + private final String description; + private final Cooldown cooldown; + + Subcommand(String commandName, String description, Cooldown cooldown) { + this.commandName = commandName; + this.description = description; + this.cooldown = cooldown; + } + + String getCommandName() { + return commandName; + } + + String getDescription() { + return description; + } + + boolean hasCooldown() { + return cooldown == Cooldown.YES; + } + + SubcommandData toSubcommandData() { + return new SubcommandData(commandName, description); + } + } + + enum Cooldown { + YES, + NO + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java index eb32a0e5ac..3337e18f6e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java @@ -1,10 +1,17 @@ package org.togetherjava.tjbot.commands.help; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.MessageBuilder; -import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageType; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.utils.messages.MessageEditBuilder; +import net.dv8tion.jda.api.utils.messages.MessageEditData; import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -112,7 +119,7 @@ private void updateOverview(TextChannel overviewChannel) { List activeThreads = helper.getActiveThreadsIn(overviewChannel); logger.debug("Found {} active questions", activeThreads.size()); - Message message = new MessageBuilder() + MessageEditData message = new MessageEditBuilder() .setContent(STATUS_TITLE + "\n\n" + createDescription(activeThreads)) .build(); @@ -165,7 +172,7 @@ private static boolean isStatusMessage(Message message) { } private RestAction sendUpdatedOverview(@Nullable Message statusMessage, - Message updatedStatusMessage, MessageChannel overviewChannel) { + MessageEditData updatedStatusMessage, MessageChannel overviewChannel) { logger.debug("Sending the updated question overview"); if (statusMessage == null) { int currentFailures = FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.incrementAndGet(); @@ -174,7 +181,8 @@ private RestAction sendUpdatedOverview(@Nullable Message statusMessage, "Failed to locate the question overview too often ({} times), sending a fresh message instead.", currentFailures); FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.set(0); - return overviewChannel.sendMessage(updatedStatusMessage); + return overviewChannel + .sendMessage(MessageCreateData.fromEditData(updatedStatusMessage)); } logger.info( 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 46086246cb..38289df454 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 @@ -3,13 +3,16 @@ 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.MessageBuilder; 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.message.MessageReceivedEvent; import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.RestAction; -import net.dv8tion.jda.api.requests.restaction.MessageAction; +import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; @@ -163,14 +166,11 @@ private RestAction handleEvent(ThreadChannel threadChannel, Message message, .flatMap(any -> notifyUser(threadChannel, message)) .flatMap(any -> message.delete()) .flatMap(any -> helper.sendExplanationMessage(threadChannel)) - .map(any -> { - helper.scheduleUncategorizedAdviceCheck(threadChannel.getIdLong(), - author.getIdLong()); - return null; - }); + .onSuccess(any -> helper.scheduleUncategorizedAdviceCheck(threadChannel.getIdLong(), + author.getIdLong())); } - private static MessageAction sendInitialMessage(ThreadChannel threadChannel, + private static MessageCreateAction sendInitialMessage(ThreadChannel threadChannel, Message originalMessage, String title) { String content = originalMessage.getContentRaw(); Member author = originalMessage.getMember(); @@ -181,17 +181,20 @@ private static MessageAction sendInitialMessage(ThreadChannel threadChannel, .setColor(HelpSystemHelper.AMBIENT_COLOR) .build(); - Message threadMessage = new MessageBuilder( - """ - %s has a question about '**%s**' and will send the details now. + MessageCreateData threadMessage = new MessageCreateBuilder() + .setContent( + """ + %s has a question about '**%s**' and will send the details now. - Please use `/change-help-category` to greatly increase the visibility of the question.""" - .formatted(author, title)).setEmbeds(embed).build(); + Please use `/help-thread change category` to greatly increase the visibility of the question.""" + .formatted(author, title)) + .setEmbeds(embed) + .build(); return threadChannel.sendMessage(threadMessage); } - private static MessageAction notifyUser(IMentionable threadChannel, Message message) { + private static MessageCreateAction notifyUser(IMentionable threadChannel, Message message) { return message.getChannel() .sendMessage( """ diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java index 3da99a69e7..0d68901753 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java @@ -2,7 +2,7 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.slf4j.Logger; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommand.java index c81e1f7be6..ead51436f8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommand.java @@ -6,13 +6,14 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; +import net.dv8tion.jda.api.utils.FileUpload; import org.scilab.forge.jlatexmath.ParseException; import org.scilab.forge.jlatexmath.TeXConstants; import org.scilab.forge.jlatexmath.TeXFormula; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.CommandVisibility; import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; import javax.imageio.ImageIO; import java.awt.Color; @@ -55,7 +56,7 @@ public final class TeXCommand extends SlashCommandAdapter { */ public TeXCommand() { super("tex", "Renders LaTeX, also supports inline $-regions like 'see this $\frac{x}{2}$'.", - SlashCommandVisibility.GUILD); + CommandVisibility.GUILD); getData().addOption(OptionType.STRING, LATEX_OPTION, "The latex which is rendered as an image", true); } @@ -110,7 +111,8 @@ private void sendImage(IDeferrableCallback event, String userID, Image image) throws IOException { ByteArrayOutputStream renderedTextImageStream = getRenderedTextImageStream(image); event.getHook() - .editOriginal(renderedTextImageStream.toByteArray(), "tex.png") + .editOriginalAttachments( + FileUpload.fromData(renderedTextImageStream.toByteArray(), "tex.png")) .setActionRow(Button.of(ButtonStyle.DANGER, generateComponentId(userID), "Delete")) .queue(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/package-info.java new file mode 100644 index 0000000000..27b8f73b65 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/package-info.java @@ -0,0 +1,10 @@ +/** + * This packages offers math commands. + */ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package org.togetherjava.tjbot.commands.mathcommands; + +import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaCommand.java index 8c650b60dc..fb549df9ce 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaCommand.java @@ -1,18 +1,18 @@ package org.togetherjava.tjbot.commands.mathcommands.wolframalpha; import io.mikael.urlbuilder.UrlBuilder; -import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.callbacks.IDeferrableCallback; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.requests.restaction.WebhookMessageUpdateAction; +import org.togetherjava.tjbot.commands.CommandVisibility; +import net.dv8tion.jda.api.utils.FileUpload; import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -40,7 +40,7 @@ public final class WolframAlphaCommand extends SlashCommandAdapter { */ public WolframAlphaCommand(Config config) { super("wolfram-alpha", "Renders mathematical queries using WolframAlpha", - SlashCommandVisibility.GUILD); + CommandVisibility.GUILD); getData().addOption(OptionType.STRING, QUERY_OPTION, "the query to send to WolframAlpha", true); appId = config.getWolframAlphaAppId(); @@ -74,13 +74,11 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { private static void sendResponse(WolframAlphaHandler.HandlerResponse response, IDeferrableCallback event) { - WebhookMessageUpdateAction action = - event.getHook().editOriginalEmbeds(response.embeds()); + List files = response.attachments() + .stream() + .map(attachment -> FileUpload.fromData(attachment.data(), attachment.name())) + .toList(); - for (WolframAlphaHandler.Attachment attachment : response.attachments()) { - action = action.addFile(attachment.data(), attachment.name()); - } - - action.queue(); + event.getHook().editOriginalEmbeds(response.embeds()).setFiles(files).queue(); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaHandler.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaHandler.java index 2d53992e68..749ddee1fe 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaHandler.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaHandler.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import io.mikael.urlbuilder.UrlBuilder; import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,18 +37,12 @@ final class WolframAlphaHandler { * Discord and do not provide a nice user experience anymore. */ private static final int MAX_IMAGE_HEIGHT_PX = 400; - /** - * Maximum amount of embeds Discord supports. - *

- * This should be replaced with a constant provided by JDA, once it does offer one. - */ - private static final int MAX_EMBEDS = 10; /** * Maximum amount of tiles to send. *

* One embed is used as initial description and summary. */ - private static final int MAX_TILES = MAX_EMBEDS - 1; + private static final int MAX_TILES = Message.MAX_EMBED_COUNT - 1; private final String query; private final String userApiQuery; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java index b2bce6613f..af29d1013f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java @@ -1,12 +1,13 @@ package org.togetherjava.tjbot.commands.mediaonly; import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.MessageBuilder; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.MessageType; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; import org.togetherjava.tjbot.config.Config; @@ -59,10 +60,10 @@ private RestAction dmUser(Message message) { .setColor(Color.ORANGE) .build(); - Message dmMessage = new MessageBuilder( + MessageCreateData dmMessage = new MessageCreateBuilder().setContent( "Hey there, you posted a message without media (image, video, link) in a media-only channel. Please see the description of the channel for details and then repost with media attached, thanks πŸ˜€") - .setEmbeds(originalMessageEmbed) - .build(); + .setEmbeds(originalMessageEmbed) + .build(); return message.getAuthor() .openPrivateChannel() diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/package-info.java new file mode 100644 index 0000000000..f4321f676a --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/package-info.java @@ -0,0 +1,10 @@ +/** + * This packages offers all the functionality for ensuring that certain content has media. + */ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package org.togetherjava.tjbot.commands.mediaonly; + +import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java index 983ff7aa24..f8e9f560bc 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java @@ -2,25 +2,30 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.MessageBuilder; -import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.TimeUtil; +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageEditBuilder; +import net.dv8tion.jda.api.utils.messages.MessageRequest; import net.dv8tion.jda.internal.requests.CompletedRestAction; +import org.togetherjava.tjbot.commands.CommandVisibility; import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; import javax.annotation.Nullable; import java.time.Instant; import java.time.ZoneOffset; import java.util.*; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -46,7 +51,7 @@ public final class AuditCommand extends SlashCommandAdapter { */ public AuditCommand(ModerationActionsStore actionsStore) { super(COMMAND_NAME, "Lists all moderation actions that have been taken against a user", - SlashCommandVisibility.GUILD); + CommandVisibility.GUILD); getData().addOption(OptionType.USER, TARGET_OPTION, "The user who to retrieve actions for", true); @@ -68,8 +73,10 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { return; } - auditUser(guild.getIdLong(), target.getIdLong(), event.getMember().getIdLong(), -1, - event.getJDA()).flatMap(event::reply).queue(); + auditUser(MessageCreateBuilder::new, guild.getIdLong(), target.getIdLong(), + event.getMember().getIdLong(), -1, event.getJDA()).map(MessageCreateBuilder::build) + .flatMap(event::reply) + .queue(); } private boolean handleChecks(Member bot, Member author, @Nullable Member target, @@ -86,7 +93,8 @@ private boolean handleChecks(Member bot, Member author, @Nullable Member target, * can contain {@link AuditCommand#MAX_PAGE_LENGTH} actions, {@code -1} encodes the last * page */ - private RestAction auditUser(long guildId, long targetId, long callerId, + private > RestAction auditUser( + Supplier messageBuilderSupplier, long guildId, long targetId, long callerId, int pageNumber, JDA jda) { List actions = actionsStore.getActionsByTargetAscending(guildId, targetId); List> groupedActions = groupActionsByPages(actions); @@ -103,8 +111,8 @@ private RestAction auditUser(long guildId, long targetId, long callerId .map(user -> createSummaryEmbed(user, actions)) .flatMap(auditEmbed -> attachEmbedFields(auditEmbed, groupedActions, pageNumberInLimits, totalPages, jda)) - .map(auditEmbed -> attachPageTurnButtons(auditEmbed, pageNumberInLimits, totalPages, - guildId, targetId, callerId)); + .map(auditEmbed -> attachPageTurnButtons(messageBuilderSupplier, auditEmbed, + pageNumberInLimits, totalPages, guildId, targetId, callerId)); } private List> groupActionsByPages(List actions) { @@ -197,20 +205,22 @@ private static String formatTime(Instant when) { return TimeUtil.getDateTimeString(when.atOffset(ZoneOffset.UTC)); } - private Message attachPageTurnButtons(EmbedBuilder auditEmbed, int pageNumber, int totalPages, - long guildId, long targetId, long callerId) { - var messageBuilder = new MessageBuilder(auditEmbed.build()); + private > R attachPageTurnButtons( + Supplier messageBuilderSupplier, EmbedBuilder auditEmbed, int pageNumber, + int totalPages, long guildId, long targetId, long callerId) { + var messageBuilder = messageBuilderSupplier.get(); + messageBuilder.setEmbeds(auditEmbed.build()); if (totalPages <= 1) { - return messageBuilder.build(); + return messageBuilder; } - ActionRow pageTurnButtons = + List