|
| 1 | +# Overview |
| 2 | + |
| 3 | +This tutorial shows how to add a custom command, the `days` command: |
| 4 | +* `/days <from> <to>` |
| 5 | + * computes the difference in days between the given dates |
| 6 | + * e.g. `/days 26.09.2021 03.10.2021` will respond with `7 days` |
| 7 | + |
| 8 | +Please read [[Add a new command]] first. |
| 9 | + |
| 10 | +## What you will learn |
| 11 | +* add a custom command |
| 12 | +* reply to messages |
| 13 | +* add options (arguments) to a command |
| 14 | +* ephemeral messages (only visible to one user) |
| 15 | +* compute the difference in days between two dates |
| 16 | + |
| 17 | +# Tutorial |
| 18 | + |
| 19 | +## Create class |
| 20 | + |
| 21 | +To get started, we have to create a new class, such as `DaysCommand`. A good place for it would be in the `org.togetherjava.tjbot.commands` package. Maybe in a new subpackage or just in the existing `org.togetherjava.tjbot.commands.base` package. |
| 22 | + |
| 23 | +The class has to implement `SlashCommand`, or alternatively just extend `SlashCommandAdapter` which gets most of the work done already. For latter, we have to add a constructor that provides a `name`, a `description` and the command `visibility`. Also, we have to implement the `onSlashCommand` method, which will be called by the system when `/days` was triggered by an user. To get started, we will just respond with `Hello World`. Our first version of this class looks like: |
| 24 | +```java |
| 25 | +package org.togetherjava.tjbot.commands.basic; |
| 26 | + |
| 27 | +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; |
| 28 | +import org.jetbrains.annotations.NotNull; |
| 29 | +import org.togetherjava.tjbot.commands.SlashCommandAdapter; |
| 30 | +import org.togetherjava.tjbot.commands.SlashCommandVisibility; |
| 31 | + |
| 32 | +public final class DaysCommand extends SlashCommandAdapter { |
| 33 | + |
| 34 | + public DaysCommand() { |
| 35 | + super("days", "Computes the difference in days between given dates", SlashCommandVisibility.GUILD); |
| 36 | + } |
| 37 | + |
| 38 | + @Override |
| 39 | + public void onSlashCommand(@NotNull SlashCommandEvent event) { |
| 40 | + event.reply("Hello World!").queue(); |
| 41 | + } |
| 42 | +} |
| 43 | +``` |
| 44 | +## Register command |
| 45 | + |
| 46 | +Next up, we have to register the command in the command system. Therefore, we open the `Commands` class (in package `org.togetherjava.tjbot.commands`) and simply append an instance of our new command to the `createSlashCommands` method. For example: |
| 47 | +```java |
| 48 | +public static @NotNull Collection<SlashCommand> createSlashCommands(@NotNull Database database) { |
| 49 | + return List.of(new PingCommand(), new DatabaseCommand(database), new DaysCommand()); |
| 50 | +} |
| 51 | +``` |
| 52 | +## Try it out |
| 53 | + |
| 54 | +The command is now ready and can already be used. |
| 55 | + |
| 56 | +After starting up the bot, we have to use `/reload` to tell Discord that we changed the slash-commands. To be precise, you have to use `/reload` each time you change the commands signature. That is mostly whenever you add or remove commands, change their names or descriptions or anything related to their `CommandData`. |
| 57 | + |
| 58 | +Now, we can use `/days` and it will respond with `"Hello World!"`. |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +## Add options |
| 63 | + |
| 64 | +The next step is to add the two options to our command, i.e. being able to write something like `/days 26.09.2021 03.10.2021`. The options are both supposed to be **required**. |
| 65 | + |
| 66 | +This has to be configured during the setup of the command, via the `CommandData` returned by `getData()`. We should do this in the constructor of our command. Like so: |
| 67 | +```java |
| 68 | +public DaysCommand() { |
| 69 | + super("days", "Computes the difference in days between given dates", |
| 70 | + SlashCommandVisibility.GUILD); |
| 71 | + |
| 72 | + getData().addOption(OptionType.STRING, "from", |
| 73 | + "the start date, in the format 'dd.MM.yyyy'", true) |
| 74 | + .addOption(OptionType.STRING, "to", |
| 75 | + "the end date, in the format 'dd.MM.yyyy'", true); |
| 76 | +} |
| 77 | +``` |
| 78 | +For starters, let us try to respond back with both entered values instead of just writing `"Hello World!"`. Therefore, in `onSlashCommand`, we retrieve the entered values using `event.getOption(...)`, like so: |
| 79 | +```java |
| 80 | +@Override |
| 81 | +public void onSlashCommand(@NotNull SlashCommandEvent event) { |
| 82 | + String from = event.getOption("from").getAsString(); |
| 83 | + String to = event.getOption("to").getAsString(); |
| 84 | + |
| 85 | + event.reply(from + ", " + to).queue(); |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +If we restart the bot, pop `/reload` again (since we added options to the command), we should now be able to enter two values and the bot will respond back with them: |
| 90 | + |
| 91 | + |
| 92 | + |
| 93 | + |
| 94 | +## Date validation |
| 95 | + |
| 96 | +The bot still allows us to enter any string we want. While it is not possible to restrict the input directly in the dialog box, we can easily refuse any invalid input and respond back with an error message instead. We can also use `setEphemeral(true)` on the `reply`, to make the error message only appear to the user who triggered the command. |
| 97 | + |
| 98 | +All in all, the code for the method now looks like: |
| 99 | +```java |
| 100 | +String from = event.getOption("from").getAsString(); |
| 101 | +String to = event.getOption("to").getAsString(); |
| 102 | + |
| 103 | +DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); |
| 104 | +try { |
| 105 | + LocalDate fromDate = LocalDate.parse(from, formatter); |
| 106 | + LocalDate toDate = LocalDate.parse(to, formatter); |
| 107 | + |
| 108 | + event.reply(from + ", " + to).queue(); |
| 109 | +} catch (DateTimeParseException e) { |
| 110 | + event.reply("The dates must be in the format 'dd.MM.yyyy', try again.") |
| 111 | + .setEphemeral(true) |
| 112 | + .queue(); |
| 113 | +} |
| 114 | +``` |
| 115 | +For trying it out, we do not have to use `/reload` again, since we only changed our logic but not the command structure itself. |
| 116 | + |
| 117 | + |
| 118 | + |
| 119 | +## Compute days |
| 120 | + |
| 121 | +Now that we have two valid dates, we only have to compute the difference in days and respond back with the result. Luckily, the `java.time` API got us covered, we can simply use `ChronoUnit.DAYS.between(fromDate, toDate)`: |
| 122 | +```java |
| 123 | +long days = ChronoUnit.DAYS.between(fromDate, toDate); |
| 124 | +event.reply(days + " days").queue(); |
| 125 | +``` |
| 126 | + |
| 127 | + |
| 128 | + |
| 129 | +## Full code |
| 130 | + |
| 131 | +After some cleanup and minor code improvements, the full code for `DaysCommand` is: |
| 132 | +```java |
| 133 | +package org.togetherjava.tjbot.commands.basic; |
| 134 | + |
| 135 | +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; |
| 136 | +import net.dv8tion.jda.api.interactions.commands.OptionType; |
| 137 | +import org.jetbrains.annotations.NotNull; |
| 138 | +import org.togetherjava.tjbot.commands.SlashCommandAdapter; |
| 139 | +import org.togetherjava.tjbot.commands.SlashCommandVisibility; |
| 140 | + |
| 141 | +import java.time.LocalDate; |
| 142 | +import java.time.format.DateTimeFormatter; |
| 143 | +import java.time.format.DateTimeParseException; |
| 144 | +import java.time.temporal.ChronoUnit; |
| 145 | +import java.util.Objects; |
| 146 | + |
| 147 | +/** |
| 148 | + * This creates a command called {@code /days}, which can calculate the difference between two given |
| 149 | + * dates in days. |
| 150 | + * <p> |
| 151 | + * For example: |
| 152 | + * |
| 153 | + * <pre> |
| 154 | + * {@code |
| 155 | + * /days from: 26.09.2021 to: 03.10.2021 |
| 156 | + * // TJ-Bot: The difference between 26.09.2021 and 03.10.2021 are 7 days |
| 157 | + * } |
| 158 | + * </pre> |
| 159 | + */ |
| 160 | +public final class DaysCommand extends SlashCommandAdapter { |
| 161 | + private static final String FROM_OPTION = "from"; |
| 162 | + private static final String TO_OPTION = "to"; |
| 163 | + private static final String FORMAT = "dd.MM.yyyy"; |
| 164 | + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(FORMAT); |
| 165 | + |
| 166 | + /** |
| 167 | + * Creates an instance of the command. |
| 168 | + */ |
| 169 | + public DaysCommand() { |
| 170 | + super("days", "Computes the difference in days between given dates", |
| 171 | + SlashCommandVisibility.GUILD); |
| 172 | + |
| 173 | + getData() |
| 174 | + .addOption(OptionType.STRING, FROM_OPTION, "the start date, in the format '" |
| 175 | + + FORMAT + "'", true) |
| 176 | + .addOption(OptionType.STRING, TO_OPTION, "the end date, in the format '" |
| 177 | + + FORMAT + "'", true); |
| 178 | + } |
| 179 | + |
| 180 | + @Override |
| 181 | + public void onSlashCommand(@NotNull SlashCommandEvent event) { |
| 182 | + String from = Objects.requireNonNull(event.getOption(FROM_OPTION)).getAsString(); |
| 183 | + String to = Objects.requireNonNull(event.getOption(TO_OPTION)).getAsString(); |
| 184 | + |
| 185 | + LocalDate fromDate; |
| 186 | + LocalDate toDate; |
| 187 | + try { |
| 188 | + fromDate = LocalDate.parse(from, FORMATTER); |
| 189 | + toDate = LocalDate.parse(to, FORMATTER); |
| 190 | + } catch (DateTimeParseException e) { |
| 191 | + event.reply("The dates must be in the format '" + FORMAT + "', try again.") |
| 192 | + .setEphemeral(true) |
| 193 | + .queue(); |
| 194 | + return; |
| 195 | + } |
| 196 | + |
| 197 | + long days = ChronoUnit.DAYS.between(fromDate, toDate); |
| 198 | + event.reply("The difference between %s and %s are %d days".formatted(from, to, days)) |
| 199 | + .queue(); |
| 200 | + } |
| 201 | +} |
| 202 | +``` |
0 commit comments