diff --git a/PP.md b/PP.md index a82deaac35..ae5cce3537 100644 --- a/PP.md +++ b/PP.md @@ -36,7 +36,7 @@ In certain circumstances, you have the following data protection rights: ## Usage of Data -**TJ-Bot** may use stored data, as defined below, to offer different features and services. No usage of data outside of the aformentioned cases will happen and the data is not shared with any third-party site or service. +**TJ-Bot** may use stored data, as defined below, to offer different features and services. No usage of data outside of the aforementioned cases will happen and the data is not shared with any third-party site or service. ### Databases @@ -55,8 +55,11 @@ For example, **TJ-Bot** may associate your `user_id` with a `message_id` and a ` **TJ-Bot** may further store data that you explicitly provided for **TJ-Bot** to offer its services. For example the reason of a moderative action when using its moderation commands. +Furthermore, upon utilization of our help service, `user_id`s and `channel_id`s are stored to track when/how many questions a user asks. The data may be stored for up to **30** days. + The stored data is not linked to any information that is personally identifiable. + No other personal information outside of the above mentioned one will be stored. In particular, **TJ-Bot** does not store the content of sent messages. ### Log Files diff --git a/README.md b/README.md index cebc069fa1..c6769bd4f8 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,24 @@ [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Together-Java_TJ-Bot&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=Together-Java_TJ-Bot) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Together-Java_TJ-Bot&metric=security_rating)](https://sonarcloud.io/dashboard?id=Together-Java_TJ-Bot) -TJ-Bot is a Discord Bot used on the [Together Java](https://discord.com/invite/XXFUXzK) server. It is maintained by the community, anyone can contribute. +TJ-Bot is a Discord Bot used on the [Together Java](https://discord.com/invite/XXFUXzK) server. It is maintained by the community, anyone can contribute! ![bot says hello](https://i.imgur.com/FE1MJTV.png) +The idea of this project is to provide learning experience on a real-world project. You will be provided with our guidance at every step of the way, and friendly but thorough reviews! + +Feel free to join our [discord server](https://discord.com/invite/XXFUXzK) if you have any questions, or require assistance with the project. :relaxed: + # Getting started -Please read [Contributing](https://github.com/Together-Java/TJ-Bot/wiki/Contributing) if you want to propose ideas and changes or even implement some yourself. +Please read [Contributing](https://github.com/Together-Java/TJ-Bot/wiki/Contributing) guidelines if you are considering helping us out! +There you will find a detailed guide on how to contribute, and plenty of resources in case this is your first time submitting a PR to an open-source project. Head over to the [Wiki](https://github.com/Together-Java/TJ-Bot/wiki) as general entry point to the project. It provides lots of tutorials, documentation and other information, for example -* creating a discord bot and a private server; -* setting up the project locally; -* adding your own custom commands; -* a technology overview; -* guidance about how to maintain the bot (e.g. VPS, logs, databases, restart). +* creating a discord bot and a private server +* setting up the project locally +* adding your own custom commands +* a technology overview # Download diff --git a/application/build.gradle b/application/build.gradle index b239730637..80b59cb2a8 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -39,6 +39,7 @@ shadowJar { } dependencies { + implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'org.jetbrains:annotations:23.0.0' implementation project(':database') @@ -61,7 +62,7 @@ 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.3.0' + implementation 'com.github.freva:ascii-table:1.4.0' implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1' diff --git a/application/config.json.template b/application/config.json.template index 6c433a2043..3a34d248fc 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -44,5 +44,6 @@ "Other" ], "categoryRoleSuffix": " - Helper" - } + }, + "mediaOnlyChannelPattern": "memes" } diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index 4dc8ec128b..fed19cc192 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -2,20 +2,17 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; -import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import org.jetbrains.annotations.NotNull; +import net.dv8tion.jda.api.requests.GatewayIntent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.togetherjava.tjbot.commands.Features; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.system.BotCore; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.commands.SlashCommandAdapter; import javax.security.auth.login.LoginException; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -27,8 +24,10 @@ * New commands can be created by implementing {@link SlashCommandInteractionEvent} or extending * {@link SlashCommandAdapter}. They can then be registered in {@link Features}. */ -public enum Application { - ; +public class Application { + private Application() { + throw new UnsupportedOperationException("Utility class, construction not supported"); + } private static final Logger logger = LoggerFactory.getLogger(Application.class); private static final String DEFAULT_CONFIG_PATH = "config.json"; @@ -112,8 +111,7 @@ private static void onShutdown() { logger.info("Bot has been stopped"); } - private static void onUncaughtException(@NotNull Thread failingThread, - @NotNull Throwable failure) { + private static void onUncaughtException(Thread failingThread, Throwable failure) { logger.error("Unknown error in thread {}.", failingThread.getName(), failure); } diff --git a/application/src/main/java/org/togetherjava/tjbot/BootstrapLauncher.java b/application/src/main/java/org/togetherjava/tjbot/BootstrapLauncher.java index 0be2bb353a..bd264ca51e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/BootstrapLauncher.java +++ b/application/src/main/java/org/togetherjava/tjbot/BootstrapLauncher.java @@ -4,8 +4,10 @@ * A bootstrap launcher with minimal dependencies that sets up needed parts and workarounds for the * main logic to take over. */ -public enum BootstrapLauncher { - ; +public class BootstrapLauncher { + private BootstrapLauncher() { + throw new UnsupportedOperationException("Utility class, construction not supported"); + } /** * Starts the main application. 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 81657ce752..0826682c08 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -1,12 +1,15 @@ package org.togetherjava.tjbot.commands; import net.dv8tion.jda.api.JDA; -import org.jetbrains.annotations.NotNull; -import org.togetherjava.tjbot.commands.basic.*; +import org.togetherjava.tjbot.commands.basic.PingCommand; +import org.togetherjava.tjbot.commands.basic.RoleSelectCommand; +import org.togetherjava.tjbot.commands.basic.SuggestionsUpDownVoter; +import org.togetherjava.tjbot.commands.basic.VcActivityCommand; import org.togetherjava.tjbot.commands.filesharing.FileSharingMessageListener; import org.togetherjava.tjbot.commands.help.*; import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; import org.togetherjava.tjbot.commands.mathcommands.wolframalpha.WolframAlphaCommand; +import org.togetherjava.tjbot.commands.mediaonly.MediaOnlyChannelListener; import org.togetherjava.tjbot.commands.moderation.*; import org.togetherjava.tjbot.commands.moderation.scam.ScamBlocker; import org.togetherjava.tjbot.commands.moderation.scam.ScamHistoryPurgeRoutine; @@ -28,6 +31,7 @@ import org.togetherjava.tjbot.moderation.ModAuditLogWriter; import org.togetherjava.tjbot.routines.ModAuditLogRoutine; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; @@ -39,8 +43,10 @@ * To add a new slash command, extend the commands returned by * {@link #createFeatures(JDA, Database, Config)}. */ -public enum Features { - ; +public class Features { + private Features() { + throw new UnsupportedOperationException("Utility class, construction not supported"); + } /** * Creates all features that should be registered with this application. @@ -53,13 +59,13 @@ public enum Features { * @param config the configuration features should use * @return a collection of all features */ - public static @NotNull Collection createFeatures(@NotNull JDA jda, - @NotNull Database database, @NotNull Config config) { + @Nonnull + public static Collection createFeatures(JDA jda, Database database, Config config) { TagSystem tagSystem = new TagSystem(database); ModerationActionsStore actionsStore = new ModerationActionsStore(database); ModAuditLogWriter modAuditLogWriter = new ModAuditLogWriter(config); ScamHistoryStore scamHistoryStore = new ScamHistoryStore(database); - HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config); + HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database); // NOTE The system can add special system relevant commands also by itself, // hence this list may not necessarily represent the full list of all commands actually @@ -73,45 +79,49 @@ public enum Features { features.add(new RemindRoutine(database)); features.add(new ScamHistoryPurgeRoutine(scamHistoryStore)); features.add(new BotMessageCleanup(config)); + features.add(new HelpThreadMetadataPurger(database)); features.add(new HelpThreadActivityUpdater(helpSystemHelper)); features .add(new AutoPruneHelperRoutine(config, helpSystemHelper, modAuditLogWriter, database)); + features.add(new HelpThreadAutoArchiver(helpSystemHelper)); // Message receivers features.add(new TopHelpersMessageListener(database, config)); features.add(new SuggestionsUpDownVoter(config)); features.add(new ScamBlocker(actionsStore, scamHistoryStore, config)); features.add(new ImplicitAskListener(config, helpSystemHelper)); + features.add(new MediaOnlyChannelListener(config)); features.add(new FileSharingMessageListener(config)); // Event receivers features.add(new RejoinModerationRoleListener(actionsStore, config)); + features.add(new OnGuildLeaveCloseThreadListener(database)); // Slash commands features.add(new LogLevelCommand()); features.add(new PingCommand()); features.add(new TeXCommand()); features.add(new TagCommand(tagSystem)); - features.add(new TagManageCommand(tagSystem, config, modAuditLogWriter)); + features.add(new TagManageCommand(tagSystem, modAuditLogWriter)); features.add(new TagsCommand(tagSystem)); features.add(new VcActivityCommand()); - features.add(new WarnCommand(actionsStore, config)); - features.add(new KickCommand(actionsStore, config)); - features.add(new BanCommand(actionsStore, config)); - features.add(new UnbanCommand(actionsStore, config)); - features.add(new AuditCommand(actionsStore, config)); + features.add(new WarnCommand(actionsStore)); + features.add(new KickCommand(actionsStore)); + features.add(new BanCommand(actionsStore)); + features.add(new UnbanCommand(actionsStore)); + features.add(new AuditCommand(actionsStore)); features.add(new MuteCommand(actionsStore, config)); features.add(new UnmuteCommand(actionsStore, config)); - features.add(new TopHelpersCommand(database, config)); + features.add(new TopHelpersCommand(database)); features.add(new RoleSelectCommand()); - features.add(new NoteCommand(actionsStore, config)); + features.add(new NoteCommand(actionsStore)); features.add(new RemindCommand(database)); features.add(new QuarantineCommand(actionsStore, config)); features.add(new UnquarantineCommand(actionsStore, config)); features.add(new WhoIsCommand()); features.add(new WolframAlphaCommand(config)); features.add(new AskCommand(config, helpSystemHelper)); - features.add(new CloseCommand(helpSystemHelper)); + features.add(new CloseCommand()); features.add(new ChangeHelpCategoryCommand(config, helpSystemHelper)); features.add(new ChangeHelpTitleCommand(helpSystemHelper)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiver.java b/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiver.java index ee679a47f0..c99e08cf98 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiver.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiver.java @@ -2,8 +2,8 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageUpdateEvent; -import org.jetbrains.annotations.NotNull; +import javax.annotation.Nonnull; import java.util.regex.Pattern; /** @@ -28,7 +28,7 @@ public interface MessageReceiver extends Feature { * * @return the pattern matching the names of relevant channels */ - @NotNull + @Nonnull Pattern getChannelNamePattern(); /** @@ -38,7 +38,7 @@ public interface MessageReceiver extends Feature { * @param event the event that triggered this, containing information about the corresponding * message that was sent and received */ - void onMessageReceived(@NotNull MessageReceivedEvent event); + void onMessageReceived(MessageReceivedEvent event); /** * Triggered by the core system whenever an existing message was edited in a text channel of a @@ -47,5 +47,5 @@ public interface MessageReceiver extends Feature { * @param event the event that triggered this, containing information about the corresponding * message that was edited */ - void onMessageUpdated(@NotNull MessageUpdateEvent event); + void onMessageUpdated(MessageUpdateEvent event); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiverAdapter.java b/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiverAdapter.java index 3d13b3b3e9..b2be566758 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiverAdapter.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiverAdapter.java @@ -2,8 +2,8 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageUpdateEvent; -import org.jetbrains.annotations.NotNull; +import javax.annotation.Nonnull; import java.util.regex.Pattern; /** @@ -24,24 +24,25 @@ public abstract class MessageReceiverAdapter implements MessageReceiver { * @param channelNamePattern the pattern matching names of channels interested in, only messages * from matching channels will be received */ - protected MessageReceiverAdapter(@NotNull Pattern channelNamePattern) { + protected MessageReceiverAdapter(Pattern channelNamePattern) { this.channelNamePattern = channelNamePattern; } @Override - public final @NotNull Pattern getChannelNamePattern() { + @Nonnull + public final Pattern getChannelNamePattern() { return channelNamePattern; } @SuppressWarnings("NoopMethodInAbstractClass") @Override - public void onMessageReceived(@NotNull MessageReceivedEvent event) { + public void onMessageReceived(MessageReceivedEvent event) { // Adapter does not react by default, subclasses may change this behavior } @SuppressWarnings("NoopMethodInAbstractClass") @Override - public void onMessageUpdated(@NotNull MessageUpdateEvent event) { + public void onMessageUpdated(MessageUpdateEvent event) { // Adapter does not react by default, subclasses may change this behavior } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Routine.java b/application/src/main/java/org/togetherjava/tjbot/commands/Routine.java index 10b8aa9d79..1d684bf6a7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Routine.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Routine.java @@ -1,8 +1,8 @@ package org.togetherjava.tjbot.commands; import net.dv8tion.jda.api.JDA; -import org.jetbrains.annotations.NotNull; +import javax.annotation.Nonnull; import java.util.concurrent.TimeUnit; /** @@ -24,7 +24,7 @@ public interface Routine extends Feature { * * @return the schedule of this routine */ - @NotNull + @Nonnull Schedule createSchedule(); /** @@ -32,7 +32,7 @@ public interface Routine extends Feature { * * @param jda the JDA instance the bot is operating with */ - void runRoutine(@NotNull JDA jda); + void runRoutine(JDA jda); /** * The schedule of routines. @@ -46,8 +46,7 @@ public interface Routine extends Feature { * @param unit the time unit for both, {@link #initialDuration} and {@link #duration}, e.g. * seconds */ - record Schedule(@NotNull ScheduleMode mode, long initialDuration, long duration, - @NotNull TimeUnit unit) { + record Schedule(ScheduleMode mode, long initialDuration, long duration, TimeUnit unit) { } 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 20f5c936b7..551e4474ec 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java @@ -9,11 +9,11 @@ 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.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.componentids.ComponentId; import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; import org.togetherjava.tjbot.commands.componentids.Lifespan; +import javax.annotation.Nonnull; import java.util.List; /** @@ -49,7 +49,7 @@ public interface SlashCommand extends UserInteractor { * * @return the description of the command */ - @NotNull + @Nonnull String getDescription(); /** @@ -59,7 +59,7 @@ public interface SlashCommand extends UserInteractor { * * @return the visibility of the command */ - @NotNull + @Nonnull SlashCommandVisibility getVisibility(); /** @@ -77,7 +77,7 @@ public interface SlashCommand extends UserInteractor { * * @return the command data of this command */ - @NotNull + @Nonnull SlashCommandData getData(); /** @@ -118,5 +118,5 @@ public interface SlashCommand extends UserInteractor { * * @param event the event that triggered this */ - void onSlashCommand(@NotNull SlashCommandInteractionEvent event); + void onSlashCommand(SlashCommandInteractionEvent 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 023c808a1d..d0d2ee94c4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java @@ -9,13 +9,13 @@ 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 org.jetbrains.annotations.NotNull; 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 javax.annotation.Nonnull; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -55,7 +55,7 @@ * } * * @Override - * public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + * public void onSlashCommand(SlashCommandInteractionEvent event) { * event.reply("Pong!").queue(); * } * } @@ -80,7 +80,7 @@ public abstract class SlashCommandAdapter implements SlashCommand { * {@link SlashCommandData#setDescription(String)} * @param visibility the visibility of the command */ - protected SlashCommandAdapter(@NotNull String name, @NotNull String description, + protected SlashCommandAdapter(String name, String description, SlashCommandVisibility visibility) { this.name = name; this.description = description; @@ -90,40 +90,43 @@ protected SlashCommandAdapter(@NotNull String name, @NotNull String description, } @Override - public final @NotNull String getName() { + @Nonnull + public final String getName() { return name; } @Override - public final @NotNull String getDescription() { + @Nonnull + public final String getDescription() { return description; } @Override - public final @NotNull SlashCommandVisibility getVisibility() { + @Nonnull + public final SlashCommandVisibility getVisibility() { return visibility; } @Override - public final @NotNull SlashCommandData getData() { + @Nonnull + public final SlashCommandData getData() { return data; } @Override - public final void acceptComponentIdGenerator(@NotNull ComponentIdGenerator generator) { + public final void acceptComponentIdGenerator(ComponentIdGenerator generator) { componentIdGenerator = generator; } @SuppressWarnings("NoopMethodInAbstractClass") @Override - public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List args) { + public void onButtonClick(ButtonInteractionEvent event, List args) { // Adapter does not react by default, subclasses may change this behavior } @SuppressWarnings("NoopMethodInAbstractClass") @Override - public void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, - @NotNull List args) { + public void onSelectionMenu(SelectMenuInteractionEvent event, List args) { // Adapter does not react by default, subclasses may change this behavior } @@ -142,7 +145,8 @@ public void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, * @return the generated component ID */ @SuppressWarnings("OverloadedVarargsMethod") - protected final @NotNull String generateComponentId(@NotNull String... args) { + @Nonnull + protected final String generateComponentId(String... args) { return generateComponentId(Lifespan.REGULAR, args); } @@ -159,8 +163,8 @@ public void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, * @return the generated component ID */ @SuppressWarnings({"OverloadedVarargsMethod", "WeakerAccess"}) - protected final @NotNull String generateComponentId(@NotNull Lifespan lifespan, - @NotNull String... args) { + @Nonnull + protected final String generateComponentId(Lifespan lifespan, String... args) { return Objects.requireNonNull(componentIdGenerator) .generate(new ComponentId(getName(), Arrays.asList(args)), lifespan); } @@ -190,8 +194,9 @@ public void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, * @return the generated list of options */ @Unmodifiable - protected static @NotNull List generateMultipleOptions( - @NotNull OptionData optionData, @Range(from = 1, to = 25) int amount) { + @Nonnull + protected static List generateMultipleOptions(OptionData optionData, + @Range(from = 1, to = 25) int amount) { String baseName = optionData.getName(); Function nameToOption = @@ -211,8 +216,9 @@ public void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, * @return all options with the given prefix */ @Unmodifiable - protected static @NotNull List getMultipleOptionsByNamePrefix( - @NotNull CommandInteractionPayload event, @NotNull String namePrefix) { + @Nonnull + protected static List getMultipleOptionsByNamePrefix( + CommandInteractionPayload event, String namePrefix) { return event.getOptions() .stream() .filter(option -> option.getName().startsWith(namePrefix)) 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 4ad0ed0da2..68a916369c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/UserInteractor.java @@ -3,9 +3,9 @@ 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.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator; +import javax.annotation.Nonnull; import java.util.List; /** @@ -27,7 +27,7 @@ public interface UserInteractor extends Feature { * * @return the name of the interactor */ - @NotNull + @Nonnull String getName(); /** @@ -49,7 +49,7 @@ public interface UserInteractor extends Feature { * {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)} for details on how * these are created */ - void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List args); + void onButtonClick(ButtonInteractionEvent event, List args); /** * Triggered by the core system when a selection menu corresponding to this implementation @@ -70,7 +70,7 @@ public interface UserInteractor extends Feature { * {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)} for details on how * these are created */ - void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, @NotNull List args); + void onSelectionMenu(SelectMenuInteractionEvent event, List args); /** * Triggered by the core system during its setup phase. It will provide the interactor a @@ -81,5 +81,5 @@ public interface UserInteractor extends Feature { * * @param generator the provided component id generator */ - void acceptComponentIdGenerator(@NotNull ComponentIdGenerator generator); + void acceptComponentIdGenerator(ComponentIdGenerator generator); } 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 17928dbd7f..fdaba47f5b 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,7 +1,6 @@ package org.togetherjava.tjbot.commands.basic; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; @@ -24,7 +23,7 @@ public PingCommand() { * @param event the corresponding event */ @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { event.reply("Pong!").queue(); } } 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 0bf97b93f8..be5550bdb9 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 @@ -13,13 +13,13 @@ import net.dv8tion.jda.api.interactions.components.selections.SelectMenu; import net.dv8tion.jda.api.interactions.components.selections.SelectOption; import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.commands.componentids.Lifespan; +import javax.annotation.Nonnull; import java.awt.Color; import java.util.*; import java.util.function.Function; @@ -80,7 +80,7 @@ public RoleSelectCommand() { } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { if (!handleHasPermissions(event)) { return; } @@ -101,7 +101,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { sendRoleSelectionMenu(event, selectedRoles); } - private boolean handleHasPermissions(@NotNull SlashCommandInteractionEvent event) { + private boolean handleHasPermissions(SlashCommandInteractionEvent event) { if (!event.getMember().hasPermission(Permission.MANAGE_ROLES)) { event.reply("You do not have the required manage role permission to use this command") .setEphemeral(true) @@ -123,7 +123,7 @@ private boolean handleHasPermissions(@NotNull SlashCommandInteractionEvent event } @Contract(pure = true) - private static boolean handleIsBotAccessibleRole(@NotNull Role role) { + private static boolean handleIsBotAccessibleRole(Role role) { boolean isSystemRole = role.isPublicRole() || role.getTags().isBot() || role.getTags().isBoost() || role.getTags().isIntegration(); @@ -135,8 +135,8 @@ private static boolean handleIsBotAccessibleRole(@NotNull Role role) { return !isSystemRole; } - private static boolean handleAccessibleRolesSelected(@NotNull IReplyCallback event, - @NotNull Collection selectedRoles) { + private static boolean handleAccessibleRolesSelected(IReplyCallback event, + Collection selectedRoles) { if (!selectedRoles.isEmpty()) { return true; } @@ -151,8 +151,8 @@ private static boolean handleAccessibleRolesSelected(@NotNull IReplyCallback eve return false; } - private static boolean handleInteractableRolesSelected(@NotNull IReplyCallback event, - @NotNull Collection selectedRoles) { + private static boolean handleInteractableRolesSelected(IReplyCallback event, + Collection selectedRoles) { List nonInteractableRoles = selectedRoles.stream() .filter(role -> !event.getGuild().getSelfMember().canInteract(role)) .toList(); @@ -173,8 +173,8 @@ private static boolean handleInteractableRolesSelected(@NotNull IReplyCallback e return false; } - private void sendRoleSelectionMenu(@NotNull final CommandInteraction event, - @NotNull final Collection selectableRoles) { + private void sendRoleSelectionMenu(final CommandInteraction event, + final Collection selectableRoles) { SelectMenu.Builder menu = SelectMenu.create(generateComponentId(Lifespan.PERMANENT, event.getUser().getId())) .setPlaceholder("Select your roles") @@ -193,8 +193,8 @@ private void sendRoleSelectionMenu(@NotNull final CommandInteraction event, event.replyEmbeds(embed).addActionRow(menu.build()).queue(); } - @NotNull - private static SelectOption mapToSelectOption(@NotNull Role role) { + @Nonnull + private static SelectOption mapToSelectOption(Role role) { RoleIcon roleIcon = role.getIcon(); SelectOption option = SelectOption.of(role.getName(), role.getId()); @@ -206,8 +206,7 @@ private static SelectOption mapToSelectOption(@NotNull Role role) { } @Override - public void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, - @NotNull List args) { + public void onSelectionMenu(SelectMenuInteractionEvent event, List args) { Guild guild = event.getGuild(); List selectedRoles = event.getSelectedOptions() .stream() @@ -223,8 +222,8 @@ public void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, handleRoleSelection(event, guild, selectedRoles); } - private static void handleRoleSelection(@NotNull SelectMenuInteractionEvent event, - @NotNull Guild guild, @NotNull Collection selectedRoles) { + private static void handleRoleSelection(SelectMenuInteractionEvent event, Guild guild, + Collection selectedRoles) { Collection rolesToAdd = new ArrayList<>(selectedRoles.size()); Collection rolesToRemove = new ArrayList<>(selectedRoles.size()); @@ -244,8 +243,8 @@ private static void handleRoleSelection(@NotNull SelectMenuInteractionEvent even modifyRoles(event, event.getMember(), guild, rolesToAdd, rolesToRemove); } - @NotNull - private static Function> optionToRole(@NotNull Guild guild) { + @Nonnull + private static Function> optionToRole(Guild guild) { return option -> { Role role = guild.getRoleById(option.getValue()); @@ -259,16 +258,15 @@ private static Function> optionToRole(@NotNull Guil }; } - private static void modifyRoles(@NotNull IReplyCallback event, @NotNull Member target, - @NotNull Guild guild, @NotNull Collection rolesToAdd, - @NotNull Collection rolesToRemove) { + private static void modifyRoles(IReplyCallback event, Member target, Guild guild, + Collection rolesToAdd, Collection rolesToRemove) { guild.modifyMemberRoles(target, rolesToAdd, rolesToRemove) .flatMap(empty -> event.reply("Your roles have been updated.").setEphemeral(true)) .queue(); } - private static @NotNull MessageEmbed createEmbed(@NotNull String title, - @NotNull CharSequence description) { + @Nonnull + private static MessageEmbed createEmbed(String title, CharSequence description) { return new EmbedBuilder().setTitle(title) .setDescription(description) .setColor(AMBIENT_COLOR) 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 280f32599e..84bd30ece1 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 @@ -6,13 +6,13 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.requests.ErrorResponse; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.SuggestionsConfig; +import javax.annotation.Nonnull; import java.util.Optional; import java.util.regex.Pattern; @@ -33,14 +33,14 @@ public final class SuggestionsUpDownVoter extends MessageReceiverAdapter { * * @param config the config to use for this */ - public SuggestionsUpDownVoter(@NotNull Config config) { + public SuggestionsUpDownVoter(Config config) { super(Pattern.compile(config.getSuggestions().getChannelPattern())); this.config = config.getSuggestions(); } @Override - public void onMessageReceived(@NotNull MessageReceivedEvent event) { + public void onMessageReceived(MessageReceivedEvent event) { if (event.getAuthor().isBot() || event.isWebhookMessage() || !event.isFromGuild()) { return; } @@ -53,7 +53,7 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { reactWith(config.getDownVoteEmoteName(), FALLBACK_DOWN_VOTE, guild, message); } - private static void createThread(@NotNull Message message) { + private static void createThread(Message message) { String title = message.getContentRaw(); if (title.length() >= TITLE_MAX_LENGTH) { @@ -69,8 +69,8 @@ private static void createThread(@NotNull Message message) { message.createThreadChannel(title).queue(); } - private static void reactWith(@NotNull String emoteName, @NotNull String fallbackUnicodeEmote, - @NotNull Guild guild, @NotNull Message message) { + private static void reactWith(String emoteName, String fallbackUnicodeEmote, Guild guild, + Message message) { getEmoteByName(emoteName, guild).map(message::addReaction).orElseGet(() -> { logger.warn( "Unable to vote on a suggestion with the configured emote ('{}'), using fallback instead.", @@ -89,8 +89,8 @@ private static void reactWith(@NotNull String emoteName, @NotNull String fallbac }); } - private static @NotNull Optional getEmoteByName(@NotNull String name, - @NotNull Guild guild) { + @Nonnull + private static Optional getEmoteByName(String name, Guild guild) { return guild.getEmotesByName(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 7e6899684f..6339bac007 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 @@ -13,13 +13,13 @@ import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.List; import java.util.Map; import java.util.Objects; @@ -144,7 +144,7 @@ public VcActivityCommand() { @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { Member member = Objects.requireNonNull(event.getMember(), "member is null"); GuildVoiceState voiceState = Objects.requireNonNull(member.getVoiceState(), "Voicestates aren't being cached, check the JDABuilder"); @@ -191,9 +191,8 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { handleSubcommand(event, voiceChannel, applicationId, maxUses, maxAgeDays, applicationName); } - - private static @NotNull Optional getKeyByValue(@NotNull Map map, - @NotNull V value) { + @Nonnull + private static Optional getKeyByValue(Map map, V value) { for (Map.Entry entry : map.entrySet()) { if (value.equals(entry.getKey())) { return Optional.of(entry.getKey()); @@ -203,10 +202,9 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { return Optional.empty(); } - private static void handleSubcommand(@NotNull SlashCommandInteractionEvent event, - @NotNull VoiceChannel voiceChannel, @NotNull String applicationId, - @Nullable Integer maxUses, @Nullable Integer maxAgeDays, - @NotNull String applicationName) { + private static void handleSubcommand(SlashCommandInteractionEvent event, + VoiceChannel voiceChannel, String applicationId, @Nullable Integer maxUses, + @Nullable Integer maxAgeDays, String applicationName) { voiceChannel.createInvite() @@ -219,9 +217,9 @@ private static void handleSubcommand(@NotNull SlashCommandInteractionEvent event } - private static @NotNull ReplyCallbackAction replyInvite( - @NotNull SlashCommandInteractionEvent event, @NotNull Invite invite, - @NotNull String applicationName) { + @Nonnull + private static ReplyCallbackAction replyInvite(SlashCommandInteractionEvent event, + Invite invite, String applicationName) { return event.reply(""" %s wants to start %s. Feel free to join by clicking %s , enjoy! @@ -229,7 +227,7 @@ private static void handleSubcommand(@NotNull SlashCommandInteractionEvent event """.formatted(event.getUser().getAsTag(), applicationName, invite.getUrl())); } - private static void handleErrors(@NotNull SlashCommandInteractionEvent event, + private static void handleErrors(SlashCommandInteractionEvent event, @Nullable Throwable throwable) { event.reply("Something went wrong :/").queue(); logger.warn("Something went wrong in the VcActivityCommand", throwable); @@ -242,7 +240,8 @@ private static void handleErrors(@NotNull SlashCommandInteractionEvent event, * @return the extracted integer if present, null otherwise **/ @Contract("null -> null") - private static @Nullable Integer requireIntOptionIfPresent(@Nullable OptionMapping option) { + @Nullable + private static Integer requireIntOptionIfPresent(@Nullable OptionMapping option) { return option == null ? null : Math.toIntExact(option.getAsLong()); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/package-info.java index 3394e9bcb5..177ae9c4c3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/package-info.java @@ -2,4 +2,7 @@ * This package offers some basic commands that act as example for how to use the command system of * the application. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.basic; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentId.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentId.java index c5205e258b..5e297c7576 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentId.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentId.java @@ -1,6 +1,5 @@ package org.togetherjava.tjbot.commands.componentids; -import org.jetbrains.annotations.NotNull; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.List; @@ -14,5 +13,5 @@ * this component ID, when triggered * @param elements the additional elements to carry along this component ID, empty if not desired */ -public record ComponentId(@NotNull String userInteractorName, @NotNull List elements) { +public record ComponentId(String userInteractorName, List elements) { } 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 7514983890..91cd7a3228 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 @@ -3,12 +3,11 @@ 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.ButtonStyle; import net.dv8tion.jda.api.interactions.components.buttons.Button; - +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; import org.togetherjava.tjbot.commands.SlashCommand; -import org.jetbrains.annotations.NotNull; +import javax.annotation.Nonnull; /** * Provides component ID generation. @@ -38,6 +37,6 @@ public interface ComponentIdGenerator { * @throws InvalidComponentIdFormatException if the given component ID was in an unexpected * format and could not be serialized */ - @NotNull - String generate(@NotNull ComponentId componentId, @NotNull Lifespan lifespan); + @Nonnull + String generate(ComponentId componentId, Lifespan lifespan); } 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 5dbfcc4374..6d5071c787 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,13 +1,12 @@ package org.togetherjava.tjbot.commands.componentids; import net.dv8tion.jda.api.entities.Emoji; -import net.dv8tion.jda.api.interactions.components.ComponentInteraction; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; +import net.dv8tion.jda.api.interactions.components.ComponentInteraction; import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; -import org.jetbrains.annotations.NotNull; - +import javax.annotation.Nonnull; import java.util.Optional; /** @@ -39,6 +38,6 @@ public interface ComponentIdParser { * @throws InvalidComponentIdFormatException if the component ID associated to the given UUID * was in an unexpected format and could not be deserialized */ - @NotNull - Optional parse(@NotNull String uuid); + @Nonnull + Optional parse(String uuid); } 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 7f93b79961..882505d8ba 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 @@ -5,15 +5,15 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import org.togetherjava.tjbot.commands.SlashCommand; -import org.jetbrains.annotations.NotNull; import org.jooq.Result; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.SlashCommand; import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.ComponentIds; import org.togetherjava.tjbot.db.generated.tables.records.ComponentIdsRecord; +import javax.annotation.Nonnull; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalUnit; @@ -84,7 +84,7 @@ public final class ComponentIdStore implements AutoCloseable { * * @param database the database to use to persist component IDs in */ - public ComponentIdStore(@NotNull Database database) { + public ComponentIdStore(Database database) { this(database, EVICT_DATABASE_EVERY_INITIAL_DELAY, EVICT_DATABASE_EVERY_DELAY, EVICT_DATABASE_EVERY_UNIT, EVICT_DATABASE_OLDER_THAN, EVICT_DATABASE_OLDER_THAN_UNIT); @@ -103,9 +103,8 @@ public ComponentIdStore(@NotNull Database database) { * @param evictOlderThanUnit the unit of the 'evictOlderThan' value */ @SuppressWarnings({"WeakerAccess", "ConstructorWithTooManyParameters"}) - public ComponentIdStore(@NotNull Database database, long evictEveryInitialDelay, - long evictEveryDelay, ChronoUnit evictEveryUnit, long evictOlderThan, - @SuppressWarnings("TypeMayBeWeakened") ChronoUnit evictOlderThanUnit) { + public ComponentIdStore(Database database, long evictEveryInitialDelay, long evictEveryDelay, + ChronoUnit evictEveryUnit, long evictOlderThan, ChronoUnit evictOlderThanUnit) { this.database = database; evictDatabaseOlderThan = evictOlderThan; evictDatabaseOlderThanUnit = evictOlderThanUnit; @@ -137,7 +136,7 @@ public ComponentIdStore(@NotNull Database database, long evictEveryInitialDelay, * * @param listener the listener to add */ - public void addComponentIdRemovedListener(@NotNull Consumer listener) { + public void addComponentIdRemovedListener(Consumer listener) { componentIdRemovedListeners.add(listener); } @@ -156,7 +155,8 @@ public void addComponentIdRemovedListener(@NotNull Consumer listene * format and could not be serialized */ @SuppressWarnings("WeakerAccess") - public @NotNull Optional get(@NotNull UUID uuid) { + @Nonnull + public Optional get(UUID uuid) { synchronized (storeLock) { // Get it from the cache or, if not found, the database return Optional.ofNullable(storeCache.getIfPresent(uuid)).or(() -> { @@ -188,8 +188,7 @@ public void addComponentIdRemovedListener(@NotNull Consumer listene * was in an unexpected format and could not be deserialized */ @SuppressWarnings("WeakerAccess") - public void putOrThrow(@NotNull UUID uuid, @NotNull ComponentId componentId, - @NotNull Lifespan lifespan) { + public void putOrThrow(UUID uuid, ComponentId componentId, Lifespan lifespan) { Supplier alreadyExistsMessageSupplier = () -> "The UUID '%s' already exists and is associated to a component id." .formatted(uuid); @@ -218,7 +217,8 @@ public void putOrThrow(@NotNull UUID uuid, @NotNull ComponentId componentId, } } - private @NotNull Optional getFromDatabase(@NotNull UUID uuid) { + @Nonnull + private Optional getFromDatabase(UUID uuid) { return database.read(context -> Optional .ofNullable(context.selectFrom(ComponentIds.COMPONENT_IDS) .where(ComponentIds.COMPONENT_IDS.UUID.eq(uuid.toString())) @@ -235,7 +235,7 @@ public void putOrThrow(@NotNull UUID uuid, @NotNull ComponentId componentId, * @param uuid the uuid to heat * @throws IllegalArgumentException if there is no, or multiple, records associated to that UUID */ - private void heatRecord(@NotNull UUID uuid) { + private void heatRecord(UUID uuid) { int updatedRecords; synchronized (storeLock) { updatedRecords = @@ -294,7 +294,8 @@ private void evictDatabase() { } } - private static @NotNull String serializeComponentId(@NotNull ComponentId componentId) { + @Nonnull + private static String serializeComponentId(ComponentId componentId) { try { return CSV.writerFor(ComponentId.class) .with(CSV.schemaFor(ComponentId.class)) @@ -304,7 +305,8 @@ private void evictDatabase() { } } - private static @NotNull ComponentId deserializeComponentId(@NotNull String componentId) { + @Nonnull + private static ComponentId deserializeComponentId(String componentId) { try { return CSV.readerFor(ComponentId.class) .with(CSV.schemaFor(ComponentId.class)) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/package-info.java index 239d79060a..e52a9953c4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/package-info.java @@ -2,7 +2,7 @@ * This package provides utilities to generate, persist and parse component IDs. *

