Skip to content

Commit 57f8c30

Browse files
authored
Added wiki-sync workflow, renamed yml to yaml, plus current wiki content (#1299)
1 parent 7396c0e commit 57f8c30

33 files changed

+2231
-0
lines changed

.github/workflows/wiki-sync.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Wiki Sync
2+
3+
on:
4+
push:
5+
branches:
6+
- 'develop'
7+
paths:
8+
- wiki/**
9+
- .github/workflows/wiki-sync.yaml
10+
11+
concurrency:
12+
group: sync-wiki
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: write
17+
18+
jobs:
19+
sync-wiki:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: Andrew-Chen-Wang/[email protected]

wiki/Access-the-VPS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Overview
2+
3+
The bot is hosted on a Virtual Private Server (VPS) by [Hetzner](https://www.hetzner.com/). The machine can be reached under the DDNS `togetherjava.duckdns.org`.
4+
5+
Access to it is usually granted only to members of the [Moderator-Team](https://github.com/orgs/Together-Java/teams/moderators).
6+
7+
# Guide
8+
9+
In order to get access to the machine, the following steps have to be followed:
10+
11+
1. Generate a private-/public-key pair for [SSH](https://en.wikipedia.org/wiki/Secure_Shell). This can be done by executing the following command:
12+
```batch
13+
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/together-java-vps
14+
```
15+
2. Put the key pair into your `.ssh` folder (Windows `C:\Users\YourUserNameHere\.ssh`, Linux `~/.ssh`)
16+
3. Give the **public** key to someone who has access already
17+
3.1. The person has to add your public key to the file `~/.ssh/authorized_keys`
18+
4. Add the following entry to your `.ssh/config` file:
19+
```
20+
Host togetherjava
21+
HostName togetherjava.duckdns.org
22+
IdentityFile ~/.ssh/together-java-vps
23+
User root
24+
Port 22
25+
```
26+
5. Connect to the machine by using the command `ssh togetherjava`, you should get a response similar to:
27+
![](https://i.imgur.com/eCyJVEt.png)
28+
6. Congrats :tada:, you are now logged in. Once you are done, close the connection using `logout`.

wiki/Add-a-new-command.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Overview
2+
3+
This tutorial shows how to add custom commands to the bot.
4+
5+
## Prerequisites
6+
* [[Setup project locally]]
7+
* you can run the bot locally from your IDE and connect it to a server
8+
9+
## What you will learn
10+
* the basic architecture of the code
11+
* how the command system works
12+
* how to add your own custom command
13+
* basics of JDA, used to communicate with Discord
14+
* basics of jOOQ, used to interact with databases
15+
* basics of SLF4J, used for logging
16+
17+
# Tutorial
18+
19+
## Code architecture
20+
21+
Before we get started, we have to familiarize with the general code structure.
22+
23+
![High level flow](https://i.imgur.com/M8381Zm.png)
24+
25+
The entry point of the bot is `Application`, which will first create instances of:
26+
* `Config`, which provides several properties read from a configuration file
27+
* `Database`, a general purpose database used by the bot and its commands
28+
* `JDA`, the main instance of the framework used to communicate with Discord
29+
30+
The `Config` is available to everyone from everywhere, it is a global singleton. You can just write `Config.getInstance()` and then use its properties. The `Database` is available to all commands, also for your custom command. You can read and write any data to it. From within a command, the `JDA` instance will also be available at any time. Almost all JDA objects, such as the events, provide a `getJDA()` method.
31+
32+
Next, the application will setup the command system.
33+
34+
## Command system
35+
36+
The command system is based around the class `CommandSystem`, which is registered as command handler to `JDA`. It receives all command events from JDA and forwards them to the corresponding registered commands.
37+
38+
Custom commands are added to the `Commands` class, where `CommandSystem` will fetch them by using its `createSlashCommands` method, also providing the database instance. This method could for example look like:
39+
```java
40+
public static Collection<SlashCommand> createSlashCommands(Database database) {
41+
return List.of(new PingCommand(), new DatabaseCommand(database));
42+
}
43+
```
44+
As an example, when someone uses the `/ping` command, the event will be send to `CommandSystem` by JDA, which will then forward it to the `PingCommand` class.
45+
46+
![command system](https://i.imgur.com/EJNanvE.png)
47+
48+
Commands have to implement the `SlashCommand` interface. Besides metadata (e.g. a name) and the command setup provided by `getData()`, it mostly demands implementation of the event action handlers:
49+
* `onSlashCommand`
50+
* `onButtonClick`
51+
* `onSelectionMenu`
52+
53+
It is also possible to extend `SlashCommandAdapter` which already implemented all methods besides `onSlashCommand`.
54+
55+
Therefore, a minimal example command, could look like:
56+
```java
57+
public final class PingCommand extends SlashCommandAdapter {
58+
public PingCommand() {
59+
super("ping", "Bot responds with 'Pong!'", SlashCommandVisibility.GUILD);
60+
}
61+
62+
@Override
63+
public void onSlashCommand(@NotNull SlashCommandEvent event) {
64+
event.reply("Pong!").queue();
65+
}
66+
}
67+
```
68+
69+
## Add your own commands
70+
71+
In the following, we will add two custom commands to the application:
72+
* `/days <from> <to>`
73+
* computes the difference in days between the given dates
74+
* e.g. `/days 26.09.2021 03.10.2021` will respond with `7 days`
75+
* `/question ask <id> <question>`, `/question get <id>`
76+
* asks a question and users can click a `Yes` or `No` button
77+
* the choice will be saved in the database from which it can be retrieved using the `get` subcommand
78+
* e.g. `/question ask "noodles" "Do you like noodles?"` and `/question get "noodles"`
79+
80+
Please refer to
81+
* [[Add days command]],
82+
* [[Add question command]] and
83+
* [[Adding context commands]] respectively.

wiki/Add-days-command.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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+
![days command hello world](https://i.imgur.com/BVaIfKw.png)
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+
![days command options dialog](https://i.imgur.com/5yt5EZl.png)
92+
![days command options response](https://i.imgur.com/JNYTVak.png)
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+
![days command invalid input](https://i.imgur.com/nB7siQV.png)
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+
![days command days difference](https://i.imgur.com/x2E4c4s.png)
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

Comments
 (0)