From 16039070c004f94f8de47990a2f31b565364f045 Mon Sep 17 00:00:00 2001 From: Islam Date: Wed, 9 Feb 2022 15:26:51 +0100 Subject: [PATCH 01/16] Pushing the remider command, which works while the bot is online. --- .../togetherjava/tjbot/commands/Features.java | 2 + .../commands/reminder/ReminderCommand.java | 151 ++++++++++++++++++ .../resources/db/V8__Add_Reminder_Actions.sql | 10 ++ 3 files changed, 163 insertions(+) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/reminder/ReminderCommand.java create mode 100644 application/src/main/resources/db/V8__Add_Reminder_Actions.sql 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 7a00a2a8da..eb7ab9d506 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -10,6 +10,7 @@ import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; import org.togetherjava.tjbot.commands.moderation.*; import org.togetherjava.tjbot.commands.moderation.temp.TemporaryModerationRoutine; +import org.togetherjava.tjbot.commands.reminder.ReminderCommand; import org.togetherjava.tjbot.commands.system.BotCore; import org.togetherjava.tjbot.commands.tags.TagCommand; import org.togetherjava.tjbot.commands.tags.TagManageCommand; @@ -86,6 +87,7 @@ public enum Features { features.add(new TopHelpersCommand(database, config)); features.add(new RoleSelectCommand()); features.add(new NoteCommand(actionsStore, config)); + features.add(new ReminderCommand()); // Mixtures features.add(new FreeCommand(config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/ReminderCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/ReminderCommand.java new file mode 100644 index 0000000000..d3cec6ef63 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/ReminderCommand.java @@ -0,0 +1,151 @@ +package org.togetherjava.tjbot.commands.reminder; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; +import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; + +import java.awt.*; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * This creates a command called {@code /remind}, which reminds the user about a certain activity or + * event + * + *

+ * For example: + * + *

+ * {@code
+ * /remind from: 26.09.2021 to: 03.10.2021 @User Message
+ * /remind 10 seconds @User User Message
+ * }
+ * 
+ */ + +public class ReminderCommand extends SlashCommandAdapter { + + static final Color AMBIENT_COLOR = Color.decode("#FA8072"); + private static final String REMINDER = "reminder"; + private static final String OPTIONAL_USER_MENTION = "remind-who"; + private static final String OPTIONAL_USER_MESSAGE = "user-message"; + + /* + * Creates an Instance of the command. + */ + public ReminderCommand() { + super("reminder", "Reminds user about certain activity or event", + SlashCommandVisibility.GUILD); + getData() + .addOption(OptionType.STRING, REMINDER, + "Reminds the user about certain activity or an event", true) + .addOption(OptionType.USER, OPTIONAL_USER_MENTION, "Optionally the user to be reminded", + false) + .addOption(OptionType.STRING, OPTIONAL_USER_MESSAGE, "Optionally to write a message", + false); + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + String reminder = Objects.requireNonNull(event.getOption(REMINDER)).getAsString(); + OptionMapping userMention = event.getOption(OPTIONAL_USER_MENTION); + OptionMapping userMessage = event.getOption(OPTIONAL_USER_MESSAGE); + + String[] data = reminder.split(" ", 2); + // TODO Make it so that it can support multiple units like: 1 minute 20 seconds, 2 hour 20 + // minutes, 2 days 20 hours + int duration = Integer.parseInt(data[0]); + ChronoUnit unit = switch (data[1]) { + case "second", "seconds" -> ChronoUnit.SECONDS; + case "minute", "minutes" -> ChronoUnit.MINUTES; + case "hour", "hours" -> ChronoUnit.HOURS; + case "day", "days" -> ChronoUnit.DAYS; + default -> throw new IllegalArgumentException( + "Unsupported duration: " + reminder + " use Numbers only (EN)"); + }; + ReplyAction option; + if (userMessage != null && userMention != null) { + option = setMentionAndMessageReply(event, reminder, userMention, userMessage, duration, + unit); + } else if (userMention != null) { + option = setUserMentionReply(event, reminder, userMention, duration, unit); + } else if (userMessage != null) { + option = setUserMessageReply(event, reminder, userMessage, duration, unit); + } else { + option = event.replyEmbeds(new EmbedBuilder().setTitle("Reminder") + .setDescription("You will be reminded in " + reminder) + .setFooter(event.getUser().getName() + " • used " + event.getCommandString()) + .setTimestamp(Instant.now()) + .setColor(AMBIENT_COLOR) + .build()); + event.getChannel() + .sendMessage(event.getUser().getAsMention() + " pinging you just to remind you.") + .queueAfter(duration, TimeUnit.of(unit)); + } + option.queue(); + } + + @NotNull + private ReplyAction setMentionAndMessageReply(@NotNull SlashCommandEvent event, String reminder, + OptionMapping userMention, OptionMapping userMessage, int duration, ChronoUnit unit) { + ReplyAction option; + option = event + .replyEmbeds(new EmbedBuilder().setTitle("Reminder") + .setDescription("User " + userMention.getAsUser().getAsMention() + + " will be reminded in " + reminder + " with the message **" + + userMessage.getAsString() + "**") + .setFooter(event.getUser().getName() + " • used " + event.getCommandString()) + .setTimestamp(Instant.now()) + .setColor(AMBIENT_COLOR) + .build()); + event.getChannel() + .sendMessage("Reminder for " + userMention.getAsUser().getAsMention() + " about **" + + userMessage.getAsString() + "**.") + .queueAfter(duration, TimeUnit.of(unit)); + return option; + } + + @NotNull + private ReplyAction setUserMentionReply(@NotNull SlashCommandEvent event, String reminder, + OptionMapping userMention, int duration, ChronoUnit unit) { + ReplyAction option; + option = event.replyEmbeds(new EmbedBuilder().setTitle("Reminder") + .setDescription("User " + userMention.getAsUser().getAsMention() + + " will be reminded in " + reminder) + .setFooter(event.getUser().getName() + " • used " + event.getCommandString()) + .setTimestamp(Instant.now()) + .setColor(AMBIENT_COLOR) + .build()); + + event.getChannel() + .sendMessage( + userMention.getAsUser().getAsMention() + " pinging you just to remind you.") + .queueAfter(duration, TimeUnit.of(unit)); + return option; + } + + @NotNull + private ReplyAction setUserMessageReply(@NotNull SlashCommandEvent event, String reminder, + OptionMapping userMessage, int duration, ChronoUnit unit) { + ReplyAction option; + option = event.replyEmbeds(new EmbedBuilder().setTitle("Reminder") + .setDescription( + reminder + " reminder with the message **" + userMessage.getAsString() + "**") + .setFooter(event.getUser().getName() + " • used " + event.getCommandString()) + .setTimestamp(Instant.now()) + .setColor(AMBIENT_COLOR) + .build()); + event.getChannel() + .sendMessage(event.getUser().getAsMention() + " reminder with the message **" + + userMessage.getAsString() + "**") + .queueAfter(duration, TimeUnit.of(unit)); + return option; + } +} diff --git a/application/src/main/resources/db/V8__Add_Reminder_Actions.sql b/application/src/main/resources/db/V8__Add_Reminder_Actions.sql new file mode 100644 index 0000000000..7529ef233d --- /dev/null +++ b/application/src/main/resources/db/V8__Add_Reminder_Actions.sql @@ -0,0 +1,10 @@ +CREATE TABLE reminder_actions +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + issued_at TIMESTAMP NOT NULL, + author_id BIGINT NOT NULL, + expires_at TIMESTAMP NOT NULL, + expired INT NOT NULL, + optional_message VARCHAR(100), + optional_mention VARCHAR(25) +) \ No newline at end of file From c7a878185f195d61bbb79d501a17de892fbdef80 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Wed, 9 Feb 2022 16:09:49 +0100 Subject: [PATCH 02/16] Reworking first part of remind command * changed database table * changed code how entries are created and stored * aligned options and color * removed the actual reminder for now --- .../togetherjava/tjbot/commands/Features.java | 4 +- .../commands/reminder/RemindCommand.java | 61 +++++++ .../commands/reminder/ReminderCommand.java | 151 ------------------ .../resources/db/V8__Add_Reminder_Actions.sql | 10 -- .../main/resources/db/V8__Add_Reminders.sql | 10 ++ 5 files changed, 73 insertions(+), 163 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindCommand.java delete mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/reminder/ReminderCommand.java delete mode 100644 application/src/main/resources/db/V8__Add_Reminder_Actions.sql create mode 100644 application/src/main/resources/db/V8__Add_Reminders.sql 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 eb7ab9d506..cfd119f4f0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -10,7 +10,7 @@ import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; import org.togetherjava.tjbot.commands.moderation.*; import org.togetherjava.tjbot.commands.moderation.temp.TemporaryModerationRoutine; -import org.togetherjava.tjbot.commands.reminder.ReminderCommand; +import org.togetherjava.tjbot.commands.reminder.RemindCommand; import org.togetherjava.tjbot.commands.system.BotCore; import org.togetherjava.tjbot.commands.tags.TagCommand; import org.togetherjava.tjbot.commands.tags.TagManageCommand; @@ -87,7 +87,7 @@ public enum Features { features.add(new TopHelpersCommand(database, config)); features.add(new RoleSelectCommand()); features.add(new NoteCommand(actionsStore, config)); - features.add(new ReminderCommand()); + features.add(new RemindCommand(database)); // Mixtures features.add(new FreeCommand(config)); 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 new file mode 100644 index 0000000000..8f3bf309a2 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindCommand.java @@ -0,0 +1,61 @@ +package org.togetherjava.tjbot.commands.reminder; + +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.Reminders; + +import java.awt.*; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +// TODO Javadoc +public final class RemindCommand extends SlashCommandAdapter { + private static final Color AMBIENT_COLOR = Color.decode("#F7F492"); + private static final String COMMAND_NAME = "remind"; + private static final String WHEN_OPTION = "when"; + private static final String CONTENT_OPTION = "content"; + + private final Database database; + + /** + * Creates an instance of the command. + * + * @param database to store and fetch the reminders from + */ + public RemindCommand(@NotNull Database database) { + super(COMMAND_NAME, "Reminds the user about something at a given time", + SlashCommandVisibility.GUILD); + + getData() + .addOption(OptionType.STRING, WHEN_OPTION, "when the reminder should be sent", true) + .addOption(OptionType.STRING, CONTENT_OPTION, "the content of the reminder", true); + + this.database = database; + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + String whenText = event.getOption(WHEN_OPTION).getAsString(); + String content = event.getOption(CONTENT_OPTION).getAsString(); + + Instant remindAt = parseWhen(whenText); + + database.write(context -> context.newRecord(Reminders.REMINDERS) + .setCreatedAt(Instant.now()) + .setGuildId(event.getGuild().getIdLong()) + .setChannelId(event.getChannel().getIdLong()) + .setAuthorId(event.getUser().getIdLong()) + .setRemindAt(remindAt) + .setContent(content) + .insert()); + } + + private static @NotNull Instant parseWhen(@NotNull String when) { + // TODO parse "when" and remove stub + return Instant.now().plus(1, ChronoUnit.HOURS); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/ReminderCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/ReminderCommand.java deleted file mode 100644 index d3cec6ef63..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/ReminderCommand.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.togetherjava.tjbot.commands.reminder; - -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; -import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; -import org.jetbrains.annotations.NotNull; -import org.togetherjava.tjbot.commands.SlashCommandAdapter; -import org.togetherjava.tjbot.commands.SlashCommandVisibility; - -import java.awt.*; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -/** - * This creates a command called {@code /remind}, which reminds the user about a certain activity or - * event - * - *

- * For example: - * - *

- * {@code
- * /remind from: 26.09.2021 to: 03.10.2021 @User Message
- * /remind 10 seconds @User User Message
- * }
- * 
- */ - -public class ReminderCommand extends SlashCommandAdapter { - - static final Color AMBIENT_COLOR = Color.decode("#FA8072"); - private static final String REMINDER = "reminder"; - private static final String OPTIONAL_USER_MENTION = "remind-who"; - private static final String OPTIONAL_USER_MESSAGE = "user-message"; - - /* - * Creates an Instance of the command. - */ - public ReminderCommand() { - super("reminder", "Reminds user about certain activity or event", - SlashCommandVisibility.GUILD); - getData() - .addOption(OptionType.STRING, REMINDER, - "Reminds the user about certain activity or an event", true) - .addOption(OptionType.USER, OPTIONAL_USER_MENTION, "Optionally the user to be reminded", - false) - .addOption(OptionType.STRING, OPTIONAL_USER_MESSAGE, "Optionally to write a message", - false); - } - - @Override - public void onSlashCommand(@NotNull SlashCommandEvent event) { - String reminder = Objects.requireNonNull(event.getOption(REMINDER)).getAsString(); - OptionMapping userMention = event.getOption(OPTIONAL_USER_MENTION); - OptionMapping userMessage = event.getOption(OPTIONAL_USER_MESSAGE); - - String[] data = reminder.split(" ", 2); - // TODO Make it so that it can support multiple units like: 1 minute 20 seconds, 2 hour 20 - // minutes, 2 days 20 hours - int duration = Integer.parseInt(data[0]); - ChronoUnit unit = switch (data[1]) { - case "second", "seconds" -> ChronoUnit.SECONDS; - case "minute", "minutes" -> ChronoUnit.MINUTES; - case "hour", "hours" -> ChronoUnit.HOURS; - case "day", "days" -> ChronoUnit.DAYS; - default -> throw new IllegalArgumentException( - "Unsupported duration: " + reminder + " use Numbers only (EN)"); - }; - ReplyAction option; - if (userMessage != null && userMention != null) { - option = setMentionAndMessageReply(event, reminder, userMention, userMessage, duration, - unit); - } else if (userMention != null) { - option = setUserMentionReply(event, reminder, userMention, duration, unit); - } else if (userMessage != null) { - option = setUserMessageReply(event, reminder, userMessage, duration, unit); - } else { - option = event.replyEmbeds(new EmbedBuilder().setTitle("Reminder") - .setDescription("You will be reminded in " + reminder) - .setFooter(event.getUser().getName() + " • used " + event.getCommandString()) - .setTimestamp(Instant.now()) - .setColor(AMBIENT_COLOR) - .build()); - event.getChannel() - .sendMessage(event.getUser().getAsMention() + " pinging you just to remind you.") - .queueAfter(duration, TimeUnit.of(unit)); - } - option.queue(); - } - - @NotNull - private ReplyAction setMentionAndMessageReply(@NotNull SlashCommandEvent event, String reminder, - OptionMapping userMention, OptionMapping userMessage, int duration, ChronoUnit unit) { - ReplyAction option; - option = event - .replyEmbeds(new EmbedBuilder().setTitle("Reminder") - .setDescription("User " + userMention.getAsUser().getAsMention() - + " will be reminded in " + reminder + " with the message **" - + userMessage.getAsString() + "**") - .setFooter(event.getUser().getName() + " • used " + event.getCommandString()) - .setTimestamp(Instant.now()) - .setColor(AMBIENT_COLOR) - .build()); - event.getChannel() - .sendMessage("Reminder for " + userMention.getAsUser().getAsMention() + " about **" - + userMessage.getAsString() + "**.") - .queueAfter(duration, TimeUnit.of(unit)); - return option; - } - - @NotNull - private ReplyAction setUserMentionReply(@NotNull SlashCommandEvent event, String reminder, - OptionMapping userMention, int duration, ChronoUnit unit) { - ReplyAction option; - option = event.replyEmbeds(new EmbedBuilder().setTitle("Reminder") - .setDescription("User " + userMention.getAsUser().getAsMention() - + " will be reminded in " + reminder) - .setFooter(event.getUser().getName() + " • used " + event.getCommandString()) - .setTimestamp(Instant.now()) - .setColor(AMBIENT_COLOR) - .build()); - - event.getChannel() - .sendMessage( - userMention.getAsUser().getAsMention() + " pinging you just to remind you.") - .queueAfter(duration, TimeUnit.of(unit)); - return option; - } - - @NotNull - private ReplyAction setUserMessageReply(@NotNull SlashCommandEvent event, String reminder, - OptionMapping userMessage, int duration, ChronoUnit unit) { - ReplyAction option; - option = event.replyEmbeds(new EmbedBuilder().setTitle("Reminder") - .setDescription( - reminder + " reminder with the message **" + userMessage.getAsString() + "**") - .setFooter(event.getUser().getName() + " • used " + event.getCommandString()) - .setTimestamp(Instant.now()) - .setColor(AMBIENT_COLOR) - .build()); - event.getChannel() - .sendMessage(event.getUser().getAsMention() + " reminder with the message **" - + userMessage.getAsString() + "**") - .queueAfter(duration, TimeUnit.of(unit)); - return option; - } -} diff --git a/application/src/main/resources/db/V8__Add_Reminder_Actions.sql b/application/src/main/resources/db/V8__Add_Reminder_Actions.sql deleted file mode 100644 index 7529ef233d..0000000000 --- a/application/src/main/resources/db/V8__Add_Reminder_Actions.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE reminder_actions -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - issued_at TIMESTAMP NOT NULL, - author_id BIGINT NOT NULL, - expires_at TIMESTAMP NOT NULL, - expired INT NOT NULL, - optional_message VARCHAR(100), - optional_mention VARCHAR(25) -) \ No newline at end of file diff --git a/application/src/main/resources/db/V8__Add_Reminders.sql b/application/src/main/resources/db/V8__Add_Reminders.sql new file mode 100644 index 0000000000..8baa628049 --- /dev/null +++ b/application/src/main/resources/db/V8__Add_Reminders.sql @@ -0,0 +1,10 @@ +CREATE TABLE reminders +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + created_at TIMESTAMP NOT NULL, + guild_id BIGINT NOT NULL, + channel_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + remind_at TIMESTAMP NOT NULL, + content TEXT NOT NULL +) \ No newline at end of file From 039fed7420bc29bb0701692077ebad43cadaadc7 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 10 Feb 2022 19:05:29 +0100 Subject: [PATCH 03/16] Adding proper "when" selection for /remind --- .../commands/reminder/RemindCommand.java | 80 +++++++++++++++---- ...ders.sql => V8__Add_Pending_Reminders.sql} | 2 +- 2 files changed, 66 insertions(+), 16 deletions(-) rename application/src/main/resources/db/{V8__Add_Reminders.sql => V8__Add_Pending_Reminders.sql} (89%) 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 8f3bf309a2..5a3795ced7 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 @@ -1,24 +1,33 @@ package org.togetherjava.tjbot.commands.reminder; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.Interaction; 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 org.togetherjava.tjbot.db.generated.tables.Reminders; -import java.awt.*; -import java.time.Instant; -import java.time.temporal.ChronoUnit; +import java.time.*; +import java.time.temporal.TemporalAmount; +import java.util.List; + +import static org.togetherjava.tjbot.db.generated.Tables.PENDING_REMINDERS; // TODO Javadoc public final class RemindCommand extends SlashCommandAdapter { - private static final Color AMBIENT_COLOR = Color.decode("#F7F492"); private static final String COMMAND_NAME = "remind"; - private static final String WHEN_OPTION = "when"; + private static final String WHEN_AMOUNT_OPTION = "when-amount"; + private static final String WHEN_UNIT_OPTION = "when-unit"; private static final String CONTENT_OPTION = "content"; + private static final int MIN_WHEN_AMOUNT = 1; + private static final int MAX_WHEN_AMOUNT = 1_000; + private static final List WHEN_UNITS = + List.of("minutes", "hours", "days", "weeks", "months", "years"); + private static final Period MAX_WHEN_PERIOD = Period.ofYears(3); + private final Database database; /** @@ -30,8 +39,14 @@ public RemindCommand(@NotNull Database database) { super(COMMAND_NAME, "Reminds the user about something at a given time", SlashCommandVisibility.GUILD); - getData() - .addOption(OptionType.STRING, WHEN_OPTION, "when the reminder should be sent", true) + OptionData whenAmount = new OptionData(OptionType.INTEGER, WHEN_AMOUNT_OPTION, + "when the reminder should be sent, amount (e.g. 5)", true) + .setRequiredRange(MIN_WHEN_AMOUNT, MAX_WHEN_AMOUNT); + OptionData whenUnit = new OptionData(OptionType.STRING, WHEN_UNIT_OPTION, + "when the reminder should be sent, unit (e.g. weeks)", true); + WHEN_UNITS.forEach(unit -> whenUnit.addChoice(unit, unit)); + + getData().addOptions(whenAmount, whenUnit) .addOption(OptionType.STRING, CONTENT_OPTION, "the content of the reminder", true); this.database = database; @@ -39,23 +54,58 @@ public RemindCommand(@NotNull Database database) { @Override public void onSlashCommand(@NotNull SlashCommandEvent event) { - String whenText = event.getOption(WHEN_OPTION).getAsString(); + int whenAmount = Math.toIntExact(event.getOption(WHEN_AMOUNT_OPTION).getAsLong()); + String whenUnit = event.getOption(WHEN_UNIT_OPTION).getAsString(); String content = event.getOption(CONTENT_OPTION).getAsString(); - Instant remindAt = parseWhen(whenText); + Instant when = parseWhen(whenAmount, whenUnit); + if (!handleIsWhenWithinLimits(when, event)) { + return; + } - database.write(context -> context.newRecord(Reminders.REMINDERS) + event.reply("Will remind you about '%s' in %d %s.".formatted(content, whenAmount, whenUnit)) + .setEphemeral(true) + .queue(); + + database.write(context -> context.newRecord(PENDING_REMINDERS) .setCreatedAt(Instant.now()) .setGuildId(event.getGuild().getIdLong()) .setChannelId(event.getChannel().getIdLong()) .setAuthorId(event.getUser().getIdLong()) - .setRemindAt(remindAt) + .setRemindAt(when) .setContent(content) .insert()); } - private static @NotNull Instant parseWhen(@NotNull String when) { - // TODO parse "when" and remove stub - return Instant.now().plus(1, ChronoUnit.HOURS); + private static @NotNull Instant parseWhen(int whenAmount, @NotNull String whenUnit) { + TemporalAmount period = switch (whenUnit) { + case "second", "seconds" -> Duration.ofSeconds(whenAmount); + case "minute", "minutes" -> Duration.ofMinutes(whenAmount); + case "hour", "hours" -> Duration.ofHours(whenAmount); + case "day", "days" -> Period.ofDays(whenAmount); + case "week", "weeks" -> Period.ofWeeks(whenAmount); + case "month", "months" -> Period.ofMonths(whenAmount); + case "year", "years" -> Period.ofYears(whenAmount); + default -> throw new IllegalArgumentException("Unsupported unit, was: " + whenUnit); + }; + + return ZonedDateTime.now(ZoneOffset.UTC).plus(period).toInstant(); + } + + private static boolean handleIsWhenWithinLimits(@NotNull Instant when, + @NotNull Interaction event) { + ZonedDateTime maxWhen = ZonedDateTime.now(ZoneOffset.UTC).plus(MAX_WHEN_PERIOD); + + if (when.atZone(ZoneOffset.UTC).isBefore(maxWhen)) { + return true; + } + + event + .reply("The reminder is set too far in the future. The maximal allowed period is '%s'." + .formatted(MAX_WHEN_PERIOD)) + .setEphemeral(true) + .queue(); + + return false; } } diff --git a/application/src/main/resources/db/V8__Add_Reminders.sql b/application/src/main/resources/db/V8__Add_Pending_Reminders.sql similarity index 89% rename from application/src/main/resources/db/V8__Add_Reminders.sql rename to application/src/main/resources/db/V8__Add_Pending_Reminders.sql index 8baa628049..af2512ad40 100644 --- a/application/src/main/resources/db/V8__Add_Reminders.sql +++ b/application/src/main/resources/db/V8__Add_Pending_Reminders.sql @@ -1,4 +1,4 @@ -CREATE TABLE reminders +CREATE TABLE pending_reminders ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, created_at TIMESTAMP NOT NULL, From 88b1b263ac752455aee1e2539d0cb7b277d6200b Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 10 Feb 2022 19:05:50 +0100 Subject: [PATCH 04/16] Adding remind routine to send reminders --- .../togetherjava/tjbot/commands/Features.java | 2 + .../commands/moderation/ModerationUtils.java | 3 +- .../commands/reminder/RemindRoutine.java | 87 +++++++++++++++++++ .../tjbot/commands/reminder/package-info.java | 5 ++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/reminder/package-info.java 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 cfd119f4f0..b371bf6520 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -11,6 +11,7 @@ import org.togetherjava.tjbot.commands.moderation.*; import org.togetherjava.tjbot.commands.moderation.temp.TemporaryModerationRoutine; import org.togetherjava.tjbot.commands.reminder.RemindCommand; +import org.togetherjava.tjbot.commands.reminder.RemindRoutine; import org.togetherjava.tjbot.commands.system.BotCore; import org.togetherjava.tjbot.commands.tags.TagCommand; import org.togetherjava.tjbot.commands.tags.TagManageCommand; @@ -62,6 +63,7 @@ public enum Features { features.add(new ModAuditLogRoutine(database, config)); features.add(new TemporaryModerationRoutine(jda, actionsStore, config)); features.add(new TopHelpersPurgeMessagesRoutine(database)); + features.add(new RemindRoutine(database)); // Message receivers features.add(new TopHelpersMessageListener(database, config)); 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 04d8ae5fd8..ec4c9bfb3c 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 @@ -369,8 +369,7 @@ public static Predicate getIsMutedRolePredicate(@NotNull Config config) case "minute", "minutes" -> ChronoUnit.MINUTES; case "hour", "hours" -> ChronoUnit.HOURS; case "day", "days" -> ChronoUnit.DAYS; - default -> throw new IllegalArgumentException( - "Unsupported mute duration: " + durationText); + default -> throw new IllegalArgumentException("Unsupported duration: " + durationText); }; return Optional.of(new TemporaryData(Instant.now().plus(duration, unit), durationText)); 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 new file mode 100644 index 0000000000..16d211effa --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java @@ -0,0 +1,87 @@ +package org.togetherjava.tjbot.commands.reminder; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +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; +import org.togetherjava.tjbot.db.Database; + +import java.awt.*; +import java.time.Instant; +import java.time.temporal.TemporalAccessor; +import java.util.concurrent.TimeUnit; + +import static org.togetherjava.tjbot.db.generated.Tables.PENDING_REMINDERS; + +// TODO Javadoc +public final class RemindRoutine implements Routine { + private static final Logger logger = LoggerFactory.getLogger(RemindRoutine.class); + private static final Color AMBIENT_COLOR = Color.decode("#F7F492"); + public static final int SCHEDULE_INTERVAL_SECONDS = 30; + private final Database database; + + /** + * Creates a new instance. + * + * @param database the database that contains the pending reminders to send. + */ + public RemindRoutine(@NotNull Database database) { + this.database = database; + } + + @Override + public @NotNull Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 0, SCHEDULE_INTERVAL_SECONDS, + TimeUnit.SECONDS); + } + + @Override + public void runRoutine(@NotNull JDA jda) { + Instant now = Instant.now(); + database.write(context -> context.selectFrom(PENDING_REMINDERS) + .where(PENDING_REMINDERS.REMIND_AT.lessOrEqual(now)) + .stream() + .forEach(pendingReminder -> { + sendReminder(jda, pendingReminder.getGuildId(), pendingReminder.getChannelId(), + pendingReminder.getAuthorId(), pendingReminder.getContent(), + pendingReminder.getCreatedAt()); + pendingReminder.delete(); + })); + } + + private static void sendReminder(@NotNull JDA jda, long guildId, long channelId, long authorId, + @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) { + Guild guild = jda.getGuildById(guildId); + if (guild == null) { + logger.debug( + "Attempted to send a reminder but the bot is not connected to the guild '{}' anymore, skipping reminder.", + guildId); + return; + } + + TextChannel channel = guild.getTextChannelById(channelId); + if (channel == null) { + logger.debug( + "Attempted to send a reminder but the guild '{}' does not have a channel with id '{}' anymore, skipping reminder.", + guildId, channelId); + return; + } + + jda.retrieveUserById(authorId).map(author -> { + String authorName = author == null ? "Unknown user" : author.getAsTag(); + String authorIconUrl = author == null ? null : author.getAvatarUrl(); + + return new EmbedBuilder().setAuthor(authorName, null, authorIconUrl) + .setTitle("Reminder") + .setDescription(content) + .setFooter("from") + .setTimestamp(createdAt) + .setColor(AMBIENT_COLOR) + .build(); + }).flatMap(channel::sendMessageEmbeds).queue(); + } +} 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 new file mode 100644 index 0000000000..a7f65b1c69 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/package-info.java @@ -0,0 +1,5 @@ +/** + * This packages offers all the functionality for the remind-command. The core class is + * {@link org.togetherjava.tjbot.commands.reminder.RemindCommand}. + */ +package org.togetherjava.tjbot.commands.reminder; From ae2e679fc513d25952f0906b781a9e8364021a1d Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Fri, 11 Feb 2022 14:24:21 +0100 Subject: [PATCH 05/16] Javadoc and cleanup for /remind --- .../commands/reminder/RemindCommand.java | 27 ++++++++++++++----- .../commands/reminder/RemindRoutine.java | 8 ++++-- 2 files changed, 27 insertions(+), 8 deletions(-) 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 5a3795ced7..9a68eb024c 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 @@ -15,11 +15,24 @@ import static org.togetherjava.tjbot.db.generated.Tables.PENDING_REMINDERS; -// TODO Javadoc +/** + * Implements the '/remind' command which can be used to automatically send reminders to oneself at + * a future date. + *

+ * Example usage: + * + *

+ * {@code
+ * /remind amount: 5 unit: weeks content: Hello World!
+ * }
+ * 
+ *

+ * Pending reminders are processed and send by {@link RemindRoutine}. + */ public final class RemindCommand extends SlashCommandAdapter { private static final String COMMAND_NAME = "remind"; - private static final String WHEN_AMOUNT_OPTION = "when-amount"; - private static final String WHEN_UNIT_OPTION = "when-unit"; + private static final String WHEN_AMOUNT_OPTION = "amount"; + private static final String WHEN_UNIT_OPTION = "unit"; private static final String CONTENT_OPTION = "content"; private static final int MIN_WHEN_AMOUNT = 1; @@ -39,11 +52,13 @@ public RemindCommand(@NotNull Database database) { super(COMMAND_NAME, "Reminds the user about something at a given time", SlashCommandVisibility.GUILD); + // TODO As soon as JDA offers date/time selector input, this should also offer + // "/remind at" next to "/remind in" and use subcommands then OptionData whenAmount = new OptionData(OptionType.INTEGER, WHEN_AMOUNT_OPTION, - "when the reminder should be sent, amount (e.g. 5)", true) - .setRequiredRange(MIN_WHEN_AMOUNT, MAX_WHEN_AMOUNT); + "amount of the period (e.g. 5)", true).setRequiredRange(MIN_WHEN_AMOUNT, + MAX_WHEN_AMOUNT); OptionData whenUnit = new OptionData(OptionType.STRING, WHEN_UNIT_OPTION, - "when the reminder should be sent, unit (e.g. weeks)", true); + "unit of the period (e.g. weeks)", true); WHEN_UNITS.forEach(unit -> whenUnit.addChoice(unit, unit)); getData().addOptions(whenAmount, whenUnit) 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 16d211effa..b023ade764 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 @@ -17,11 +17,15 @@ import static org.togetherjava.tjbot.db.generated.Tables.PENDING_REMINDERS; -// TODO Javadoc +/** + * Routine that processes and sends pending reminders. + * + * Reminders can be set by using {@link RemindCommand}. + */ public final class RemindRoutine implements Routine { private static final Logger logger = LoggerFactory.getLogger(RemindRoutine.class); private static final Color AMBIENT_COLOR = Color.decode("#F7F492"); - public static final int SCHEDULE_INTERVAL_SECONDS = 30; + private static final int SCHEDULE_INTERVAL_SECONDS = 30; private final Database database; /** From f928715281f6a4849d1dfae15601dc592cc148d3 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Fri, 11 Feb 2022 15:12:42 +0100 Subject: [PATCH 06/16] Mention the user in the reminder --- .../tjbot/commands/reminder/RemindRoutine.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 b023ade764..0d6b6b1384 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 @@ -3,6 +3,7 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.TextChannel; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -19,7 +20,7 @@ /** * Routine that processes and sends pending reminders. - * + *

* Reminders can be set by using {@link RemindCommand}. */ public final class RemindRoutine implements Routine { @@ -75,17 +76,19 @@ private static void sendReminder(@NotNull JDA jda, long guildId, long channelId, return; } - jda.retrieveUserById(authorId).map(author -> { + jda.retrieveUserById(authorId).flatMap(author -> { String authorName = author == null ? "Unknown user" : author.getAsTag(); String authorIconUrl = author == null ? null : author.getAvatarUrl(); - return new EmbedBuilder().setAuthor(authorName, null, authorIconUrl) - .setTitle("Reminder") + MessageEmbed embed = new EmbedBuilder().setAuthor(authorName, null, authorIconUrl) .setDescription(content) - .setFooter("from") + .setFooter("reminder from") .setTimestamp(createdAt) .setColor(AMBIENT_COLOR) .build(); - }).flatMap(channel::sendMessageEmbeds).queue(); + + return channel.sendMessage(author == null ? "" : author.getAsMention()) + .setEmbeds(embed); + }).queue(); } } From e7975503e1167e0e46f1e1ad1f7b13ac55637dcc Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Fri, 11 Feb 2022 15:29:41 +0100 Subject: [PATCH 07/16] Bugfix with user not found apparently `retrieveUserById` fails instead of giving null when user not found. fortunately, there is `onErrorMap(error -> null)`. --- .../tjbot/commands/moderation/AuditCommand.java | 1 + .../tjbot/commands/reminder/RemindRoutine.java | 8 +++++--- .../togetherjava/tjbot/routines/ModAuditLogRoutine.java | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) 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 366aca3aa1..9a8c67e2d3 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 @@ -92,6 +92,7 @@ public AuditCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Confi .getDateTimeString(action.actionExpiresAt().atOffset(ZoneOffset.UTC))); return jda.retrieveUserById(action.authorId()) + .onErrorMap(error -> null) .map(author -> new EmbedBuilder().setTitle(action.actionType().name()) .setAuthor(author == null ? "(unknown user)" : author.getAsTag(), null, author == null ? null : author.getAvatarUrl()) 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 0d6b6b1384..23e43b397f 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 @@ -76,7 +76,7 @@ private static void sendReminder(@NotNull JDA jda, long guildId, long channelId, return; } - jda.retrieveUserById(authorId).flatMap(author -> { + jda.retrieveUserById(authorId).onErrorMap(error -> null).flatMap(author -> { String authorName = author == null ? "Unknown user" : author.getAsTag(); String authorIconUrl = author == null ? null : author.getAvatarUrl(); @@ -87,8 +87,10 @@ private static void sendReminder(@NotNull JDA jda, long guildId, long channelId, .setColor(AMBIENT_COLOR) .build(); - return channel.sendMessage(author == null ? "" : author.getAsMention()) - .setEmbeds(embed); + if (author == null) { + return channel.sendMessageEmbeds(embed); + } + return channel.sendMessage(author.getAsMention()).setEmbeds(embed); }).queue(); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java b/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java index d6f23d1bbd..2e9cb9f95f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java @@ -95,6 +95,7 @@ private static RestAction getTargetTagFromEntry(@NotNull AuditLogEntry e // If the target is null, the user got deleted in the meantime return entry.getJDA() .retrieveUserById(entry.getTargetIdLong()) + .onErrorMap(error -> null) .map(target -> target == null ? "(user unknown)" : target.getAsTag()); } From cce771f4a8cab6ed68be9d649a27a9aad70b9fe2 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Fri, 11 Feb 2022 15:45:25 +0100 Subject: [PATCH 08/16] Adding max-pending-reminders-per-user limit --- .../commands/reminder/RemindCommand.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) 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 9a68eb024c..df33fb6c85 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 @@ -1,5 +1,7 @@ package org.togetherjava.tjbot.commands.reminder; +import net.dv8tion.jda.api.entities.ISnowflake; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.interactions.Interaction; import net.dv8tion.jda.api.interactions.commands.OptionType; @@ -20,7 +22,7 @@ * a future date. *

* Example usage: - * + * *

  * {@code
  * /remind amount: 5 unit: weeks content: Hello World!
@@ -40,6 +42,7 @@ public final class RemindCommand extends SlashCommandAdapter {
     private static final List WHEN_UNITS =
             List.of("minutes", "hours", "days", "weeks", "months", "years");
     private static final Period MAX_WHEN_PERIOD = Period.ofYears(3);
+    private static final int MAX_PENDING_REMINDERS_PER_USER = 100;
 
     private final Database database;
 
@@ -74,9 +77,14 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) {
         String content = event.getOption(CONTENT_OPTION).getAsString();
 
         Instant when = parseWhen(whenAmount, whenUnit);
+        User author = event.getUser();
+
         if (!handleIsWhenWithinLimits(when, event)) {
             return;
         }
+        if (!handleIsUserBelowMaxPendingReminders(author, event)) {
+            return;
+        }
 
         event.reply("Will remind you about '%s' in %d %s.".formatted(content, whenAmount, whenUnit))
             .setEphemeral(true)
@@ -86,7 +94,7 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) {
             .setCreatedAt(Instant.now())
             .setGuildId(event.getGuild().getIdLong())
             .setChannelId(event.getChannel().getIdLong())
-            .setAuthorId(event.getUser().getIdLong())
+            .setAuthorId(author.getIdLong())
             .setRemindAt(when)
             .setContent(content)
             .insert());
@@ -123,4 +131,22 @@ private static boolean handleIsWhenWithinLimits(@NotNull Instant when,
 
         return false;
     }
+
+    private boolean handleIsUserBelowMaxPendingReminders(@NotNull ISnowflake author,
+            @NotNull Interaction event) {
+        int pendingReminders = database.read(context -> context.fetchCount(PENDING_REMINDERS,
+                PENDING_REMINDERS.AUTHOR_ID.equal(author.getIdLong())));
+
+        if (pendingReminders < MAX_PENDING_REMINDERS_PER_USER) {
+            return true;
+        }
+
+        event.reply(
+                "You have reached the maximum amount of pending reminders per user (%s). Please wait until some of them have been sent."
+                    .formatted(MAX_PENDING_REMINDERS_PER_USER))
+            .setEphemeral(true)
+            .queue();
+
+        return false;
+    }
 }

From c4ddb876a37b4afd4ad88ecf18ad2808ccb870b1 Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Mon, 14 Feb 2022 16:24:01 +0100
Subject: [PATCH 09/16] Send reminder per DM if failing to send in guild

---
 .../commands/reminder/RemindRoutine.java      | 109 ++++++++++++------
 1 file changed, 76 insertions(+), 33 deletions(-)

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 23e43b397f..adf254bab9 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,10 +2,10 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.entities.Guild;
-import net.dv8tion.jda.api.entities.MessageEmbed;
-import net.dv8tion.jda.api.entities.TextChannel;
+import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.requests.RestAction;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.togetherjava.tjbot.commands.Routine;
@@ -51,46 +51,89 @@ public void runRoutine(@NotNull JDA jda) {
             .where(PENDING_REMINDERS.REMIND_AT.lessOrEqual(now))
             .stream()
             .forEach(pendingReminder -> {
-                sendReminder(jda, pendingReminder.getGuildId(), pendingReminder.getChannelId(),
-                        pendingReminder.getAuthorId(), pendingReminder.getContent(),
-                        pendingReminder.getCreatedAt());
+                sendReminder(jda, pendingReminder.getId(), pendingReminder.getGuildId(),
+                        pendingReminder.getChannelId(), pendingReminder.getAuthorId(),
+                        pendingReminder.getContent(), pendingReminder.getCreatedAt());
                 pendingReminder.delete();
             }));
     }
 
-    private static void sendReminder(@NotNull JDA jda, long guildId, long channelId, long authorId,
-            @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) {
+    private static void sendReminder(@NotNull JDA jda, long id, long guildId, long channelId,
+            long authorId, @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) {
+        RestAction route = computeReminderRoute(jda, guildId, channelId, authorId);
+        sendReminderViaRoute(route, id, content, createdAt);
+    }
+
+    private static RestAction computeReminderRoute(@NotNull JDA jda, long guildId,
+            long channelId, long authorId) {
+        // If guild and channel can still be found, send there
         Guild guild = jda.getGuildById(guildId);
-        if (guild == null) {
-            logger.debug(
-                    "Attempted to send a reminder but the bot is not connected to the guild '{}' anymore, skipping reminder.",
-                    guildId);
-            return;
+        if (guild != null) {
+            TextChannel channel = guild.getTextChannelById(channelId);
+            if (channel != null) {
+                return createGuildReminderRoute(jda, authorId, channel);
+            }
         }
 
-        TextChannel channel = guild.getTextChannelById(channelId);
-        if (channel == null) {
-            logger.debug(
-                    "Attempted to send a reminder but the guild '{}' does not have a channel with id '{}' anymore, skipping reminder.",
-                    guildId, channelId);
-            return;
-        }
+        // Otherwise, attempt to DM the user directly
+        return createDmReminderRoute(jda, authorId);
+    }
 
-        jda.retrieveUserById(authorId).onErrorMap(error -> null).flatMap(author -> {
-            String authorName = author == null ? "Unknown user" : author.getAsTag();
-            String authorIconUrl = author == null ? null : author.getAvatarUrl();
+    private static @NotNull RestAction createGuildReminderRoute(@NotNull JDA jda,
+            long authorId, @NotNull TextChannel channel) {
+        return jda.retrieveUserById(authorId)
+            .onErrorMap(error -> null)
+            .map(author -> new ReminderRoute(channel, author,
+                    author == null ? null : author.getAsMention()));
+    }
 
-            MessageEmbed embed = new EmbedBuilder().setAuthor(authorName, null, authorIconUrl)
-                .setDescription(content)
-                .setFooter("reminder from")
-                .setTimestamp(createdAt)
-                .setColor(AMBIENT_COLOR)
-                .build();
+    private static @NotNull RestAction createDmReminderRoute(@NotNull JDA jda,
+            long authorId) {
+        return jda.openPrivateChannelById(authorId)
+            .onErrorMap(error -> null)
+            .map(channel -> channel == null ? new ReminderRoute(null, null, null)
+                    : 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)"));
+    }
 
-            if (author == null) {
-                return channel.sendMessageEmbeds(embed);
+    private static void sendReminderViaRoute(@NotNull RestAction routeAction,
+            long id, @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) {
+        routeAction.flatMap(route -> {
+            if (route.isUndeliverable()) {
+                throw new IllegalStateException("Route is not deliverable");
             }
-            return channel.sendMessage(author.getAsMention()).setEmbeds(embed);
-        }).queue();
+
+            MessageEmbed embed = createReminderEmbed(content, createdAt, route.target());
+            if (route.description() == null) {
+                return route.channel().sendMessageEmbeds(embed);
+            }
+            return route.channel().sendMessage(route.description()).setEmbeds(embed);
+        }).queue(message -> {
+        }, failure -> logger.warn(
+                "Failed to send a reminder (id '{}'), skipping it. This can be due to a network issue,"
+                        + " but also happen if the bot disconnected from the target guild and the"
+                        + " user has disabled DMs or has been deleted.",
+                id));
+    }
+
+    private static @NotNull MessageEmbed createReminderEmbed(@NotNull CharSequence content,
+            @NotNull TemporalAccessor createdAt, @Nullable User author) {
+        String authorName = author == null ? "Unknown user" : author.getAsTag();
+        String authorIconUrl = author == null ? null : author.getAvatarUrl();
+
+        return new EmbedBuilder().setAuthor(authorName, null, authorIconUrl)
+            .setDescription(content)
+            .setFooter("reminder from")
+            .setTimestamp(createdAt)
+            .setColor(AMBIENT_COLOR)
+            .build();
+    }
+
+    private record ReminderRoute(@Nullable MessageChannel channel, @Nullable User target,
+            @Nullable String description) {
+        boolean isUndeliverable() {
+            return channel == null && target == null;
+        }
     }
 }

From 0f9fafad0098d3dfd7ee4af49c43bcb629c34754 Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Tue, 15 Feb 2022 17:15:49 +0100
Subject: [PATCH 10/16] Improvements from CR

by Tais
---
 .../commands/reminder/RemindRoutine.java      | 74 +++++++++----------
 1 file changed, 36 insertions(+), 38 deletions(-)

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 adf254bab9..e419a63ba6 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,8 +2,12 @@
 
 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.TextChannel;
+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;
@@ -15,6 +19,8 @@
 import java.time.Instant;
 import java.time.temporal.TemporalAccessor;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 import static org.togetherjava.tjbot.db.generated.Tables.PENDING_REMINDERS;
 
@@ -51,28 +57,25 @@ public void runRoutine(@NotNull JDA jda) {
             .where(PENDING_REMINDERS.REMIND_AT.lessOrEqual(now))
             .stream()
             .forEach(pendingReminder -> {
-                sendReminder(jda, pendingReminder.getId(), pendingReminder.getGuildId(),
-                        pendingReminder.getChannelId(), pendingReminder.getAuthorId(),
-                        pendingReminder.getContent(), pendingReminder.getCreatedAt());
+                sendReminder(jda, pendingReminder.getId(), pendingReminder.getChannelId(),
+                        pendingReminder.getAuthorId(), pendingReminder.getContent(),
+                        pendingReminder.getCreatedAt());
                 pendingReminder.delete();
             }));
     }
 
-    private static void sendReminder(@NotNull JDA jda, long id, long guildId, long channelId,
-            long authorId, @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) {
-        RestAction route = computeReminderRoute(jda, guildId, channelId, authorId);
+    private static void sendReminder(@NotNull JDA jda, long id, long channelId, long authorId,
+            @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) {
+        RestAction route = computeReminderRoute(jda, channelId, authorId);
         sendReminderViaRoute(route, id, content, createdAt);
     }
 
-    private static RestAction computeReminderRoute(@NotNull JDA jda, long guildId,
-            long channelId, long authorId) {
-        // If guild and channel can still be found, send there
-        Guild guild = jda.getGuildById(guildId);
-        if (guild != null) {
-            TextChannel channel = guild.getTextChannelById(channelId);
-            if (channel != null) {
-                return createGuildReminderRoute(jda, authorId, channel);
-            }
+    private static RestAction computeReminderRoute(@NotNull JDA jda, long channelId,
+            long authorId) {
+        // If guild channel can still be found, send there
+        TextChannel channel = jda.getTextChannelById(channelId);
+        if (channel != null) {
+            return createGuildReminderRoute(jda, authorId, channel);
         }
 
         // Otherwise, attempt to DM the user directly
@@ -90,31 +93,24 @@ private static RestAction computeReminderRoute(@NotNull JDA jda,
     private static @NotNull RestAction createDmReminderRoute(@NotNull JDA jda,
             long authorId) {
         return jda.openPrivateChannelById(authorId)
-            .onErrorMap(error -> null)
-            .map(channel -> channel == null ? new ReminderRoute(null, null, null)
-                    : 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)"));
+            .map(channel -> 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)"));
     }
 
     private static void sendReminderViaRoute(@NotNull RestAction routeAction,
             long id, @NotNull CharSequence content, @NotNull TemporalAccessor createdAt) {
-        routeAction.flatMap(route -> {
-            if (route.isUndeliverable()) {
-                throw new IllegalStateException("Route is not deliverable");
-            }
-
-            MessageEmbed embed = createReminderEmbed(content, createdAt, route.target());
-            if (route.description() == null) {
-                return route.channel().sendMessageEmbeds(embed);
-            }
-            return route.channel().sendMessage(route.description()).setEmbeds(embed);
-        }).queue(message -> {
-        }, failure -> logger.warn(
+        Function sendMessage = route -> route.channel
+            .sendMessageEmbeds(createReminderEmbed(content, createdAt, route.target()))
+            .content(route.description());
+
+        Consumer logFailure = failure -> logger.warn(
                 "Failed to send a reminder (id '{}'), skipping it. This can be due to a network issue,"
                         + " but also happen if the bot disconnected from the target guild and the"
                         + " user has disabled DMs or has been deleted.",
-                id));
+                id);
+
+        routeAction.flatMap(sendMessage).queue(doNothing(), logFailure);
     }
 
     private static @NotNull MessageEmbed createReminderEmbed(@NotNull CharSequence content,
@@ -130,10 +126,12 @@ private static void sendReminderViaRoute(@NotNull RestAction rout
             .build();
     }
 
-    private record ReminderRoute(@Nullable MessageChannel channel, @Nullable User target,
+    private static  @NotNull Consumer doNothing() {
+        return a -> {
+        };
+    }
+
+    private record ReminderRoute(@NotNull MessageChannel channel, @Nullable User target,
             @Nullable String description) {
-        boolean isUndeliverable() {
-            return channel == null && target == null;
-        }
     }
 }

From e5bb21c9b1f86c16a960e4c066cd712635ee0328 Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Mon, 21 Feb 2022 10:34:38 +0100
Subject: [PATCH 11/16] UX renamings of the remind options

---
 .../tjbot/commands/reminder/RemindCommand.java     | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

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 df33fb6c85..aa4ddb12b0 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
@@ -33,8 +33,8 @@
  */
 public final class RemindCommand extends SlashCommandAdapter {
     private static final String COMMAND_NAME = "remind";
-    private static final String WHEN_AMOUNT_OPTION = "amount";
-    private static final String WHEN_UNIT_OPTION = "unit";
+    private static final String WHEN_AMOUNT_OPTION = "when-amount";
+    private static final String WHEN_UNIT_OPTION = "when-unit";
     private static final String CONTENT_OPTION = "content";
 
     private static final int MIN_WHEN_AMOUNT = 1;
@@ -52,20 +52,20 @@ public final class RemindCommand extends SlashCommandAdapter {
      * @param database to store and fetch the reminders from
      */
     public RemindCommand(@NotNull Database database) {
-        super(COMMAND_NAME, "Reminds the user about something at a given time",
+        super(COMMAND_NAME, "Reminds you after a given time period has passed (e.g. in 5 weeks)",
                 SlashCommandVisibility.GUILD);
 
         // TODO As soon as JDA offers date/time selector input, this should also offer
         // "/remind at" next to "/remind in" and use subcommands then
         OptionData whenAmount = new OptionData(OptionType.INTEGER, WHEN_AMOUNT_OPTION,
-                "amount of the period (e.g. 5)", true).setRequiredRange(MIN_WHEN_AMOUNT,
-                        MAX_WHEN_AMOUNT);
+                "when to remind you, the amount of the time period (e.g. [5] weeks)", true)
+                    .setRequiredRange(MIN_WHEN_AMOUNT, MAX_WHEN_AMOUNT);
         OptionData whenUnit = new OptionData(OptionType.STRING, WHEN_UNIT_OPTION,
-                "unit of the period (e.g. weeks)", true);
+                "when to remind you, the unit of the time period (e.g. 5 [weeks])", true);
         WHEN_UNITS.forEach(unit -> whenUnit.addChoice(unit, unit));
 
         getData().addOptions(whenAmount, whenUnit)
-            .addOption(OptionType.STRING, CONTENT_OPTION, "the content of the reminder", true);
+            .addOption(OptionType.STRING, CONTENT_OPTION, "what to remind you about", true);
 
         this.database = database;
     }

From f35a82ff526af8658e56062331ff74464ec4dafe Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Mon, 21 Feb 2022 10:24:41 +0100
Subject: [PATCH 12/16] ReminderRoute factories for readability

CR by Tais
---
 .../commands/reminder/RemindRoutine.java      | 23 +++++++++++--------
 1 file changed, 14 insertions(+), 9 deletions(-)

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 e419a63ba6..7a583b48a8 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,10 +2,7 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.entities.MessageChannel;
-import net.dv8tion.jda.api.entities.MessageEmbed;
-import net.dv8tion.jda.api.entities.TextChannel;
-import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.*;
 import net.dv8tion.jda.api.requests.RestAction;
 import net.dv8tion.jda.api.requests.restaction.MessageAction;
 import org.jetbrains.annotations.NotNull;
@@ -60,6 +57,7 @@ public void runRoutine(@NotNull JDA jda) {
                 sendReminder(jda, pendingReminder.getId(), pendingReminder.getChannelId(),
                         pendingReminder.getAuthorId(), pendingReminder.getContent(),
                         pendingReminder.getCreatedAt());
+
                 pendingReminder.delete();
             }));
     }
@@ -86,16 +84,13 @@ private static RestAction computeReminderRoute(@NotNull JDA jda,
             long authorId, @NotNull TextChannel channel) {
         return jda.retrieveUserById(authorId)
             .onErrorMap(error -> null)
-            .map(author -> new ReminderRoute(channel, author,
-                    author == null ? null : author.getAsMention()));
+            .map(author -> ReminderRoute.toPublic(channel, author));
     }
 
     private static @NotNull RestAction createDmReminderRoute(@NotNull JDA jda,
             long authorId) {
         return jda.openPrivateChannelById(authorId)
-            .map(channel -> 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)"));
+            .map(channel -> ReminderRoute.toPrivate(channel, channel.getUser()));
     }
 
     private static void sendReminderViaRoute(@NotNull RestAction routeAction,
@@ -133,5 +128,15 @@ private static void sendReminderViaRoute(@NotNull RestAction rout
 
     private record ReminderRoute(@NotNull MessageChannel channel, @Nullable User target,
             @Nullable String description) {
+        static ReminderRoute toPublic(@NotNull TextChannel channel, @Nullable User target) {
+            return new ReminderRoute(channel, target,
+                    target == null ? null : target.getAsMention());
+        }
+
+        static ReminderRoute toPrivate(@NotNull PrivateChannel channel, @NotNull User target) {
+            return new ReminderRoute(channel, target,
+                    "(Sending your reminder directly, because I was unable to locate"
+                            + " the original channel you wanted it to be send to)");
+        }
     }
 }

From ca51485d2958849530cb74791cf6e5b604ec27dd Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Tue, 22 Feb 2022 08:11:00 +0100
Subject: [PATCH 13/16] UX swapped whenUnit and whenAmount

CR by tais
---
 .../org/togetherjava/tjbot/commands/reminder/RemindCommand.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 aa4ddb12b0..336d1935f7 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
@@ -64,7 +64,7 @@ public RemindCommand(@NotNull Database database) {
                 "when to remind you, the unit of the time period (e.g. 5 [weeks])", true);
         WHEN_UNITS.forEach(unit -> whenUnit.addChoice(unit, unit));
 
-        getData().addOptions(whenAmount, whenUnit)
+        getData().addOptions(whenUnit, whenAmount)
             .addOption(OptionType.STRING, CONTENT_OPTION, "what to remind you about", true);
 
         this.database = database;

From f9b107af43b5a90a1684054d3f4a223471104ccb Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Tue, 22 Feb 2022 14:54:28 +0100
Subject: [PATCH 14/16] Multiline log message

textblocks with \
---
 .../tjbot/commands/reminder/RemindRoutine.java             | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

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 7a583b48a8..01811c32aa 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
@@ -100,9 +100,10 @@ private static void sendReminderViaRoute(@NotNull RestAction rout
             .content(route.description());
 
         Consumer logFailure = failure -> logger.warn(
-                "Failed to send a reminder (id '{}'), skipping it. This can be due to a network issue,"
-                        + " but also happen if the bot disconnected from the target guild and the"
-                        + " user has disabled DMs or has been deleted.",
+                """
+                        Failed to send a reminder (id '{}'), skipping it. This can be due to a network issue, \
+                        but also happen if the bot disconnected from the target guild and the \
+                        user has disabled DMs or has been deleted.""",
                 id);
 
         routeAction.flatMap(sendMessage).queue(doNothing(), logFailure);

From dc670b22ca0fdcb9074222234d8406fd137de6f5 Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Wed, 23 Feb 2022 09:27:12 +0100
Subject: [PATCH 15/16] Renamings from CR

by marko
---
 .../commands/reminder/RemindCommand.java      | 48 +++++++++----------
 1 file changed, 24 insertions(+), 24 deletions(-)

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 336d1935f7..72396dfd37 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
@@ -33,15 +33,15 @@
  */
 public final class RemindCommand extends SlashCommandAdapter {
     private static final String COMMAND_NAME = "remind";
-    private static final String WHEN_AMOUNT_OPTION = "when-amount";
-    private static final String WHEN_UNIT_OPTION = "when-unit";
+    private static final String TIME_AMOUNT_OPTION = "time-amount";
+    private static final String TIME_UNIT_OPTION = "time-unit";
     private static final String CONTENT_OPTION = "content";
 
-    private static final int MIN_WHEN_AMOUNT = 1;
-    private static final int MAX_WHEN_AMOUNT = 1_000;
-    private static final List WHEN_UNITS =
+    private static final int MIN_TIME_AMOUNT = 1;
+    private static final int MAX_TIME_AMOUNT = 1_000;
+    private static final List TIME_UNITS =
             List.of("minutes", "hours", "days", "weeks", "months", "years");
-    private static final Period MAX_WHEN_PERIOD = Period.ofYears(3);
+    private static final Period MAX_TIME_PERIOD = Period.ofYears(3);
     private static final int MAX_PENDING_REMINDERS_PER_USER = 100;
 
     private final Database database;
@@ -57,14 +57,14 @@ public RemindCommand(@NotNull Database database) {
 
         // TODO As soon as JDA offers date/time selector input, this should also offer
         // "/remind at" next to "/remind in" and use subcommands then
-        OptionData whenAmount = new OptionData(OptionType.INTEGER, WHEN_AMOUNT_OPTION,
-                "when to remind you, the amount of the time period (e.g. [5] weeks)", true)
-                    .setRequiredRange(MIN_WHEN_AMOUNT, MAX_WHEN_AMOUNT);
-        OptionData whenUnit = new OptionData(OptionType.STRING, WHEN_UNIT_OPTION,
-                "when to remind you, the unit of the time period (e.g. 5 [weeks])", true);
-        WHEN_UNITS.forEach(unit -> whenUnit.addChoice(unit, unit));
-
-        getData().addOptions(whenUnit, whenAmount)
+        OptionData timeAmount = new OptionData(OptionType.INTEGER, TIME_AMOUNT_OPTION,
+                "period to remind you in, the amount of time (e.g. [5] weeks)", true)
+                    .setRequiredRange(MIN_TIME_AMOUNT, MAX_TIME_AMOUNT);
+        OptionData timeUnit = new OptionData(OptionType.STRING, TIME_UNIT_OPTION,
+                "period to remind you in, the unit of time (e.g. 5 [weeks])", true);
+        TIME_UNITS.forEach(unit -> timeUnit.addChoice(unit, unit));
+
+        getData().addOptions(timeUnit, timeAmount)
             .addOption(OptionType.STRING, CONTENT_OPTION, "what to remind you about", true);
 
         this.database = database;
@@ -72,21 +72,21 @@ public RemindCommand(@NotNull Database database) {
 
     @Override
     public void onSlashCommand(@NotNull SlashCommandEvent event) {
-        int whenAmount = Math.toIntExact(event.getOption(WHEN_AMOUNT_OPTION).getAsLong());
-        String whenUnit = event.getOption(WHEN_UNIT_OPTION).getAsString();
+        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();
 
-        Instant when = parseWhen(whenAmount, whenUnit);
+        Instant remindAt = parseWhen(timeAmount, timeUnit);
         User author = event.getUser();
 
-        if (!handleIsWhenWithinLimits(when, event)) {
+        if (!handleIsRemindAtWithinLimits(remindAt, event)) {
             return;
         }
         if (!handleIsUserBelowMaxPendingReminders(author, event)) {
             return;
         }
 
-        event.reply("Will remind you about '%s' in %d %s.".formatted(content, whenAmount, whenUnit))
+        event.reply("Will remind you about '%s' in %d %s.".formatted(content, timeAmount, timeUnit))
             .setEphemeral(true)
             .queue();
 
@@ -95,7 +95,7 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) {
             .setGuildId(event.getGuild().getIdLong())
             .setChannelId(event.getChannel().getIdLong())
             .setAuthorId(author.getIdLong())
-            .setRemindAt(when)
+            .setRemindAt(remindAt)
             .setContent(content)
             .insert());
     }
@@ -115,17 +115,17 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) {
         return ZonedDateTime.now(ZoneOffset.UTC).plus(period).toInstant();
     }
 
-    private static boolean handleIsWhenWithinLimits(@NotNull Instant when,
+    private static boolean handleIsRemindAtWithinLimits(@NotNull Instant remindAt,
             @NotNull Interaction event) {
-        ZonedDateTime maxWhen = ZonedDateTime.now(ZoneOffset.UTC).plus(MAX_WHEN_PERIOD);
+        ZonedDateTime maxWhen = ZonedDateTime.now(ZoneOffset.UTC).plus(MAX_TIME_PERIOD);
 
-        if (when.atZone(ZoneOffset.UTC).isBefore(maxWhen)) {
+        if (remindAt.atZone(ZoneOffset.UTC).isBefore(maxWhen)) {
             return true;
         }
 
         event
             .reply("The reminder is set too far in the future. The maximal allowed period is '%s'."
-                .formatted(MAX_WHEN_PERIOD))
+                .formatted(MAX_TIME_PERIOD))
             .setEphemeral(true)
             .queue();
 

From 658cae910f69247b8b5b9d52e241f6cfe0d5fa3a Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Wed, 23 Feb 2022 15:49:54 +0100
Subject: [PATCH 16/16] Simplified DM route creation

---
 .../tjbot/commands/reminder/RemindRoutine.java             | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

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 01811c32aa..4e0e167b42 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
@@ -89,8 +89,7 @@ private static RestAction computeReminderRoute(@NotNull JDA jda,
 
     private static @NotNull RestAction createDmReminderRoute(@NotNull JDA jda,
             long authorId) {
-        return jda.openPrivateChannelById(authorId)
-            .map(channel -> ReminderRoute.toPrivate(channel, channel.getUser()));
+        return jda.openPrivateChannelById(authorId).map(ReminderRoute::toPrivate);
     }
 
     private static void sendReminderViaRoute(@NotNull RestAction routeAction,
@@ -134,8 +133,8 @@ static ReminderRoute toPublic(@NotNull TextChannel channel, @Nullable User targe
                     target == null ? null : target.getAsMention());
         }
 
-        static ReminderRoute toPrivate(@NotNull PrivateChannel channel, @NotNull User target) {
-            return new ReminderRoute(channel, target,
+        static ReminderRoute toPrivate(@NotNull 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)");
         }