* See - * {@link org.togetherjava.tjbot.commands.SlashCommand#onSlashCommand(net.dv8tion.jda.api.events.interaction.SlashCommandInteractionEvent)} + * {@link org.togetherjava.tjbot.commands.SlashCommand#onSlashCommand(net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent)} * for details on component IDs. *

* The class {@link org.togetherjava.tjbot.commands.componentids.ComponentIdStore} is the central @@ -10,4 +10,7 @@ * {@link org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator} and * {@link org.togetherjava.tjbot.commands.componentids.ComponentIdParser}. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.componentids; + +import javax.annotation.ParametersAreNonnullByDefault; 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 1e1c0a3639..2267fe851d 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 @@ -8,12 +8,12 @@ import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.interactions.components.buttons.Button; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -23,7 +23,10 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; @@ -49,8 +52,13 @@ public class FileSharingMessageListener extends MessageReceiverAdapter { private final Predicate isStagingChannelName; private final Predicate isOverviewChannelName; - - public FileSharingMessageListener(@NotNull Config config) { + /** + * Creates a new instance. + * + * @param config used to get api key and channel names. + * @see org.togetherjava.tjbot.commands.Features + */ + public FileSharingMessageListener(Config config) { super(Pattern.compile(".*")); gistApiKey = config.getGistApiKey(); @@ -61,7 +69,7 @@ public FileSharingMessageListener(@NotNull Config config) { } @Override - public void onMessageReceived(@NotNull MessageReceivedEvent event) { + public void onMessageReceived(MessageReceivedEvent event) { User author = event.getAuthor(); if (author.isBot() || event.isWebhookMessage()) { return; @@ -78,16 +86,22 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { .filter(this::isAttachmentRelevant) .toList(); + if (attachments.isEmpty()) { + return; + } + CompletableFuture.runAsync(() -> { try { processAttachments(event, attachments); } catch (Exception e) { - LOGGER.error("Unknown error while processing attachments", e); + LOGGER.error( + "Unknown error while processing attachments. Channel: {}, Author: {}, Message ID: {}.", + event.getChannel().getName(), author.getId(), event.getMessageId(), e); } }); } - private boolean isAttachmentRelevant(@NotNull Message.Attachment attachment) { + private boolean isAttachmentRelevant(Message.Attachment attachment) { String extension = attachment.getFileExtension(); if (extension == null) { return false; @@ -96,8 +110,8 @@ private boolean isAttachmentRelevant(@NotNull Message.Attachment attachment) { } - private void processAttachments(@NotNull MessageReceivedEvent event, - @NotNull List attachments) { + private void processAttachments(MessageReceivedEvent event, + List attachments) { Map nameToFile = new ConcurrentHashMap<>(); @@ -119,15 +133,17 @@ private void processAttachments(@NotNull MessageReceivedEvent event, sendResponse(event, url); } - private @NotNull String readAttachment(@NotNull InputStream stream) { - try { + @Nonnull + private String readAttachment(InputStream stream) { + try (stream) { return new String(stream.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException e) { throw new UncheckedIOException(e); } } - private @NotNull String getNameOf(@NotNull Message.Attachment attachment) { + @Nonnull + private String getNameOf(Message.Attachment attachment) { String fileName = attachment.getFileName(); String fileExtension = attachment.getFileExtension(); @@ -147,7 +163,8 @@ private void processAttachments(@NotNull MessageReceivedEvent event, return fileName; } - private @NotNull String uploadToGist(@NotNull GistRequest jsonRequest) { + @Nonnull + private String uploadToGist(GistRequest jsonRequest) { String body; try { body = JSON.writeValueAsString(jsonRequest); @@ -179,7 +196,8 @@ private void processAttachments(@NotNull MessageReceivedEvent event, if (statusCode < HttpURLConnection.HTTP_OK || statusCode >= HttpURLConnection.HTTP_MULT_CHOICE) { - throw new IllegalStateException("Gist API unexpected response: " + apiResponse.body()); + throw new IllegalStateException("Gist API unexpected response: %s. Request JSON: %s" + .formatted(apiResponse.body(), body)); } GistResponse gistResponse; @@ -192,7 +210,7 @@ private void processAttachments(@NotNull MessageReceivedEvent event, return gistResponse.getHtmlUrl(); } - private void sendResponse(@NotNull MessageReceivedEvent event, @NotNull String url) { + private void sendResponse(MessageReceivedEvent event, String url) { Message message = event.getMessage(); String messageContent = "I uploaded your attachments as **gist**. That way, they are easier to read for everyone, especially mobile users 👍"; @@ -200,7 +218,7 @@ private void sendResponse(@NotNull MessageReceivedEvent event, @NotNull String u message.reply(messageContent).setActionRow(Button.link(url, "gist")).queue(); } - private boolean isHelpThread(@NotNull MessageReceivedEvent event) { + private boolean isHelpThread(MessageReceivedEvent event) { if (event.getChannelType() != ChannelType.GUILD_PUBLIC_THREAD) { return false; } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java index cba4472b4e..343a7a1730 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java @@ -1,10 +1,8 @@ package org.togetherjava.tjbot.commands.filesharing; -import org.jetbrains.annotations.NotNull; - /** * @see Create a Gist via * API */ -record GistFile(@NotNull String content) { +record GistFile(String content) { } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java index 3d3f8ca032..1541a676c7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java @@ -1,7 +1,6 @@ package org.togetherjava.tjbot.commands.filesharing; import com.fasterxml.jackson.annotation.JsonAnyGetter; -import org.jetbrains.annotations.NotNull; import java.util.Map; @@ -9,5 +8,5 @@ * @see Create a Gist via * API */ -record GistFiles(@NotNull @JsonAnyGetter Map nameToContent) { +record GistFiles(@JsonAnyGetter Map nameToContent) { } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java index d1e1156547..6b1dd78575 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java @@ -1,12 +1,10 @@ package org.togetherjava.tjbot.commands.filesharing; import com.fasterxml.jackson.annotation.JsonProperty; -import org.jetbrains.annotations.NotNull; /** * @see Create a Gist via * API */ -record GistRequest(@NotNull String description, @JsonProperty("public") boolean isPublic, - @NotNull GistFiles files) { +record GistRequest(String description, @JsonProperty("public") boolean isPublic, GistFiles files) { } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java index af7d3f2ddf..1d35277fcb 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java @@ -2,7 +2,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; /** * @see Create a Gist via @@ -13,11 +14,12 @@ final class GistResponse { @JsonProperty("html_url") private String htmlUrl; - public @NotNull String getHtmlUrl() { - return this.htmlUrl; + @Nonnull + public String getHtmlUrl() { + return htmlUrl; } - public void setHtmlUrl(@NotNull String htmlUrl) { + public void setHtmlUrl(String htmlUrl) { this.htmlUrl = htmlUrl; } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java index 3eba1af69d..8f5f6befeb 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java @@ -2,4 +2,7 @@ * This package offers all the functionality for automatically uploading files to sharing services. * The core class is {@link org.togetherjava.tjbot.commands.filesharing.FileSharingMessageListener}. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.filesharing; + +import javax.annotation.ParametersAreNonnullByDefault; 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 d29f72807b..16968d2d9b 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 @@ -9,13 +9,13 @@ import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.RestAction; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; import java.util.Optional; import static org.togetherjava.tjbot.commands.help.HelpSystemHelper.TITLE_COMPACT_LENGTH_MAX; @@ -46,7 +46,6 @@ public final class AskCommand extends SlashCommandAdapter { private static final String TITLE_OPTION = "title"; private static final String CATEGORY_OPTION = "category"; - private final HelpSystemHelper helper; /** @@ -55,7 +54,7 @@ public final class AskCommand extends SlashCommandAdapter { * @param config the config to use * @param helper the helper to use */ - public AskCommand(@NotNull Config config, @NotNull HelpSystemHelper helper) { + public AskCommand(Config config, HelpSystemHelper helper) { super("ask", "Ask a question - use this in the staging channel", SlashCommandVisibility.GUILD); @@ -73,14 +72,10 @@ public AskCommand(@NotNull Config config, @NotNull HelpSystemHelper helper) { } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { String title = event.getOption(TITLE_OPTION).getAsString(); String category = event.getOption(CATEGORY_OPTION).getAsString(); - if (!handleIsStagingChannel(event)) { - return; - } - if (!handleIsValidTitle(title, event)) { return; } @@ -107,43 +102,35 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { }, e -> handleFailure(e, eventHook)); } - private boolean handleIsStagingChannel(@NotNull IReplyCallback event) { - if (helper.isStagingChannelName(event.getChannel().getName())) { - return true; - } - - event.reply("Sorry, but this command can only be used in the help staging channel.") - .setEphemeral(true) - .queue(); - - return false; - } - - private boolean handleIsValidTitle(@NotNull CharSequence title, @NotNull IReplyCallback event) { + 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)) + event.reply(""" + Sorry, but your title is invalid. Please pick a title where: + • length is between %d and %d + • must not contain the word 'help' + Thanks, and sorry for the inconvenience 👍 + """.formatted(TITLE_COMPACT_LENGTH_MIN, TITLE_COMPACT_LENGTH_MAX)) .setEphemeral(true) .queue(); return false; } - private @NotNull RestAction handleEvent(@NotNull InteractionHook eventHook, - @NotNull ThreadChannel threadChannel, @NotNull Member author, @NotNull String title, - @NotNull String category, @NotNull Guild guild) { + @Nonnull + private RestAction handleEvent(InteractionHook eventHook, ThreadChannel threadChannel, + Member author, String title, String category, Guild guild) { + helper.writeHelpThreadToDatabase(author, threadChannel); return sendInitialMessage(guild, threadChannel, author, title, category) .flatMap(any -> notifyUser(eventHook, threadChannel)) .flatMap(any -> helper.sendExplanationMessage(threadChannel)); } - private RestAction sendInitialMessage(@NotNull Guild guild, - @NotNull ThreadChannel threadChannel, @NotNull Member author, @NotNull String title, - @NotNull String category) { + @Nonnull + private RestAction sendInitialMessage(Guild guild, ThreadChannel threadChannel, + Member author, String title, String category) { String roleMentionDescription = helper.handleFindRoleForCategory(category, guild) .map(role -> " (%s)".formatted(role.getAsMention())) .orElse(""); @@ -161,15 +148,15 @@ private RestAction sendInitialMessage(@NotNull Guild guild, .flatMap(message -> message.editMessage(contentWithRole)); } - private static @NotNull RestAction notifyUser(@NotNull InteractionHook eventHook, - @NotNull IMentionable threadChannel) { + @Nonnull + private static RestAction notifyUser(InteractionHook eventHook, + IMentionable threadChannel) { return eventHook.editOriginal(""" Created a thread for you: %s Please ask your question there, thanks.""".formatted(threadChannel.getAsMention())); } - private static void handleFailure(@NotNull Throwable exception, - @NotNull InteractionHook eventHook) { + private static void handleFailure(Throwable exception, InteractionHook eventHook) { if (exception instanceof ErrorResponseException responseException) { ErrorResponse response = responseException.getErrorResponse(); if (response == ErrorResponse.MAX_CHANNELS 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 f633972da8..bef93f9889 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 @@ -5,7 +5,6 @@ import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.TextChannel; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.Routine; @@ -13,6 +12,7 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.moderation.ModAuditLogWriter; +import javax.annotation.Nonnull; import java.time.Duration; import java.time.Instant; import java.time.Period; @@ -50,8 +50,8 @@ public final class AutoPruneHelperRoutine implements Routine { * @param modAuditLogWriter to inform mods when manual pruning becomes necessary * @param database to determine whether an user is inactive */ - public AutoPruneHelperRoutine(@NotNull Config config, @NotNull HelpSystemHelper helper, - @NotNull ModAuditLogWriter modAuditLogWriter, @NotNull Database database) { + public AutoPruneHelperRoutine(Config config, HelpSystemHelper helper, + ModAuditLogWriter modAuditLogWriter, Database database) { allCategories = config.getHelpSystem().getCategories(); this.helper = helper; this.modAuditLogWriter = modAuditLogWriter; @@ -59,16 +59,17 @@ public AutoPruneHelperRoutine(@NotNull Config config, @NotNull HelpSystemHelper } @Override - public @NotNull Schedule createSchedule() { + @Nonnull + public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 0, 1, TimeUnit.HOURS); } @Override - public void runRoutine(@NotNull JDA jda) { + public void runRoutine(JDA jda) { jda.getGuildCache().forEach(this::pruneForGuild); } - private void pruneForGuild(@NotNull Guild guild) { + private void pruneForGuild(Guild guild) { TextChannel overviewChannel = guild.getTextChannels() .stream() .filter(channel -> helper.isOverviewChannelName(channel.getName())) @@ -83,8 +84,7 @@ private void pruneForGuild(@NotNull Guild guild) { .forEach(role -> pruneRoleIfFull(role, overviewChannel, now)); } - private void pruneRoleIfFull(@NotNull Role role, @NotNull TextChannel overviewChannel, - @NotNull Instant when) { + private void pruneRoleIfFull(Role role, TextChannel overviewChannel, Instant when) { role.getGuild().findMembersWithRoles(role).onSuccess(members -> { if (isRoleFull(members)) { logger.debug("Helper role {} is full, starting to prune.", role.getName()); @@ -93,12 +93,12 @@ private void pruneRoleIfFull(@NotNull Role role, @NotNull TextChannel overviewCh }); } - private boolean isRoleFull(@NotNull Collection members) { + private boolean isRoleFull(Collection members) { return members.size() >= ROLE_FULL_THRESHOLD; } - private void pruneRole(@NotNull Role role, @NotNull List members, - @NotNull TextChannel overviewChannel, @NotNull Instant when) { + private void pruneRole(Role role, List members, TextChannel overviewChannel, + Instant when) { List membersShuffled = new ArrayList<>(members); Collections.shuffle(membersShuffled); @@ -124,7 +124,7 @@ private void pruneRole(@NotNull Role role, @NotNull List membe membersToPrune.forEach(member -> pruneMemberFromRole(member, role, overviewChannel)); } - private boolean isMemberInactive(@NotNull Member member, @NotNull Instant when) { + private boolean isMemberInactive(Member member, Instant when) { if (member.hasTimeJoined()) { Instant memberJoined = member.getTimeJoined().toInstant(); if (Duration.between(memberJoined, when).toDays() <= RECENTLY_JOINED_DAYS) { @@ -143,8 +143,7 @@ private boolean isMemberInactive(@NotNull Member member, @NotNull Instant when) .and(HELP_CHANNEL_MESSAGES.SENT_AT.greaterThan(latestActiveMoment)))) == 0; } - private void pruneMemberFromRole(@NotNull Member member, @NotNull Role role, - @NotNull TextChannel overviewChannel) { + private void pruneMemberFromRole(Member member, Role role, TextChannel overviewChannel) { Guild guild = member.getGuild(); String dmMessage = @@ -160,7 +159,7 @@ private void pruneMemberFromRole(@NotNull Member member, @NotNull Role role, .queue(); } - private void warnModsAbout(@NotNull String message, @NotNull Guild guild) { + private void warnModsAbout(String message, Guild guild) { logger.warn(message); modAuditLogWriter.write("Auto-prune helpers", message, null, Instant.now(), guild); 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 8584b29430..f066380f27 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 @@ -7,13 +7,13 @@ import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.internal.requests.CompletedRestAction; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.Routine; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.HelpSystemConfig; +import javax.annotation.Nonnull; import java.time.Duration; import java.time.Instant; import java.time.OffsetDateTime; @@ -46,7 +46,7 @@ public final class BotMessageCleanup implements Routine { * * @param config the config to use */ - public BotMessageCleanup(@NotNull Config config) { + public BotMessageCleanup(Config config) { this.config = config.getHelpSystem(); isStagingChannelName = Pattern.compile(config.getHelpSystem().getStagingChannelPattern()) @@ -54,16 +54,17 @@ public BotMessageCleanup(@NotNull Config config) { } @Override - public @NotNull Schedule createSchedule() { + @Nonnull + public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 1, 1, TimeUnit.MINUTES); } @Override - public void runRoutine(@NotNull JDA jda) { + public void runRoutine(JDA jda) { jda.getGuildCache().forEach(this::cleanupBotMessagesForGuild); } - private void cleanupBotMessagesForGuild(@NotNull Guild guild) { + private void cleanupBotMessagesForGuild(Guild guild) { Optional maybeStagingChannel = handleRequireStagingChannel(guild); if (maybeStagingChannel.isEmpty()) { @@ -78,7 +79,8 @@ private void cleanupBotMessagesForGuild(@NotNull Guild guild) { .queue(); } - private @NotNull Optional handleRequireStagingChannel(@NotNull Guild guild) { + @Nonnull + private Optional handleRequireStagingChannel(Guild guild) { Optional maybeChannel = guild.getTextChannelCache() .stream() .filter(channel -> isStagingChannelName.test(channel.getName())) @@ -94,7 +96,7 @@ private void cleanupBotMessagesForGuild(@NotNull Guild guild) { return maybeChannel; } - private static boolean shouldMessageBeCleanedUp(@NotNull Message message) { + private static boolean shouldMessageBeCleanedUp(Message message) { if (!message.getAuthor().isBot()) { return false; } @@ -106,8 +108,9 @@ private static boolean shouldMessageBeCleanedUp(@NotNull Message message) { return deleteWhen.isBefore(Instant.now()); } - private static @NotNull RestAction cleanupBotMessages( - @NotNull GuildMessageChannel channel, @NotNull Collection messages) { + @Nonnull + private static RestAction cleanupBotMessages(GuildMessageChannel channel, + Collection messages) { logger.debug("Cleaning up old bot messages in the staging channel"); List messageIdsToDelete = messages.stream() .filter(BotMessageCleanup::shouldMessageBeCleanedUp) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java index 576890ecb3..1f84add910 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpCategoryCommand.java @@ -11,11 +11,11 @@ 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.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Locale; @@ -47,7 +47,7 @@ public final class ChangeHelpCategoryCommand extends SlashCommandAdapter { * @param config the config to use * @param helper the helper to use */ - public ChangeHelpCategoryCommand(@NotNull Config config, @NotNull HelpSystemHelper helper) { + public ChangeHelpCategoryCommand(Config config, HelpSystemHelper helper) { super("change-help-category", "changes the category of a help thread", SlashCommandVisibility.GUILD); @@ -68,13 +68,9 @@ public ChangeHelpCategoryCommand(@NotNull Config config, @NotNull HelpSystemHelp } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { String category = event.getOption(CATEGORY_OPTION).getAsString(); - if (!helper.handleIsHelpThread(event)) { - return; - } - ThreadChannel helpThread = event.getThreadChannel(); if (helpThread.isArchived()) { event.reply("This thread is already closed.").setEphemeral(true).queue(); @@ -100,9 +96,9 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { .queue(); } - private @NotNull RestAction sendCategoryChangedMessage(@NotNull Guild guild, - @NotNull InteractionHook hook, @NotNull ThreadChannel helpThread, - @NotNull String category) { + @Nonnull + 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); @@ -123,7 +119,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { .flatMap(message -> message.editMessage(headsUpWithRole))); } - private boolean isHelpThreadOnCooldown(@NotNull ThreadChannel helpThread) { + private boolean isHelpThreadOnCooldown(ThreadChannel helpThread) { return Optional .ofNullable(helpThreadIdToLastCategoryChange.getIfPresent(helpThread.getIdLong())) .map(lastCategoryChange -> lastCategoryChange.plus(COOLDOWN_DURATION_VALUE, 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 index 14a6d0cb5b..8ea5288f25 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpTitleCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/ChangeHelpTitleCommand.java @@ -6,7 +6,6 @@ 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.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; @@ -40,7 +39,7 @@ public final class ChangeHelpTitleCommand extends SlashCommandAdapter { * * @param helper the helper to use */ - public ChangeHelpTitleCommand(@NotNull HelpSystemHelper helper) { + public ChangeHelpTitleCommand(HelpSystemHelper helper) { super("change-help-title", "changes the title of a help thread", SlashCommandVisibility.GUILD); @@ -55,10 +54,10 @@ public ChangeHelpTitleCommand(@NotNull HelpSystemHelper helper) { } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { String title = event.getOption(TITLE_OPTION).getAsString(); - if (!helper.handleIsHelpThread(event) || !handleIsValidTitle(title, event)) { + if (!handleIsValidTitle(title, event)) { return; } @@ -84,7 +83,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { .queue(); } - private boolean isHelpThreadOnCooldown(@NotNull ThreadChannel helpThread) { + private boolean isHelpThreadOnCooldown(ThreadChannel helpThread) { return Optional .ofNullable(helpThreadIdToLastTitleChange.getIfPresent(helpThread.getIdLong())) .map(lastCategoryChange -> lastCategoryChange.plus(COOLDOWN_DURATION_VALUE, @@ -93,7 +92,7 @@ private boolean isHelpThreadOnCooldown(@NotNull ThreadChannel helpThread) { .isPresent(); } - private boolean handleIsValidTitle(@NotNull CharSequence title, @NotNull IReplyCallback event) { + private boolean handleIsValidTitle(CharSequence title, IReplyCallback event) { if (HelpSystemHelper.isTitleValid(title)) { return true; } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java index 14106631ee..3b0e348e7e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/CloseCommand.java @@ -6,7 +6,6 @@ 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.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; @@ -26,31 +25,22 @@ 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 HelpSystemHelper helper; private final Cache helpThreadIdToLastClose; /** * Creates a new instance. - * - * @param helper the helper to use */ - public CloseCommand(@NotNull HelpSystemHelper helper) { + 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(); - - this.helper = helper; } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { - if (!helper.handleIsHelpThread(event)) { - return; - } - + public void onSlashCommand(SlashCommandInteractionEvent event) { ThreadChannel helpThread = event.getThreadChannel(); if (helpThread.isArchived()) { event.reply("This thread is already closed.").setEphemeral(true).queue(); @@ -75,7 +65,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { event.replyEmbeds(embed).flatMap(any -> helpThread.getManager().setArchived(true)).queue(); } - private boolean isHelpThreadOnCooldown(@NotNull ThreadChannel helpThread) { + private boolean isHelpThreadOnCooldown(ThreadChannel helpThread) { return Optional.ofNullable(helpThreadIdToLastClose.getIfPresent(helpThread.getIdLong())) .map(lastCategoryChange -> lastCategoryChange.plus(COOLDOWN_DURATION_VALUE, COOLDOWN_DURATION_UNIT)) 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 e9c00a7460..e8500af163 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,21 +2,25 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.*; -import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.MessageAction; import net.dv8tion.jda.internal.requests.CompletedRestAction; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.HelpSystemConfig; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.HelpThreads; +import org.togetherjava.tjbot.db.generated.tables.records.HelpThreadsRecord; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.awt.Color; import java.io.InputStream; import java.util.List; +import java.util.Locale; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -48,15 +52,17 @@ public final class HelpSystemHelper { private final Predicate isStagingChannelName; private final String stagingChannelPattern; private final String categoryRoleSuffix; - + private final Database database; /** * Creates a new instance. * * @param config the config to use + * @param database the database to store help thread metadata in */ - public HelpSystemHelper(@NotNull Config config) { + public HelpSystemHelper(Config config, Database database) { HelpSystemConfig helpConfig = config.getHelpSystem(); + this.database = database; overviewChannelPattern = helpConfig.getOverviewChannelPattern(); isOverviewChannelName = Pattern.compile(overviewChannelPattern).asMatchPredicate(); @@ -67,7 +73,8 @@ public HelpSystemHelper(@NotNull Config config) { categoryRoleSuffix = helpConfig.getCategoryRoleSuffix(); } - RestAction sendExplanationMessage(@NotNull MessageChannel threadChannel) { + @Nonnull + RestAction sendExplanationMessage(MessageChannel threadChannel) { boolean useCodeSyntaxExampleImage = true; InputStream codeSyntaxExampleData = AskCommand.class.getResourceAsStream("/" + CODE_SYNTAX_EXAMPLE_PATH); @@ -90,7 +97,9 @@ RestAction sendExplanationMessage(@NotNull MessageChannel threadChannel If nobody is calling back, that usually means that your question was **not well asked** and \ hence nobody feels confident enough answering. Try to use your time to elaborate, \ **provide details**, context, more code, examples and maybe some screenshots. \ - With enough info, someone knows the answer for sure.""")); + 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.")); MessageAction action = threadChannel.sendMessage(message); if (useCodeSyntaxExampleImage) { @@ -99,36 +108,33 @@ RestAction sendExplanationMessage(@NotNull MessageChannel threadChannel return action.setEmbeds(embeds); } - private static @NotNull MessageEmbed embedWith(@NotNull CharSequence message) { + void writeHelpThreadToDatabase(Member author, ThreadChannel threadChannel) { + database.write(content -> { + HelpThreadsRecord helpThreadsRecord = content.newRecord(HelpThreads.HELP_THREADS) + .setAuthorId(author.getIdLong()) + .setChannelId(threadChannel.getIdLong()) + .setCreatedAt(threadChannel.getTimeCreated().toInstant()); + if (helpThreadsRecord.update() == 0) { + helpThreadsRecord.insert(); + } + }); + } + + @Nonnull + private static MessageEmbed embedWith(CharSequence message) { return embedWith(message, null); } - private static @NotNull MessageEmbed embedWith(@NotNull CharSequence message, - @Nullable String imageUrl) { + @Nonnull + private static MessageEmbed embedWith(CharSequence message, @Nullable String imageUrl) { return new EmbedBuilder().setColor(AMBIENT_COLOR) .setDescription(message) .setImage(imageUrl) .build(); } - boolean handleIsHelpThread(@NotNull IReplyCallback event) { - if (event.getChannelType() == ChannelType.GUILD_PUBLIC_THREAD) { - ThreadChannel thread = event.getThreadChannel(); - - if (isOverviewChannelName.test(thread.getParentChannel().getName())) { - return true; - } - } - - event.reply("Sorry, but this command can only be used in a help thread.") - .setEphemeral(true) - .queue(); - - return false; - } - - @NotNull - Optional handleFindRoleForCategory(@NotNull String category, @NotNull Guild guild) { + @Nonnull + Optional handleFindRoleForCategory(String category, Guild guild) { String roleName = category + categoryRoleSuffix; Optional maybeHelperRole = guild.getRolesByName(roleName, true).stream().findAny(); @@ -139,14 +145,13 @@ Optional handleFindRoleForCategory(@NotNull String category, @NotNull Guil return maybeHelperRole; } - @NotNull - Optional getCategoryOfChannel(@NotNull Channel channel) { + @Nonnull + Optional getCategoryOfChannel(Channel channel) { return Optional.ofNullable(HelpThreadName.ofChannelName(channel.getName()).category); } - @NotNull - RestAction renameChannelToCategory(@NotNull GuildChannel channel, - @NotNull String category) { + @Nonnull + RestAction renameChannelToCategory(GuildChannel channel, String category) { HelpThreadName currentName = HelpThreadName.ofChannelName(channel.getName()); HelpThreadName nextName = new HelpThreadName(currentName.activity, category, currentName.title); @@ -154,8 +159,8 @@ RestAction renameChannelToCategory(@NotNull GuildChannel channel, return renameChannel(channel, currentName, nextName); } - @NotNull - RestAction renameChannelToTitle(@NotNull GuildChannel channel, @NotNull String title) { + @Nonnull + RestAction renameChannelToTitle(GuildChannel channel, String title) { HelpThreadName currentName = HelpThreadName.ofChannelName(channel.getName()); HelpThreadName nextName = new HelpThreadName(currentName.activity, currentName.category, title); @@ -163,9 +168,8 @@ RestAction renameChannelToTitle(@NotNull GuildChannel channel, @NotNull St return renameChannel(channel, currentName, nextName); } - @NotNull - RestAction renameChannelToActivity(@NotNull GuildChannel channel, - @NotNull ThreadActivity activity) { + @Nonnull + RestAction renameChannelToActivity(GuildChannel channel, ThreadActivity activity) { HelpThreadName currentName = HelpThreadName.ofChannelName(channel.getName()); HelpThreadName nextName = new HelpThreadName(activity, currentName.category, currentName.title); @@ -173,9 +177,9 @@ RestAction renameChannelToActivity(@NotNull GuildChannel channel, return renameChannel(channel, currentName, nextName); } - @NotNull - private RestAction renameChannel(@NotNull GuildChannel channel, - @NotNull HelpThreadName currentName, @NotNull HelpThreadName nextName) { + @Nonnull + private RestAction renameChannel(GuildChannel channel, HelpThreadName currentName, + HelpThreadName nextName) { if (currentName.equals(nextName)) { // Do not stress rate limits if no actual change is done return new CompletedRestAction<>(channel.getJDA(), null); @@ -184,34 +188,35 @@ private RestAction renameChannel(@NotNull GuildChannel channel, return channel.getManager().setName(nextName.toChannelName()); } - boolean isOverviewChannelName(@NotNull String channelName) { + boolean isOverviewChannelName(String channelName) { return isOverviewChannelName.test(channelName); } - @NotNull + @Nonnull String getOverviewChannelPattern() { return overviewChannelPattern; } - boolean isStagingChannelName(@NotNull String channelName) { + boolean isStagingChannelName(String channelName) { return isStagingChannelName.test(channelName); } - @NotNull + @Nonnull String getStagingChannelPattern() { return stagingChannelPattern; } - static boolean isTitleValid(@NotNull CharSequence title) { + static boolean isTitleValid(CharSequence title) { String titleCompact = TITLE_COMPACT_REMOVAL_PATTERN.matcher(title).replaceAll(""); return titleCompact.length() >= TITLE_COMPACT_LENGTH_MIN - && titleCompact.length() <= TITLE_COMPACT_LENGTH_MAX; + && titleCompact.length() <= TITLE_COMPACT_LENGTH_MAX + && !titleCompact.toLowerCase(Locale.US).contains("help"); } - @NotNull - Optional handleRequireOverviewChannelForAsk(@NotNull Guild guild, - @NotNull MessageChannel respondTo) { + @Nonnull + Optional handleRequireOverviewChannel(Guild guild, + Consumer consumeChannelPatternIfNotFound) { Predicate isChannelName = this::isOverviewChannelName; String channelPattern = getOverviewChannelPattern(); @@ -221,6 +226,16 @@ Optional handleRequireOverviewChannelForAsk(@NotNull Guild guild, .findAny(); if (maybeChannel.isEmpty()) { + consumeChannelPatternIfNotFound.accept(channelPattern); + } + + return maybeChannel; + } + + @Nonnull + Optional handleRequireOverviewChannelForAsk(Guild guild, + MessageChannel respondTo) { + return handleRequireOverviewChannel(guild, channelPattern -> { logger.warn( "Attempted to create a help thread, did not find the overview channel matching the configured pattern '{}' for guild '{}'", channelPattern, guild.getName()); @@ -228,15 +243,21 @@ Optional handleRequireOverviewChannelForAsk(@NotNull Guild guild, respondTo.sendMessage( "Sorry, I was unable to locate the overview channel. The server seems wrongly configured, please contact a moderator.") .queue(); - return Optional.empty(); - } + }); + } - return maybeChannel; + @Nonnull + List getActiveThreadsIn(TextChannel channel) { + return channel.getThreadChannels() + .stream() + .filter(Predicate.not(ThreadChannel::isArchived)) + .toList(); } record HelpThreadName(@Nullable ThreadActivity activity, @Nullable String category, - @NotNull String title) { - static @NotNull HelpThreadName ofChannelName(@NotNull CharSequence channelName) { + String title) { + @Nonnull + static HelpThreadName ofChannelName(CharSequence channelName) { Matcher matcher = EXTRACT_HELP_NAME_PATTERN.matcher(channelName); if (!matcher.matches()) { @@ -253,7 +274,7 @@ record HelpThreadName(@Nullable ThreadActivity activity, @Nullable String catego return new HelpThreadName(activity, category, title); } - @NotNull + @Nonnull String toChannelName() { String activityText = activity == null ? "" : activity.getSymbol() + " "; String categoryText = category == null ? "" : "[%s] ".formatted(category); @@ -269,15 +290,17 @@ enum ThreadActivity { private final String symbol; - ThreadActivity(@NotNull String symbol) { + ThreadActivity(String symbol) { this.symbol = symbol; } - public @NotNull String getSymbol() { + @Nonnull + public String getSymbol() { return symbol; } - static @NotNull ThreadActivity ofSymbol(@NotNull String symbol) { + @Nonnull + static ThreadActivity ofSymbol(String symbol) { return Stream.of(values()) .filter(activity -> activity.getSymbol().equals(symbol)) .findAny() 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 3114410ff4..9cf9a42c30 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 @@ -3,11 +3,11 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.requests.RestAction; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.Routine; +import javax.annotation.Nonnull; import java.util.List; import java.util.Map; import java.util.Optional; @@ -33,22 +33,26 @@ public final class HelpThreadActivityUpdater implements Routine { * * @param helper the helper to use */ - public HelpThreadActivityUpdater(@NotNull HelpSystemHelper helper) { + public HelpThreadActivityUpdater(HelpSystemHelper helper) { this.helper = helper; } @Override - public @NotNull Schedule createSchedule() { + @Nonnull + public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 1, SCHEDULE_MINUTES, TimeUnit.MINUTES); } @Override - public void runRoutine(@NotNull JDA jda) { + public void runRoutine(JDA jda) { jda.getGuildCache().forEach(this::updateActivityForGuild); } - private void updateActivityForGuild(@NotNull Guild guild) { - Optional maybeOverviewChannel = handleRequireOverviewChannel(guild); + private void updateActivityForGuild(Guild guild) { + Optional maybeOverviewChannel = helper + .handleRequireOverviewChannel(guild, channelPattern -> logger.warn( + "Unable to update help thread overview, did not find an overview channel matching the configured pattern '{}' for guild '{}'", + channelPattern, guild.getName())); if (maybeOverviewChannel.isEmpty()) { return; @@ -56,44 +60,22 @@ private void updateActivityForGuild(@NotNull Guild guild) { logger.debug("Updating activities of active questions"); - List activeThreads = maybeOverviewChannel.orElseThrow() - .getThreadChannels() - .stream() - .filter(Predicate.not(ThreadChannel::isArchived)) - .toList(); - + List activeThreads = + helper.getActiveThreadsIn(maybeOverviewChannel.orElseThrow()); logger.debug("Found {} active questions", activeThreads.size()); activeThreads.forEach(this::updateActivityForThread); } - private @NotNull Optional handleRequireOverviewChannel(@NotNull Guild guild) { - Predicate isChannelName = helper::isOverviewChannelName; - String channelPattern = helper.getOverviewChannelPattern(); - - Optional maybeChannel = guild.getTextChannelCache() - .stream() - .filter(channel -> isChannelName.test(channel.getName())) - .findAny(); - - if (maybeChannel.isEmpty()) { - logger.warn( - "Unable to update help thread overview, did not find an overview channel matching the configured pattern '{}' for guild '{}'", - channelPattern, guild.getName()); - return Optional.empty(); - } - - return maybeChannel; - } - - private void updateActivityForThread(@NotNull ThreadChannel threadChannel) { + private void updateActivityForThread(ThreadChannel threadChannel) { determineActivity(threadChannel) .flatMap( threadActivity -> helper.renameChannelToActivity(threadChannel, threadActivity)) .queue(); } - private static @NotNull RestAction determineActivity( + @Nonnull + private static RestAction determineActivity( MessageChannel channel) { return channel.getHistory().retrievePast(ACTIVITY_DETERMINE_MESSAGE_LIMIT).map(messages -> { if (messages.size() >= ACTIVITY_DETERMINE_MESSAGE_LIMIT) { @@ -114,7 +96,7 @@ private void updateActivityForThread(@NotNull ThreadChannel threadChannel) { }); } - private static boolean isBotMessage(@NotNull Message message) { + private static boolean isBotMessage(Message message) { return message.getAuthor().equals(message.getJDA().getSelfUser()); } } 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 new file mode 100644 index 0000000000..b8f3689666 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java @@ -0,0 +1,100 @@ +package org.togetherjava.tjbot.commands.help; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.utils.TimeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.Routine; + +import javax.annotation.Nonnull; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * Routine, which periodically checks all help threads and archives them if there has not been any + * recent activity. + */ +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 final HelpSystemHelper helper; + + /** + * Creates a new instance. + * + * @param helper the helper to use + */ + public HelpThreadAutoArchiver(HelpSystemHelper helper) { + this.helper = helper; + } + + @Override + @Nonnull + public Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 0, SCHEDULE_MINUTES, TimeUnit.MINUTES); + } + + @Override + public void runRoutine(JDA jda) { + jda.getGuildCache().forEach(this::autoArchiveForGuild); + } + + private void autoArchiveForGuild(Guild guild) { + Optional maybeOverviewChannel = helper + .handleRequireOverviewChannel(guild, channelPattern -> logger.warn( + "Unable to auto archive help threads, did not find an overview channel matching the configured pattern '{}' for guild '{}'", + channelPattern, guild.getName())); + + if (maybeOverviewChannel.isEmpty()) { + return; + } + + logger.debug("Auto archiving of help threads"); + + List activeThreads = + helper.getActiveThreadsIn(maybeOverviewChannel.orElseThrow()); + logger.debug("Found {} active questions", activeThreads.size()); + + Instant archiveAfterMoment = computeArchiveAfterMoment(); + activeThreads + .forEach(activeThread -> autoArchiveForThread(activeThread, archiveAfterMoment)); + } + + @Nonnull + private Instant computeArchiveAfterMoment() { + return Instant.now().minus(ARCHIVE_AFTER_INACTIVITY_OF); + } + + private void autoArchiveForThread(ThreadChannel threadChannel, Instant archiveAfterMoment) { + if (shouldBeArchived(threadChannel, archiveAfterMoment)) { + logger.debug("Auto archiving help thread {}", threadChannel.getId()); + + 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 👍""") + .setColor(HelpSystemHelper.AMBIENT_COLOR) + .build(); + + threadChannel.sendMessageEmbeds(embed) + .flatMap(any -> threadChannel.getManager().setArchived(true)) + .queue(); + } + } + + private static boolean shouldBeArchived(MessageChannel channel, Instant archiveAfterMoment) { + Instant lastActivity = + TimeUtil.getTimeCreated(channel.getLatestMessageIdLong()).toInstant(); + + return lastActivity.isBefore(archiveAfterMoment); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadMetadataPurger.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadMetadataPurger.java new file mode 100644 index 0000000000..4393b837be --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadMetadataPurger.java @@ -0,0 +1,50 @@ +package org.togetherjava.tjbot.commands.help; + +import net.dv8tion.jda.api.JDA; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.Routine; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.HelpThreads; + +import javax.annotation.Nonnull; +import java.time.Instant; +import java.time.Period; +import java.util.concurrent.TimeUnit; + +/** + * Purge Routine to get rid of old thread creations in the database. + */ +public class HelpThreadMetadataPurger implements Routine { + private final Database database; + private static final Logger logger = LoggerFactory.getLogger(HelpThreadMetadataPurger.class); + private static final Period DELETE_MESSAGE_RECORDS_AFTER = Period.ofDays(30); + + /** + * Creates a new instance. + * + * @param database the database used to purge help thread metadata + */ + public HelpThreadMetadataPurger(Database database) { + this.database = database; + } + + @Override + @Nonnull + public Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 0, 4, TimeUnit.HOURS); + } + + @Override + public void runRoutine(JDA jda) { + int recordsDeleted = + database.writeAndProvide(content -> content.deleteFrom(HelpThreads.HELP_THREADS)) + .where(HelpThreads.HELP_THREADS.CREATED_AT + .lessOrEqual(Instant.now().minus(DELETE_MESSAGE_RECORDS_AFTER))) + .execute(); + if (recordsDeleted > 0) { + logger.debug("{} old thread channels deleted because they are older than {}.", + recordsDeleted, DELETE_MESSAGE_RECORDS_AFTER); + } + } +} 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 aaac70c55c..0905c4991b 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 @@ -6,29 +6,28 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.internal.requests.CompletedRestAction; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; import org.togetherjava.tjbot.commands.Routine; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Provides and updates an overview of all active questions in an overview channel. - * + *

* The process runs on a schedule, but is also triggered whenever a new question has been asked in * the staging channel. - * + *

* Active questions are automatically picked up and grouped by categories. */ public final class HelpThreadOverviewUpdater extends MessageReceiverAdapter implements Routine { @@ -52,7 +51,7 @@ public final class HelpThreadOverviewUpdater extends MessageReceiverAdapter impl * @param config the config to use * @param helper the helper to use */ - public HelpThreadOverviewUpdater(@NotNull Config config, @NotNull HelpSystemHelper helper) { + public HelpThreadOverviewUpdater(Config config, HelpSystemHelper helper) { super(Pattern.compile(config.getHelpSystem().getOverviewChannelPattern())); allCategories = config.getHelpSystem().getCategories(); @@ -60,17 +59,18 @@ public HelpThreadOverviewUpdater(@NotNull Config config, @NotNull HelpSystemHelp } @Override - public @NotNull Schedule createSchedule() { + @Nonnull + public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 1, 1, TimeUnit.MINUTES); } @Override - public void runRoutine(@NotNull JDA jda) { + public void runRoutine(JDA jda) { jda.getGuildCache().forEach(this::updateOverviewForGuild); } @Override - public void onMessageReceived(@NotNull MessageReceivedEvent event) { + public void onMessageReceived(MessageReceivedEvent event) { // Update whenever a thread was created Message message = event.getMessage(); if (message.getType() != MessageType.THREAD_CREATED) { @@ -95,8 +95,11 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { UPDATE_SERVICE.schedule(updateOverviewCommand, 2, TimeUnit.SECONDS); } - private void updateOverviewForGuild(@NotNull Guild guild) { - Optional maybeOverviewChannel = handleRequireOverviewChannel(guild); + private void updateOverviewForGuild(Guild guild) { + Optional maybeOverviewChannel = helper + .handleRequireOverviewChannel(guild, channelPattern -> logger.warn( + "Unable to update help thread overview, did not find an overview channel matching the configured pattern '{}' for guild '{}'", + channelPattern, guild.getName())); if (maybeOverviewChannel.isEmpty()) { return; @@ -105,33 +108,10 @@ private void updateOverviewForGuild(@NotNull Guild guild) { updateOverview(maybeOverviewChannel.orElseThrow()); } - private @NotNull Optional handleRequireOverviewChannel(@NotNull Guild guild) { - Predicate isChannelName = helper::isOverviewChannelName; - String channelPattern = helper.getOverviewChannelPattern(); - - Optional maybeChannel = guild.getTextChannelCache() - .stream() - .filter(channel -> isChannelName.test(channel.getName())) - .findAny(); - - if (maybeChannel.isEmpty()) { - logger.warn( - "Unable to update help thread overview, did not find an overview channel matching the configured pattern '{}' for guild '{}'", - channelPattern, guild.getName()); - return Optional.empty(); - } - - return maybeChannel; - } - - private void updateOverview(@NotNull TextChannel overviewChannel) { + private void updateOverview(TextChannel overviewChannel) { logger.debug("Updating overview of active questions"); - List activeThreads = overviewChannel.getThreadChannels() - .stream() - .filter(Predicate.not(ThreadChannel::isArchived)) - .toList(); - + List activeThreads = helper.getActiveThreadsIn(overviewChannel); logger.debug("Found {} active questions", activeThreads.size()); Message message = new MessageBuilder() @@ -144,7 +124,8 @@ private void updateOverview(@NotNull TextChannel overviewChannel) { .queue(); } - private @NotNull String createDescription(@NotNull Collection activeThreads) { + @Nonnull + private String createDescription(Collection activeThreads) { if (activeThreads.isEmpty()) { return "Currently none."; } @@ -169,8 +150,8 @@ private void updateOverview(@NotNull TextChannel overviewChannel) { .collect(Collectors.joining("\n\n")); } - private static @NotNull RestAction> getStatusMessage( - @NotNull MessageChannel channel) { + @Nonnull + private static RestAction> getStatusMessage(MessageChannel channel) { return channel.getHistory() .retrievePast(1) .map(messages -> messages.stream() @@ -178,7 +159,7 @@ private void updateOverview(@NotNull TextChannel overviewChannel) { .filter(HelpThreadOverviewUpdater::isStatusMessage)); } - private static boolean isStatusMessage(@NotNull Message message) { + private static boolean isStatusMessage(Message message) { if (!message.getAuthor().equals(message.getJDA().getSelfUser())) { return false; } @@ -187,8 +168,9 @@ private static boolean isStatusMessage(@NotNull Message message) { return content.startsWith(STATUS_TITLE); } - private @NotNull RestAction sendUpdatedOverview(@Nullable Message statusMessage, - @NotNull Message updatedStatusMessage, @NotNull MessageChannel overviewChannel) { + @Nonnull + private RestAction sendUpdatedOverview(@Nullable Message statusMessage, + Message updatedStatusMessage, MessageChannel overviewChannel) { logger.debug("Sending the updated question overview"); if (statusMessage == null) { int currentFailures = FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.incrementAndGet(); @@ -211,8 +193,7 @@ private static boolean isStatusMessage(@NotNull Message message) { return overviewChannel.editMessageById(statusMessageId, updatedStatusMessage); } - private record CategoryWithThreads(@NotNull String category, - @NotNull List threads) { + private record CategoryWithThreads(String category, List threads) { String toDiscordString() { String threadListText = threads.stream() @@ -222,8 +203,9 @@ String toDiscordString() { return "**%s**:%n%s".formatted(category, threadListText); } - static @NotNull CategoryWithThreads ofEntry( - Map.@NotNull Entry> categoryAndThreads) { + @Nonnull + static CategoryWithThreads ofEntry( + Map.Entry> categoryAndThreads) { return new CategoryWithThreads(categoryAndThreads.getKey(), categoryAndThreads.getValue()); } 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 3652ef0a4a..3a9c37452d 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 @@ -10,12 +10,12 @@ import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.MessageAction; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Optional; @@ -58,7 +58,7 @@ public final class ImplicitAskListener extends MessageReceiverAdapter { * @param config the config to use * @param helper the helper to use */ - public ImplicitAskListener(@NotNull Config config, @NotNull HelpSystemHelper helper) { + public ImplicitAskListener(Config config, HelpSystemHelper helper) { super(Pattern.compile(config.getHelpSystem().getStagingChannelPattern())); userIdToLastHelpThread = Caffeine.newBuilder() @@ -70,7 +70,7 @@ public ImplicitAskListener(@NotNull Config config, @NotNull HelpSystemHelper hel } @Override - public void onMessageReceived(@NotNull MessageReceivedEvent event) { + public void onMessageReceived(MessageReceivedEvent event) { // Only listen to regular messages from users if (event.isWebhookMessage() || event.getMessage().getType() != MessageType.DEFAULT || event.getAuthor().isBot()) { @@ -101,7 +101,7 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { }, ImplicitAskListener::handleFailure); } - private boolean handleIsNotOnCooldown(@NotNull Message message) { + private boolean handleIsNotOnCooldown(Message message) { Member author = message.getMember(); Optional maybeLastHelpThread = @@ -125,6 +125,7 @@ private boolean handleIsNotOnCooldown(@NotNull Message message) { return false; } + @Nonnull private Optional getLastHelpThreadIfOnCooldown(long userId) { return Optional.ofNullable(userIdToLastHelpThread.getIfPresent(userId)) .filter(lastHelpThread -> { @@ -136,7 +137,8 @@ private Optional getLastHelpThreadIfOnCooldown(long userId) { }); } - private static @NotNull String createTitle(@NotNull String message) { + @Nonnull + private static String createTitle(String message) { String titleCandidate; if (message.length() < TITLE_MAX_LENGTH) { titleCandidate = message; @@ -154,9 +156,10 @@ private Optional getLastHelpThreadIfOnCooldown(long userId) { return HelpSystemHelper.isTitleValid(titleCandidate) ? titleCandidate : "Untitled"; } - private @NotNull RestAction handleEvent(@NotNull ThreadChannel threadChannel, - @NotNull Message message, @NotNull String title) { + @Nonnull + private RestAction handleEvent(ThreadChannel threadChannel, Message message, String title) { Member author = message.getMember(); + helper.writeHelpThreadToDatabase(author, threadChannel); userIdToLastHelpThread.put(author.getIdLong(), new HelpThread(threadChannel.getIdLong(), author.getIdLong(), Instant.now())); @@ -166,13 +169,15 @@ private Optional getLastHelpThreadIfOnCooldown(long userId) { .flatMap(any -> helper.sendExplanationMessage(threadChannel)); } - private static @NotNull RestAction inviteUsersToThread( - @NotNull ThreadChannel threadChannel, @NotNull Member author) { + @Nonnull + private static RestAction inviteUsersToThread(ThreadChannel threadChannel, + Member author) { return threadChannel.addThreadMember(author); } - private static @NotNull MessageAction sendInitialMessage(@NotNull ThreadChannel threadChannel, - @NotNull Message originalMessage, @NotNull String title) { + @Nonnull + private static MessageAction sendInitialMessage(ThreadChannel threadChannel, + Message originalMessage, String title) { String content = originalMessage.getContentRaw(); Member author = originalMessage.getMember(); @@ -193,8 +198,8 @@ private Optional getLastHelpThreadIfOnCooldown(long userId) { return threadChannel.sendMessage(threadMessage); } - private static @NotNull MessageAction notifyUser(@NotNull IMentionable threadChannel, - @NotNull Message message) { + @Nonnull + private static MessageAction notifyUser(IMentionable threadChannel, Message message) { return message.getChannel() .sendMessage( """ @@ -204,7 +209,7 @@ private Optional getLastHelpThreadIfOnCooldown(long userId) { threadChannel.getAsMention())); } - private static void handleFailure(@NotNull Throwable exception) { + private static void handleFailure(Throwable exception) { if (exception instanceof ErrorResponseException responseException) { ErrorResponse response = responseException.getErrorResponse(); if (response == ErrorResponse.MAX_CHANNELS @@ -216,6 +221,6 @@ private static void handleFailure(@NotNull Throwable exception) { logger.error("Attempted to create a help thread, but failed", exception); } - private record HelpThread(long channelId, long authorId, @NotNull Instant creationTime) { + private record HelpThread(long channelId, long authorId, Instant creationTime) { } } 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 new file mode 100644 index 0000000000..3da99a69e7 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java @@ -0,0 +1,66 @@ +package org.togetherjava.tjbot.commands.help; + +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.guild.member.GuildMemberRemoveEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.EventReceiver; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.HelpThreads; + +import java.util.HashSet; +import java.util.Set; + +/** + * Remove all thread channels associated to a user when they leave the guild. + */ +public class OnGuildLeaveCloseThreadListener extends ListenerAdapter implements EventReceiver { + private static final Logger logger = + LoggerFactory.getLogger(OnGuildLeaveCloseThreadListener.class); + private final Database database; + + /** + * Creates a new instance. + * + * @param database database to use + */ + public OnGuildLeaveCloseThreadListener(Database database) { + this.database = database; + } + + @Override + public void onGuildMemberRemove(GuildMemberRemoveEvent leaveEvent) { + Set channelIds = getThreadsCreatedByLeaver(leaveEvent.getUser().getIdLong()); + for (long channelId : channelIds) { + closeThread(channelId, leaveEvent); + } + } + + private Set getThreadsCreatedByLeaver(long leaverId) { + return new HashSet<>(database + .readTransaction(context -> context.select(HelpThreads.HELP_THREADS.CHANNEL_ID)) + .from(HelpThreads.HELP_THREADS) + .where(HelpThreads.HELP_THREADS.AUTHOR_ID.eq(leaverId)) + .fetch(databaseMapper -> databaseMapper.getValue(HelpThreads.HELP_THREADS.CHANNEL_ID))); + } + + private void closeThread(long channelId, GuildMemberRemoveEvent leaveEvent) { + ThreadChannel threadChannel = leaveEvent.getGuild().getThreadChannelById(channelId); + if (threadChannel == null) { + logger.warn( + "Attempted to archive thread id: '{}' but could not find thread in guild: '{}'.", + channelId, leaveEvent.getGuild().getName()); + return; + } + MessageEmbed embed = new EmbedBuilder().setTitle("OP left") + .setDescription("Closing thread...") + .setColor(HelpSystemHelper.AMBIENT_COLOR) + .build(); + threadChannel.sendMessageEmbeds(embed) + .flatMap(any -> threadChannel.getManager().setArchived(true)) + .queue(); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/package-info.java index d6deedbaf4..351316091a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/package-info.java @@ -2,4 +2,7 @@ * This package offers all functionality for the help system. For example commands that let users * ask questions, such as {@link org.togetherjava.tjbot.commands.help.AskCommand}. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.help; + +import javax.annotation.ParametersAreNonnullByDefault; 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 00d1037c57..2b52137b76 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,7 +6,6 @@ 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 org.jetbrains.annotations.NotNull; import org.scilab.forge.jlatexmath.ParseException; import org.scilab.forge.jlatexmath.TeXConstants; import org.scilab.forge.jlatexmath.TeXFormula; @@ -15,6 +14,7 @@ import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import javax.annotation.Nonnull; import javax.imageio.ImageIO; import java.awt.Color; import java.awt.Image; @@ -62,7 +62,7 @@ public TeXCommand() { } @Override - public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { + public void onSlashCommand(final SlashCommandInteractionEvent event) { String latex = Objects.requireNonNull(event.getOption(LATEX_OPTION)).getAsString(); String userID = (Objects.requireNonNull(event.getMember()).getId()); TeXFormula formula; @@ -97,7 +97,8 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { } } - private @NotNull Image renderImage(@NotNull TeXFormula formula) { + @Nonnull + private Image renderImage(TeXFormula formula) { Image image = formula.createBufferedImage(TeXConstants.STYLE_DISPLAY, DEFAULT_IMAGE_SIZE, FOREGROUND_COLOR, BACKGROUND_COLOR); @@ -107,8 +108,8 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { return image; } - private void sendImage(@NotNull IDeferrableCallback event, @NotNull String userID, - @NotNull Image image) throws IOException { + private void sendImage(IDeferrableCallback event, String userID, Image image) + throws IOException { ByteArrayOutputStream renderedTextImageStream = getRenderedTextImageStream(image); event.getHook() .editOriginal(renderedTextImageStream.toByteArray(), "tex.png") @@ -116,9 +117,8 @@ private void sendImage(@NotNull IDeferrableCallback event, @NotNull String userI .queue(); } - @NotNull - private ByteArrayOutputStream getRenderedTextImageStream(@NotNull Image image) - throws IOException { + @Nonnull + private ByteArrayOutputStream getRenderedTextImageStream(Image image) throws IOException { BufferedImage renderedTextImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_4BYTE_ABGR); @@ -137,8 +137,8 @@ private ByteArrayOutputStream getRenderedTextImageStream(@NotNull Image image) * @param latex the latex to convert * @return the converted latex */ - @NotNull - private String convertInlineLatexToFull(@NotNull String latex) { + @Nonnull + private String convertInlineLatexToFull(String latex) { if (isInvalidInlineFormat(latex)) { throw new ParseException(INVALID_INLINE_FORMAT_ERROR_MESSAGE); } @@ -158,13 +158,12 @@ private String convertInlineLatexToFull(@NotNull String latex) { return sb.toString(); } - private boolean isInvalidInlineFormat(@NotNull String latex) { + private boolean isInvalidInlineFormat(String latex) { return latex.chars().filter(charAsInt -> charAsInt == '$').count() % 2 == 1; } @Override - public void onButtonClick(@NotNull final ButtonInteractionEvent event, - @NotNull final List args) { + public void onButtonClick(final ButtonInteractionEvent event, final List args) { if (!args.get(0).equals(Objects.requireNonNull(event.getMember()).getId())) { event.reply("You are not the person who executed the command, you cannot do that") .setEphemeral(true) 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 90109410c2..8c650b60dc 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 @@ -6,7 +6,6 @@ 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.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; @@ -39,7 +38,7 @@ public final class WolframAlphaCommand extends SlashCommandAdapter { * * @param config the config to use */ - public WolframAlphaCommand(@NotNull Config config) { + public WolframAlphaCommand(Config config) { super("wolfram-alpha", "Renders mathematical queries using WolframAlpha", SlashCommandVisibility.GUILD); getData().addOption(OptionType.STRING, QUERY_OPTION, "the query to send to WolframAlpha", @@ -48,7 +47,7 @@ public WolframAlphaCommand(@NotNull Config config) { } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { String query = event.getOption(QUERY_OPTION).getAsString(); WolframAlphaHandler handler = new WolframAlphaHandler(query); @@ -73,8 +72,8 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { .thenAccept(response -> sendResponse(response, event)); } - private static void sendResponse(@NotNull WolframAlphaHandler.HandlerResponse response, - @NotNull IDeferrableCallback event) { + private static void sendResponse(WolframAlphaHandler.HandlerResponse response, + IDeferrableCallback event) { WebhookMessageUpdateAction action = event.getHook().editOriginalEmbeds(response.embeds()); 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 7362501427..753e2ddc70 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 @@ -4,12 +4,12 @@ import io.mikael.urlbuilder.UrlBuilder; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.mathcommands.wolframalpha.api.Error; import org.togetherjava.tjbot.commands.mathcommands.wolframalpha.api.*; +import javax.annotation.Nonnull; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; @@ -58,7 +58,7 @@ final class WolframAlphaHandler { * * @param query the original query send to the API */ - WolframAlphaHandler(@NotNull String query) { + WolframAlphaHandler(String query) { this.query = query; userApiQuery = UrlBuilder.fromString(USER_API_ENDPOINT) @@ -73,8 +73,8 @@ final class WolframAlphaHandler { * @param apiResponse response of the Wolfram Alpha API query * @return user-friendly message for display, as list of embeds */ - @NotNull - HandlerResponse handleApiResponse(@NotNull HttpResponse apiResponse) { + @Nonnull + HandlerResponse handleApiResponse(HttpResponse apiResponse) { // Check status code int statusCode = apiResponse.statusCode(); if (statusCode != HttpURLConnection.HTTP_OK) { @@ -111,7 +111,8 @@ HandlerResponse handleApiResponse(@NotNull HttpResponse apiResponse) { return handleSuccessfulResponse(queryResult); } - private @NotNull HandlerResponse handleMisunderstoodQuery(@NotNull QueryResult result) { + @Nonnull + private HandlerResponse handleMisunderstoodQuery(QueryResult result) { StringJoiner output = new StringJoiner("\n"); output.add("Sorry, I did not understand your query."); @@ -151,7 +152,8 @@ HandlerResponse handleApiResponse(@NotNull HttpResponse apiResponse) { return responseOf(output.toString()); } - private static @NotNull String createBulletPointList(Collection elements, + @Nonnull + private static String createBulletPointList(Collection elements, Function elementToText) { return elements.stream() .map(elementToText) @@ -159,7 +161,8 @@ HandlerResponse handleApiResponse(@NotNull HttpResponse apiResponse) { .collect(Collectors.joining("\n")); } - private @NotNull HandlerResponse handleSuccessfulResponse(@NotNull QueryResult queryResult) { + @Nonnull + private HandlerResponse handleSuccessfulResponse(QueryResult queryResult) { StringJoiner messages = new StringJoiner("\n\n"); messages.add("Click the link to see full results."); @@ -198,7 +201,8 @@ HandlerResponse handleApiResponse(@NotNull HttpResponse apiResponse) { return responseOf(messages.toString(), tilesToDisplay); } - private @NotNull HandlerResponse responseOf(@NotNull CharSequence text) { + @Nonnull + private HandlerResponse responseOf(CharSequence text) { MessageEmbed embed = new EmbedBuilder().setTitle(buildTitle(), userApiQuery) .setDescription(text) .setColor(AMBIENT_COLOR) @@ -207,8 +211,9 @@ HandlerResponse handleApiResponse(@NotNull HttpResponse apiResponse) { return new HandlerResponse(List.of(embed), List.of()); } - private @NotNull HandlerResponse responseOf(@NotNull CharSequence text, - @NotNull Collection tiles) { + @Nonnull + private HandlerResponse responseOf(CharSequence text, + Collection tiles) { List embeds = new ArrayList<>(); embeds.add(new EmbedBuilder().setTitle(buildTitle(), userApiQuery) .setDescription(text) @@ -232,16 +237,16 @@ HandlerResponse handleApiResponse(@NotNull HttpResponse apiResponse) { return new HandlerResponse(embeds, attachments); } - private @NotNull String buildTitle() { + @Nonnull + private String buildTitle() { return query + " - " + SERVICE_NAME; } - record HandlerResponse(@NotNull List embeds, - @NotNull List attachments) { + record HandlerResponse(List embeds, List attachments) { } - record Attachment(@NotNull String name, byte @NotNull [] data) { + record Attachment(String name, byte[] data) { @Override public boolean equals(Object o) { if (this == o) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaImages.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaImages.java index 00c319ec6f..3adbbf9ed7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaImages.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaImages.java @@ -1,9 +1,9 @@ package org.togetherjava.tjbot.commands.mathcommands.wolframalpha; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.mathcommands.wolframalpha.api.SubPod; import org.togetherjava.tjbot.commands.mathcommands.wolframalpha.api.WolframAlphaImage; +import javax.annotation.Nonnull; import javax.imageio.ImageIO; import java.awt.Color; import java.awt.Font; @@ -25,8 +25,7 @@ * Utility class to work with images returned by the Wolfram Alpha API. For example to render and * combine them. */ -enum WolframAlphaImages { - ; +class WolframAlphaImages { static final String IMAGE_FORMAT = "png"; private static final Color IMAGE_BACKGROUND = Color.WHITE; private static final int IMAGE_MARGIN_PX = 10; @@ -35,9 +34,13 @@ enum WolframAlphaImages { new FontRenderContext(new AffineTransform(), true, true); private static final Color TITLE_COLOR = Color.decode("#3C3C3C"); private static final Font TITLE_FONT = new Font("Arial", Font.BOLD, 15); - private static final int TITLE_HEIGHT_PX = 20; - static @NotNull BufferedImage renderTitle(@NotNull String title) { + private WolframAlphaImages() { + throw new UnsupportedOperationException("Utility class, construction not supported"); + } + + @Nonnull + static BufferedImage renderTitle(String title) { Rectangle2D titleBounds = TITLE_FONT.getStringBounds(title, TITLE_RENDER_CONTEXT); int widthPx = (int) Math.ceil(titleBounds.getWidth()) + 2 * IMAGE_MARGIN_PX; int heightPx = (int) Math.ceil(titleBounds.getHeight()) + IMAGE_MARGIN_PX; @@ -53,7 +56,8 @@ enum WolframAlphaImages { return image; } - static @NotNull BufferedImage renderSubPod(@NotNull SubPod subPod) { + @Nonnull + static BufferedImage renderSubPod(SubPod subPod) { WolframAlphaImage sourceImage = subPod.getImage(); int widthPx = sourceImage.getWidth() + 2 * IMAGE_MARGIN_PX; @@ -73,12 +77,14 @@ enum WolframAlphaImages { return destinationImage; } - static @NotNull BufferedImage renderFooter() { + @Nonnull + static BufferedImage renderFooter() { return new BufferedImage(1, IMAGE_MARGIN_PX, BufferedImage.TYPE_4BYTE_ABGR); } - static @NotNull List combineImagesIntoTiles( - @NotNull Collection images, int maxTileHeight) { + @Nonnull + static List combineImagesIntoTiles(Collection images, + int maxTileHeight) { if (images.isEmpty()) { throw new IllegalArgumentException("Images must not be empty"); } @@ -118,8 +124,9 @@ private static boolean wouldTileBeTooLargeIfAddingImage(int tileHeight, int heig return tileHeight != 0 && tileHeight + heightOfImageToAdd > maxTileHeight; } - private static @NotNull BufferedImage combineImages( - @NotNull Collection images, int widthPx) { + @Nonnull + private static BufferedImage combineImages(Collection images, + int widthPx) { if (images.isEmpty()) { throw new IllegalArgumentException("Images must not be empty"); } @@ -144,7 +151,8 @@ private static boolean wouldTileBeTooLargeIfAddingImage(int tileHeight, int heig return destinationImage; } - static byte @NotNull [] imageToBytes(@NotNull RenderedImage img) { + @Nonnull + static byte[] imageToBytes(RenderedImage img) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { ImageIO.write(img, IMAGE_FORMAT, outputStream); return outputStream.toByteArray(); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/api/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/api/package-info.java index f4513dbb6d..eb5e9633c4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/api/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/api/package-info.java @@ -3,4 +3,7 @@ * WolframAlpha * API. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.mathcommands.wolframalpha.api; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/package-info.java index d864dfb435..eb9db3d28a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/package-info.java @@ -2,4 +2,7 @@ * This packages offers all the functionality for the wolfram-alpha command. Sending queries to * their official API, rendering results and displaying them. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.mathcommands.wolframalpha; + +import javax.annotation.ParametersAreNonnullByDefault; 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 new file mode 100644 index 0000000000..b67b6031cf --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java @@ -0,0 +1,80 @@ +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.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; +import org.togetherjava.tjbot.commands.MessageReceiverAdapter; +import org.togetherjava.tjbot.config.Config; + +import javax.annotation.Nonnull; +import java.awt.Color; +import java.util.regex.Pattern; + +/** + * Listener that receives all sent messages from the Media Only Channels, checks if the message has + * media attached. + *

+ * If there was no media attached, delete the message and send the user a DM, telling what they did + * wrong. + */ +public final class MediaOnlyChannelListener extends MessageReceiverAdapter { + + /** + * Creates a MediaOnlyChannelListener to receive all message sent in MediaOnly channel. + * + * @param config to find MediaOnly channels + */ + public MediaOnlyChannelListener(Config config) { + super(Pattern.compile(config.getMediaOnlyChannelPattern())); + } + + + @Override + public void onMessageReceived(MessageReceivedEvent event) { + if (event.getAuthor().isBot() || event.isWebhookMessage()) { + return; + } + + if (messageHasNoMediaAttached(event)) { + deleteMessage(event).flatMap(any -> dmUser(event)).queue(); + } + } + + private static boolean messageHasNoMediaAttached(MessageReceivedEvent event) { + Message message = event.getMessage(); + return message.getAttachments().isEmpty() && message.getEmbeds().isEmpty() + && !message.getContentRaw().contains("http"); + } + + @Nonnull + private AuditableRestAction deleteMessage(MessageReceivedEvent event) { + return event.getMessage().delete(); + } + + @Nonnull + private RestAction dmUser(MessageReceivedEvent event) { + return dmUser(event.getMessage()); + } + + @Nonnull + private RestAction dmUser(Message originalMessage) { + String originalMessageContent = originalMessage.getContentRaw(); + MessageEmbed originalMessageEmbed = + new EmbedBuilder().setDescription(originalMessageContent) + .setColor(Color.ORANGE) + .build(); + + Message dmMessage = new MessageBuilder( + "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(); + + return originalMessage.getAuthor() + .openPrivateChannel() + .flatMap(channel -> channel.sendMessage(dmMessage)); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ActionRecord.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ActionRecord.java index 910b7c8663..f7ca36833a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ActionRecord.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ActionRecord.java @@ -1,9 +1,9 @@ package org.togetherjava.tjbot.commands.moderation; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.togetherjava.tjbot.db.generated.tables.records.ModerationActionsRecord; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.time.Instant; /** @@ -20,9 +20,8 @@ * {@code null} * @param reason the reason why this action was executed */ -public record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId, long authorId, - long targetId, @NotNull ModerationAction actionType, @Nullable Instant actionExpiresAt, - @NotNull String reason) { +public record ActionRecord(int caseId, Instant issuedAt, long guildId, long authorId, long targetId, + ModerationAction actionType, @Nullable Instant actionExpiresAt, String reason) { /** * Creates the action record that corresponds to the given action entry from the database table. @@ -30,8 +29,8 @@ public record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId, * @param action the action to convert * @return the corresponding action record */ - @SuppressWarnings("StaticMethodOnlyUsedInOneClass") - static @NotNull ActionRecord of(@NotNull ModerationActionsRecord action) { + @Nonnull + static ActionRecord of(ModerationActionsRecord action) { return new ActionRecord(action.getCaseId(), action.getIssuedAt(), action.getGuildId(), action.getAuthorId(), action.getTargetId(), ModerationAction.valueOf(action.getActionType()), action.getActionExpiresAt(), 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 6097286232..c59884b593 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 @@ -11,21 +11,17 @@ 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 org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - +import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; -import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.time.Instant; import java.time.ZoneOffset; import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -39,40 +35,108 @@ public final class AuditCommand extends SlashCommandAdapter { private static final String TARGET_OPTION = "user"; private static final String COMMAND_NAME = "audit"; private static final String ACTION_VERB = "audit"; - private static final int MAX_PAGE_LENGTH = 25; + private static final int MAX_PAGE_LENGTH = 10; private static final String PREVIOUS_BUTTON_LABEL = "⬅"; private static final String NEXT_BUTTON_LABEL = "➡"; - private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; /** * Constructs an instance. - * + * * @param actionsStore used to store actions issued by this command - * @param config the config to use for this */ - public AuditCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public AuditCommand(ModerationActionsStore actionsStore) { super(COMMAND_NAME, "Lists all moderation actions that have been taken against a user", SlashCommandVisibility.GUILD); getData().addOption(OptionType.USER, TARGET_OPTION, "The user who to retrieve actions for", true); - hasRequiredRole = - Pattern.compile(config.getHeavyModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private static @NotNull EmbedBuilder createSummaryEmbed(@NotNull User user, - @NotNull Collection actions) { + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + OptionMapping targetOption = + Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null"); + User target = targetOption.getAsUser(); + Member author = Objects.requireNonNull(event.getMember(), "The author is null"); + + Guild guild = Objects.requireNonNull(event.getGuild()); + Member bot = guild.getSelfMember(); + + if (!handleChecks(bot, author, targetOption.getAsMember(), event)) { + return; + } + + auditUser(guild.getIdLong(), target.getIdLong(), event.getMember().getIdLong(), -1, + event.getJDA()).flatMap(event::reply).queue(); + } + + private boolean handleChecks(Member bot, Member author, @Nullable Member target, + IReplyCallback event) { + // Member doesn't exist if attempting to audit a user who is not part of the guild. + if (target == null) { + return true; + } + return ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, target, event); + } + + /** + * @param pageNumber page number to display when actions are divided into pages and each page + * can contain {@link AuditCommand#MAX_PAGE_LENGTH} actions, {@code -1} encodes the last + * page + */ + @Nonnull + private RestAction auditUser(long guildId, long targetId, long callerId, + int pageNumber, JDA jda) { + List actions = actionsStore.getActionsByTargetAscending(guildId, targetId); + List> groupedActions = groupActionsByPages(actions); + int totalPages = groupedActions.size(); + + int pageNumberInLimits; + if (pageNumber == -1) { + pageNumberInLimits = totalPages; + } else { + pageNumberInLimits = clamp(1, pageNumber, totalPages); + } + + return jda.retrieveUserById(targetId) + .map(user -> createSummaryEmbed(user, actions)) + .flatMap(auditEmbed -> attachEmbedFields(auditEmbed, groupedActions, pageNumberInLimits, + totalPages, jda)) + .map(auditEmbed -> attachPageTurnButtons(auditEmbed, pageNumberInLimits, totalPages, + guildId, targetId, callerId)); + } + + @Nonnull + private List> groupActionsByPages(List actions) { + List> groupedActions = new ArrayList<>(); + for (int i = 0; i < actions.size(); i++) { + if (i % AuditCommand.MAX_PAGE_LENGTH == 0) { + groupedActions.add(new ArrayList<>(AuditCommand.MAX_PAGE_LENGTH)); + } + + groupedActions.get(groupedActions.size() - 1).add(actions.get(i)); + } + + return groupedActions; + } + + private static int clamp(int minInclusive, int value, int maxInclusive) { + return Math.min(Math.max(minInclusive, value), maxInclusive); + } + + @Nonnull + private static EmbedBuilder createSummaryEmbed(User user, Collection actions) { return new EmbedBuilder().setTitle("Audit log of **%s**".formatted(user.getAsTag())) .setAuthor(user.getName(), null, user.getAvatarUrl()) .setDescription(createSummaryMessageDescription(actions)) .setColor(ModerationUtils.AMBIENT_COLOR); } - private static @NotNull String createSummaryMessageDescription( - @NotNull Collection actions) { + @Nonnull + private static String createSummaryMessageDescription(Collection actions) { int actionAmount = actions.size(); String shortSummary = "There are **%s actions** against the user." @@ -97,135 +161,99 @@ public AuditCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Confi return shortSummary + "\n" + typeCountSummary; } - private static @NotNull MessageEmbed.Field actionToField(@NotNull ActionRecord action, - @NotNull JDA jda) { - Function formatTime = instant -> { - if (instant == null) { - return ""; - } - return TimeUtil.getDateTimeString(instant.atOffset(ZoneOffset.UTC)); - }; - - User author = jda.getUserById(action.authorId()); - - Instant expiresAt = action.actionExpiresAt(); - String expiresAtFormatted = expiresAt == null ? "" - : "\nTemporary action, expires at: " + formatTime.apply(expiresAt); - - return new MessageEmbed.Field( - action.actionType().name() + " by " - + (author == null ? "(unknown user)" : author.getAsTag()), - action.reason() + "\nIssued at: " + formatTime.apply(action.issuedAt()) - + expiresAtFormatted, - false); - } - - @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { - OptionMapping targetOption = - Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null"); - User target = targetOption.getAsUser(); - Member author = Objects.requireNonNull(event.getMember(), "The author is null"); + @Nonnull + private RestAction attachEmbedFields(EmbedBuilder auditEmbed, + List> groupedActions, int pageNumber, int totalPages, + JDA jda) { + if (groupedActions.isEmpty()) { + return new CompletedRestAction<>(jda, auditEmbed); + } - Guild guild = Objects.requireNonNull(event.getGuild()); - Member bot = guild.getSelfMember(); + List> embedFieldTasks = new ArrayList<>(); + groupedActions.get(pageNumber - 1) + .forEach(action -> embedFieldTasks.add(actionToField(action, jda))); - if (!handleChecks(bot, author, targetOption.getAsMember(), event)) { - return; - } + return RestAction.allOf(embedFieldTasks).map(embedFields -> { + embedFields.forEach(auditEmbed::addField); - event - .reply(auditUser(guild.getIdLong(), target.getIdLong(), event.getMember().getIdLong(), - 1, event.getJDA())) - .queue(); + auditEmbed.setFooter("Page: " + pageNumber + "/" + totalPages); + return auditEmbed; + }); } - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, @NotNull IReplyCallback event) { - // Member doesn't exist if attempting to audit a user who is not part of the guild. - if (target != null && !ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, - target, event)) { - return false; - } - return ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event); + @Nonnull + private static RestAction actionToField(ActionRecord action, JDA jda) { + return jda.retrieveUserById(action.authorId()) + .map(author -> author == null ? "(unknown user)" : author.getAsTag()) + .map(authorText -> { + String expiresAtFormatted = action.actionExpiresAt() == null ? "" + : "\nTemporary action, expires at: " + formatTime(action.actionExpiresAt()); + + String fieldName = "%s by %s".formatted(action.actionType().name(), authorText); + String fieldDescription = """ + %s + Issued at: %s%s + """.formatted(action.reason(), formatTime(action.issuedAt()), + expiresAtFormatted); + + return new MessageEmbed.Field(fieldName, fieldDescription, false); + }); } - private @NotNull List> groupActionsByPages( - @NotNull List actions) { - List> groupedActions = new ArrayList<>(); - for (int i = 0; i < actions.size(); i++) { - if (i % AuditCommand.MAX_PAGE_LENGTH == 0) { - groupedActions.add(new ArrayList<>(AuditCommand.MAX_PAGE_LENGTH)); - } - - groupedActions.get(groupedActions.size() - 1).add(actions.get(i)); - } - - return groupedActions; + @Nonnull + private static String formatTime(Instant when) { + return TimeUtil.getDateTimeString(when.atOffset(ZoneOffset.UTC)); } - /** - * @param pageNumber page number to display when actions are divided into pages and each page - * can contain {@link AuditCommand#MAX_PAGE_LENGTH} actions - */ - private @NotNull Message auditUser(long guildId, long targetId, long callerId, int pageNumber, - @NotNull JDA jda) { - List actions = actionsStore.getActionsByTargetAscending(guildId, targetId); - List> groupedActions = groupActionsByPages(actions); - int totalPages = groupedActions.size(); + @Nonnull + private Message attachPageTurnButtons(EmbedBuilder auditEmbed, int pageNumber, int totalPages, + long guildId, long targetId, long callerId) { + var messageBuilder = new MessageBuilder(auditEmbed.build()); - // Handles the case of too low page number and too high page number - pageNumber = Math.max(1, pageNumber); - pageNumber = Math.min(totalPages, pageNumber); - - EmbedBuilder audit = createSummaryEmbed(jda.retrieveUserById(targetId).complete(), actions); - - if (groupedActions.isEmpty()) { - return new MessageBuilder(audit.build()).build(); + if (totalPages <= 1) { + return messageBuilder.build(); } + ActionRow pageTurnButtons = + createPageTurnButtons(guildId, targetId, callerId, pageNumber, totalPages); - groupedActions.get(pageNumber - 1) - .forEach(action -> audit.addField(actionToField(action, jda))); - - return new MessageBuilder(audit.setFooter("Page: " + pageNumber + "/" + totalPages).build()) - .setActionRows(makeActionRow(guildId, targetId, callerId, pageNumber, totalPages)) - .build(); + return messageBuilder.setActionRows(pageTurnButtons).build(); } - private @NotNull ActionRow makeActionRow(long guildId, long targetId, long callerId, + @Nonnull + private ActionRow createPageTurnButtons(long guildId, long targetId, long callerId, int pageNumber, int totalPages) { int previousButtonTurnPageBy = -1; Button previousButton = createPageTurnButton(PREVIOUS_BUTTON_LABEL, guildId, targetId, callerId, pageNumber, previousButtonTurnPageBy); - if (pageNumber == 1) { + if (pageNumber <= 1) { previousButton = previousButton.asDisabled(); } int nextButtonTurnPageBy = 1; Button nextButton = createPageTurnButton(NEXT_BUTTON_LABEL, guildId, targetId, callerId, pageNumber, nextButtonTurnPageBy); - if (pageNumber == totalPages) { + if (pageNumber >= totalPages) { nextButton = nextButton.asDisabled(); } return ActionRow.of(previousButton, nextButton); } - private @NotNull Button createPageTurnButton(@NotNull String label, long guildId, long targetId, - long callerId, long pageNumber, int turnPageBy) { + @Nonnull + private Button createPageTurnButton(String label, long guildId, long targetId, long callerId, + long pageNumber, int turnPageBy) { return Button.primary(generateComponentId(String.valueOf(guildId), String.valueOf(targetId), String.valueOf(callerId), String.valueOf(pageNumber), String.valueOf(turnPageBy)), label); } @Override - public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List args) { - long callerId = Long.parseLong(args.get(2)); - long interactorId = event.getMember().getIdLong(); + public void onButtonClick(ButtonInteractionEvent event, List args) { + long commandUserId = Long.parseLong(args.get(2)); + long buttonUserId = event.getMember().getIdLong(); - if (callerId != interactorId) { - event.reply("Only the user who triggered the command can use these buttons.") + if (commandUserId != buttonUserId) { + event.reply("Only the user who triggered the command can turn pages.") .setEphemeral(true) .queue(); @@ -239,7 +267,8 @@ public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List DURATIONS = List.of(ModerationUtils.PERMANENT_DURATION, "1 hour", "3 hours", "1 day", "2 days", "3 days", "7 days", "30 days"); - private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; /** * Constructs an instance. * * @param actionsStore used to store actions issued by this command - * @param config the config to use for this */ - public BanCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public BanCommand(ModerationActionsStore actionsStore) { super(COMMAND_NAME, "Bans the given user from the server", SlashCommandVisibility.GUILD); OptionData durationData = new OptionData(OptionType.STRING, DURATION_OPTION, @@ -71,13 +66,11 @@ public BanCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config "the amount of days of the message history to delete, none means no messages are deleted.", true).addChoice("none", 0).addChoice("recent", 1).addChoice("all", 7)); - hasRequiredRole = - Pattern.compile(config.getHeavyModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private static RestAction handleAlreadyBanned(@NotNull Guild.Ban ban, - @NotNull IReplyCallback event) { + private static RestAction handleAlreadyBanned(Guild.Ban ban, + IReplyCallback event) { String reason = ban.getReason(); String reasonText = reason == null || reason.isBlank() ? "" : " (reason: %s)".formatted(reason); @@ -87,9 +80,9 @@ private static RestAction handleAlreadyBanned(@NotNull Guild.Ba return event.reply(message).setEphemeral(true); } - private static RestAction sendDm(@NotNull ISnowflake target, - @Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason, - @NotNull Guild guild, @NotNull GenericEvent event) { + private static RestAction sendDm(ISnowflake target, + @Nullable ModerationUtils.TemporaryData temporaryData, String reason, Guild guild, + GenericEvent event) { String durationMessage = temporaryData == null ? "permanently" : "for " + temporaryData.duration(); String dmMessage = @@ -107,9 +100,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, .map(Result::isSuccess); } - private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull User target, - @NotNull Member author, @Nullable ModerationUtils.TemporaryData temporaryData, - @NotNull String reason) { + @Nonnull + private static MessageEmbed sendFeedback(boolean hasSentDm, User target, Member author, + @Nullable ModerationUtils.TemporaryData temporaryData, String reason) { String durationText = "The ban duration is: " + (temporaryData == null ? "permanent" : temporaryData.duration()); String dmNoticeText = ""; @@ -120,9 +113,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, durationText + dmNoticeText, reason); } + @Nonnull private static Optional> handleNotAlreadyBannedResponse( - @NotNull Throwable alreadyBannedFailure, @NotNull IReplyCallback event, - @NotNull Guild guild, @NotNull User target) { + Throwable alreadyBannedFailure, IReplyCallback event, Guild guild, User target) { if (alreadyBannedFailure instanceof ErrorResponseException errorResponseException) { if (errorResponseException.getErrorResponse() == ErrorResponse.UNKNOWN_BAN) { return Optional.empty(); @@ -143,11 +136,10 @@ private static Optional> handleNotAlreadyBannedRespo .setEphemeral(true)); } - @SuppressWarnings("MethodWithTooManyParameters") - private RestAction banUserFlow(@NotNull User target, @NotNull Member author, - @Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason, - int deleteHistoryDays, @NotNull Guild guild, - @NotNull SlashCommandInteractionEvent event) { + @Nonnull + private RestAction banUserFlow(User target, Member author, + @Nullable ModerationUtils.TemporaryData temporaryData, String reason, + int deleteHistoryDays, Guild guild, SlashCommandInteractionEvent event) { return sendDm(target, temporaryData, reason, guild, event) .flatMap(hasSentDm -> banUser(target, author, temporaryData, reason, deleteHistoryDays, guild).map(banResult -> hasSentDm)) @@ -155,10 +147,10 @@ private RestAction banUserFlow(@NotNull User target, @NotNull M .flatMap(event::replyEmbeds); } - @SuppressWarnings("MethodWithTooManyParameters") - private AuditableRestAction banUser(@NotNull User target, @NotNull Member author, - @Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason, - int deleteHistoryDays, @NotNull Guild guild) { + @Nonnull + private AuditableRestAction banUser(User target, Member author, + @Nullable ModerationUtils.TemporaryData temporaryData, String reason, + int deleteHistoryDays, Guild guild) { String durationMessage = temporaryData == null ? "permanently" : "for " + temporaryData.duration(); logger.info( @@ -174,30 +166,26 @@ private AuditableRestAction banUser(@NotNull User target, @NotNull Member } @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"}) - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild, - @NotNull IReplyCallback event) { + private boolean handleChecks(Member bot, Member author, @Nullable Member target, + CharSequence reason, Guild guild, IReplyCallback event) { // Member doesn't exist if attempting to ban a user who is not part of the guild. if (target != null && !ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, target, event)) { return false; } - if (!ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event)) { - return false; - } if (!ModerationUtils.handleHasBotPermissions(ACTION_VERB, Permission.BAN_MEMBERS, bot, guild, event)) { return false; } if (!ModerationUtils.handleHasAuthorPermissions(ACTION_VERB, Permission.BAN_MEMBERS, author, - guild, event)) { + event)) { return false; } return ModerationUtils.handleReason(reason, event); } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { OptionMapping targetOption = Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null"); User target = targetOption.getAsUser(); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java index 17bd9cf460..1b6a7e23ca 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java @@ -12,18 +12,14 @@ import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.utils.Result; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; -import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Objects; -import java.util.function.Predicate; -import java.util.regex.Pattern; - /** * This command can kicks users. Kicking can also be paired with a kick reason. The command will @@ -38,34 +34,30 @@ public final class KickCommand extends SlashCommandAdapter { private static final String REASON_OPTION = "reason"; private static final String COMMAND_NAME = "kick"; private static final String ACTION_VERB = "kick"; - private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; /** * Constructs an instance. * * @param actionsStore used to store actions issued by this command - * @param config the config to use for this */ - public KickCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public KickCommand(ModerationActionsStore actionsStore) { super(COMMAND_NAME, "Kicks the given user from the server", SlashCommandVisibility.GUILD); getData().addOption(OptionType.USER, TARGET_OPTION, "The user who you want to kick", true) .addOption(OptionType.STRING, REASON_OPTION, "Why the user should be kicked", true); - hasRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private static void handleAbsentTarget(@NotNull IReplyCallback event) { + private static void handleAbsentTarget(IReplyCallback event) { event.reply("I can not kick the given user since they are not part of the guild anymore.") .setEphemeral(true) .queue(); } - private void kickUserFlow(@NotNull Member target, @NotNull Member author, - @NotNull String reason, @NotNull Guild guild, - @NotNull SlashCommandInteractionEvent event) { + private void kickUserFlow(Member target, Member author, String reason, Guild guild, + SlashCommandInteractionEvent event) { sendDm(target, reason, guild, event) .flatMap(hasSentDm -> kickUser(target, author, reason, guild) .map(kickResult -> hasSentDm)) @@ -74,8 +66,9 @@ private void kickUserFlow(@NotNull Member target, @NotNull Member author, .queue(); } - private static RestAction sendDm(@NotNull ISnowflake target, @NotNull String reason, - @NotNull Guild guild, @NotNull GenericEvent event) { + @Nonnull + private static RestAction sendDm(ISnowflake target, String reason, Guild guild, + GenericEvent event) { return event.getJDA() .openPrivateChannelById(target.getId()) .flatMap(channel -> channel.sendMessage( @@ -89,8 +82,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S .map(Result::isSuccess); } - private AuditableRestAction kickUser(@NotNull Member target, @NotNull Member author, - @NotNull String reason, @NotNull Guild guild) { + @Nonnull + private AuditableRestAction kickUser(Member target, Member author, String reason, + Guild guild) { logger.info("'{}' ({}) kicked the user '{}' ({}) from guild '{}' for reason '{}'.", author.getUser().getAsTag(), author.getId(), target.getUser().getAsTag(), target.getId(), guild.getName(), reason); @@ -101,8 +95,9 @@ private AuditableRestAction kickUser(@NotNull Member target, @NotNull Memb return guild.kick(target, reason).reason(reason); } - private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull Member target, - @NotNull Member author, @NotNull String reason) { + @Nonnull + private static MessageEmbed sendFeedback(boolean hasSentDm, Member target, Member author, + String reason) { String dmNoticeText = ""; if (!hasSentDm) { dmNoticeText = "(Unable to send them a DM.)"; @@ -111,10 +106,9 @@ private AuditableRestAction kickUser(@NotNull Member target, @NotNull Memb target.getUser(), dmNoticeText, reason); } - @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"}) - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild, - @NotNull IReplyCallback event) { + @Nonnull + private boolean handleChecks(Member bot, Member author, @Nullable Member target, + CharSequence reason, Guild guild, IReplyCallback event) { // Member doesn't exist if attempting to kick a user who is not part of the guild anymore. if (target == null) { handleAbsentTarget(event); @@ -123,22 +117,19 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, if (!ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, target, event)) { return false; } - if (!ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event)) { - return false; - } if (!ModerationUtils.handleHasBotPermissions(ACTION_VERB, Permission.KICK_MEMBERS, bot, guild, event)) { return false; } if (!ModerationUtils.handleHasAuthorPermissions(ACTION_VERB, Permission.KICK_MEMBERS, - author, guild, event)) { + author, event)) { return false; } return ModerationUtils.handleReason(reason, event); } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { Member target = Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null") .getAsMember(); Member author = Objects.requireNonNull(event.getMember(), "The author is null"); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationAction.java index 76121063ef..047a371300 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationAction.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationAction.java @@ -1,6 +1,6 @@ package org.togetherjava.tjbot.commands.moderation; -import org.jetbrains.annotations.NotNull; +import javax.annotation.Nonnull; /** * All available moderation actions. @@ -51,7 +51,7 @@ public enum ModerationAction { * @param verb the verb of the action, as it would be used in a sentence, such as "banned" or * "kicked" */ - ModerationAction(@NotNull String verb) { + ModerationAction(String verb) { this.verb = verb; } @@ -62,7 +62,8 @@ public enum ModerationAction { * * @return the verb of this action */ - public @NotNull String getVerb() { + @Nonnull + public String getVerb() { return verb; } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java index 3ee603b93a..89ffd7c231 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java @@ -1,12 +1,12 @@ package org.togetherjava.tjbot.commands.moderation; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.jooq.Condition; import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.ModerationActions; import org.togetherjava.tjbot.db.generated.tables.records.ModerationActionsRecord; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.time.Instant; import java.util.List; import java.util.Objects; @@ -35,7 +35,7 @@ public final class ModerationActionsStore { * * @param database the database to write and retrieve actions from */ - public ModerationActionsStore(@NotNull Database database) { + public ModerationActionsStore(Database database) { this.database = Objects.requireNonNull(database); } @@ -45,7 +45,8 @@ public ModerationActionsStore(@NotNull Database database) { * * @return a list of all expired actions, chronologically ascending */ - public @NotNull List getExpiredActionsAscending() { + @Nonnull + public List getExpiredActionsAscending() { return getActionsAscendingWhere( ModerationActions.MODERATION_ACTIONS.ACTION_EXPIRES_AT.isNotNull() .and(ModerationActions.MODERATION_ACTIONS.ACTION_EXPIRES_AT @@ -61,8 +62,8 @@ public ModerationActionsStore(@NotNull Database database) { * @param actionType the type of action to filter for * @return a list of all actions with the given type, chronologically ascending */ - public @NotNull List getActionsByTypeAscending(long guildId, - @NotNull ModerationAction actionType) { + @Nonnull + public List getActionsByTypeAscending(long guildId, ModerationAction actionType) { Objects.requireNonNull(actionType); return getActionsFromGuildAscending(guildId, @@ -78,7 +79,8 @@ public ModerationActionsStore(@NotNull Database database) { * @param targetId the id of the target user to filter for * @return a list of all actions executed against the target, chronologically ascending */ - public @NotNull List getActionsByTargetAscending(long guildId, long targetId) { + @Nonnull + public List getActionsByTargetAscending(long guildId, long targetId) { return getActionsFromGuildAscending(guildId, ModerationActions.MODERATION_ACTIONS.TARGET_ID.eq(targetId)); } @@ -92,7 +94,8 @@ public ModerationActionsStore(@NotNull Database database) { * @param authorId the id of the author user to filter for * @return a list of all actions executed by the author, chronologically ascending */ - public @NotNull List getActionsByAuthorAscending(long guildId, long authorId) { + @Nonnull + public List getActionsByAuthorAscending(long guildId, long authorId) { return getActionsFromGuildAscending(guildId, ModerationActions.MODERATION_ACTIONS.AUTHOR_ID.eq(authorId)); } @@ -107,8 +110,9 @@ public ModerationActionsStore(@NotNull Database database) { * @param actionType the type of the action * @return the last action issued against the given user of the given type, if present */ - public @NotNull Optional findLastActionAgainstTargetByType(long guildId, - long targetId, @NotNull ModerationAction actionType) { + @Nonnull + public Optional findLastActionAgainstTargetByType(long guildId, long targetId, + ModerationAction actionType) { return database .read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) @@ -126,7 +130,8 @@ public ModerationActionsStore(@NotNull Database database) { * @param caseId the actions' case id to search for * @return the action with the given case id, if present */ - public @NotNull Optional findActionByCaseId(int caseId) { + @Nonnull + public Optional findActionByCaseId(int caseId) { return database .read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) .where(ModerationActions.MODERATION_ACTIONS.CASE_ID.eq(caseId)) @@ -152,10 +157,8 @@ public ModerationActionsStore(@NotNull Database database) { * @param reason the reason why this action was executed * @return the unique case id associated with the action */ - @SuppressWarnings("MethodWithTooManyParameters") - public int addAction(long guildId, long authorId, long targetId, - @NotNull ModerationAction actionType, @Nullable Instant actionExpiresAt, - @NotNull String reason) { + public int addAction(long guildId, long authorId, long targetId, ModerationAction actionType, + @Nullable Instant actionExpiresAt, String reason) { Objects.requireNonNull(actionType); Objects.requireNonNull(reason); @@ -174,15 +177,16 @@ public int addAction(long guildId, long authorId, long targetId, }); } - private @NotNull List getActionsFromGuildAscending(long guildId, - @NotNull Condition condition) { + @Nonnull + private List getActionsFromGuildAscending(long guildId, Condition condition) { Objects.requireNonNull(condition); return getActionsAscendingWhere( ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId).and(condition)); } - private @NotNull List getActionsAscendingWhere(@NotNull Condition condition) { + @Nonnull + private List getActionsAscendingWhere(Condition condition) { Objects.requireNonNull(condition); return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationUtils.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationUtils.java index 48119596d4..bf36aed4ed 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationUtils.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationUtils.java @@ -4,13 +4,13 @@ import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.Config; -import java.awt.*; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.awt.Color; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalUnit; @@ -21,8 +21,10 @@ /** * Utility class offering helpers revolving around user moderation, such as banning or kicking. */ -public enum ModerationUtils { - ; +public class ModerationUtils { + private ModerationUtils() { + throw new UnsupportedOperationException("Utility class, construction not supported"); + } private static final Logger logger = LoggerFactory.getLogger(ModerationUtils.class); /** @@ -49,8 +51,7 @@ public enum ModerationUtils { * @param event the event used to respond to the user * @return whether the reason is valid */ - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - static boolean handleReason(@NotNull CharSequence reason, @NotNull IReplyCallback event) { + static boolean handleReason(CharSequence reason, IReplyCallback event) { if (reason.length() <= REASON_MAX_LENGTH) { return true; } @@ -76,9 +77,8 @@ static boolean handleReason(@NotNull CharSequence reason, @NotNull IReplyCallbac * @param event the event used to respond to the user * @return Whether the author and bot can interact with the target user */ - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - static boolean handleCanInteractWithTarget(@NotNull String actionVerb, @NotNull Member bot, - @NotNull Member author, @NotNull Member target, @NotNull IReplyCallback event) { + static boolean handleCanInteractWithTarget(String actionVerb, Member bot, Member author, + Member target, IReplyCallback event) { String targetTag = target.getUser().getAsTag(); if (!author.canInteract(target)) { event @@ -111,9 +111,8 @@ static boolean handleCanInteractWithTarget(@NotNull String actionVerb, @NotNull * @param event the event used to respond to the user * @return Whether the author and bot can interact with the role */ - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - static boolean handleCanInteractWithRole(@NotNull Member bot, @NotNull Member author, - @NotNull Role role, @NotNull IReplyCallback event) { + static boolean handleCanInteractWithRole(Member bot, Member author, Role role, + IReplyCallback event) { if (!author.canInteract(role)) { event .reply("The role %s is too powerful for you to interact with." @@ -146,10 +145,8 @@ static boolean handleCanInteractWithRole(@NotNull Member bot, @NotNull Member au * @param event the event used to respond to the user * @return Whether the bot has the required permission */ - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - static boolean handleHasBotPermissions(@NotNull String actionVerb, - @NotNull Permission permission, @NotNull IPermissionHolder bot, @NotNull Guild guild, - @NotNull IReplyCallback event) { + static boolean handleHasBotPermissions(String actionVerb, Permission permission, + IPermissionHolder bot, Guild guild, IReplyCallback event) { if (!bot.hasPermission(permission)) { event .reply("I can not %s users in this guild since I do not have the %s permission." @@ -164,8 +161,7 @@ static boolean handleHasBotPermissions(@NotNull String actionVerb, return true; } - private static void handleAbsentTarget(@NotNull String actionVerb, - @NotNull IReplyCallback event) { + private static void handleAbsentTarget(String actionVerb, IReplyCallback event) { event .reply("I can not %s the given user since they are not part of the guild anymore." .formatted(actionVerb)) @@ -185,7 +181,6 @@ private static void handleAbsentTarget(@NotNull String actionVerb, *

  • the target is not member of the guild
  • *
  • the bot or author do not have enough permissions to interact with the target
  • *
  • the bot or author do not have enough permissions to interact with the role
  • - *
  • the author does not have the required role for this interaction
  • *
  • the bot does not have the MANAGE_ROLES permission
  • *
  • the given reason is too long
  • * @@ -197,17 +192,16 @@ private static void handleAbsentTarget(@NotNull String actionVerb, * @param bot the bot executing this interaction * @param author the author attempting to interact with the target * @param guild the guild this interaction is executed on - * @param hasRequiredRole a predicate used to identify required roles by their name * @param reason the reason for this interaction * @param event the event used to respond to the user * @return Whether the bot and the author have enough permission */ - @SuppressWarnings({"MethodWithTooManyParameters", "BooleanMethodNameMustStartWithQuestion", - "squid:S107"}) - static boolean handleRoleChangeChecks(@Nullable Role role, @NotNull String actionVerb, - @Nullable Member target, @NotNull Member bot, @NotNull Member author, - @NotNull Guild guild, @NotNull Predicate hasRequiredRole, - @NotNull CharSequence reason, @NotNull IReplyCallback event) { + // Sonar complains about having too many parameters. Not incorrect, but not easy to work around + // for now + @SuppressWarnings({"MethodWithTooManyParameters", "squid:S107"}) + static boolean handleRoleChangeChecks(@Nullable Role role, String actionVerb, + @Nullable Member target, Member bot, Member author, Guild guild, CharSequence reason, + IReplyCallback event) { if (role == null) { event .reply("Can not %s the user, unable to find the corresponding role on this server" @@ -231,9 +225,6 @@ static boolean handleRoleChangeChecks(@Nullable Role role, @NotNull String actio if (!handleCanInteractWithRole(bot, author, role, event)) { return false; } - if (!handleHasAuthorRole(actionVerb, hasRequiredRole, author, event)) { - return false; - } if (!handleHasBotPermissions(actionVerb, Permission.MANAGE_ROLES, bot, guild, event)) { return false; } @@ -252,10 +243,8 @@ static boolean handleRoleChangeChecks(@Nullable Role role, @NotNull String actio * @param event the event used to respond to the user * @return Whether the author has the required permission */ - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - static boolean handleHasAuthorPermissions(@NotNull String actionVerb, - @NotNull Permission permission, @NotNull IPermissionHolder author, @NotNull Guild guild, - @NotNull IReplyCallback event) { + static boolean handleHasAuthorPermissions(String actionVerb, Permission permission, + IPermissionHolder author, IReplyCallback event) { if (!author.hasPermission(permission)) { event .reply("You can not %s users in this guild since you do not have the %s permission." @@ -267,33 +256,6 @@ static boolean handleHasAuthorPermissions(@NotNull String actionVerb, return true; } - /** - * Checks whether the given bot has enough permission to execute the given action. For example - * whether it has enough permissions to ban users. - *

    - * If not, it will handle the situation and respond to the user. - * - * @param actionVerb the interaction as verb, for example {@code "ban"} or {@code "kick"} - * @param hasRequiredRole a predicate used to identify required roles by their name - * @param author the author attempting to interact with the target - * @param event the event used to respond to the user - * @return Whether the bot has the required permission - */ - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - static boolean handleHasAuthorRole(@NotNull String actionVerb, - @NotNull Predicate hasRequiredRole, @NotNull Member author, - @NotNull IReplyCallback event) { - if (author.getRoles().stream().map(Role::getName).anyMatch(hasRequiredRole)) { - return true; - } - event - .reply("You can not %s users in this guild since you do not have the required role." - .formatted(actionVerb)) - .setEphemeral(true) - .queue(); - return false; - } - /** * Creates a message to be displayed as response to a moderation action. *

    @@ -308,9 +270,9 @@ static boolean handleHasAuthorRole(@NotNull String actionVerb, * @param reason an optional reason for why the action is executed, {@code null} if not desired * @return the created response */ - static @NotNull MessageEmbed createActionResponse(@NotNull User author, - @NotNull ModerationAction action, @NotNull User target, @Nullable String extraMessage, - @Nullable String reason) { + @Nonnull + static MessageEmbed createActionResponse(User author, ModerationAction action, User target, + @Nullable String extraMessage, @Nullable String reason) { String description = "%s **%s** (id: %s).".formatted(action.getVerb(), target.getAsTag(), target.getId()); if (extraMessage != null && !extraMessage.isBlank()) { @@ -332,7 +294,7 @@ static boolean handleHasAuthorRole(@NotNull String actionVerb, * @param config the config used to identify the muted role * @return predicate that matches the name of the muted role */ - public static Predicate getIsMutedRolePredicate(@NotNull Config config) { + public static Predicate getIsMutedRolePredicate(Config config) { return Pattern.compile(config.getMutedRolePattern()).asMatchPredicate(); } @@ -343,8 +305,8 @@ public static Predicate getIsMutedRolePredicate(@NotNull Config config) * @param config the config used to identify the muted role * @return the muted role, if found */ - public static @NotNull Optional getMutedRole(@NotNull Guild guild, - @NotNull Config config) { + @Nonnull + public static Optional getMutedRole(Guild guild, Config config) { Predicate isMutedRole = getIsMutedRolePredicate(config); return guild.getRoles().stream().filter(role -> isMutedRole.test(role.getName())).findAny(); } @@ -355,7 +317,7 @@ public static Predicate getIsMutedRolePredicate(@NotNull Config config) * @param config the config used to identify the quarantined role * @return predicate that matches the name of the quarantined role */ - public static Predicate getIsQuarantinedRolePredicate(@NotNull Config config) { + public static Predicate getIsQuarantinedRolePredicate(Config config) { return Pattern.compile(config.getQuarantinedRolePattern()).asMatchPredicate(); } @@ -366,8 +328,8 @@ public static Predicate getIsQuarantinedRolePredicate(@NotNull Config co * @param config the config used to identify the quarantined role * @return the quarantined role, if found */ - public static @NotNull Optional getQuarantinedRole(@NotNull Guild guild, - @NotNull Config config) { + @Nonnull + public static Optional getQuarantinedRole(Guild guild, Config config) { Predicate isQuarantinedRole = getIsQuarantinedRolePredicate(config); return guild.getRoles() .stream() @@ -384,7 +346,8 @@ public static Predicate getIsQuarantinedRolePredicate(@NotNull Config co * @return the temporary data represented by the given duration or empty if the duration is * {@code "permanent"} */ - static @NotNull Optional computeTemporaryData(@NotNull String durationText) { + @Nonnull + static Optional computeTemporaryData(String durationText) { if (PERMANENT_DURATION.equals(durationText)) { return Optional.empty(); } @@ -409,6 +372,6 @@ public static Predicate getIsQuarantinedRolePredicate(@NotNull Config co * @param duration a human-readable text representing the duration of the temporary action, such * as {@code "1 day"}. */ - record TemporaryData(@NotNull Instant expiresAt, @NotNull String duration) { + record TemporaryData(Instant expiresAt, String duration) { } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/MuteCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/MuteCommand.java index 7b4f244ef1..7e3f40977d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/MuteCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/MuteCommand.java @@ -9,20 +9,18 @@ import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.utils.Result; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.time.Instant; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.function.Predicate; -import java.util.regex.Pattern; /** * This command can mute users. Muting can also be paired with a reason. The command will also try @@ -41,7 +39,6 @@ public final class MuteCommand extends SlashCommandAdapter { @SuppressWarnings("StaticCollection") private static final List DURATIONS = List.of("10 minutes", "30 minutes", "1 hour", "3 hours", "1 day", "3 days", "7 days", ModerationUtils.PERMANENT_DURATION); - private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; private final Config config; @@ -51,7 +48,7 @@ public final class MuteCommand extends SlashCommandAdapter { * @param actionsStore used to store actions issued by this command * @param config the config to use for this */ - public MuteCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public MuteCommand(ModerationActionsStore actionsStore, Config config) { super(COMMAND_NAME, "Mutes the given user so that they can not send messages anymore", SlashCommandVisibility.GUILD); @@ -64,17 +61,17 @@ public MuteCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config .addOption(OptionType.STRING, REASON_OPTION, "Why the user should be muted", true); this.config = config; - hasRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private static void handleAlreadyMutedTarget(@NotNull IReplyCallback event) { + private static void handleAlreadyMutedTarget(IReplyCallback event) { event.reply("The user is already muted.").setEphemeral(true).queue(); } - private static RestAction sendDm(@NotNull ISnowflake target, - @Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason, - @NotNull Guild guild, @NotNull GenericEvent event) { + @Nonnull + private static RestAction sendDm(ISnowflake target, + @Nullable ModerationUtils.TemporaryData temporaryData, String reason, Guild guild, + GenericEvent event) { String durationMessage = temporaryData == null ? "permanently" : "for " + temporaryData.duration(); String dmMessage = @@ -92,9 +89,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, .map(Result::isSuccess); } - private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull Member target, - @NotNull Member author, @Nullable ModerationUtils.TemporaryData temporaryData, - @NotNull String reason) { + @Nonnull + private static MessageEmbed sendFeedback(boolean hasSentDm, Member target, Member author, + @Nullable ModerationUtils.TemporaryData temporaryData, String reason) { String durationText = "The mute duration is: " + (temporaryData == null ? "permanent" : temporaryData.duration()); String dmNoticeText = ""; @@ -105,9 +102,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, target.getUser(), durationText + dmNoticeText, reason); } - private AuditableRestAction muteUser(@NotNull Member target, @NotNull Member author, - @Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason, - @NotNull Guild guild) { + @Nonnull + private AuditableRestAction muteUser(Member target, Member author, + @Nullable ModerationUtils.TemporaryData temporaryData, String reason, Guild guild) { String durationMessage = temporaryData == null ? "permanently" : "for " + temporaryData.duration(); logger.info("'{}' ({}) muted the user '{}' ({}) {} in guild '{}' for reason '{}'.", @@ -123,10 +120,9 @@ private AuditableRestAction muteUser(@NotNull Member target, @NotNull Memb .reason(reason); } - @SuppressWarnings("MethodWithTooManyParameters") - private void muteUserFlow(@NotNull Member target, @NotNull Member author, - @Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason, - @NotNull Guild guild, @NotNull SlashCommandInteractionEvent event) { + private void muteUserFlow(Member target, Member author, + @Nullable ModerationUtils.TemporaryData temporaryData, String reason, Guild guild, + SlashCommandInteractionEvent event) { sendDm(target, temporaryData, reason, guild, event) .flatMap(hasSentDm -> muteUser(target, author, temporaryData, reason, guild) .map(result -> hasSentDm)) @@ -135,13 +131,11 @@ private void muteUserFlow(@NotNull Member target, @NotNull Member author, .queue(); } - @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"}) - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild, - @NotNull IReplyCallback event) { + private boolean handleChecks(Member bot, Member author, @Nullable Member target, + CharSequence reason, Guild guild, IReplyCallback event) { if (!ModerationUtils.handleRoleChangeChecks( ModerationUtils.getMutedRole(guild, config).orElse(null), ACTION_VERB, target, bot, - author, guild, hasRequiredRole, reason, event)) { + author, guild, reason, event)) { return false; } if (Objects.requireNonNull(target) @@ -156,7 +150,7 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { Member target = Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null") .getAsMember(); Member author = Objects.requireNonNull(event.getMember(), "The author is null"); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/NoteCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/NoteCommand.java index 285e4973ca..3d0cc9cf2e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/NoteCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/NoteCommand.java @@ -5,18 +5,13 @@ 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 org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; -import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nullable; import java.util.Objects; -import java.util.function.Predicate; -import java.util.regex.Pattern; - /** * This command allows users to write notes about others. Notes are persisted and can be retrieved @@ -32,15 +27,13 @@ public final class NoteCommand extends SlashCommandAdapter { private static final String CONTENT_OPTION = "content"; private static final String ACTION_VERB = "write a note about"; private final ModerationActionsStore actionsStore; - private final Predicate hasRequiredRole; /** * Creates a new instance. * * @param actionsStore used to store actions issued by this command - * @param config the config to use for this */ - public NoteCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public NoteCommand(ModerationActionsStore actionsStore) { super("note", "Writes a note about the given user", SlashCommandVisibility.GUILD); getData() @@ -49,12 +42,11 @@ public NoteCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config .addOption(OptionType.STRING, CONTENT_OPTION, "The content of the note you want to write", true); - hasRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { OptionMapping targetOption = event.getOption(USER_OPTION); Member author = event.getMember(); Guild guild = event.getGuild(); @@ -68,29 +60,23 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { sendNote(targetOption.getAsUser(), author, content, guild, event); } - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, CharSequence content, @NotNull IReplyCallback event) { + private boolean handleChecks(Member bot, Member author, @Nullable Member target, + CharSequence content, IReplyCallback event) { if (target != null && !ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, target, event)) { return false; } - if (!ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event)) { - return false; - } - return ModerationUtils.handleReason(content, event); } - private void sendNote(@NotNull User target, @NotNull Member author, @NotNull String content, - @NotNull ISnowflake guild, @NotNull IReplyCallback event) { + private void sendNote(User target, Member author, String content, ISnowflake guild, + IReplyCallback event) { storeNote(target, author, content, guild); sendFeedback(target, author, content, event); } - private void storeNote(@NotNull User target, @NotNull Member author, @NotNull String content, - @NotNull ISnowflake guild) { + private void storeNote(User target, Member author, String content, ISnowflake guild) { logger.info("'{}' ({}) wrote a note about the user '{}' ({}) with content '{}'.", author.getUser().getAsTag(), author.getId(), target.getAsTag(), target.getId(), content); @@ -99,8 +85,8 @@ private void storeNote(@NotNull User target, @NotNull Member author, @NotNull St ModerationAction.NOTE, null, content); } - private static void sendFeedback(@NotNull User target, @NotNull Member author, - @NotNull String noteContent, @NotNull IReplyCallback event) { + private static void sendFeedback(User target, Member author, String noteContent, + IReplyCallback event) { MessageEmbed feedback = ModerationUtils.createActionResponse(author.getUser(), ModerationAction.NOTE, target, null, noteContent); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/QuarantineCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/QuarantineCommand.java index cc3453dff5..73091d3c2e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/QuarantineCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/QuarantineCommand.java @@ -8,17 +8,15 @@ import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.utils.Result; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Objects; -import java.util.function.Predicate; -import java.util.regex.Pattern; /** * This command can quarantine users. Quarantining can also be paired with a reason. The command @@ -34,7 +32,6 @@ public final class QuarantineCommand extends SlashCommandAdapter { private static final String REASON_OPTION = "reason"; private static final String COMMAND_NAME = "quarantine"; private static final String ACTION_VERB = "quarantine"; - private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; private final Config config; @@ -44,7 +41,7 @@ public final class QuarantineCommand extends SlashCommandAdapter { * @param actionsStore used to store actions issued by this command * @param config the config to use for this */ - public QuarantineCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public QuarantineCommand(ModerationActionsStore actionsStore, Config config) { super(COMMAND_NAME, "Puts the given user under quarantine. They can not interact with anyone anymore then.", SlashCommandVisibility.GUILD); @@ -55,16 +52,16 @@ public QuarantineCommand(@NotNull ModerationActionsStore actionsStore, @NotNull true); this.config = config; - hasRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private static void handleAlreadyQuarantinedTarget(@NotNull IReplyCallback event) { + private static void handleAlreadyQuarantinedTarget(IReplyCallback event) { event.reply("The user is already quarantined.").setEphemeral(true).queue(); } - private static RestAction sendDm(@NotNull ISnowflake target, @NotNull String reason, - @NotNull Guild guild, @NotNull GenericEvent event) { + @Nonnull + private static RestAction sendDm(ISnowflake target, String reason, Guild guild, + GenericEvent event) { String dmMessage = """ Hey there, sorry to tell you but unfortunately you have been put under quarantine in the server %s. @@ -81,8 +78,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S .map(Result::isSuccess); } - private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull Member target, - @NotNull Member author, @NotNull String reason) { + @Nonnull + private static MessageEmbed sendFeedback(boolean hasSentDm, Member target, Member author, + String reason) { String dmNoticeText = ""; if (!hasSentDm) { dmNoticeText = "\n(Unable to send them a DM.)"; @@ -91,8 +89,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S target.getUser(), dmNoticeText, reason); } - private AuditableRestAction quarantineUser(@NotNull Member target, @NotNull Member author, - @NotNull String reason, @NotNull Guild guild) { + @Nonnull + private AuditableRestAction quarantineUser(Member target, Member author, String reason, + Guild guild) { logger.info("'{}' ({}) quarantined the user '{}' ({}) in guild '{}' for reason '{}'.", author.getUser().getAsTag(), author.getId(), target.getUser().getAsTag(), target.getId(), guild.getName(), reason); @@ -106,9 +105,8 @@ private AuditableRestAction quarantineUser(@NotNull Member target, @NotNul .reason(reason); } - private void quarantineUserFlow(@NotNull Member target, @NotNull Member author, - @NotNull String reason, @NotNull Guild guild, - @NotNull SlashCommandInteractionEvent event) { + private void quarantineUserFlow(Member target, Member author, String reason, Guild guild, + SlashCommandInteractionEvent event) { sendDm(target, reason, guild, event) .flatMap(hasSentDm -> quarantineUser(target, author, reason, guild) .map(result -> hasSentDm)) @@ -117,13 +115,11 @@ private void quarantineUserFlow(@NotNull Member target, @NotNull Member author, .queue(); } - @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"}) - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild, - @NotNull IReplyCallback event) { + private boolean handleChecks(Member bot, Member author, @Nullable Member target, + CharSequence reason, Guild guild, IReplyCallback event) { if (!ModerationUtils.handleRoleChangeChecks( ModerationUtils.getQuarantinedRole(guild, config).orElse(null), ACTION_VERB, target, - bot, author, guild, hasRequiredRole, reason, event)) { + bot, author, guild, reason, event)) { return false; } @@ -140,7 +136,7 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { Member target = event.getOption(TARGET_OPTION).getAsMember(); Member author = event.getMember(); String reason = event.getOption(REASON_OPTION).getAsString(); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/RejoinModerationRoleListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/RejoinModerationRoleListener.java index 2d306f0247..0df83b86d3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/RejoinModerationRoleListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/RejoinModerationRoleListener.java @@ -6,7 +6,6 @@ import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.events.GenericEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.EventReceiver; @@ -39,8 +38,7 @@ public final class RejoinModerationRoleListener implements EventReceiver { * user should be e.g. muted * @param config the config to use for this */ - public RejoinModerationRoleListener(@NotNull ModerationActionsStore actionsStore, - @NotNull Config config) { + public RejoinModerationRoleListener(ModerationActionsStore actionsStore, Config config) { this.actionsStore = actionsStore; moderationRoles = List.of( @@ -52,13 +50,13 @@ public RejoinModerationRoleListener(@NotNull ModerationActionsStore actionsStore } @Override - public void onEvent(@NotNull GenericEvent event) { + public void onEvent(GenericEvent event) { if (event instanceof GuildMemberJoinEvent joinEvent) { onGuildMemberJoin(joinEvent); } } - private void onGuildMemberJoin(@NotNull GuildMemberJoinEvent event) { + private void onGuildMemberJoin(GuildMemberJoinEvent event) { Member member = event.getMember(); for (ModerationRole moderationRole : moderationRoles) { @@ -68,8 +66,8 @@ private void onGuildMemberJoin(@NotNull GuildMemberJoinEvent event) { } } - private boolean shouldApplyModerationRole(@NotNull ModerationRole moderationRole, - @NotNull IPermissionHolder member) { + private boolean shouldApplyModerationRole(ModerationRole moderationRole, + IPermissionHolder member) { Optional lastApplyAction = actionsStore.findLastActionAgainstTargetByType( member.getGuild().getIdLong(), member.getIdLong(), moderationRole.applyAction); if (lastApplyAction.isEmpty()) { @@ -93,8 +91,7 @@ private boolean shouldApplyModerationRole(@NotNull ModerationRole moderationRole return false; } - private static void applyModerationRole(@NotNull ModerationRole moderationRole, - @NotNull Member member) { + private static void applyModerationRole(ModerationRole moderationRole, Member member) { Guild guild = member.getGuild(); logger.info("Reapplied existing {} to user '{}' ({}) in guild '{}' after rejoining.", moderationRole.actionName, member.getUser().getAsTag(), member.getId(), @@ -106,7 +103,7 @@ private static void applyModerationRole(@NotNull ModerationRole moderationRole, .queue(); } - private record ModerationRole(@NotNull String actionName, @NotNull ModerationAction applyAction, - @NotNull ModerationAction revokeAction, @NotNull Function guildToRole) { + private record ModerationRole(String actionName, ModerationAction applyAction, + ModerationAction revokeAction, Function guildToRole) { } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java index c26670f831..e9a6ad2bd6 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java @@ -7,16 +7,12 @@ import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.requests.ErrorResponse; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; -import org.togetherjava.tjbot.config.Config; import java.util.Objects; -import java.util.function.Predicate; -import java.util.regex.Pattern; /** * Unbans a given user. Unbanning can also be paired with a reason. The command fails if the user is @@ -28,16 +24,14 @@ public final class UnbanCommand extends SlashCommandAdapter { private static final String REASON_OPTION = "reason"; private static final String COMMAND_NAME = "unban"; private static final String ACTION_VERB = "unban"; - private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; /** * Constructs an instance. * * @param actionsStore used to store actions issued by this command - * @param config the config to use for this */ - public UnbanCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public UnbanCommand(ModerationActionsStore actionsStore) { super(COMMAND_NAME, "Unbans the given user from the server", SlashCommandVisibility.GUILD); getData() @@ -45,13 +39,11 @@ public UnbanCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Confi true) .addOption(OptionType.STRING, REASON_OPTION, "Why the user should be unbanned", true); - hasRequiredRole = - Pattern.compile(config.getHeavyModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private void unban(@NotNull User target, @NotNull Member author, @NotNull String reason, - @NotNull Guild guild, @NotNull IReplyCallback event) { + private void unban(User target, Member author, String reason, Guild guild, + IReplyCallback event) { guild.unban(target).reason(reason).queue(result -> { MessageEmbed message = ModerationUtils.createActionResponse(author.getUser(), ModerationAction.UNBAN, target, null, reason); @@ -66,8 +58,7 @@ private void unban(@NotNull User target, @NotNull Member author, @NotNull String }, unbanFailure -> handleFailure(unbanFailure, target, event)); } - private static void handleFailure(@NotNull Throwable unbanFailure, @NotNull User target, - @NotNull IReplyCallback event) { + private static void handleFailure(Throwable unbanFailure, User target, IReplyCallback event) { String targetTag = target.getAsTag(); if (unbanFailure instanceof ErrorResponseException errorResponseException) { if (errorResponseException.getErrorResponse() == ErrorResponse.UNKNOWN_USER) { @@ -89,18 +80,14 @@ private static void handleFailure(@NotNull Throwable unbanFailure, @NotNull User targetTag, unbanFailure); } - @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion"}) - private boolean handleChecks(@NotNull IPermissionHolder bot, @NotNull Member author, - @NotNull CharSequence reason, @NotNull Guild guild, @NotNull IReplyCallback event) { - if (!ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event)) { - return false; - } + private boolean handleChecks(IPermissionHolder bot, Member author, CharSequence reason, + Guild guild, IReplyCallback event) { if (!ModerationUtils.handleHasBotPermissions(ACTION_VERB, Permission.BAN_MEMBERS, bot, guild, event)) { return false; } if (!ModerationUtils.handleHasAuthorPermissions(ACTION_VERB, Permission.BAN_MEMBERS, author, - guild, event)) { + event)) { return false; } @@ -108,7 +95,7 @@ private boolean handleChecks(@NotNull IPermissionHolder bot, @NotNull Member aut } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { User target = Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null") .getAsUser(); Member author = Objects.requireNonNull(event.getMember(), "The author is null"); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnmuteCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnmuteCommand.java index bd2e79bdd7..f0414df212 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnmuteCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnmuteCommand.java @@ -8,17 +8,15 @@ import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.utils.Result; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Objects; -import java.util.function.Predicate; -import java.util.regex.Pattern; /** * This command can unmute muted users. Unmuting can also be paired with a reason. The command will @@ -33,7 +31,6 @@ public final class UnmuteCommand extends SlashCommandAdapter { private static final String REASON_OPTION = "reason"; private static final String COMMAND_NAME = "unmute"; private static final String ACTION_VERB = "unmute"; - private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; private final Config config; @@ -43,7 +40,7 @@ public final class UnmuteCommand extends SlashCommandAdapter { * @param actionsStore used to store actions issued by this command * @param config the config to use for this */ - public UnmuteCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public UnmuteCommand(ModerationActionsStore actionsStore, Config config) { super(COMMAND_NAME, "Unmutes the given already muted user so that they can send messages again", SlashCommandVisibility.GUILD); @@ -52,16 +49,16 @@ public UnmuteCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Conf .addOption(OptionType.STRING, REASON_OPTION, "Why the user should be unmuted", true); this.config = config; - hasRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private static void handleNotMutedTarget(@NotNull IReplyCallback event) { + private static void handleNotMutedTarget(IReplyCallback event) { event.reply("The user is not muted.").setEphemeral(true).queue(); } - private static RestAction sendDm(@NotNull ISnowflake target, @NotNull String reason, - @NotNull Guild guild, @NotNull GenericEvent event) { + @Nonnull + private static RestAction sendDm(ISnowflake target, String reason, Guild guild, + GenericEvent event) { String dmMessage = """ Hey there, you have been unmuted in the server %s. This means you can now send messages in the server again. @@ -74,8 +71,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S .map(Result::isSuccess); } - private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull Member target, - @NotNull Member author, @NotNull String reason) { + @Nonnull + private static MessageEmbed sendFeedback(boolean hasSentDm, Member target, Member author, + String reason) { String dmNoticeText = ""; if (!hasSentDm) { dmNoticeText = "(Unable to send them a DM.)"; @@ -84,8 +82,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S target.getUser(), dmNoticeText, reason); } - private AuditableRestAction unmuteUser(@NotNull Member target, @NotNull Member author, - @NotNull String reason, @NotNull Guild guild) { + @Nonnull + private AuditableRestAction unmuteUser(Member target, Member author, String reason, + Guild guild) { logger.info("'{}' ({}) unmuted the user '{}' ({}) in guild '{}' for reason '{}'.", author.getUser().getAsTag(), author.getId(), target.getUser().getAsTag(), target.getId(), guild.getName(), reason); @@ -98,9 +97,8 @@ private AuditableRestAction unmuteUser(@NotNull Member target, @NotNull Me .reason(reason); } - private void unmuteUserFlow(@NotNull Member target, @NotNull Member author, - @NotNull String reason, @NotNull Guild guild, - @NotNull SlashCommandInteractionEvent event) { + private void unmuteUserFlow(Member target, Member author, String reason, Guild guild, + SlashCommandInteractionEvent event) { sendDm(target, reason, guild, event) .flatMap( hasSentDm -> unmuteUser(target, author, reason, guild).map(result -> hasSentDm)) @@ -109,13 +107,11 @@ private void unmuteUserFlow(@NotNull Member target, @NotNull Member author, .queue(); } - @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"}) - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild, - @NotNull IReplyCallback event) { + private boolean handleChecks(Member bot, Member author, @Nullable Member target, + CharSequence reason, Guild guild, IReplyCallback event) { if (!ModerationUtils.handleRoleChangeChecks( ModerationUtils.getMutedRole(guild, config).orElse(null), ACTION_VERB, target, bot, - author, guild, hasRequiredRole, reason, event)) { + author, guild, reason, event)) { return false; } if (Objects.requireNonNull(target) @@ -130,7 +126,7 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { Member target = Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null") .getAsMember(); Member author = Objects.requireNonNull(event.getMember(), "The author is null"); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnquarantineCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnquarantineCommand.java index 9e33c4841f..117d61d5ce 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnquarantineCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnquarantineCommand.java @@ -8,17 +8,15 @@ import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.utils.Result; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Objects; -import java.util.function.Predicate; -import java.util.regex.Pattern; /** * This command can unquarantine quarantined users. Unquarantining can also be paired with a reason. @@ -34,7 +32,6 @@ public final class UnquarantineCommand extends SlashCommandAdapter { private static final String REASON_OPTION = "reason"; private static final String COMMAND_NAME = "unquarantine"; private static final String ACTION_VERB = "unquarantine"; - private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; private final Config config; @@ -44,8 +41,7 @@ public final class UnquarantineCommand extends SlashCommandAdapter { * @param actionsStore used to store actions issued by this command * @param config the config to use for this */ - public UnquarantineCommand(@NotNull ModerationActionsStore actionsStore, - @NotNull Config config) { + public UnquarantineCommand(ModerationActionsStore actionsStore, Config config) { super(COMMAND_NAME, "Unquarantines the given already quarantined user so that they can interact again", SlashCommandVisibility.GUILD); @@ -57,16 +53,16 @@ public UnquarantineCommand(@NotNull ModerationActionsStore actionsStore, true); this.config = config; - hasRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private static void handleNotQuarantinedTarget(@NotNull IReplyCallback event) { + private static void handleNotQuarantinedTarget(IReplyCallback event) { event.reply("The user is not quarantined.").setEphemeral(true).queue(); } - private static RestAction sendDm(@NotNull ISnowflake target, @NotNull String reason, - @NotNull Guild guild, @NotNull GenericEvent event) { + @Nonnull + private static RestAction sendDm(ISnowflake target, String reason, Guild guild, + GenericEvent event) { String dmMessage = """ Hey there, you have been put out of quarantine in the server %s. This means you can now interact with others in the server again. @@ -80,8 +76,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S .map(Result::isSuccess); } - private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull Member target, - @NotNull Member author, @NotNull String reason) { + @Nonnull + private static MessageEmbed sendFeedback(boolean hasSentDm, Member target, Member author, + String reason) { String dmNoticeText = ""; if (!hasSentDm) { dmNoticeText = "(Unable to send them a DM.)"; @@ -90,8 +87,9 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S target.getUser(), dmNoticeText, reason); } - private AuditableRestAction unquarantineUser(@NotNull Member target, - @NotNull Member author, @NotNull String reason, @NotNull Guild guild) { + @Nonnull + private AuditableRestAction unquarantineUser(Member target, Member author, String reason, + Guild guild) { logger.info("'{}' ({}) unquarantined the user '{}' ({}) in guild '{}' for reason '{}'.", author.getUser().getAsTag(), author.getId(), target.getUser().getAsTag(), target.getId(), guild.getName(), reason); @@ -105,9 +103,8 @@ private AuditableRestAction unquarantineUser(@NotNull Member target, .reason(reason); } - private void unquarantineUserFlow(@NotNull Member target, @NotNull Member author, - @NotNull String reason, @NotNull Guild guild, - @NotNull SlashCommandInteractionEvent event) { + private void unquarantineUserFlow(Member target, Member author, String reason, Guild guild, + SlashCommandInteractionEvent event) { sendDm(target, reason, guild, event) .flatMap(hasSentDm -> unquarantineUser(target, author, reason, guild) .map(result -> hasSentDm)) @@ -116,13 +113,11 @@ private void unquarantineUserFlow(@NotNull Member target, @NotNull Member author .queue(); } - @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"}) - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild, - @NotNull IReplyCallback event) { + private boolean handleChecks(Member bot, Member author, @Nullable Member target, + CharSequence reason, Guild guild, IReplyCallback event) { if (!ModerationUtils.handleRoleChangeChecks( ModerationUtils.getQuarantinedRole(guild, config).orElse(null), ACTION_VERB, target, - bot, author, guild, hasRequiredRole, reason, event)) { + bot, author, guild, reason, event)) { return false; } @@ -139,7 +134,7 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { Member target = event.getOption(TARGET_OPTION).getAsMember(); Member author = event.getMember(); String reason = event.getOption(REASON_OPTION).getAsString(); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/WarnCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/WarnCommand.java index 32c84a400d..a852a56728 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/WarnCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/WarnCommand.java @@ -7,18 +7,14 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.Result; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; -import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Objects; -import java.util.function.Predicate; -import java.util.regex.Pattern; - /** * This command can warn users. The command will also try to DM the user to inform them about the @@ -33,28 +29,24 @@ public final class WarnCommand extends SlashCommandAdapter { private static final String REASON_OPTION = "reason"; private static final String ACTION_VERB = "warn"; private final ModerationActionsStore actionsStore; - private final Predicate hasRequiredRole; /** * Creates a new instance. * * @param actionsStore used to store actions issued by this command - * @param config the config to use for this */ - public WarnCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public WarnCommand(ModerationActionsStore actionsStore) { super("warn", "Warns the given user", SlashCommandVisibility.GUILD); getData().addOption(OptionType.USER, USER_OPTION, "The user who you want to warn", true) .addOption(OptionType.STRING, REASON_OPTION, "Why you want to warn the user", true); - hasRequiredRole = - Pattern.compile(config.getHeavyModerationRolePattern()).asMatchPredicate(); this.actionsStore = Objects.requireNonNull(actionsStore); } - private @NotNull RestAction warnUserFlow(@NotNull User target, - @NotNull Member author, @NotNull String reason, @NotNull Guild guild, - @NotNull SlashCommandInteractionEvent event) { + @Nonnull + private RestAction warnUserFlow(User target, Member author, String reason, + Guild guild, SlashCommandInteractionEvent event) { return dmUser(target, reason, guild, event).map(hasSentDm -> { warnUser(target, author, reason, guild); return hasSentDm; @@ -63,9 +55,9 @@ public WarnCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config .flatMap(event::replyEmbeds); } - private static @NotNull RestAction dmUser(@NotNull ISnowflake target, - @NotNull String reason, @NotNull Guild guild, - @NotNull SlashCommandInteractionEvent event) { + @Nonnull + private static RestAction dmUser(ISnowflake target, String reason, Guild guild, + SlashCommandInteractionEvent event) { return event.getJDA() .openPrivateChannelById(target.getId()) .flatMap(channel -> channel.sendMessage( @@ -79,8 +71,7 @@ public WarnCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Config .map(Result::isSuccess); } - private void warnUser(@NotNull User target, @NotNull Member author, @NotNull String reason, - @NotNull Guild guild) { + private void warnUser(User target, Member author, String reason, Guild guild) { logger.info("'{}' ({}) warned the user '{}' ({}) for reason '{}'.", author.getUser().getAsTag(), author.getId(), target.getAsTag(), target.getId(), reason); @@ -89,8 +80,9 @@ private void warnUser(@NotNull User target, @NotNull Member author, @NotNull Str ModerationAction.WARN, null, reason); } - private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull User target, - @NotNull Member author, @NotNull String reason) { + @Nonnull + private static MessageEmbed sendFeedback(boolean hasSentDm, User target, Member author, + String reason) { String dmNoticeText = ""; if (!hasSentDm) { dmNoticeText = "(Unable to send them a DM.)"; @@ -100,7 +92,7 @@ private void warnUser(@NotNull User target, @NotNull Member author, @NotNull Str } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { OptionMapping targetOption = Objects.requireNonNull(event.getOption(USER_OPTION), "The target is null"); Member author = Objects.requireNonNull(event.getMember(), "The author is null"); @@ -116,16 +108,12 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { warnUserFlow(targetOption.getAsUser(), author, reason, guild, event).queue(); } - @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") - private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, String reason, @NotNull SlashCommandInteractionEvent event) { + private boolean handleChecks(Member bot, Member author, @Nullable Member target, String reason, + SlashCommandInteractionEvent event) { if (target != null && !ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, target, event)) { return false; } - if (!ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event)) { - return false; - } return ModerationUtils.handleReason(reason, event); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/WhoIsCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/WhoIsCommand.java index 4403353880..c54cefb97e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/WhoIsCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/WhoIsCommand.java @@ -8,12 +8,12 @@ import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.commands.utils.DiscordClientAction; import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; import java.awt.Color; import java.time.Instant; import java.time.OffsetDateTime; @@ -49,7 +49,7 @@ public WhoIsCommand() { } @Override - public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { + public void onSlashCommand(final SlashCommandInteractionEvent event) { OptionMapping userOption = Objects.requireNonNull(event.getOption(USER_OPTION), "The given user option cannot be null"); OptionMapping showServerSpecificInfoOption = event.getOption(SHOW_SERVER_INFO_OPTION); @@ -70,8 +70,9 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { } @CheckReturnValue - private static @NotNull ReplyCallbackAction handleWhoIsUser(final @NotNull IReplyCallback event, - final @NotNull User user, final @NotNull User.Profile profile) { + @Nonnull + private static ReplyCallbackAction handleWhoIsUser(final IReplyCallback event, final User user, + final User.Profile profile) { String description = userIdentificationToStringItem(user) + "\n**Is bot:** " + user.isBot() + userFlagsToStringItem(user.getFlags()) + "\n**Registration date:** " + DATE_TIME_FORMAT.format(user.getTimeCreated()); @@ -85,9 +86,9 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { } @CheckReturnValue - private static @NotNull ReplyCallbackAction handleWhoIsMember( - final @NotNull IReplyCallback event, final @NotNull Member member, - final @NotNull User.Profile profile) { + @Nonnull + private static ReplyCallbackAction handleWhoIsMember(final IReplyCallback event, + final Member member, final User.Profile profile) { User user = member.getUser(); Color memberColor = member.getColor(); @@ -108,15 +109,16 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { return sendEmbedWithProfileAction(event, embedBuilder.build(), user.getId()); } - private static @NotNull ReplyCallbackAction sendEmbedWithProfileAction( - final @NotNull IReplyCallback event, @NotNull MessageEmbed embed, - @NotNull String userId) { + @Nonnull + private static ReplyCallbackAction sendEmbedWithProfileAction(final IReplyCallback event, + MessageEmbed embed, String userId) { return event.replyEmbeds(embed) .addActionRow( DiscordClientAction.General.USER.asLinkButton("Click to see profile!", userId)); } - private static @NotNull String voiceStateToStringItem(@NotNull final Member member) { + @Nonnull + private static String voiceStateToStringItem(final Member member) { GuildVoiceState voiceState = Objects.requireNonNull(member.getVoiceState(), "The given voiceState cannot be null"); @@ -137,9 +139,9 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { * @param effectiveColor the {@link Color} that the embed will become * @return the generated {@link EmbedBuilder} */ - private static @NotNull EmbedBuilder generateEmbedBuilder(@NotNull final Interaction event, - @NotNull final User user, final @NotNull User.Profile profile, - final Color effectiveColor) { + @Nonnull + private static EmbedBuilder generateEmbedBuilder(final Interaction event, final User user, + final User.Profile profile, final Color effectiveColor) { EmbedBuilder embedBuilder = new EmbedBuilder().setThumbnail(user.getEffectiveAvatarUrl()) .setColor(effectiveColor) @@ -160,7 +162,8 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { * @param member the {@link Member} to take the booster properties from * @return user readable {@link String} */ - private static @NotNull String possibleBoosterToStringItem(final @NotNull Member member) { + @Nonnull + private static String possibleBoosterToStringItem(final Member member) { OffsetDateTime timeBoosted = member.getTimeBoosted(); if (null == timeBoosted) { @@ -177,7 +180,8 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { * @param user the {@link User} to take the identifiers from * @return user readable {@link String} */ - private static @NotNull String userIdentificationToStringItem(final @NotNull User user) { + @Nonnull + private static String userIdentificationToStringItem(final User user) { return "**Mention:** " + user.getAsMention() + "\n**Tag:** " + user.getAsTag() + "\n**ID:** " + user.getId(); } @@ -188,7 +192,8 @@ public void onSlashCommand(@NotNull final SlashCommandInteractionEvent event) { * @param member member to take the Roles from * @return user readable {@link String} of the roles */ - private static String formatRoles(final @NotNull Member member) { + @Nonnull + private static String formatRoles(final Member member) { return member.getRoles().stream().map(Role::getAsMention).collect(Collectors.joining(", ")); } @@ -199,8 +204,8 @@ private static String formatRoles(final @NotNull Member member) { * (recommend {@link java.util.EnumSet} * @return user readable {@link StringBuilder} */ - private static @NotNull StringBuilder userFlagsToStringItem( - final @NotNull Collection flags) { + @Nonnull + private static StringBuilder userFlagsToStringItem(final Collection flags) { String formattedFlags = formatUserFlags(flags); StringBuilder result = hypeSquadToStringItem(flags); @@ -218,8 +223,8 @@ private static String formatRoles(final @NotNull Member member) { * (recommend {@link java.util.EnumSet} * @return user readable {@link StringBuilder} */ - private static @NotNull StringBuilder hypeSquadToStringItem( - final @NotNull Collection flags) { + @Nonnull + private static StringBuilder hypeSquadToStringItem(final Collection flags) { StringBuilder stringBuilder = new StringBuilder("**\nHypesquad:** "); if (flags.contains(User.UserFlag.HYPESQUAD_BALANCE)) { @@ -242,8 +247,8 @@ private static String formatRoles(final @NotNull Member member) { * (recommend {@link java.util.EnumSet} * @return the user readable string */ - @NotNull - private static String formatUserFlags(final @NotNull Collection flags) { + @Nonnull + private static String formatUserFlags(final Collection flags) { return flags.stream() .map(User.UserFlag::getName) .filter(name -> (name.contains("Hypesquad"))) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/package-info.java index 399fc1fa1f..779ef299a7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/package-info.java @@ -2,4 +2,7 @@ * This package offers all the moderation commands from the application such as banning and kicking * users. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.moderation; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamBlocker.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamBlocker.java index e4240cedb2..7c1e812d0d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamBlocker.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamBlocker.java @@ -12,8 +12,6 @@ import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; import net.dv8tion.jda.api.requests.ErrorResponse; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; @@ -28,6 +26,8 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.ScamBlockerConfig; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.awt.Color; import java.util.*; import java.util.function.Consumer; @@ -66,8 +66,8 @@ public final class ScamBlocker extends MessageReceiverAdapter implements UserInt * @param scamHistoryStore to store and retrieve scam history from * @param config the config to use for this */ - public ScamBlocker(@NotNull ModerationActionsStore actionsStore, - @NotNull ScamHistoryStore scamHistoryStore, @NotNull Config config) { + public ScamBlocker(ModerationActionsStore actionsStore, ScamHistoryStore scamHistoryStore, + Config config) { super(Pattern.compile(".*")); this.actionsStore = actionsStore; @@ -84,23 +84,23 @@ public ScamBlocker(@NotNull ModerationActionsStore actionsStore, } @Override - public @NotNull String getName() { + @Nonnull + public String getName() { return "scam-blocker"; } @Override - public void onSelectionMenu(@NotNull SelectMenuInteractionEvent event, - @NotNull List args) { + public void onSelectionMenu(SelectMenuInteractionEvent event, List args) { throw new UnsupportedOperationException("Not used"); } @Override - public void acceptComponentIdGenerator(@NotNull ComponentIdGenerator generator) { + public void acceptComponentIdGenerator(ComponentIdGenerator generator) { componentIdGenerator = generator; } @Override - public void onMessageReceived(@NotNull MessageReceivedEvent event) { + public void onMessageReceived(MessageReceivedEvent event) { if (event.getAuthor().isBot() || event.isWebhookMessage()) { return; } @@ -123,7 +123,7 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { takeAction(event); } - private void takeActionWasAlreadyReported(@NotNull MessageReceivedEvent event) { + private void takeActionWasAlreadyReported(MessageReceivedEvent event) { // The user recently send the same scam already, and that was already reported and handled addScamToHistory(event); @@ -133,7 +133,7 @@ private void takeActionWasAlreadyReported(@NotNull MessageReceivedEvent event) { } } - private void takeAction(@NotNull MessageReceivedEvent event) { + private void takeAction(MessageReceivedEvent event) { switch (mode) { case OFF -> throw new AssertionError( "The OFF-mode should be detected earlier already to prevent expensive computation"); @@ -146,25 +146,25 @@ private void takeAction(@NotNull MessageReceivedEvent event) { } } - private void takeActionLogOnly(@NotNull MessageReceivedEvent event) { + private void takeActionLogOnly(MessageReceivedEvent event) { addScamToHistory(event); logScamMessage(event); } - private void takeActionApproveFirst(@NotNull MessageReceivedEvent event) { + private void takeActionApproveFirst(MessageReceivedEvent event) { addScamToHistory(event); logScamMessage(event); reportScamMessage(event, "Is this scam?", createConfirmDialog(event)); } - private void takeActionAutoDeleteButApproveQuarantine(@NotNull MessageReceivedEvent event) { + private void takeActionAutoDeleteButApproveQuarantine(MessageReceivedEvent event) { addScamToHistory(event); logScamMessage(event); deleteMessage(event); reportScamMessage(event, "Is this scam? (already deleted)", createConfirmDialog(event)); } - private void takeActionAutoDeleteAndQuarantine(@NotNull MessageReceivedEvent event) { + private void takeActionAutoDeleteAndQuarantine(MessageReceivedEvent event) { addScamToHistory(event); logScamMessage(event); deleteMessage(event); @@ -173,26 +173,25 @@ private void takeActionAutoDeleteAndQuarantine(@NotNull MessageReceivedEvent eve reportScamMessage(event, "Detected and handled scam", null); } - private void addScamToHistory(@NotNull MessageReceivedEvent event) { + private void addScamToHistory(MessageReceivedEvent event) { scamHistoryStore.addScam(event.getMessage(), MODES_WITH_IMMEDIATE_DELETION.contains(mode)); } - private void logScamMessage(@NotNull MessageReceivedEvent event) { + private void logScamMessage(MessageReceivedEvent event) { logger.warn("Detected a scam message ('{}') from user '{}' in channel '{}' of guild '{}'.", event.getMessageId(), event.getAuthor().getId(), event.getChannel().getId(), event.getGuild().getId()); } - private void deleteMessage(@NotNull MessageReceivedEvent event) { + private void deleteMessage(MessageReceivedEvent event) { event.getMessage().delete().queue(); } - private void quarantineAuthor(@NotNull MessageReceivedEvent event) { + private void quarantineAuthor(MessageReceivedEvent event) { quarantineAuthor(event.getGuild(), event.getMember(), event.getJDA().getSelfUser()); } - private void quarantineAuthor(@NotNull Guild guild, @NotNull Member author, - @NotNull SelfUser bot) { + private void quarantineAuthor(Guild guild, Member author, SelfUser bot) { String reason = "User posted scam that was automatically detected"; actionsStore.addAction(guild.getIdLong(), bot.getIdLong(), author.getIdLong(), @@ -205,7 +204,7 @@ private void quarantineAuthor(@NotNull Guild guild, @NotNull Member author, .queue(); } - private void reportScamMessage(@NotNull MessageReceivedEvent event, @NotNull String reportTitle, + private void reportScamMessage(MessageReceivedEvent event, String reportTitle, @Nullable ActionRow confirmDialog) { Guild guild = event.getGuild(); Optional reportChannel = getReportChannel(guild); @@ -231,11 +230,11 @@ private void reportScamMessage(@NotNull MessageReceivedEvent event, @NotNull Str reportChannel.orElseThrow().sendMessage(message).queue(); } - private void dmUser(@NotNull MessageReceivedEvent event) { + private void dmUser(MessageReceivedEvent event) { dmUser(event.getGuild(), event.getAuthor().getIdLong(), event.getJDA()); } - private void dmUser(@NotNull Guild guild, long userId, @NotNull JDA jda) { + private void dmUser(Guild guild, long userId, JDA jda) { String dmMessage = """ Hey there, we detected that you did send scam in the server %s and therefore put you under quarantine. @@ -251,11 +250,13 @@ If you think this was a mistake (for example, your account was hacked, but you g .queue(); } - private @NotNull Optional getReportChannel(@NotNull Guild guild) { + @Nonnull + private Optional getReportChannel(Guild guild) { return guild.getTextChannelCache().stream().filter(isReportChannel).findAny(); } - private @NotNull ActionRow createConfirmDialog(@NotNull MessageReceivedEvent event) { + @Nonnull + private ActionRow createConfirmDialog(MessageReceivedEvent event) { ComponentIdArguments args = new ComponentIdArguments(mode, event.getGuild().getIdLong(), event.getChannel().getIdLong(), event.getMessageIdLong(), event.getAuthor().getIdLong(), @@ -265,14 +266,14 @@ If you think this was a mistake (for example, your account was hacked, but you g Button.danger(generateComponentId(args), "No")); } - private @NotNull String generateComponentId(@NotNull ComponentIdArguments args) { + @Nonnull + private String generateComponentId(ComponentIdArguments args) { return Objects.requireNonNull(componentIdGenerator) .generate(new ComponentId(getName(), args.toList()), Lifespan.REGULAR); } @Override - public void onButtonClick(@NotNull ButtonInteractionEvent event, - @NotNull List argsRaw) { + public void onButtonClick(ButtonInteractionEvent event, List argsRaw) { ComponentIdArguments args = ComponentIdArguments.fromList(argsRaw); if (event.getMember().getRoles().stream().map(Role::getName).noneMatch(hasRequiredRole)) { event.reply( @@ -334,10 +335,11 @@ public void onButtonClick(@NotNull ButtonInteractionEvent event, } - private record ComponentIdArguments(@NotNull ScamBlockerConfig.Mode mode, long guildId, - long channelId, long messageId, long authorId, @NotNull String contentHash) { + private record ComponentIdArguments(ScamBlockerConfig.Mode mode, long guildId, long channelId, + long messageId, long authorId, String contentHash) { - static @NotNull ComponentIdArguments fromList(@NotNull List args) { + @Nonnull + static ComponentIdArguments fromList(List args) { ScamBlockerConfig.Mode mode = ScamBlockerConfig.Mode.valueOf(args.get(0)); long guildId = Long.parseLong(args.get(1)); long channelId = Long.parseLong(args.get(2)); @@ -348,7 +350,7 @@ private record ComponentIdArguments(@NotNull ScamBlockerConfig.Mode mode, long g contentHash); } - @NotNull + @Nonnull List toList() { return List.of(mode.name(), Long.toString(guildId), Long.toString(channelId), Long.toString(messageId), Long.toString(authorId), contentHash); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamDetector.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamDetector.java index 15d32d15a9..d4a2281834 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamDetector.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamDetector.java @@ -1,6 +1,5 @@ package org.togetherjava.tjbot.commands.moderation.scam; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.utils.StringDistances; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.ScamBlockerConfig; @@ -23,7 +22,7 @@ public final class ScamDetector { * * @param config the scam blocker config to use */ - public ScamDetector(@NotNull Config config) { + public ScamDetector(Config config) { this.config = config.getScamBlocker(); } @@ -33,20 +32,20 @@ public ScamDetector(@NotNull Config config) { * @param message the message to analyze * @return Whether the message classifies as scam */ - public boolean isScam(@NotNull CharSequence message) { + public boolean isScam(CharSequence message) { AnalyseResults results = new AnalyseResults(); TOKENIZER.splitAsStream(message).forEach(token -> analyzeToken(token, results)); return isScam(results); } - private boolean isScam(@NotNull AnalyseResults results) { + private boolean isScam(AnalyseResults results) { if (results.pingsEveryone && results.containsNitroKeyword && results.hasUrl) { return true; } return results.containsNitroKeyword && results.hasSuspiciousUrl; } - private void analyzeToken(@NotNull String token, @NotNull AnalyseResults results) { + private void analyzeToken(String token, AnalyseResults results) { if ("@everyone".equalsIgnoreCase(token)) { results.pingsEveryone = true; } @@ -60,7 +59,7 @@ private void analyzeToken(@NotNull String token, @NotNull AnalyseResults results } } - private void analyzeUrl(@NotNull String url, @NotNull AnalyseResults results) { + private void analyzeUrl(String url, AnalyseResults results) { String host; try { host = URI.create(url).getHost(); @@ -92,7 +91,7 @@ private void analyzeUrl(@NotNull String url, @NotNull AnalyseResults results) { } } - private boolean isHostSimilarToKeyword(@NotNull String host, @NotNull String keyword) { + private boolean isHostSimilarToKeyword(String host, String keyword) { // NOTE This algorithm is far from optimal. // It is good enough for our purpose though and not that complex. diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamHistoryPurgeRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamHistoryPurgeRoutine.java index 649f793b88..6c6ed71de9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamHistoryPurgeRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamHistoryPurgeRoutine.java @@ -1,9 +1,9 @@ package org.togetherjava.tjbot.commands.moderation.scam; import net.dv8tion.jda.api.JDA; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.Routine; +import javax.annotation.Nonnull; import java.time.Instant; import java.time.Period; import java.util.concurrent.TimeUnit; @@ -20,17 +20,18 @@ public final class ScamHistoryPurgeRoutine implements Routine { * * @param scamHistoryStore containing the scam history to purge */ - public ScamHistoryPurgeRoutine(@NotNull ScamHistoryStore scamHistoryStore) { + public ScamHistoryPurgeRoutine(ScamHistoryStore scamHistoryStore) { this.scamHistoryStore = scamHistoryStore; } @Override - public @NotNull Schedule createSchedule() { + @Nonnull + public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 0, 1, TimeUnit.DAYS); } @Override - public void runRoutine(@NotNull JDA jda) { + public void runRoutine(JDA jda) { scamHistoryStore.deleteHistoryOlderThan(Instant.now().minus(DELETE_SCAM_RECORDS_AFTER)); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamHistoryStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamHistoryStore.java index 3154820dc5..9f1802610b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamHistoryStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamHistoryStore.java @@ -1,12 +1,12 @@ package org.togetherjava.tjbot.commands.moderation.scam; import net.dv8tion.jda.api.entities.Message; -import org.jetbrains.annotations.NotNull; import org.jooq.Result; import org.togetherjava.tjbot.commands.utils.Hashing; import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.records.ScamHistoryRecord; +import javax.annotation.Nonnull; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -39,7 +39,7 @@ public final class ScamHistoryStore { * * @param database containing the scam history to work with */ - public ScamHistoryStore(@NotNull Database database) { + public ScamHistoryStore(Database database) { this.database = database; } @@ -49,7 +49,7 @@ public ScamHistoryStore(@NotNull Database database) { * @param scam the message to add * @param isDeleted whether the message is already, or about to get, deleted */ - public void addScam(@NotNull Message scam, boolean isDeleted) { + public void addScam(Message scam, boolean isDeleted) { Objects.requireNonNull(scam); database.write(context -> context.newRecord(SCAM_HISTORY) @@ -71,8 +71,8 @@ public void addScam(@NotNull Message scam, boolean isDeleted) { * @return identifications of all scam messages that have just been marked deleted, which * previously have not been marked accordingly yet */ - public @NotNull Collection markScamDuplicatesDeleted( - @NotNull Message scam) { + @Nonnull + public Collection markScamDuplicatesDeleted(Message scam) { return markScamDuplicatesDeleted(scam.getGuild().getIdLong(), scam.getAuthor().getIdLong(), hashMessageContent(scam)); } @@ -87,8 +87,9 @@ public void addScam(@NotNull Message scam, boolean isDeleted) { * @return identifications of all scam messages that have just been marked deleted, which * previously have not been marked accordingly yet */ - public @NotNull Collection markScamDuplicatesDeleted(long guildId, - long authorId, @NotNull String contentHash) { + @Nonnull + public Collection markScamDuplicatesDeleted(long guildId, long authorId, + String contentHash) { return database.writeAndProvide(context -> { Result undeletedDuplicates = context.selectFrom(SCAM_HISTORY) .where(SCAM_HISTORY.GUILD_ID.eq(guildId) @@ -111,7 +112,7 @@ public void addScam(@NotNull Message scam, boolean isDeleted) { * @param scam the scam message to look for duplicates * @return whether there are recent duplicates */ - public boolean hasRecentScamDuplicate(@NotNull Message scam) { + public boolean hasRecentScamDuplicate(Message scam) { Instant recentScamThreshold = Instant.now().minus(RECENT_SCAM_DURATION); return database.read(context -> context.fetchCount(SCAM_HISTORY, @@ -138,7 +139,8 @@ public void deleteHistoryOlderThan(Instant olderThan) { * @param message the message to hash * @return a text representation of the hash */ - public static @NotNull String hashMessageContent(@NotNull Message message) { + @Nonnull + public static String hashMessageContent(Message message) { return Hashing.bytesToHex(Hashing.hash(HASH_METHOD, message.getContentRaw().getBytes(StandardCharsets.UTF_8))); } @@ -154,8 +156,8 @@ public void deleteHistoryOlderThan(Instant olderThan) { */ public record ScamIdentification(long guildId, long channelId, long messageId, long authorId, String contentHash) { - private static ScamIdentification ofDatabaseRecord( - @NotNull ScamHistoryRecord scamHistoryRecord) { + @Nonnull + private static ScamIdentification ofDatabaseRecord(ScamHistoryRecord scamHistoryRecord) { return new ScamIdentification(scamHistoryRecord.getGuildId(), scamHistoryRecord.getChannelId(), scamHistoryRecord.getMessageId(), scamHistoryRecord.getAuthorId(), scamHistoryRecord.getContentHash()); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/package-info.java index 40b4605eff..2cf89b16e6 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/package-info.java @@ -2,4 +2,7 @@ * This package offers classes dealing with detecting scam messages and taking appropriate action, * see {@link org.togetherjava.tjbot.commands.moderation.scam.ScamBlocker} as main entry point. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.moderation.scam; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/RevocableModerationAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/RevocableModerationAction.java index b2cedc3007..b74290afd1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/RevocableModerationAction.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/RevocableModerationAction.java @@ -3,9 +3,10 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.requests.RestAction; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.moderation.ModerationAction; +import javax.annotation.Nonnull; + /** * Represents revocable moderation actions, such as temporary bans. Primarily used by * {@link TemporaryModerationRoutine} to identify and revoke such actions. @@ -34,7 +35,7 @@ enum FailureIdentification { * * @return the type to apply the temporary action */ - @NotNull + @Nonnull ModerationAction getApplyType(); /** @@ -43,7 +44,7 @@ enum FailureIdentification { * * @return the type to revoke the temporary action */ - @NotNull + @Nonnull ModerationAction getRevokeType(); /** @@ -54,9 +55,8 @@ enum FailureIdentification { * @param reason why the action is revoked * @return the unsubmitted revocation action */ - @NotNull - RestAction revokeAction(@NotNull Guild guild, @NotNull User target, - @NotNull String reason); + @Nonnull + RestAction revokeAction(Guild guild, User target, String reason); /** * Handle a failure that might occur during revocation, i.e. execution of the action returned by @@ -67,6 +67,6 @@ RestAction revokeAction(@NotNull Guild guild, @NotNull User target, * @return a classification of the failure, decides whether the surrounding flow will continue * to handle the error further or not */ - @NotNull - FailureIdentification handleRevokeFailure(@NotNull Throwable failure, long targetId); + @Nonnull + FailureIdentification handleRevokeFailure(Throwable failure, long targetId); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/RevocableRoleBasedAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/RevocableRoleBasedAction.java index d289b00f49..bb7094f3a2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/RevocableRoleBasedAction.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/RevocableRoleBasedAction.java @@ -1,10 +1,11 @@ package org.togetherjava.tjbot.commands.moderation.temp; import net.dv8tion.jda.api.exceptions.ErrorResponseException; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; + /** * Role based moderation actions that can be revoked, for example a {@link TemporaryMuteAction} or a * {@link TemporaryBanAction}, which are applied implicitly purely by the presence of a role. @@ -20,13 +21,13 @@ abstract class RevocableRoleBasedAction implements RevocableModerationAction { * @param actionName the action name to be used in logging in case of a failure, e.g. * {@code "mute"}, {@code "quarantine"} */ - RevocableRoleBasedAction(@NotNull String actionName) { + RevocableRoleBasedAction(String actionName) { this.actionName = actionName; } @Override - public @NotNull FailureIdentification handleRevokeFailure(@NotNull Throwable failure, - long targetId) { + @Nonnull + public FailureIdentification handleRevokeFailure(Throwable failure, long targetId) { if (failure instanceof ErrorResponseException errorResponseException) { switch (errorResponseException.getErrorResponse()) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryBanAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryBanAction.java index 0941224872..eaa42e34c9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryBanAction.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryBanAction.java @@ -5,11 +5,12 @@ import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.RestAction; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.moderation.ModerationAction; +import javax.annotation.Nonnull; + /** * Action to revoke temporary bans, as applied by * {@link org.togetherjava.tjbot.commands.moderation.BanCommand} and executed by @@ -19,24 +20,26 @@ final class TemporaryBanAction implements RevocableModerationAction { private static final Logger logger = LoggerFactory.getLogger(TemporaryBanAction.class); @Override - public @NotNull ModerationAction getApplyType() { + @Nonnull + public ModerationAction getApplyType() { return ModerationAction.BAN; } @Override - public @NotNull ModerationAction getRevokeType() { + @Nonnull + public ModerationAction getRevokeType() { return ModerationAction.UNBAN; } @Override - public @NotNull RestAction revokeAction(@NotNull Guild guild, @NotNull User target, - @NotNull String reason) { + @Nonnull + public RestAction revokeAction(Guild guild, User target, String reason) { return guild.unban(target).reason(reason); } @Override - public @NotNull FailureIdentification handleRevokeFailure(@NotNull Throwable failure, - long targetId) { + @Nonnull + public FailureIdentification handleRevokeFailure(Throwable failure, long targetId) { if (failure instanceof ErrorResponseException errorResponseException) { if (errorResponseException.getErrorResponse() == ErrorResponse.UNKNOWN_USER) { logger.debug( diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryModerationRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryModerationRoutine.java index 78e393e3b0..34df11d906 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryModerationRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryModerationRoutine.java @@ -4,7 +4,6 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.requests.RestAction; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.Routine; @@ -13,6 +12,7 @@ import org.togetherjava.tjbot.commands.moderation.ModerationActionsStore; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -43,8 +43,7 @@ public final class TemporaryModerationRoutine implements Routine { * @param actionsStore the store used to retrieve temporary moderation actions * @param config the config to use for this */ - public TemporaryModerationRoutine(@NotNull JDA jda, - @NotNull ModerationActionsStore actionsStore, @NotNull Config config) { + public TemporaryModerationRoutine(JDA jda, ModerationActionsStore actionsStore, Config config) { this.actionsStore = actionsStore; this.jda = jda; @@ -56,12 +55,13 @@ public TemporaryModerationRoutine(@NotNull JDA jda, } @Override - public void runRoutine(@NotNull JDA jda) { + public void runRoutine(JDA jda) { checkExpiredActions(); } @Override - public @NotNull Schedule createSchedule() { + @Nonnull + public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_DELAY, 5, 5, TimeUnit.MINUTES); } @@ -78,7 +78,7 @@ private void checkExpiredActions() { logger.debug("Finished checking expired temporary moderation actions to revoke."); } - private void processGroupedActions(@NotNull RevocationGroupIdentifier groupIdentifier) { + private void processGroupedActions(RevocationGroupIdentifier groupIdentifier) { // Do not revoke an action which was overwritten by a still effective action that was issued // afterwards // For example if a user was perm-banned after being temp-banned @@ -108,7 +108,7 @@ private void processGroupedActions(@NotNull RevocationGroupIdentifier groupIdent revokeAction(groupIdentifier); } - private void revokeAction(@NotNull RevocationGroupIdentifier groupIdentifier) { + private void revokeAction(RevocationGroupIdentifier groupIdentifier) { Guild guild = jda.getGuildById(groupIdentifier.guildId); if (guild == null) { logger.debug( @@ -123,8 +123,9 @@ private void revokeAction(@NotNull RevocationGroupIdentifier groupIdentifier) { }, failure -> handleFailure(failure, groupIdentifier)); } - private @NotNull RestAction executeRevocation(@NotNull Guild guild, @NotNull User target, - @NotNull ModerationAction actionType) { + @Nonnull + private RestAction executeRevocation(Guild guild, User target, + ModerationAction actionType) { logger.info("Revoked temporary action {} against user '{}' ({}).", actionType, target.getAsTag(), target.getId()); RevocableModerationAction action = getRevocableActionByType(actionType); @@ -136,8 +137,7 @@ private void revokeAction(@NotNull RevocationGroupIdentifier groupIdentifier) { return action.revokeAction(guild, target, reason); } - private void handleFailure(@NotNull Throwable failure, - @NotNull RevocationGroupIdentifier groupIdentifier) { + private void handleFailure(Throwable failure, RevocationGroupIdentifier groupIdentifier) { if (getRevocableActionByType(groupIdentifier.type).handleRevokeFailure(failure, groupIdentifier.targetId) == RevocableModerationAction.FailureIdentification.KNOWN) { return; @@ -148,15 +148,14 @@ private void handleFailure(@NotNull Throwable failure, groupIdentifier.targetId, failure); } - private @NotNull RevocableModerationAction getRevocableActionByType( - @NotNull ModerationAction type) { + @Nonnull + private RevocableModerationAction getRevocableActionByType(ModerationAction type) { return Objects.requireNonNull(typeToRevocableAction.get(type), "Action type is not revocable: " + type); } - private record RevocationGroupIdentifier(long guildId, long targetId, - @NotNull ModerationAction type) { - static RevocationGroupIdentifier of(@NotNull ActionRecord actionRecord) { + private record RevocationGroupIdentifier(long guildId, long targetId, ModerationAction type) { + static RevocationGroupIdentifier of(ActionRecord actionRecord) { return new RevocationGroupIdentifier(actionRecord.guildId(), actionRecord.targetId(), actionRecord.actionType()); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryMuteAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryMuteAction.java index 921fe8b06a..94afeb2948 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryMuteAction.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryMuteAction.java @@ -3,11 +3,12 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.requests.RestAction; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.moderation.ModerationAction; import org.togetherjava.tjbot.commands.moderation.ModerationUtils; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; + /** * Action to revoke temporary mutes, as applied by * {@link org.togetherjava.tjbot.commands.moderation.MuteCommand} and executed by @@ -21,25 +22,27 @@ final class TemporaryMuteAction extends RevocableRoleBasedAction { * * @param config the config to use to identify the muted role */ - TemporaryMuteAction(@NotNull Config config) { + TemporaryMuteAction(Config config) { super("mute"); this.config = config; } @Override - public @NotNull ModerationAction getApplyType() { + @Nonnull + public ModerationAction getApplyType() { return ModerationAction.MUTE; } @Override - public @NotNull ModerationAction getRevokeType() { + @Nonnull + public ModerationAction getRevokeType() { return ModerationAction.UNMUTE; } @Override - public @NotNull RestAction revokeAction(@NotNull Guild guild, @NotNull User target, - @NotNull String reason) { + @Nonnull + public RestAction revokeAction(Guild guild, User target, String reason) { return guild .removeRoleFromMember(target.getIdLong(), ModerationUtils.getMutedRole(guild, config).orElseThrow()) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryQuarantineAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryQuarantineAction.java index 2e8c7908a9..67393450f3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryQuarantineAction.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryQuarantineAction.java @@ -3,11 +3,12 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.requests.RestAction; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.moderation.ModerationAction; import org.togetherjava.tjbot.commands.moderation.ModerationUtils; import org.togetherjava.tjbot.config.Config; +import javax.annotation.Nonnull; + /** * Action to revoke temporary quarantines, as applied by * {@link org.togetherjava.tjbot.commands.moderation.QuarantineCommand} and executed by @@ -21,25 +22,27 @@ final class TemporaryQuarantineAction extends RevocableRoleBasedAction { * * @param config the config to use to identify the quarantined role */ - TemporaryQuarantineAction(@NotNull Config config) { + TemporaryQuarantineAction(Config config) { super("quarantine"); this.config = config; } @Override - public @NotNull ModerationAction getApplyType() { + @Nonnull + public ModerationAction getApplyType() { return ModerationAction.QUARANTINE; } @Override - public @NotNull ModerationAction getRevokeType() { + @Nonnull + public ModerationAction getRevokeType() { return ModerationAction.UNQUARANTINE; } @Override - public @NotNull RestAction revokeAction(@NotNull Guild guild, @NotNull User target, - @NotNull String reason) { + @Nonnull + public RestAction revokeAction(Guild guild, User target, String reason) { return guild .removeRoleFromMember(target.getIdLong(), ModerationUtils.getQuarantinedRole(guild, config).orElseThrow()) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/package-info.java index ba0fe1bbe9..6a11e771d2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/package-info.java @@ -1,4 +1,7 @@ /** * This package offers classes dealing with temporary moderation actions, such as temporary bans. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.moderation.temp; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/package-info.java index 17e5e3366c..0a00d8bf03 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/package-info.java @@ -9,4 +9,7 @@ * {@link org.togetherjava.tjbot.commands.SlashCommand} or using the adapter * {@link org.togetherjava.tjbot.commands.SlashCommandAdapter} for convenience. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindCommand.java index ef1128f035..513e72438a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindCommand.java @@ -7,11 +7,11 @@ import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.db.Database; +import javax.annotation.Nonnull; import java.time.*; import java.time.temporal.TemporalAmount; import java.util.List; @@ -52,7 +52,7 @@ public final class RemindCommand extends SlashCommandAdapter { * * @param database to store and fetch the reminders from */ - public RemindCommand(@NotNull Database database) { + public RemindCommand(Database database) { super(COMMAND_NAME, "Reminds you after a given time period has passed (e.g. in 5 weeks)", SlashCommandVisibility.GUILD); @@ -72,7 +72,7 @@ public RemindCommand(@NotNull Database database) { } @Override - public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommand(SlashCommandInteractionEvent event) { int timeAmount = Math.toIntExact(event.getOption(TIME_AMOUNT_OPTION).getAsLong()); String timeUnit = event.getOption(TIME_UNIT_OPTION).getAsString(); String content = event.getOption(CONTENT_OPTION).getAsString(); @@ -102,7 +102,8 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { .insert()); } - private static @NotNull Instant parseWhen(int whenAmount, @NotNull String whenUnit) { + @Nonnull + private static Instant parseWhen(int whenAmount, String whenUnit) { TemporalAmount period = switch (whenUnit) { case "second", "seconds" -> Duration.ofSeconds(whenAmount); case "minute", "minutes" -> Duration.ofMinutes(whenAmount); @@ -117,8 +118,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { return ZonedDateTime.now(ZoneOffset.UTC).plus(period).toInstant(); } - private static boolean handleIsRemindAtWithinLimits(@NotNull Instant remindAt, - @NotNull IReplyCallback event) { + private static boolean handleIsRemindAtWithinLimits(Instant remindAt, IReplyCallback event) { ZonedDateTime maxWhen = ZonedDateTime.now(ZoneOffset.UTC).plus(MAX_TIME_PERIOD); if (remindAt.atZone(ZoneOffset.UTC).isBefore(maxWhen)) { @@ -134,8 +134,8 @@ private static boolean handleIsRemindAtWithinLimits(@NotNull Instant remindAt, return false; } - private boolean handleIsUserBelowMaxPendingReminders(@NotNull ISnowflake author, - @NotNull ISnowflake guild, @NotNull IReplyCallback event) { + private boolean handleIsUserBelowMaxPendingReminders(ISnowflake author, ISnowflake guild, + IReplyCallback event) { int pendingReminders = database.read(context -> context.fetchCount(PENDING_REMINDERS, PENDING_REMINDERS.AUTHOR_ID.equal(author.getIdLong()) .and(PENDING_REMINDERS.GUILD_ID.equal(guild.getIdLong())))); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java index 563231afb3..c755fe0974 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java @@ -2,17 +2,20 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.PrivateChannel; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.MessageAction; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.Routine; import org.togetherjava.tjbot.db.Database; -import java.awt.*; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.awt.Color; import java.time.Instant; import java.time.temporal.TemporalAccessor; import java.util.concurrent.TimeUnit; @@ -37,18 +40,19 @@ public final class RemindRoutine implements Routine { * * @param database the database that contains the pending reminders to send. */ - public RemindRoutine(@NotNull Database database) { + public RemindRoutine(Database database) { this.database = database; } @Override - public @NotNull Schedule createSchedule() { + @Nonnull + public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 0, SCHEDULE_INTERVAL_SECONDS, TimeUnit.SECONDS); } @Override - public void runRoutine(@NotNull JDA jda) { + public void runRoutine(JDA jda) { Instant now = Instant.now(); database.write(context -> context.selectFrom(PENDING_REMINDERS) .where(PENDING_REMINDERS.REMIND_AT.lessOrEqual(now)) @@ -62,13 +66,13 @@ public void runRoutine(@NotNull JDA jda) { })); } - private static void sendReminder(@NotNull JDA jda, long id, long channelId, long authorId, - @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) { + private static void sendReminder(JDA jda, long id, long channelId, long authorId, + CharSequence content, TemporalAccessor createdAt) { RestAction route = computeReminderRoute(jda, channelId, authorId); sendReminderViaRoute(route, id, content, createdAt); } - private static RestAction computeReminderRoute(@NotNull JDA jda, long channelId, + private static RestAction computeReminderRoute(JDA jda, long channelId, long authorId) { // If guild channel can still be found, send there MessageChannel channel = jda.getChannelById(MessageChannel.class, channelId); @@ -80,20 +84,21 @@ private static RestAction computeReminderRoute(@NotNull JDA jda, return createDmReminderRoute(jda, authorId); } - private static @NotNull RestAction createGuildReminderRoute(@NotNull JDA jda, - long authorId, @NotNull MessageChannel channel) { + @Nonnull + private static RestAction createGuildReminderRoute(JDA jda, long authorId, + MessageChannel channel) { return jda.retrieveUserById(authorId) .onErrorMap(error -> null) .map(author -> ReminderRoute.toPublic(channel, author)); } - private static @NotNull RestAction createDmReminderRoute(@NotNull JDA jda, - long authorId) { + @Nonnull + private static RestAction createDmReminderRoute(JDA jda, long authorId) { return jda.openPrivateChannelById(authorId).map(ReminderRoute::toPrivate); } - private static void sendReminderViaRoute(@NotNull RestAction routeAction, - long id, @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) { + private static void sendReminderViaRoute(RestAction routeAction, long id, + CharSequence content, TemporalAccessor createdAt) { Function sendMessage = route -> route.channel .sendMessageEmbeds(createReminderEmbed(content, createdAt, route.target())) .content(route.description()); @@ -108,8 +113,9 @@ Failed to send a reminder (id '{}'), skipping it. This can be due to a network i routeAction.flatMap(sendMessage).queue(doNothing(), logFailure); } - private static @NotNull MessageEmbed createReminderEmbed(@NotNull CharSequence content, - @NotNull TemporalAccessor createdAt, @Nullable User author) { + @Nonnull + private static MessageEmbed createReminderEmbed(CharSequence content, + TemporalAccessor createdAt, @Nullable User author) { String authorName = author == null ? "Unknown user" : author.getAsTag(); String authorIconUrl = author == null ? null : author.getAvatarUrl(); @@ -121,19 +127,22 @@ Failed to send a reminder (id '{}'), skipping it. This can be due to a network i .build(); } - private static @NotNull Consumer doNothing() { + @Nonnull + private static Consumer doNothing() { return a -> { }; } - private record ReminderRoute(@NotNull MessageChannel channel, @Nullable User target, + private record ReminderRoute(MessageChannel channel, @Nullable User target, @Nullable String description) { - static ReminderRoute toPublic(@NotNull MessageChannel channel, @Nullable User target) { + @Nonnull + static ReminderRoute toPublic(MessageChannel channel, @Nullable User target) { return new ReminderRoute(channel, target, target == null ? null : target.getAsMention()); } - static ReminderRoute toPrivate(@NotNull PrivateChannel channel) { + @Nonnull + static ReminderRoute toPrivate(PrivateChannel channel) { return new ReminderRoute(channel, channel.getUser(), "(Sending your reminder directly, because I was unable to locate" + " the original channel you wanted it to be send to)"); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/package-info.java index a7f65b1c69..f4fab40923 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/package-info.java @@ -2,4 +2,7 @@ * This packages offers all the functionality for the remind-command. The core class is * {@link org.togetherjava.tjbot.commands.reminder.RemindCommand}. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.commands.reminder; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java index 5921a542fc..f7eae74609 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java @@ -15,7 +15,6 @@ import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.components.ComponentInteraction; import net.dv8tion.jda.api.requests.ErrorResponse; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.*; @@ -26,6 +25,7 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; +import javax.annotation.Nonnull; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -71,8 +71,7 @@ public final class BotCore extends ListenerAdapter implements SlashCommandProvid * @param database the database that commands may use to persist data * @param config the configuration to use for this system */ - @SuppressWarnings("ThisEscapedInObjectConstruction") - public BotCore(@NotNull JDA jda, @NotNull Database database, @NotNull Config config) { + public BotCore(JDA jda, Database database, Config config) { this.config = config; Collection features = Features.createFeatures(jda, database, config); @@ -126,7 +125,8 @@ public BotCore(@NotNull JDA jda, @NotNull Database database, @NotNull Config con } @Override - public @NotNull Collection getSlashCommands() { + @Nonnull + public Collection getSlashCommands() { return nameToInteractor.values() .stream() .filter(SlashCommand.class::isInstance) @@ -135,7 +135,8 @@ public BotCore(@NotNull JDA jda, @NotNull Database database, @NotNull Config con } @Override - public @NotNull Optional getSlashCommand(@NotNull String name) { + @Nonnull + public Optional getSlashCommand(String name) { return Optional.ofNullable(nameToInteractor.get(name)) .filter(SlashCommand.class::isInstance) .map(SlashCommand.class::cast); @@ -146,7 +147,7 @@ public BotCore(@NotNull JDA jda, @NotNull Database database, @NotNull Config con * * @param jda the JDA instance to work with */ - public void onReady(@NotNull JDA jda) { + public void onReady(JDA jda) { if (!receivedOnReady.compareAndSet(false, true)) { // Ensures that we only enter the event once return; @@ -163,7 +164,7 @@ public void onReady(@NotNull JDA jda) { scheduleRoutines(jda); } - private void scheduleRoutines(@NotNull JDA jda) { + private void scheduleRoutines(JDA jda) { routines.forEach(routine -> { Runnable command = () -> { String routineName = routine.getClass().getSimpleName(); @@ -188,7 +189,7 @@ private void scheduleRoutines(@NotNull JDA jda) { } @Override - public void onMessageReceived(@NotNull final MessageReceivedEvent event) { + public void onMessageReceived(final MessageReceivedEvent event) { if (event.isFromGuild()) { getMessageReceiversSubscribedTo(event.getChannel()) .forEach(messageReceiver -> messageReceiver.onMessageReceived(event)); @@ -196,15 +197,15 @@ public void onMessageReceived(@NotNull final MessageReceivedEvent event) { } @Override - public void onMessageUpdate(@NotNull final MessageUpdateEvent event) { + public void onMessageUpdate(final MessageUpdateEvent event) { if (event.isFromGuild()) { getMessageReceiversSubscribedTo(event.getChannel()) .forEach(messageReceiver -> messageReceiver.onMessageUpdated(event)); } } - private @NotNull Stream getMessageReceiversSubscribedTo( - @NotNull Channel channel) { + @Nonnull + private Stream getMessageReceiversSubscribedTo(Channel channel) { String channelName = channel.getName(); return channelNameToMessageReceiver.entrySet() .stream() @@ -215,14 +216,14 @@ public void onMessageUpdate(@NotNull final MessageUpdateEvent event) { } @Override - public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) { + public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { logger.debug("Received slash command '{}' (#{}) on guild '{}'", event.getName(), event.getId(), event.getGuild()); COMMAND_SERVICE.execute(() -> requireSlashCommand(event.getName()).onSlashCommand(event)); } @Override - public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { + public void onButtonInteraction(ButtonInteractionEvent event) { logger.debug("Received button click '{}' (#{}) on guild '{}'", event.getComponentId(), event.getId(), event.getGuild()); COMMAND_SERVICE @@ -230,14 +231,14 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { } @Override - public void onSelectMenuInteraction(@NotNull SelectMenuInteractionEvent event) { + public void onSelectMenuInteraction(SelectMenuInteractionEvent event) { logger.debug("Received selection menu event '{}' (#{}) on guild '{}'", event.getComponentId(), event.getId(), event.getGuild()); COMMAND_SERVICE .execute(() -> forwardComponentCommand(event, UserInteractor::onSelectionMenu)); } - private void registerReloadCommand(@NotNull Guild guild) { + private void registerReloadCommand(Guild guild) { guild.retrieveCommands().queue(commands -> { // Has it been registered already? if (commands.stream().map(Command::getName).anyMatch(RELOAD_COMMAND::equals)) { @@ -257,7 +258,6 @@ private void registerReloadCommand(@NotNull Guild guild) { /** * Forwards the given component event to the associated user interactor. *

    - *

    * An example call might look like: * *

    @@ -271,8 +271,8 @@ private void registerReloadCommand(@NotNull Guild guild) {
          *        providing the event and list of arguments for consumption
          * @param  the type of the component interaction that should be forwarded
          */
    -    private  void forwardComponentCommand(@NotNull T event,
    -            @NotNull TriConsumer> interactorArgumentConsumer) {
    +    private  void forwardComponentCommand(T event,
    +            TriConsumer> interactorArgumentConsumer) {
             Optional componentIdOpt;
             try {
                 componentIdOpt = componentIdParser.parse(event.getComponentId());
    @@ -308,7 +308,8 @@ private  void forwardComponentCommand(@NotNull T
          * @return the command with the given name
          * @throws NullPointerException if the command with the given name was not registered
          */
    -    private @NotNull SlashCommand requireSlashCommand(@NotNull String name) {
    +    @Nonnull
    +    private SlashCommand requireSlashCommand(String name) {
             return getSlashCommand(name).orElseThrow(
                     () -> new NullPointerException("There is no slash command with name " + name));
         }
    @@ -320,7 +321,8 @@ private  void forwardComponentCommand(@NotNull T
          * @return the user interactor with the given name
          * @throws NullPointerException if the user interactor with the given name was not registered
          */
    -    private @NotNull UserInteractor requireUserInteractor(@NotNull String name) {
    +    @Nonnull
    +    private UserInteractor requireUserInteractor(String name) {
             return Objects.requireNonNull(nameToInteractor.get(name));
         }
     
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/LogLevelCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/LogLevelCommand.java
    index 32a66f41b4..2780f3f8ed 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/LogLevelCommand.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/LogLevelCommand.java
    @@ -6,7 +6,6 @@
     import org.apache.logging.log4j.Level;
     import org.apache.logging.log4j.LogManager;
     import org.apache.logging.log4j.core.config.Configurator;
    -import org.jetbrains.annotations.NotNull;
     import org.togetherjava.tjbot.commands.SlashCommandAdapter;
     import org.togetherjava.tjbot.commands.SlashCommandVisibility;
     
    @@ -44,7 +43,7 @@ public LogLevelCommand() {
         // Security warning about changing log configs. We only change the level, that is safe.
         @SuppressWarnings("squid:S4792")
         @Override
    -    public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
    +    public void onSlashCommand(SlashCommandInteractionEvent event) {
             String levelText = event.getOption(LOG_LEVEL_OPTION).getAsString();
             Level level = Level.getLevel(levelText);
     
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/ReloadCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/ReloadCommand.java
    index 256faa7d5e..1b715aad28 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/ReloadCommand.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/ReloadCommand.java
    @@ -10,7 +10,6 @@
     import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
     import net.dv8tion.jda.api.requests.RestAction;
     import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction;
    -import org.jetbrains.annotations.NotNull;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     import org.togetherjava.tjbot.commands.SlashCommand;
    @@ -18,6 +17,7 @@
     import org.togetherjava.tjbot.commands.SlashCommandVisibility;
     import org.togetherjava.tjbot.commands.utils.MessageUtils;
     
    +import javax.annotation.Nonnull;
     import java.util.ArrayList;
     import java.util.Collections;
     import java.util.List;
    @@ -46,7 +46,7 @@ public final class ReloadCommand extends SlashCommandAdapter {
          * @param commandProvider the provider of slash commands to reload when this command is
          *        triggered
          */
    -    public ReloadCommand(@NotNull SlashCommandProvider commandProvider) {
    +    public ReloadCommand(SlashCommandProvider commandProvider) {
             super("reload",
                     "Uploads all existing slash-commands to Discord so they are fully up-to-date.",
                     SlashCommandVisibility.GUILD);
    @@ -54,7 +54,7 @@ public ReloadCommand(@NotNull SlashCommandProvider commandProvider) {
         }
     
         @Override
    -    public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
    +    public void onSlashCommand(SlashCommandInteractionEvent event) {
             Member member = Objects.requireNonNull(event.getMember());
     
             if (!member.hasPermission(Permission.MANAGE_SERVER)) {
    @@ -75,7 +75,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
         }
     
         @Override
    -    public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List args) {
    +    public void onButtonClick(ButtonInteractionEvent event, List args) {
             // Ignore if another user clicked the button
             String userId = args.get(0);
             if (!userId.equals(Objects.requireNonNull(event.getMember()).getId())) {
    @@ -102,8 +102,7 @@ public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List updateCommandsIf(
    @@ -133,9 +132,9 @@ public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List commandFilter,
    -            @NotNull CommandListUpdateAction updateAction) {
    +    @Nonnull
    +    private CommandListUpdateAction updateCommandsIf(Predicate commandFilter,
    +            CommandListUpdateAction updateAction) {
             return commandProvider.getSlashCommands()
                 .stream()
                 .filter(commandFilter)
    @@ -143,12 +142,13 @@ public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List x);
         }
     
    -    private static @NotNull CommandListUpdateAction getGlobalUpdateAction(@NotNull JDA jda) {
    +    @Nonnull
    +    private static CommandListUpdateAction getGlobalUpdateAction(JDA jda) {
             return jda.updateCommands();
         }
     
    -    private static @NotNull Stream getGuildUpdateActions(
    -            @NotNull JDA jda) {
    +    @Nonnull
    +    private static Stream getGuildUpdateActions(JDA jda) {
             return jda.getGuildCache().stream().map(Guild::updateCommands);
         }
     }
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/SlashCommandProvider.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/SlashCommandProvider.java
    index ef9f0357bd..85cc662511 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/SlashCommandProvider.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/SlashCommandProvider.java
    @@ -1,8 +1,8 @@
     package org.togetherjava.tjbot.commands.system;
     
    -import org.jetbrains.annotations.NotNull;
     import org.togetherjava.tjbot.commands.SlashCommand;
     
    +import javax.annotation.Nonnull;
     import java.util.Collection;
     import java.util.Optional;
     
    @@ -15,7 +15,7 @@ public interface SlashCommandProvider {
          *
          * @return all slash commands
          */
    -    @NotNull
    +    @Nonnull
         Collection getSlashCommands();
     
         /**
    @@ -24,6 +24,6 @@ public interface SlashCommandProvider {
          * @param name the name of the command
          * @return the command registered under this name, if any
          */
    -    @NotNull
    -    Optional getSlashCommand(@NotNull String name);
    +    @Nonnull
    +    Optional getSlashCommand(String name);
     }
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/package-info.java
    index 60670e1341..48e796b587 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/package-info.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/package-info.java
    @@ -2,4 +2,7 @@
      * This package represents the core of the command system. The entry point is
      * {@link org.togetherjava.tjbot.commands.system.BotCore}.
      */
    +@ParametersAreNonnullByDefault
     package org.togetherjava.tjbot.commands.system;
    +
    +import javax.annotation.ParametersAreNonnullByDefault;
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java
    index de59fae319..615ee799ae 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java
    @@ -5,7 +5,6 @@
     import net.dv8tion.jda.api.interactions.commands.OptionMapping;
     import net.dv8tion.jda.api.interactions.commands.OptionType;
     import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
    -import org.jetbrains.annotations.NotNull;
     import org.togetherjava.tjbot.commands.SlashCommandAdapter;
     import org.togetherjava.tjbot.commands.SlashCommandVisibility;
     
    @@ -43,7 +42,7 @@ public TagCommand(TagSystem tagSystem) {
         }
     
         @Override
    -    public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
    +    public void onSlashCommand(SlashCommandInteractionEvent event) {
             String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
             OptionMapping replyToUserOption = event.getOption(REPLY_TO_USER_OPTION);
     
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
    index 3eb996a7cb..9b34c08a12 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
    @@ -2,8 +2,6 @@
     
     import net.dv8tion.jda.api.EmbedBuilder;
     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.User;
     import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
     import net.dv8tion.jda.api.exceptions.ErrorResponseException;
    @@ -12,26 +10,20 @@
     import net.dv8tion.jda.api.interactions.commands.OptionType;
     import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
     import net.dv8tion.jda.api.requests.ErrorResponse;
    -import org.jetbrains.annotations.NotNull;
    -import org.jetbrains.annotations.Nullable;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     import org.togetherjava.tjbot.commands.SlashCommandAdapter;
     import org.togetherjava.tjbot.commands.SlashCommandVisibility;
    -import org.togetherjava.tjbot.config.Config;
     import org.togetherjava.tjbot.moderation.ModAuditLogWriter;
     
    -import java.time.temporal.TemporalAccessor;
    -import java.util.*;
    -import java.util.NoSuchElementException;
    +import javax.annotation.Nonnull;
    +import javax.annotation.Nullable;
     import java.nio.charset.StandardCharsets;
     import java.time.Instant;
    -import java.util.Objects;
    -import java.util.OptionalLong;
    +import java.time.temporal.TemporalAccessor;
    +import java.util.*;
     import java.util.function.BiConsumer;
     import java.util.function.Consumer;
    -import java.util.function.Predicate;
    -import java.util.regex.Pattern;
     
     /**
      * Implements the {@code /tag-manage} command which allows management of tags, such as creating,
    @@ -67,7 +59,6 @@ public final class TagManageCommand extends SlashCommandAdapter {
         private static final String UNABLE_TO_GET_CONTENT_MESSAGE = "Was unable to retrieve content";
     
         private final TagSystem tagSystem;
    -    private final Predicate hasRequiredRole;
     
         private final ModAuditLogWriter modAuditLogWriter;
     
    @@ -75,15 +66,12 @@ public final class TagManageCommand extends SlashCommandAdapter {
          * Creates a new instance, using the given tag system as base.
          *
          * @param tagSystem the system providing the actual tag data
    -     * @param config the config to use for this
          * @param modAuditLogWriter to log tag changes for audition
          */
    -    public TagManageCommand(@NotNull TagSystem tagSystem, @NotNull Config config,
    -            @NotNull ModAuditLogWriter modAuditLogWriter) {
    +    public TagManageCommand(TagSystem tagSystem, ModAuditLogWriter modAuditLogWriter) {
             super("tag-manage", "Provides commands to manage all tags", SlashCommandVisibility.GUILD);
     
             this.tagSystem = tagSystem;
    -        hasRequiredRole = Pattern.compile(config.getTagManageRolePattern()).asMatchPredicate();
     
             this.modAuditLogWriter = modAuditLogWriter;
     
    @@ -112,8 +100,7 @@ public TagManageCommand(@NotNull TagSystem tagSystem, @NotNull Config config,
                         .addOption(OptionType.STRING, ID_OPTION, ID_DESCRIPTION, true));
         }
     
    -    private static void sendSuccessMessage(@NotNull IReplyCallback event, @NotNull String id,
    -            @NotNull String actionVerb) {
    +    private static void sendSuccessMessage(IReplyCallback event, String id, String actionVerb) {
             logger.info("User '{}' {} the tag with id '{}'.", event.getUser().getId(), actionVerb, id);
     
             event
    @@ -136,8 +123,8 @@ private static void sendSuccessMessage(@NotNull IReplyCallback event, @NotNull S
          * @param event the event to send messages with
          * @return the parsed message id, if successful
          */
    -    private static OptionalLong parseMessageIdAndHandle(@NotNull String messageId,
    -            @NotNull IReplyCallback event) {
    +    @Nonnull
    +    private static OptionalLong parseMessageIdAndHandle(String messageId, IReplyCallback event) {
             try {
                 return OptionalLong.of(Long.parseLong(messageId));
             } catch (NumberFormatException e) {
    @@ -151,14 +138,7 @@ private static OptionalLong parseMessageIdAndHandle(@NotNull String messageId,
         }
     
         @Override
    -    public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
    -        if (!hasTagManageRole(Objects.requireNonNull(event.getMember()))) {
    -            event.reply("Tags can only be managed by users with a corresponding role.")
    -                .setEphemeral(true)
    -                .queue();
    -            return;
    -        }
    -
    +    public void onSlashCommand(SlashCommandInteractionEvent event) {
             switch (Subcommand.fromName(event.getSubcommandName())) {
                 case RAW -> rawTag(event);
                 case CREATE -> createTag(event);
    @@ -171,7 +151,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
             }
         }
     
    -    private void rawTag(@NotNull SlashCommandInteractionEvent event) {
    +    private void rawTag(SlashCommandInteractionEvent event) {
             String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
             if (tagSystem.handleIsUnknownTag(id, event)) {
                 return;
    @@ -183,31 +163,31 @@ private void rawTag(@NotNull SlashCommandInteractionEvent event) {
                 .queue();
         }
     
    -    private void createTag(@NotNull CommandInteraction event) {
    +    private void createTag(CommandInteraction event) {
             String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString();
     
             handleAction(TagStatus.NOT_EXISTS, id -> tagSystem.putTag(id, content), event,
                     Subcommand.CREATE, content);
         }
     
    -    private void createTagWithMessage(@NotNull CommandInteraction event) {
    +    private void createTagWithMessage(CommandInteraction event) {
             handleActionWithMessage(TagStatus.NOT_EXISTS, tagSystem::putTag, event,
                     Subcommand.CREATE_WITH_MESSAGE);
         }
     
    -    private void editTag(@NotNull CommandInteraction event) {
    +    private void editTag(CommandInteraction event) {
             String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString();
     
             handleAction(TagStatus.EXISTS, id -> tagSystem.putTag(id, content), event, Subcommand.EDIT,
                     content);
         }
     
    -    private void editTagWithMessage(@NotNull CommandInteraction event) {
    +    private void editTagWithMessage(CommandInteraction event) {
             handleActionWithMessage(TagStatus.EXISTS, tagSystem::putTag, event,
                     Subcommand.EDIT_WITH_MESSAGE);
         }
     
    -    private void deleteTag(@NotNull CommandInteraction event) {
    +    private void deleteTag(CommandInteraction event) {
             handleAction(TagStatus.EXISTS, tagSystem::deleteTag, event, Subcommand.DELETE, null);
         }
     
    @@ -223,9 +203,8 @@ private void deleteTag(@NotNull CommandInteraction event) {
          * @param subcommand the subcommand to be executed
          * @param newContent the new content of the tag, or null if content is unchanged
          */
    -    private void handleAction(@NotNull TagStatus requiredTagStatus,
    -            @NotNull Consumer idAction, @NotNull CommandInteraction event,
    -            @NotNull Subcommand subcommand, @Nullable String newContent) {
    +    private void handleAction(TagStatus requiredTagStatus, Consumer idAction,
    +            CommandInteraction event, Subcommand subcommand, @Nullable String newContent) {
     
             String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
             if (isWrongTagStatusAndHandle(requiredTagStatus, id, event)) {
    @@ -258,9 +237,9 @@ private void handleAction(@NotNull TagStatus requiredTagStatus,
          *        {@code message-id} option set
          * @param subcommand the subcommand to be executed
          */
    -    private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus,
    -            @NotNull BiConsumer idAndContentAction,
    -            @NotNull CommandInteraction event, @NotNull Subcommand subcommand) {
    +    private void handleActionWithMessage(TagStatus requiredTagStatus,
    +            BiConsumer idAndContentAction, CommandInteraction event,
    +            Subcommand subcommand) {
     
             String tagId = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
             OptionalLong messageIdOpt = parseMessageIdAndHandle(
    @@ -310,8 +289,8 @@ private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus,
          * @param id the id of the tag to get its content
          * @return the content of the tag, if present
          */
    -    private @NotNull Optional getTagContent(@NotNull Subcommand subcommand,
    -            @NotNull String id) {
    +    @Nonnull
    +    private Optional getTagContent(Subcommand subcommand, String id) {
             if (Subcommand.SUBCOMMANDS_WITH_PREVIOUS_CONTENT.contains(subcommand)) {
                 try {
                     return tagSystem.getTag(id);
    @@ -337,8 +316,8 @@ private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus,
          * @param event the event to send messages with
          * @return whether the status of the given tag is not equal to the required status
          */
    -    private boolean isWrongTagStatusAndHandle(@NotNull TagStatus requiredTagStatus,
    -            @NotNull String id, @NotNull IReplyCallback event) {
    +    private boolean isWrongTagStatusAndHandle(TagStatus requiredTagStatus, String id,
    +            IReplyCallback event) {
             if (requiredTagStatus == TagStatus.EXISTS) {
                 return tagSystem.handleIsUnknownTag(id, event);
             } else if (requiredTagStatus == TagStatus.NOT_EXISTS) {
    @@ -354,9 +333,9 @@ private boolean isWrongTagStatusAndHandle(@NotNull TagStatus requiredTagStatus,
             return false;
         }
     
    -    private void logAction(@NotNull Subcommand subcommand, @NotNull Guild guild,
    -            @NotNull User author, @NotNull TemporalAccessor triggeredAt, @NotNull String id,
    -            @Nullable String newContent, @Nullable String previousContent) {
    +    private void logAction(Subcommand subcommand, Guild guild, User author,
    +            TemporalAccessor triggeredAt, String id, @Nullable String newContent,
    +            @Nullable String previousContent) {
     
             List attachments = new ArrayList<>();
     
    @@ -399,10 +378,6 @@ private void logAction(@NotNull Subcommand subcommand, @NotNull Guild guild,
                     triggeredAt, guild, attachments.toArray(ModAuditLogWriter.Attachment[]::new));
         }
     
    -    private boolean hasTagManageRole(@NotNull Member member) {
    -        return member.getRoles().stream().map(Role::getName).anyMatch(hasRequiredRole);
    -    }
    -
         private enum TagStatus {
             EXISTS,
             NOT_EXISTS
    @@ -426,17 +401,18 @@ enum Subcommand {
             private final String name;
             private final String actionVerb;
     
    -        Subcommand(@NotNull String name, @NotNull String actionVerb) {
    +        Subcommand(String name, String actionVerb) {
                 this.name = name;
                 this.actionVerb = actionVerb;
             }
     
    -        @NotNull
    +        @Nonnull
             String getName() {
                 return name;
             }
     
    -        static Subcommand fromName(@NotNull String name) {
    +        @Nonnull
    +        static Subcommand fromName(String name) {
                 for (Subcommand subcommand : Subcommand.values()) {
                     if (subcommand.name.equals(name)) {
                         return subcommand;
    @@ -446,7 +422,7 @@ static Subcommand fromName(@NotNull String name) {
                         "Subcommand with name '%s' is unknown".formatted(name));
             }
     
    -        @NotNull
    +        @Nonnull
             String getActionVerb() {
                 return actionVerb;
             }
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
    index 0a9090d0d5..13272b6131 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
    @@ -4,13 +4,13 @@
     import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
     import net.dv8tion.jda.api.interactions.components.buttons.Button;
     import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
    -import org.jetbrains.annotations.NotNull;
     import org.togetherjava.tjbot.commands.utils.StringDistances;
     import org.togetherjava.tjbot.db.Database;
     import org.togetherjava.tjbot.db.generated.tables.Tags;
     import org.togetherjava.tjbot.db.generated.tables.records.TagsRecord;
     
    -import java.awt.*;
    +import javax.annotation.Nonnull;
    +import java.awt.Color;
     import java.util.Optional;
     import java.util.Set;
     import java.util.stream.Collectors;
    @@ -58,8 +58,7 @@ static Button createDeleteButton(String componentId) {
          * @param event the event to send messages with
          * @return whether the given tag is unknown to the system
          */
    -    @SuppressWarnings("BooleanMethodNameMustStartWithQuestion")
    -    boolean handleIsUnknownTag(@NotNull String id, @NotNull IReplyCallback event) {
    +    boolean handleIsUnknownTag(String id, IReplyCallback event) {
             if (hasTag(id)) {
                 return false;
             }
    @@ -123,6 +122,7 @@ void putTag(String id, String content) {
          * @param id the id of the tag to get
          * @return the content of the tag, if the tag is known to the system
          */
    +    @Nonnull
         Optional getTag(String id) {
             return database.readTransaction(context -> Optional
                 .ofNullable(context.selectFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(id)).fetchOne())
    @@ -134,6 +134,7 @@ Optional getTag(String id) {
          *
          * @return a set of all ids known to the system, not backed
          */
    +    @Nonnull
         Set getAllIds() {
             return database.readTransaction(context -> context.select(Tags.TAGS.ID)
                 .from(Tags.TAGS)
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java
    index 8606c7f933..3432720cb9 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java
    @@ -4,7 +4,6 @@
     import net.dv8tion.jda.api.Permission;
     import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
     import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
    -import org.jetbrains.annotations.NotNull;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     import org.togetherjava.tjbot.commands.SlashCommandAdapter;
    @@ -49,7 +48,7 @@ public TagsCommand(TagSystem tagSystem) {
         }
     
         @Override
    -    public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
    +    public void onSlashCommand(SlashCommandInteractionEvent event) {
             Collection tagIds = tagSystem.getAllIds();
             if (tagIds.size() > MAX_TAGS_THRESHOLD_WARNING) {
                 // TODO Implement the edge case
    @@ -73,7 +72,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
         }
     
         @Override
    -    public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List args) {
    +    public void onButtonClick(ButtonInteractionEvent event, List args) {
             String userId = args.get(0);
     
             if (!event.getUser().getId().equals(userId) && !Objects.requireNonNull(event.getMember())
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/package-info.java
    index e250c06ae9..41eb96651e 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/package-info.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/package-info.java
    @@ -4,4 +4,7 @@
      * like {@link org.togetherjava.tjbot.commands.tags.TagCommand} as entry point to the package's
      * offered functionality.
      */
    +@ParametersAreNonnullByDefault
     package org.togetherjava.tjbot.commands.tags;
    +
    +import javax.annotation.ParametersAreNonnullByDefault;
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java
    index 479b1dae19..5556384cfe 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java
    @@ -5,32 +5,27 @@
     import com.github.freva.asciitable.ColumnData;
     import com.github.freva.asciitable.HorizontalAlign;
     import net.dv8tion.jda.api.entities.Member;
    -import net.dv8tion.jda.api.entities.Role;
     import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
     import net.dv8tion.jda.api.interactions.callbacks.IDeferrableCallback;
    -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.commands.build.OptionData;
    -import org.jetbrains.annotations.NotNull;
    -import org.jetbrains.annotations.Nullable;
     import org.jooq.Records;
     import org.jooq.impl.DSL;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     import org.togetherjava.tjbot.commands.SlashCommandAdapter;
     import org.togetherjava.tjbot.commands.SlashCommandVisibility;
    -import org.togetherjava.tjbot.config.Config;
     import org.togetherjava.tjbot.db.Database;
     
    +import javax.annotation.Nonnull;
    +import javax.annotation.Nullable;
     import java.math.BigDecimal;
     import java.time.*;
     import java.time.format.TextStyle;
     import java.util.*;
     import java.util.function.Function;
     import java.util.function.IntFunction;
    -import java.util.function.Predicate;
    -import java.util.regex.Pattern;
     import java.util.stream.Collectors;
     import java.util.stream.IntStream;
     
    @@ -49,15 +44,13 @@ public final class TopHelpersCommand extends SlashCommandAdapter {
         private static final int TOP_HELPER_LIMIT = 20;
     
         private final Database database;
    -    private final Predicate hasRequiredRole;
     
         /**
          * Creates a new instance.
          *
          * @param database the database containing the message records of top helpers
    -     * @param config the config to use for this
          */
    -    public TopHelpersCommand(@NotNull Database database, @NotNull Config config) {
    +    public TopHelpersCommand(Database database) {
             super(COMMAND_NAME, "Lists top helpers for the last month, or a given month",
                     SlashCommandVisibility.GUILD);
     
    @@ -68,15 +61,11 @@ public TopHelpersCommand(@NotNull Database database, @NotNull Config config) {
                         month.getDisplayName(TextStyle.FULL_STANDALONE, Locale.US), month.name()));
             getData().addOptions(monthData);
     
    -        hasRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate();
             this.database = database;
         }
     
         @Override
    -    public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
    -        if (!handleHasAuthorRole(event.getMember(), event)) {
    -            return;
    -        }
    +    public void onSlashCommand(SlashCommandInteractionEvent event) {
             OptionMapping atMonthData = event.getOption(MONTH_OPTION);
     
             TimeRange timeRange = computeTimeRange(computeMonth(atMonthData));
    @@ -99,18 +88,8 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {
                 .onSuccess(members -> handleTopHelpers(topHelpers, members, timeRange, event));
         }
     
    -    @SuppressWarnings("BooleanMethodNameMustStartWithQuestion")
    -    private boolean handleHasAuthorRole(@NotNull Member author, @NotNull IReplyCallback event) {
    -        if (author.getRoles().stream().map(Role::getName).anyMatch(hasRequiredRole)) {
    -            return true;
    -        }
    -        event.reply("You can not compute the top-helpers since you do not have the required role.")
    -            .setEphemeral(true)
    -            .queue();
    -        return false;
    -    }
    -
    -    private static @NotNull Month computeMonth(@Nullable OptionMapping atMonthData) {
    +    @Nonnull
    +    private static Month computeMonth(@Nullable OptionMapping atMonthData) {
             if (atMonthData != null) {
                 return Month.valueOf(atMonthData.getAsString());
             }
    @@ -119,7 +98,8 @@ private boolean handleHasAuthorRole(@NotNull Member author, @NotNull IReplyCallb
             return Instant.now().atZone(ZoneOffset.UTC).minusMonths(1).getMonth();
         }
     
    -    private static @NotNull TimeRange computeTimeRange(@NotNull Month atMonth) {
    +    @Nonnull
    +    private static TimeRange computeTimeRange(Month atMonth) {
             ZonedDateTime now = Instant.now().atZone(ZoneOffset.UTC);
     
             int atYear = now.getYear();
    @@ -137,8 +117,8 @@ private boolean handleHasAuthorRole(@NotNull Member author, @NotNull IReplyCallb
             return new TimeRange(start, end, description);
         }
     
    -    private @NotNull List computeTopHelpersDescending(long guildId,
    -            @NotNull TimeRange timeRange) {
    +    @Nonnull
    +    private List computeTopHelpersDescending(long guildId, TimeRange timeRange) {
             return database.read(context -> context
                 .select(HELP_CHANNEL_MESSAGES.AUTHOR_ID, DSL.sum(HELP_CHANNEL_MESSAGES.MESSAGE_LENGTH))
                 .from(HELP_CHANNEL_MESSAGES)
    @@ -150,14 +130,13 @@ private boolean handleHasAuthorRole(@NotNull Member author, @NotNull IReplyCallb
                 .fetch(Records.mapping(TopHelperResult::new)));
         }
     
    -    private static void handleError(@NotNull Throwable error, @NotNull IDeferrableCallback event) {
    +    private static void handleError(Throwable error, IDeferrableCallback event) {
             logger.warn("Failed to compute top-helpers", error);
             event.getHook().editOriginal("Sorry, something went wrong.").queue();
         }
     
    -    private static void handleTopHelpers(@NotNull Collection topHelpers,
    -            @NotNull Collection members, @NotNull TimeRange timeRange,
    -            @NotNull IDeferrableCallback event) {
    +    private static void handleTopHelpers(Collection topHelpers,
    +            Collection members, TimeRange timeRange, IDeferrableCallback event) {
             Map userIdToMember =
                     members.stream().collect(Collectors.toMap(Member::getIdLong, Function.identity()));
     
    @@ -172,7 +151,8 @@ private static void handleTopHelpers(@NotNull Collection topHel
             event.getHook().editOriginal(message).queue();
         }
     
    -    private static @NotNull List topHelperToDataRow(@NotNull TopHelperResult topHelper,
    +    @Nonnull
    +    private static List topHelperToDataRow(TopHelperResult topHelper,
                 @Nullable Member member) {
             String id = Long.toString(topHelper.authorId());
             String name = member == null ? "UNKNOWN_USER" : member.getEffectiveName();
    @@ -181,8 +161,9 @@ private static void handleTopHelpers(@NotNull Collection topHel
             return List.of(id, name, messageLengths);
         }
     
    -    private static @NotNull String dataTableToString(@NotNull Collection> dataTable,
    -            @NotNull TimeRange timeRange) {
    +    @Nonnull
    +    private static String dataTableToString(Collection> dataTable,
    +            TimeRange timeRange) {
             return dataTableToAsciiTable(dataTable,
                     List.of(new ColumnSetting("Id", HorizontalAlign.RIGHT),
                             new ColumnSetting("Name", HorizontalAlign.RIGHT),
    @@ -191,9 +172,9 @@ private static void handleTopHelpers(@NotNull Collection topHel
                                     HorizontalAlign.RIGHT)));
         }
     
    -    private static @NotNull String dataTableToAsciiTable(
    -            @NotNull Collection> dataTable,
    -            @NotNull List columnSettings) {
    +    @Nonnull
    +    private static String dataTableToAsciiTable(Collection> dataTable,
    +            List columnSettings) {
             IntFunction headerToAlignment = i -> columnSettings.get(i).headerName();
             IntFunction indexToAlignment = i -> columnSettings.get(i).alignment();
     
    @@ -208,15 +189,14 @@ private static void handleTopHelpers(@NotNull Collection topHel
             return AsciiTable.getTable(AsciiTable.BASIC_ASCII_NO_DATA_SEPARATORS, dataTable, columns);
         }
     
    -    private record TimeRange(@NotNull Instant start, @NotNull Instant end,
    -            @NotNull String description) {
    +    private record TimeRange(Instant start, Instant end, String description) {
         }
     
     
    -    private record TopHelperResult(long authorId, @NotNull BigDecimal messageLengths) {
    +    private record TopHelperResult(long authorId, BigDecimal messageLengths) {
         }
     
     
    -    private record ColumnSetting(@NotNull String headerName, @NotNull HorizontalAlign alignment) {
    +    private record ColumnSetting(String headerName, HorizontalAlign alignment) {
         }
     }
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java
    index 5d43a1d68e..a363c61898 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java
    @@ -3,7 +3,6 @@
     import net.dv8tion.jda.api.entities.ChannelType;
     import net.dv8tion.jda.api.entities.ThreadChannel;
     import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
    -import org.jetbrains.annotations.NotNull;
     import org.togetherjava.tjbot.commands.MessageReceiverAdapter;
     import org.togetherjava.tjbot.config.Config;
     import org.togetherjava.tjbot.db.Database;
    @@ -29,7 +28,7 @@ public final class TopHelpersMessageListener extends MessageReceiverAdapter {
          * @param database to store message meta-data in
          * @param config the config to use for this
          */
    -    public TopHelpersMessageListener(@NotNull Database database, @NotNull Config config) {
    +    public TopHelpersMessageListener(Database database, Config config) {
             super(Pattern.compile(".*"));
     
             this.database = database;
    @@ -41,7 +40,7 @@ public TopHelpersMessageListener(@NotNull Database database, @NotNull Config con
         }
     
         @Override
    -    public void onMessageReceived(@NotNull MessageReceivedEvent event) {
    +    public void onMessageReceived(MessageReceivedEvent event) {
             if (event.getAuthor().isBot() || event.isWebhookMessage()) {
                 return;
             }
    @@ -53,7 +52,7 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) {
             addMessageRecord(event);
         }
     
    -    private boolean isHelpThread(@NotNull MessageReceivedEvent event) {
    +    private boolean isHelpThread(MessageReceivedEvent event) {
             if (event.getChannelType() != ChannelType.GUILD_PUBLIC_THREAD) {
                 return false;
             }
    @@ -64,7 +63,7 @@ private boolean isHelpThread(@NotNull MessageReceivedEvent event) {
                     || isOverviewChannelName.test(rootChannelName);
         }
     
    -    private void addMessageRecord(@NotNull MessageReceivedEvent event) {
    +    private void addMessageRecord(MessageReceivedEvent event) {
             database.write(context -> context.newRecord(HELP_CHANNEL_MESSAGES)
                 .setMessageId(event.getMessage().getIdLong())
                 .setGuildId(event.getGuild().getIdLong())
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersPurgeMessagesRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersPurgeMessagesRoutine.java
    index 527c908f1d..a685f1af18 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersPurgeMessagesRoutine.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersPurgeMessagesRoutine.java
    @@ -1,12 +1,12 @@
     package org.togetherjava.tjbot.commands.tophelper;
     
     import net.dv8tion.jda.api.JDA;
    -import org.jetbrains.annotations.NotNull;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     import org.togetherjava.tjbot.commands.Routine;
     import org.togetherjava.tjbot.db.Database;
     
    +import javax.annotation.Nonnull;
     import java.time.Instant;
     import java.time.Period;
     import java.util.concurrent.TimeUnit;
    @@ -28,17 +28,18 @@ public final class TopHelpersPurgeMessagesRoutine implements Routine {
          *
          * @param database the database that contains the messages to purge
          */
    -    public TopHelpersPurgeMessagesRoutine(@NotNull Database database) {
    +    public TopHelpersPurgeMessagesRoutine(Database database) {
             this.database = database;
         }
     
         @Override
    -    public @NotNull Schedule createSchedule() {
    +    @Nonnull
    +    public Schedule createSchedule() {
             return new Schedule(ScheduleMode.FIXED_RATE, 0, 4, TimeUnit.HOURS);
         }
     
         @Override
    -    public void runRoutine(@NotNull JDA jda) {
    +    public void runRoutine(JDA jda) {
             int recordsDeleted =
                     database.writeAndProvide(context -> context.deleteFrom(HELP_CHANNEL_MESSAGES)
                         .where(HELP_CHANNEL_MESSAGES.SENT_AT
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/package-info.java
    index 61969abb57..224148b934 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/package-info.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/package-info.java
    @@ -2,4 +2,7 @@
      * This packages offers all the functionality for the top-helpers command system. The core class is
      * {@link org.togetherjava.tjbot.commands.tophelper.TopHelpersCommand}.
      */
    +@ParametersAreNonnullByDefault
     package org.togetherjava.tjbot.commands.tophelper;
    +
    +import javax.annotation.ParametersAreNonnullByDefault;
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/utils/DiscordClientAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/utils/DiscordClientAction.java
    index bf3b947e44..0f98a3da4a 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/utils/DiscordClientAction.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/utils/DiscordClientAction.java
    @@ -3,8 +3,8 @@
     import net.dv8tion.jda.api.interactions.components.buttons.Button;
     import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
     import org.jetbrains.annotations.Contract;
    -import org.jetbrains.annotations.NotNull;
     
    +import javax.annotation.Nonnull;
     import java.util.regex.Pattern;
     
     /**
    @@ -261,7 +261,8 @@ public String getRawUrl() {
          * @return The formatted URL as an {@link String}
          * @throws IllegalArgumentException When missing arguments
          */
    -    public String formatUrl(final String @NotNull... arguments) {
    +    @Nonnull
    +    public String formatUrl(final String... arguments) {
             String localUrl = url;
     
             for (final String argument : arguments) {
    @@ -284,11 +285,13 @@ public String formatUrl(final String @NotNull... arguments) {
          * @return A {@link Button} of {@link ButtonStyle#LINK} with the given label
          * @throws IllegalArgumentException When missing arguments
          */
    -    public Button asLinkButton(@NotNull final String label, final String... arguments) {
    +    @Nonnull
    +    public Button asLinkButton(final String label, final String... arguments) {
             return Button.link(formatUrl(arguments), label);
         }
     
         @Override
    +    @Nonnull
         public String toString() {
             return "DiscordClientAction{" + "url='" + url + '\'' + '}';
         }
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/utils/Hashing.java b/application/src/main/java/org/togetherjava/tjbot/commands/utils/Hashing.java
    index c82ed92bfe..6b63de1120 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/utils/Hashing.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/utils/Hashing.java
    @@ -1,7 +1,6 @@
     package org.togetherjava.tjbot.commands.utils;
     
    -import org.jetbrains.annotations.NotNull;
    -
    +import javax.annotation.Nonnull;
     import java.nio.charset.StandardCharsets;
     import java.security.MessageDigest;
     import java.security.NoSuchAlgorithmException;
    @@ -10,8 +9,10 @@
     /**
      * Utility for hashing data.
      */
    -public enum Hashing {
    -    ;
    +public class Hashing {
    +    private Hashing() {
    +        throw new UnsupportedOperationException("Utility class, construction not supported");
    +    }
     
         /**
          * All characters available in the hexadecimal-system, as UTF-8 encoded array.
    @@ -24,9 +25,8 @@ public enum Hashing {
          * @param bytes the binary data to convert
          * @return a hexadecimal representation
          */
    -    @SuppressWarnings("MagicNumber")
    -    @NotNull
    -    public static String bytesToHex(byte @NotNull [] bytes) {
    +    @Nonnull
    +    public static String bytesToHex(byte[] bytes) {
             Objects.requireNonNull(bytes);
             // See https://stackoverflow.com/a/9855338/2411243
             // noinspection MultiplyOrDivideByPowerOfTwo
    @@ -50,7 +50,8 @@ public static String bytesToHex(byte @NotNull [] bytes) {
          * @param data the data to hash
          * @return the computed hash
          */
    -    public static byte @NotNull [] hash(@NotNull String method, byte @NotNull [] data) {
    +    @Nonnull
    +    public static byte[] hash(String method, byte[] data) {
             Objects.requireNonNull(method);
             Objects.requireNonNull(data);
             try {
    diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/utils/MessageUtils.java b/application/src/main/java/org/togetherjava/tjbot/commands/utils/MessageUtils.java
    index 9ef485e9f2..c14a667fe0 100644
    --- a/application/src/main/java/org/togetherjava/tjbot/commands/utils/MessageUtils.java
    +++ b/application/src/main/java/org/togetherjava/tjbot/commands/utils/MessageUtils.java
    @@ -4,8 +4,8 @@
     import net.dv8tion.jda.api.interactions.components.ActionRow;
     import net.dv8tion.jda.api.interactions.components.buttons.Button;
     import net.dv8tion.jda.api.utils.MarkdownSanitizer;
    -import org.jetbrains.annotations.NotNull;
     
    +import javax.annotation.Nonnull;
     import java.util.List;
     
     /**
    @@ -14,8 +14,10 @@
      * This class is meant to contain all utility methods for {@link Message} that can be used on all
      * other commands to avoid similar methods appearing everywhere.
      */
    -public enum MessageUtils {
    -    ;
    +public class MessageUtils {
    +    private MessageUtils() {
    +        throw new UnsupportedOperationException("Utility class, construction not supported");
    +    }
     
         /**
          * Disables all the buttons that a message has. Disabling buttons deems it as not clickable to
    @@ -26,7 +28,7 @@ public enum MessageUtils {
          * @param message the message that contains at least one button
          * @throws IllegalArgumentException when the given message does not contain any button
          */
    -    public static void disableButtons(@NotNull Message message) {
    +    public static void disableButtons(Message message) {
             List
    * * @param t the object to consume on success + * @param restActionType class token of the type of the Rest Action to return * @param the type of the object to consume + * @param the specific type of the Rest Action to return * @return the mocked action */ + @Nonnull @SuppressWarnings("unchecked") - public @NotNull RestAction createSucceededActionMock(@Nullable T t) { - RestAction action = (RestAction) mock(RestAction.class); + public > R createSucceededActionMock(@Nullable T t, + Class restActionType) { + R action = mock(restActionType); Answer successExecution = invocation -> { Consumer successConsumer = invocation.getArgument(0); @@ -397,7 +416,7 @@ public JdaTester() { Answer> mapExecution = invocation -> { Function mapFunction = invocation.getArgument(0); Object result = mapFunction.apply(t); - return createSucceededActionMock(result); + return createSucceededActionMock(result, RestAction.class); }; Answer> flatMapExecution = invocation -> { Function> flatMapFunction = invocation.getArgument(0); @@ -417,6 +436,21 @@ public JdaTester() { return action; } + /** + * Variant of {@link #createSucceededActionMock(Object, Class)} returning a plain + * {@link RestAction}. + * + * @param t the object to consume on success + * @param the type of the object to consume + * @return the mocked action + * @see #createSucceededActionMock(Object, Class) + */ + @SuppressWarnings("unchecked") + @Nonnull + public RestAction createSucceededActionMock(@Nullable T t) { + return createSucceededActionMock(t, RestAction.class); + } + /** * Creates a mocked action that always fails and consumes the given failure reason. *

    @@ -429,19 +463,23 @@ public JdaTester() { * var jdaTester = new JdaTester(); * * var reason = new FooException(); - * var action = jdaTester.createFailedActionMock(reason); + * var action = jdaTester.createFailedActionMock(reason, RestAction.class); * * doReturn(action).when(jdaTester.getTextChannelSpy()).retrieveMessageById("1"); * } * * * @param failureReason the reason to consume on failure + * @param restActionType class token of the type of the Rest Action to return * @param the type of the object the action would contain if it would succeed + * @param the specific type of the Rest Action to return * @return the mocked action */ @SuppressWarnings("unchecked") - public @NotNull RestAction createFailedActionMock(@NotNull Throwable failureReason) { - RestAction action = (RestAction) mock(RestAction.class); + @Nonnull + public > R createFailedActionMock(Throwable failureReason, + Class restActionType) { + R action = mock(restActionType); Answer failureExecution = invocation -> { Consumer failureConsumer = invocation.getArgument(1); @@ -452,12 +490,13 @@ public JdaTester() { Answer> errorMapExecution = invocation -> { Function mapFunction = invocation.getArgument(0); Object result = mapFunction.apply(failureReason); - return createSucceededActionMock(result); + return createSucceededActionMock(result, RestAction.class); }; - Answer> mapExecution = invocation -> createFailedActionMock(failureReason); + Answer> mapExecution = + invocation -> createFailedActionMock(failureReason, RestAction.class); Answer> flatMapExecution = - invocation -> createFailedActionMock(failureReason); + invocation -> createFailedActionMock(failureReason, RestAction.class); doNothing().when(action).queue(); doNothing().when(action).queue(any()); @@ -473,6 +512,21 @@ public JdaTester() { return action; } + /** + * Variant of {@link #createFailedActionMock(Throwable, Class)} returning a plain + * {@link RestAction}. + * + * @param failureReason the reason to consume on failure + * @param the type of the object the action would contain if it would succeed + * @return the mocked action + * @see #createFailedActionMock(Throwable, Class) + */ + @SuppressWarnings("unchecked") + @Nonnull + public RestAction createFailedActionMock(Throwable failureReason) { + return createFailedActionMock(failureReason, RestAction.class); + } + /** * Creates an exception used by JDA on failure in most calls to the Discord API. *

    @@ -482,12 +536,30 @@ public JdaTester() { * @param reason the reason of the error * @return the created exception */ - public @NotNull ErrorResponseException createErrorResponseException( - @NotNull ErrorResponse reason) { + @Nonnull + public ErrorResponseException createErrorResponseException(ErrorResponse reason) { return ErrorResponseException.create(reason, new Response(null, -1, "", -1, Set.of())); } - private void mockInteraction(@NotNull IReplyCallback interaction) { + /** + * Creates a Mockito mocked message receive event, which can be used for + * {@link org.togetherjava.tjbot.commands.MessageReceiver#onMessageReceived(MessageReceivedEvent)}. + * + * @param message the message that has been received + * @param attachments attachments of the message, empty if none + * @return the event of receiving the given message + */ + @Nonnull + public MessageReceivedEvent createMessageReceiveEvent(Message message, + List attachments) { + Message spyMessage = spy(message); + mockMessage(spyMessage); + doReturn(attachments).when(spyMessage).getAttachments(); + + return new MessageReceivedEvent(jda, responseNumber.getAndIncrement(), spyMessage); + } + + private void mockInteraction(IReplyCallback interaction) { doReturn(replyAction).when(interaction).reply(anyString()); doReturn(replyAction).when(interaction).replyEmbeds(ArgumentMatchers.any()); doReturn(replyAction).when(interaction).replyEmbeds(anyCollection()); @@ -506,13 +578,13 @@ private void mockInteraction(@NotNull IReplyCallback interaction) { doReturn(replyCallbackAction).when(interaction).deferReply(anyBoolean()); } - private void mockButtonClickEvent(@NotNull ButtonInteractionEvent event) { + private void mockButtonClickEvent(ButtonInteractionEvent event) { mockInteraction(event); doReturn(replyAction).when(event).editButton(any()); } - private void mockMessage(@NotNull Message message) { + private void mockMessage(Message message) { doReturn(messageAction).when(message).reply(anyString()); doReturn(messageAction).when(message).replyEmbeds(ArgumentMatchers.any()); doReturn(messageAction).when(message).replyEmbeds(anyCollection()); @@ -524,5 +596,12 @@ private void mockMessage(@NotNull Message message) { doReturn(member).when(message).getMember(); doReturn(member.getUser()).when(message).getAuthor(); + + doReturn(textChannel).when(message).getChannel(); + doReturn(1L).when(message).getIdLong(); + doReturn(false).when(message).isWebhookMessage(); + + doReturn(message.getContentRaw()).when(message).getContentDisplay(); + doReturn(message.getContentRaw()).when(message).getContentStripped(); } } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/SlashCommandInteractionEventBuilder.java b/application/src/test/java/org/togetherjava/tjbot/jda/SlashCommandInteractionEventBuilder.java index 4f4323944d..82275f4698 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/SlashCommandInteractionEventBuilder.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/SlashCommandInteractionEventBuilder.java @@ -11,13 +11,13 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.interactions.command.SlashCommandInteractionImpl; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.togetherjava.tjbot.commands.SlashCommand; import org.togetherjava.tjbot.jda.payloads.PayloadMember; import org.togetherjava.tjbot.jda.payloads.PayloadUser; import org.togetherjava.tjbot.jda.payloads.slashcommand.*; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -77,7 +77,7 @@ public final class SlashCommandInteractionEventBuilder { private String subcommand; private Member userWhoTriggered; - SlashCommandInteractionEventBuilder(@NotNull JDAImpl jda, + SlashCommandInteractionEventBuilder(JDAImpl jda, UnaryOperator mockOperator) { this.jda = jda; this.mockOperator = mockOperator; @@ -97,8 +97,8 @@ public final class SlashCommandInteractionEventBuilder { * @throws IllegalArgumentException if the option does not exist in the corresponding command, * as specified by its {@link SlashCommand#getData()} */ - public @NotNull SlashCommandInteractionEventBuilder setOption(@NotNull String name, - @NotNull String value) { + @Nonnull + public SlashCommandInteractionEventBuilder setOption(String name, String value) { putOptionRaw(name, value, OptionType.STRING); return this; } @@ -117,8 +117,8 @@ public final class SlashCommandInteractionEventBuilder { * @throws IllegalArgumentException if the option does not exist in the corresponding command, * as specified by its {@link SlashCommand#getData()} */ - public @NotNull SlashCommandInteractionEventBuilder setOption(@NotNull String name, - long value) { + @Nonnull + public SlashCommandInteractionEventBuilder setOption(String name, long value) { putOptionRaw(name, value, OptionType.INTEGER); return this; } @@ -137,8 +137,8 @@ public final class SlashCommandInteractionEventBuilder { * @throws IllegalArgumentException if the option does not exist in the corresponding command, * as specified by its {@link SlashCommand#getData()} */ - public @NotNull SlashCommandInteractionEventBuilder setOption(@NotNull String name, - @NotNull User value) { + @Nonnull + public SlashCommandInteractionEventBuilder setOption(String name, User value) { putOptionRaw(name, value, OptionType.USER); return this; } @@ -157,8 +157,8 @@ public final class SlashCommandInteractionEventBuilder { * @throws IllegalArgumentException if the option does not exist in the corresponding command, * as specified by its {@link SlashCommand#getData()} */ - public @NotNull SlashCommandInteractionEventBuilder setOption(@NotNull String name, - @NotNull Member value) { + @Nonnull + public SlashCommandInteractionEventBuilder setOption(String name, Member value) { putOptionRaw(name, value, OptionType.USER); return this; } @@ -168,7 +168,8 @@ public final class SlashCommandInteractionEventBuilder { * * @return this builder instance for chaining */ - public @NotNull SlashCommandInteractionEventBuilder clearOptions() { + @Nonnull + public SlashCommandInteractionEventBuilder clearOptions() { nameToOption.clear(); return this; } @@ -184,7 +185,8 @@ public final class SlashCommandInteractionEventBuilder { * @throws IllegalArgumentException if the subcommand does not exist in the corresponding * command, as specified by its {@link SlashCommand#getData()} */ - public @NotNull SlashCommandInteractionEventBuilder setSubcommand(@Nullable String subcommand) { + @Nonnull + public SlashCommandInteractionEventBuilder setSubcommand(@Nullable String subcommand) { if (subcommand != null) { requireSubcommand(subcommand); } @@ -199,45 +201,44 @@ public final class SlashCommandInteractionEventBuilder { * @param userWhoTriggered the user who triggered the slash command * @return this builder instance for chaining */ - @NotNull - public SlashCommandInteractionEventBuilder setUserWhoTriggered( - @NotNull Member userWhoTriggered) { + @Nonnull + public SlashCommandInteractionEventBuilder setUserWhoTriggered(Member userWhoTriggered) { this.userWhoTriggered = userWhoTriggered; return this; } - @NotNull - SlashCommandInteractionEventBuilder setCommand(@NotNull SlashCommand command) { + @Nonnull + SlashCommandInteractionEventBuilder setCommand(SlashCommand command) { this.command = command; return this; } - @NotNull - SlashCommandInteractionEventBuilder setChannelId(@NotNull String channelId) { + @Nonnull + SlashCommandInteractionEventBuilder setChannelId(String channelId) { this.channelId = channelId; return this; } - @NotNull - SlashCommandInteractionEventBuilder setToken(@NotNull String token) { + @Nonnull + SlashCommandInteractionEventBuilder setToken(String token) { this.token = token; return this; } - @NotNull - SlashCommandInteractionEventBuilder setApplicationId(@NotNull String applicationId) { + @Nonnull + SlashCommandInteractionEventBuilder setApplicationId(String applicationId) { this.applicationId = applicationId; return this; } - @NotNull - SlashCommandInteractionEventBuilder setGuildId(@NotNull String guildId) { + @Nonnull + SlashCommandInteractionEventBuilder setGuildId(String guildId) { this.guildId = guildId; return this; } - @NotNull - SlashCommandInteractionEventBuilder setUserId(@NotNull String userId) { + @Nonnull + SlashCommandInteractionEventBuilder setUserId(String userId) { this.userId = userId; return this; } @@ -248,7 +249,8 @@ SlashCommandInteractionEventBuilder setUserId(@NotNull String userId) { * * @return the created slash command instance */ - public @NotNull SlashCommandInteractionEvent build() { + @Nonnull + public SlashCommandInteractionEvent build() { PayloadSlashCommand event = createEvent(); String json; @@ -261,6 +263,7 @@ SlashCommandInteractionEventBuilder setUserId(@NotNull String userId) { return spySlashCommandEvent(json); } + @Nonnull private SlashCommandInteractionEvent spySlashCommandEvent(String jsonData) { SlashCommandInteractionEvent event = spy(new SlashCommandInteractionEvent(jda, 0, new SlashCommandInteractionImpl(jda, DataObject.fromJson(jsonData)))); @@ -273,7 +276,8 @@ private SlashCommandInteractionEvent spySlashCommandEvent(String jsonData) { return event; } - private @NotNull PayloadSlashCommand createEvent() { + @Nonnull + private PayloadSlashCommand createEvent() { // TODO Validate that required options are set, check that subcommand is given if the // command has one // TODO Make as much of this configurable as needed @@ -296,8 +300,9 @@ private SlashCommandInteractionEvent spySlashCommandEvent(String jsonData) { applicationId, token, member, data); } - private static @Nullable List extractOptionsOrNull( - @NotNull Map> nameToOption) { + @Nullable + private static List extractOptionsOrNull( + Map> nameToOption) { if (nameToOption.isEmpty()) { return null; } @@ -308,8 +313,8 @@ private SlashCommandInteractionEvent spySlashCommandEvent(String jsonData) { .toList(); } - private static @NotNull String serializeOptionValue(@NotNull T value, - @NotNull OptionType type) { + @Nonnull + private static String serializeOptionValue(T value, OptionType type) { if (type == OptionType.STRING) { return (String) value; } else if (type == OptionType.INTEGER) { @@ -337,8 +342,9 @@ private SlashCommandInteractionEvent spySlashCommandEvent(String jsonData) { .formatted(type, value.getClass())); } - private static @Nullable PayloadSlashCommandResolved extractResolvedOrNull( - @NotNull Map> nameToOption) { + @Nullable + private static PayloadSlashCommandResolved extractResolvedOrNull( + Map> nameToOption) { PayloadSlashCommandUsers users = extractUsersOrNull(nameToOption); PayloadSlashCommandMembers members = extractMembersOrNull(nameToOption); @@ -349,8 +355,9 @@ private SlashCommandInteractionEvent spySlashCommandEvent(String jsonData) { return new PayloadSlashCommandResolved(members, users); } - private static @Nullable PayloadSlashCommandUsers extractUsersOrNull( - @NotNull Map> nameToOption) { + @Nullable + private static PayloadSlashCommandUsers extractUsersOrNull( + Map> nameToOption) { Map idToUser = nameToOption.values() .stream() .filter(option -> option.type == OptionType.USER) @@ -366,8 +373,9 @@ private SlashCommandInteractionEvent spySlashCommandEvent(String jsonData) { return idToUser.isEmpty() ? null : new PayloadSlashCommandUsers(idToUser); } - private static @Nullable PayloadSlashCommandMembers extractMembersOrNull( - @NotNull Map> nameToOption) { + @Nullable + private static PayloadSlashCommandMembers extractMembersOrNull( + Map> nameToOption) { Map idToMember = nameToOption.values() .stream() .filter(option -> option.type == OptionType.USER) @@ -379,14 +387,14 @@ private SlashCommandInteractionEvent spySlashCommandEvent(String jsonData) { return idToMember.isEmpty() ? null : new PayloadSlashCommandMembers(idToMember); } - private void putOptionRaw(@NotNull String name, @NotNull T value, - @NotNull OptionType type) { + private void putOptionRaw(String name, T value, OptionType type) { requireOption(name, type); nameToOption.put(name, new Option<>(name, value, type)); } @SuppressWarnings("UnusedReturnValue") - private @NotNull OptionData requireOption(@NotNull String name, @NotNull OptionType type) { + @Nonnull + private OptionData requireOption(String name, OptionType type) { List options = subcommand == null ? command.getData().getOptions() : requireSubcommand(subcommand).getOptions(); @@ -405,7 +413,8 @@ private void putOptionRaw(@NotNull String name, @NotNull T value, }); } - private @NotNull SubcommandData requireSubcommand(@NotNull String name) { + @Nonnull + private SubcommandData requireSubcommand(String name) { return command.getData() .getSubcommands() .stream() @@ -418,6 +427,6 @@ private void putOptionRaw(@NotNull String name, @NotNull T value, }); } - private record Option (@NotNull String name, @NotNull T value, @NotNull OptionType type) { + private record Option (String name, T value, OptionType type) { } } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/package-info.java b/application/src/test/java/org/togetherjava/tjbot/jda/package-info.java index f157e47b53..a6de4614ad 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/package-info.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/package-info.java @@ -2,4 +2,7 @@ * Provides utilities for testing with JDA, such as mocks. See * {@link org.togetherjava.tjbot.jda.JdaTester} as entry point. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.jda; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/PayloadMember.java b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/PayloadMember.java index 774e6b76df..b0d259a96a 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/PayloadMember.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/PayloadMember.java @@ -4,9 +4,9 @@ import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -28,11 +28,9 @@ public final class PayloadMember { private boolean isPending; private PayloadUser user; - @SuppressWarnings("ConstructorWithTooManyParameters") - public PayloadMember(@Nullable String premiumSince, @Nullable String nick, - @NotNull String joinedAt, @NotNull String permissions, @NotNull List roles, - boolean pending, boolean deaf, boolean mute, @Nullable String avatar, boolean isPending, - PayloadUser user) { + public PayloadMember(@Nullable String premiumSince, @Nullable String nick, String joinedAt, + String permissions, List roles, boolean pending, boolean deaf, boolean mute, + @Nullable String avatar, boolean isPending, PayloadUser user) { this.premiumSince = premiumSince; this.nick = nick; this.joinedAt = joinedAt; @@ -46,7 +44,8 @@ public PayloadMember(@Nullable String premiumSince, @Nullable String nick, this.user = user; } - public static @NotNull PayloadMember of(@NotNull Member member) { + @Nonnull + public static PayloadMember of(Member member) { String permissions = Long .toString(Permission.getRaw(member.getPermissions().toArray(Permission[]::new))); List roles = member.getRoles().stream().map(Role::getId).toList(); @@ -57,11 +56,12 @@ public PayloadMember(@Nullable String premiumSince, @Nullable String nick, member.isPending(), user); } - public @NotNull PayloadUser getUser() { + @Nonnull + public PayloadUser getUser() { return user; } - public void setUser(@NotNull PayloadUser user) { + public void setUser(PayloadUser user) { this.user = user; } @@ -83,30 +83,30 @@ public void setNick(@Nullable String nick) { this.nick = nick; } - @NotNull + @Nonnull public String getJoinedAt() { return joinedAt; } - public void setJoinedAt(@NotNull String joinedAt) { + public void setJoinedAt(String joinedAt) { this.joinedAt = joinedAt; } - @NotNull + @Nonnull public String getPermissions() { return permissions; } - public void setPermissions(@NotNull String permissions) { + public void setPermissions(String permissions) { this.permissions = permissions; } - @NotNull + @Nonnull public List getRoles() { return Collections.unmodifiableList(roles); } - public void setRoles(@NotNull List roles) { + public void setRoles(List roles) { this.roles = new ArrayList<>(roles); } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/PayloadUser.java b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/PayloadUser.java index caa79dcc55..19ea46e4dc 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/PayloadUser.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/PayloadUser.java @@ -2,8 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import net.dv8tion.jda.api.entities.User; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; public final class PayloadUser { private boolean bot; @@ -14,8 +15,8 @@ public final class PayloadUser { private String username; private String discriminator; - public PayloadUser(boolean bot, long publicFlags, @NotNull String id, @Nullable String avatar, - @NotNull String username, @NotNull String discriminator) { + public PayloadUser(boolean bot, long publicFlags, String id, @Nullable String avatar, + String username, String discriminator) { this.publicFlags = publicFlags; this.id = id; this.avatar = avatar; @@ -23,7 +24,8 @@ public PayloadUser(boolean bot, long publicFlags, @NotNull String id, @Nullable this.discriminator = discriminator; } - public static @NotNull PayloadUser of(@NotNull User user) { + @Nonnull + public static PayloadUser of(User user) { return new PayloadUser(user.isBot(), user.getFlagsRaw(), user.getId(), user.getAvatarId(), user.getName(), user.getDiscriminator()); } @@ -44,12 +46,12 @@ public void setPublicFlags(long publicFlags) { this.publicFlags = publicFlags; } - @NotNull + @Nonnull public String getId() { return id; } - public void setId(@NotNull String id) { + public void setId(String id) { this.id = id; } @@ -62,21 +64,21 @@ public void setAvatar(@Nullable String avatar) { this.avatar = avatar; } - @NotNull + @Nonnull public String getUsername() { return username; } - public void setUsername(@NotNull String username) { + public void setUsername(String username) { this.username = username; } - @NotNull + @Nonnull public String getDiscriminator() { return discriminator; } - public void setDiscriminator(@NotNull String discriminator) { + public void setDiscriminator(String discriminator) { this.discriminator = discriminator; } } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommand.java b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommand.java index 8246909bc6..53152aa6b7 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommand.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommand.java @@ -1,9 +1,10 @@ package org.togetherjava.tjbot.jda.payloads.slashcommand; import com.fasterxml.jackson.annotation.JsonProperty; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.jda.payloads.PayloadMember; +import javax.annotation.Nonnull; + public final class PayloadSlashCommand { @JsonProperty("guild_id") private String guildId; @@ -18,10 +19,9 @@ public final class PayloadSlashCommand { private PayloadMember member; private PayloadSlashCommandData data; - @SuppressWarnings("ConstructorWithTooManyParameters") - public PayloadSlashCommand(@NotNull String guildId, @NotNull String id, int type, int version, - @NotNull String channelId, @NotNull String applicationId, @NotNull String token, - @NotNull PayloadMember member, @NotNull PayloadSlashCommandData data) { + public PayloadSlashCommand(String guildId, String id, int type, int version, String channelId, + String applicationId, String token, PayloadMember member, + PayloadSlashCommandData data) { this.guildId = guildId; this.id = id; this.type = type; @@ -33,21 +33,21 @@ public PayloadSlashCommand(@NotNull String guildId, @NotNull String id, int type this.data = data; } - @NotNull + @Nonnull public String getGuildId() { return guildId; } - public void setGuildId(@NotNull String guildId) { + public void setGuildId(String guildId) { this.guildId = guildId; } - @NotNull + @Nonnull public String getId() { return id; } - public void setId(@NotNull String id) { + public void setId(String id) { this.id = id; } @@ -67,48 +67,48 @@ public void setVersion(int version) { this.version = version; } - @NotNull + @Nonnull public String getChannelId() { return channelId; } - public void setChannelId(@NotNull String channelId) { + public void setChannelId(String channelId) { this.channelId = channelId; } - @NotNull + @Nonnull public String getApplicationId() { return applicationId; } - public void setApplicationId(@NotNull String applicationId) { + public void setApplicationId(String applicationId) { this.applicationId = applicationId; } - @NotNull + @Nonnull public String getToken() { return token; } - public void setToken(@NotNull String token) { + public void setToken(String token) { this.token = token; } - @NotNull + @Nonnull public PayloadMember getMember() { return member; } - public void setMember(@NotNull PayloadMember member) { + public void setMember(PayloadMember member) { this.member = member; } - @NotNull + @Nonnull public PayloadSlashCommandData getData() { return data; } - public void setData(@NotNull PayloadSlashCommandData data) { + public void setData(PayloadSlashCommandData data) { this.data = data; } } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandData.java b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandData.java index b84a318961..1a7fbb53fa 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandData.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandData.java @@ -1,9 +1,9 @@ package org.togetherjava.tjbot.jda.payloads.slashcommand; import com.fasterxml.jackson.annotation.JsonInclude; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -17,7 +17,7 @@ public final class PayloadSlashCommandData { @JsonInclude(JsonInclude.Include.NON_NULL) private PayloadSlashCommandResolved resolved; - public PayloadSlashCommandData(@NotNull String name, @NotNull String id, int type, + public PayloadSlashCommandData(String name, String id, int type, @Nullable List options, @Nullable PayloadSlashCommandResolved resolved) { this.name = name; @@ -27,21 +27,21 @@ public PayloadSlashCommandData(@NotNull String name, @NotNull String id, int typ this.resolved = resolved; } - @NotNull + @Nonnull public String getName() { return name; } - public void setName(@NotNull String name) { + public void setName(String name) { this.name = name; } - @NotNull + @Nonnull public String getId() { return id; } - public void setId(@NotNull String id) { + public void setId(String id) { this.id = id; } @@ -53,7 +53,8 @@ public void setType(int type) { this.type = type; } - public @Nullable List getOptions() { + @Nullable + public List getOptions() { return options == null ? null : Collections.unmodifiableList(options); } @@ -61,7 +62,8 @@ public void setOptions(@Nullable List options) { this.options = options == null ? null : new ArrayList<>(options); } - public @Nullable PayloadSlashCommandResolved getResolved() { + @Nullable + public PayloadSlashCommandResolved getResolved() { return resolved; } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandMembers.java b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandMembers.java index 99e9fbc51e..19fd999b19 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandMembers.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandMembers.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.jda.payloads.PayloadMember; +import javax.annotation.Nonnull; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -12,17 +12,18 @@ public final class PayloadSlashCommandMembers { private Map idToMember; - public PayloadSlashCommandMembers(@NotNull Map idToMember) { + public PayloadSlashCommandMembers(Map idToMember) { this.idToMember = new HashMap<>(idToMember); } @JsonAnyGetter - public @NotNull Map getIdToMember() { + @Nonnull + public Map getIdToMember() { return Collections.unmodifiableMap(idToMember); } @JsonAnySetter - public void setIdToMember(@NotNull Map idToMember) { + public void setIdToMember(Map idToMember) { this.idToMember = new HashMap<>(idToMember); } } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandOption.java b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandOption.java index cd63baf3c8..0075b12391 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandOption.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandOption.java @@ -1,9 +1,9 @@ package org.togetherjava.tjbot.jda.payloads.slashcommand; import com.fasterxml.jackson.annotation.JsonInclude; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -15,7 +15,7 @@ public final class PayloadSlashCommandOption { @JsonInclude(JsonInclude.Include.NON_NULL) private List options; - public PayloadSlashCommandOption(@NotNull String name, int type, @Nullable String value, + public PayloadSlashCommandOption(String name, int type, @Nullable String value, @Nullable List options) { this.name = name; this.type = type; @@ -23,12 +23,12 @@ public PayloadSlashCommandOption(@NotNull String name, int type, @Nullable Strin this.options = options == null ? null : new ArrayList<>(options); } - @NotNull + @Nonnull public String getName() { return name; } - public void setName(@NotNull String name) { + public void setName(String name) { this.name = name; } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandResolved.java b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandResolved.java index ae286c51e6..4207b0197b 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandResolved.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandResolved.java @@ -1,7 +1,7 @@ package org.togetherjava.tjbot.jda.payloads.slashcommand; import com.fasterxml.jackson.annotation.JsonInclude; -import org.jetbrains.annotations.Nullable; +import javax.annotation.Nullable; public final class PayloadSlashCommandResolved { @JsonInclude(JsonInclude.Include.NON_NULL) @@ -15,7 +15,8 @@ public PayloadSlashCommandResolved(@Nullable PayloadSlashCommandMembers members, this.users = users; } - public @Nullable PayloadSlashCommandMembers getMembers() { + @Nullable + public PayloadSlashCommandMembers getMembers() { return members; } @@ -23,7 +24,8 @@ public void setMembers(@Nullable PayloadSlashCommandMembers members) { this.members = members; } - public @Nullable PayloadSlashCommandUsers getUsers() { + @Nullable + public PayloadSlashCommandUsers getUsers() { return users; } diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandUsers.java b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandUsers.java index 437beed587..d411e1ad34 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandUsers.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/payloads/slashcommand/PayloadSlashCommandUsers.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; -import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.jda.payloads.PayloadUser; +import javax.annotation.Nonnull; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -12,17 +12,18 @@ public final class PayloadSlashCommandUsers { private Map idToUser; - public PayloadSlashCommandUsers(@NotNull Map idToUser) { + public PayloadSlashCommandUsers(Map idToUser) { this.idToUser = new HashMap<>(idToUser); } @JsonAnyGetter - public @NotNull Map getIdToUser() { + @Nonnull + public Map getIdToUser() { return Collections.unmodifiableMap(idToUser); } @JsonAnySetter - public void setIdToUser(@NotNull Map idToUser) { + public void setIdToUser(Map idToUser) { this.idToUser = new HashMap<>(idToUser); } } diff --git a/build.gradle b/build.gradle index 2c1bf4ffeb..c0d9a4d04a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id "com.diffplug.spotless" version "6.9.1" + id "com.diffplug.spotless" version "6.10.0" id "org.sonarqube" version "3.4.0.2513" id "name.remal.sonarlint" version "1.5.0" } diff --git a/database/build.gradle b/database/build.gradle index f7a7c2e634..0979d396f8 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -5,8 +5,9 @@ plugins { var sqliteVersion = "3.39.2.0" dependencies { + implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation "org.xerial:sqlite-jdbc:${sqliteVersion}" - implementation 'org.flywaydb:flyway-core:9.1.3' + implementation 'org.flywaydb:flyway-core:9.2.0' implementation 'org.jooq:jooq:3.17.2' } diff --git a/database/src/main/java/org/togetherjava/tjbot/db/package-info.java b/database/src/main/java/org/togetherjava/tjbot/db/package-info.java index 434fd192e9..d51338e128 100644 --- a/database/src/main/java/org/togetherjava/tjbot/db/package-info.java +++ b/database/src/main/java/org/togetherjava/tjbot/db/package-info.java @@ -2,4 +2,7 @@ * This package contains the database management of the application. See * {@link org.togetherjava.tjbot.db.Database} to get started. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.db; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/database/src/main/java/org/togetherjava/tjbot/db/util/package-info.java b/database/src/main/java/org/togetherjava/tjbot/db/util/package-info.java index b0e0802fb3..d097ce638c 100644 --- a/database/src/main/java/org/togetherjava/tjbot/db/util/package-info.java +++ b/database/src/main/java/org/togetherjava/tjbot/db/util/package-info.java @@ -2,4 +2,7 @@ * General utility package containing various helper classes and methods used throughout the * application. */ +@ParametersAreNonnullByDefault package org.togetherjava.tjbot.db.util; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/settings.gradle b/settings.gradle index 811b17ba0f..ebc2eba2d0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,3 +6,5 @@ include 'formatter' // NOTE The logviewer does not properly work as of today. // But it is causing major build slowdowns, so we exclude it for the time being. // include 'logviewer' +include 'website' + diff --git a/website/build.gradle b/website/build.gradle new file mode 100644 index 0000000000..c37398427d --- /dev/null +++ b/website/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'org.springframework.boot' version '2.7.3' + id 'io.spring.dependency-management' version '1.0.13.RELEASE' + id "com.google.cloud.tools.jib" version "3.1.4" + id 'java' +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +var outputImage = 'togetherjava.duckdns.org:5001/togetherjava/website:' + System.getenv('BRANCH_NAME') ?: 'latest' + +jib { + from.image = 'eclipse-temurin:18' + to { + image = outputImage + auth { + username = System.getenv('REGISTRY_USER') ?: '' + password = System.getenv('REGISTRY_PASSWORD') ?: '' + } + } + container { + setPorts(["5051"].asList()) + setCreationTime(Instant.now().toString()) + } +} diff --git a/website/src/main/java/org/togetherjava/tjbot/website/Application.java b/website/src/main/java/org/togetherjava/tjbot/website/Application.java new file mode 100644 index 0000000000..99e2dd2dbb --- /dev/null +++ b/website/src/main/java/org/togetherjava/tjbot/website/Application.java @@ -0,0 +1,19 @@ +package org.togetherjava.tjbot.website; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Spring boot application to serve the bots welcome webpage. + */ +@SpringBootApplication +public class Application { + /** + * Starts the application. + * + * @param args Not supported + */ + public static void main(final String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/website/src/main/resources/application.properties b/website/src/main/resources/application.properties new file mode 100644 index 0000000000..26d0a1edc8 --- /dev/null +++ b/website/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=5051 diff --git a/website/src/main/resources/public/index.html b/website/src/main/resources/public/index.html new file mode 100644 index 0000000000..8d5d569277 --- /dev/null +++ b/website/src/main/resources/public/index.html @@ -0,0 +1,12 @@ + + + + + + Together Java + + +

    Hello from Together Java

    +Visit us at: discord.gg/P3UmanFmvK + +