diff --git a/.github/workflows/basic-checks.yml b/.github/workflows/basic-checks.yaml similarity index 100% rename from .github/workflows/basic-checks.yml rename to .github/workflows/basic-checks.yaml diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yaml similarity index 100% rename from .github/workflows/code-analysis.yml rename to .github/workflows/code-analysis.yaml diff --git a/.github/workflows/discord-member.yml b/.github/workflows/discord-member.yaml similarity index 100% rename from .github/workflows/discord-member.yml rename to .github/workflows/discord-member.yaml diff --git a/.github/workflows/wiki-sync.yaml b/.github/workflows/wiki-sync.yaml new file mode 100644 index 0000000000..145d8cf861 --- /dev/null +++ b/.github/workflows/wiki-sync.yaml @@ -0,0 +1,23 @@ +name: Wiki Sync + +on: + push: + branches: + - 'develop' + paths: + - wiki/** + - .github/workflows/wiki-sync.yaml + +concurrency: + group: sync-wiki + cancel-in-progress: true + +permissions: + contents: write + +jobs: + sync-wiki: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: Andrew-Chen-Wang/github-wiki-action@v5.0.1 diff --git a/wiki/Access-the-VPS.md b/wiki/Access-the-VPS.md new file mode 100644 index 0000000000..1e0d96965f --- /dev/null +++ b/wiki/Access-the-VPS.md @@ -0,0 +1,28 @@ +# Overview + +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`. + +Access to it is usually granted only to members of the [Moderator-Team](https://github.com/orgs/Together-Java/teams/moderators). + +# Guide + +In order to get access to the machine, the following steps have to be followed: + +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: +```batch +ssh-keygen -t ed25519 -C "your_email@address.here" -f ~/.ssh/together-java-vps +``` +2. Put the key pair into your `.ssh` folder (Windows `C:\Users\YourUserNameHere\.ssh`, Linux `~/.ssh`) +3. Give the **public** key to someone who has access already + 3.1. The person has to add your public key to the file `~/.ssh/authorized_keys` +4. Add the following entry to your `.ssh/config` file: +``` +Host togetherjava +HostName togetherjava.duckdns.org +IdentityFile ~/.ssh/together-java-vps +User root +Port 22 +``` +5. Connect to the machine by using the command `ssh togetherjava`, you should get a response similar to: +![](https://i.imgur.com/eCyJVEt.png) +6. Congrats :tada:, you are now logged in. Once you are done, close the connection using `logout`. \ No newline at end of file diff --git a/wiki/Add-a-new-command.md b/wiki/Add-a-new-command.md new file mode 100644 index 0000000000..890b226c28 --- /dev/null +++ b/wiki/Add-a-new-command.md @@ -0,0 +1,83 @@ +# Overview + +This tutorial shows how to add custom commands to the bot. + +## Prerequisites +* [[Setup project locally]] + * you can run the bot locally from your IDE and connect it to a server + +## What you will learn +* the basic architecture of the code +* how the command system works +* how to add your own custom command +* basics of JDA, used to communicate with Discord +* basics of jOOQ, used to interact with databases +* basics of SLF4J, used for logging + +# Tutorial + +## Code architecture + +Before we get started, we have to familiarize with the general code structure. + +![High level flow](https://i.imgur.com/M8381Zm.png) + +The entry point of the bot is `Application`, which will first create instances of: +* `Config`, which provides several properties read from a configuration file +* `Database`, a general purpose database used by the bot and its commands +* `JDA`, the main instance of the framework used to communicate with Discord + +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. + +Next, the application will setup the command system. + +## Command system + +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. + +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: +```java +public static Collection createSlashCommands(Database database) { + return List.of(new PingCommand(), new DatabaseCommand(database)); +} +``` +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. + +![command system](https://i.imgur.com/EJNanvE.png) + +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: +* `onSlashCommand` +* `onButtonClick` +* `onSelectionMenu` + +It is also possible to extend `SlashCommandAdapter` which already implemented all methods besides `onSlashCommand`. + +Therefore, a minimal example command, could look like: +```java +public final class PingCommand extends SlashCommandAdapter { + public PingCommand() { + super("ping", "Bot responds with 'Pong!'", SlashCommandVisibility.GUILD); + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + event.reply("Pong!").queue(); + } +} +``` + +## Add your own commands + +In the following, we will add two custom commands to the application: +* `/days ` + * computes the difference in days between the given dates + * e.g. `/days 26.09.2021 03.10.2021` will respond with `7 days` +* `/question ask `, `/question get ` + * asks a question and users can click a `Yes` or `No` button + * the choice will be saved in the database from which it can be retrieved using the `get` subcommand + * e.g. `/question ask "noodles" "Do you like noodles?"` and `/question get "noodles"` + +Please refer to +* [[Add days command]], +* [[Add question command]] and +* [[Adding context commands]] respectively. \ No newline at end of file diff --git a/wiki/Add-days-command.md b/wiki/Add-days-command.md new file mode 100644 index 0000000000..e2f06a5c9b --- /dev/null +++ b/wiki/Add-days-command.md @@ -0,0 +1,202 @@ +# Overview + +This tutorial shows how to add a custom command, the `days` command: +* `/days ` + * computes the difference in days between the given dates + * e.g. `/days 26.09.2021 03.10.2021` will respond with `7 days` + +Please read [[Add a new command]] first. + +## What you will learn +* add a custom command +* reply to messages +* add options (arguments) to a command +* ephemeral messages (only visible to one user) +* compute the difference in days between two dates + +# Tutorial + +## Create class + +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. + +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: +```java +package org.togetherjava.tjbot.commands.basic; + +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; + +public final class DaysCommand extends SlashCommandAdapter { + + public DaysCommand() { + super("days", "Computes the difference in days between given dates", SlashCommandVisibility.GUILD); + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + event.reply("Hello World!").queue(); + } +} +``` +## Register command + +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: +```java +public static @NotNull Collection createSlashCommands(@NotNull Database database) { + return List.of(new PingCommand(), new DatabaseCommand(database), new DaysCommand()); +} +``` +## Try it out + +The command is now ready and can already be used. + +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`. + +Now, we can use `/days` and it will respond with `"Hello World!"`. + +![days command hello world](https://i.imgur.com/BVaIfKw.png) + +## Add options + +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**. + +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: +```java +public DaysCommand() { + super("days", "Computes the difference in days between given dates", + SlashCommandVisibility.GUILD); + + getData().addOption(OptionType.STRING, "from", + "the start date, in the format 'dd.MM.yyyy'", true) + .addOption(OptionType.STRING, "to", + "the end date, in the format 'dd.MM.yyyy'", true); +} +``` +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: +```java +@Override +public void onSlashCommand(@NotNull SlashCommandEvent event) { + String from = event.getOption("from").getAsString(); + String to = event.getOption("to").getAsString(); + + event.reply(from + ", " + to).queue(); +} +``` + +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: + +![days command options dialog](https://i.imgur.com/5yt5EZl.png) +![days command options response](https://i.imgur.com/JNYTVak.png) + +## Date validation + +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. + +All in all, the code for the method now looks like: +```java +String from = event.getOption("from").getAsString(); +String to = event.getOption("to").getAsString(); + +DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); +try { + LocalDate fromDate = LocalDate.parse(from, formatter); + LocalDate toDate = LocalDate.parse(to, formatter); + + event.reply(from + ", " + to).queue(); +} catch (DateTimeParseException e) { + event.reply("The dates must be in the format 'dd.MM.yyyy', try again.") + .setEphemeral(true) + .queue(); +} +``` +For trying it out, we do not have to use `/reload` again, since we only changed our logic but not the command structure itself. + +![days command invalid input](https://i.imgur.com/nB7siQV.png) + +## Compute days + +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)`: +```java +long days = ChronoUnit.DAYS.between(fromDate, toDate); +event.reply(days + " days").queue(); +``` + +![days command days difference](https://i.imgur.com/x2E4c4s.png) + +## Full code + +After some cleanup and minor code improvements, the full code for `DaysCommand` is: +```java +package org.togetherjava.tjbot.commands.basic; + +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 java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + +/** + * This creates a command called {@code /days}, which can calculate the difference between two given + * dates in days. + *

+ * For example: + * + *

+ * {@code
+ * /days from: 26.09.2021 to: 03.10.2021
+ * // TJ-Bot: The difference between 26.09.2021 and 03.10.2021 are 7 days
+ * }
+ * 
+ */ +public final class DaysCommand extends SlashCommandAdapter { + private static final String FROM_OPTION = "from"; + private static final String TO_OPTION = "to"; + private static final String FORMAT = "dd.MM.yyyy"; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(FORMAT); + + /** + * Creates an instance of the command. + */ + public DaysCommand() { + super("days", "Computes the difference in days between given dates", + SlashCommandVisibility.GUILD); + + getData() + .addOption(OptionType.STRING, FROM_OPTION, "the start date, in the format '" + + FORMAT + "'", true) + .addOption(OptionType.STRING, TO_OPTION, "the end date, in the format '" + + FORMAT + "'", true); + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + String from = Objects.requireNonNull(event.getOption(FROM_OPTION)).getAsString(); + String to = Objects.requireNonNull(event.getOption(TO_OPTION)).getAsString(); + + LocalDate fromDate; + LocalDate toDate; + try { + fromDate = LocalDate.parse(from, FORMATTER); + toDate = LocalDate.parse(to, FORMATTER); + } catch (DateTimeParseException e) { + event.reply("The dates must be in the format '" + FORMAT + "', try again.") + .setEphemeral(true) + .queue(); + return; + } + + long days = ChronoUnit.DAYS.between(fromDate, toDate); + event.reply("The difference between %s and %s are %d days".formatted(from, to, days)) + .queue(); + } +} +``` \ No newline at end of file diff --git a/wiki/Add-question-command.md b/wiki/Add-question-command.md new file mode 100644 index 0000000000..8f675d8c10 --- /dev/null +++ b/wiki/Add-question-command.md @@ -0,0 +1,458 @@ +# Overview + +This tutorial shows how to add a custom command, the `question` command: +* `/question ask `, `/question get ` + * asks a question and users can click a `Yes` or `No` button + * the choice will be saved in the database from which it can be retrieved using the `get` subcommand + * e.g. `/question ask "noodles" "Do you like noodles?"` and `/question get "noodles"` + +Please read [[Add a new command]] and [[Add days command]] first. + +## What you will learn +* add a custom command +* reply to messages +* add sub-commands to a command +* add options (arguments) to a sub-command +* ephemeral messages (only visible to one user) +* add buttons to a message +* memorize data inside a button +* react to button click +* disable buttons from an old message +* create a new database table and migrate it +* read and write from/to a database (using Flyway, jOOQ and SQLite) +* basic logging (using SLF4J) + +# Tutorial + +## Setup + +The next command focuses on how to use sub-commands and a database. We start with the same base setup as before, but this time we need a `Database` argument: +```java +public final class QuestionCommand extends SlashCommandAdapter { + private final Database database; + + public QuestionCommand(Database database) { + super("question", "Asks users questions, responses are saved and can be retrieved back", + SlashCommandVisibility.GUILD); + this.database = database; + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + event.reply("Hello World!").queue(); + } +} +``` +Also, we again have to register our new command by adding it to the list of commands in the `Commands` class, but this time providing the database instance: +```java +public static @NotNull Collection createSlashCommands(@NotNull Database database) { + return List.of(new PingCommand(), new DatabaseCommand(database), new QuestionCommand(database)); +} +``` + +## Add sub-commands + +As first step, we have to add the two sub-commands `ask` and `get` to the command. +* `ask` expects two options, `id` and `question`, +* while `get` only expects one option, `id`. + +We can configure both, the sub-commands and their options, again via the `CommandData` object returned by `getData()`, which has to be done during construction of the command: +```java +public QuestionCommand(Database database) { + super("question", "Asks users questions, responses are saved and can be retrieved back", + SlashCommandVisibility.GUILD); + this.database = database; + + getData().addSubcommands( + new SubcommandData("ask", "Asks the users a question, responses will be saved") + .addOption(OptionType.STRING, "id", "Unique ID under which the question should be saved", true) + .addOption(OptionType.STRING, "question", "Question to ask", true), + new SubcommandData("get", "Gets the response to the given question") + .addOption(OptionType.STRING, "id", "Unique ID of the question to retrieve", true)); +} +``` +We can retrieve back the used sub-command using `event.getSubcommandName()`, and the corresponding option values using `event.getOption(...)`. To simplify handling the command, we split them into two helper methods and `switch` on the command name: +```java +@Override +public void onSlashCommand(@NotNull SlashCommandEvent event) { + switch (Objects.requireNonNull(event.getSubcommandName())) { + case "ask" -> handleAskCommand(event); + case "get" -> handleGetCommand(event); + default -> throw new AssertionError(); + } +} + +private void handleAskCommand(@NotNull SlashCommandEvent event) { + String id = event.getOption("id").getAsString(); + String question = event.getOption("question").getAsString(); + + event.reply("Ask command: " + id + ", " + question).queue(); +} + +private void handleGetCommand(@NotNull SlashCommandEvent event) { + String id = event.getOption("id").getAsString(); + + event.reply("Get command: " + id).queue(); +} +``` + +## Try it out + +At this point, we should try out the code. Do not forget to use `/reload` before though. You should now be able to use `/question ask` with two required options and `/question get` with only one required option. And the bot should respond back correspondly. + +![question sub commands](https://i.imgur.com/3MI6ITN.png) + +## `Ask` sub-command + +### Add buttons + +Instead of just writing down a question, we also want to give the user the opportunity to respond by clicking one of two buttons. This can be done by using `.addActionRow(...)` on our `reply` and then making use of `Button.of(...)`. + +Note that a button needs a so called **component ID**. The rules for this id are quite complex and can be read about in the documentation of `SlashCommand#onSlashCommand`. Fortunately, there is a helper that can generate component IDs easily. Since we extended `SlashCommandAdapter`, it is already directly available as `generateComponentId()` (alternatively, use the helper class `ComponentIds`). + +Additionally, we have to remember the question ID during the dialog, since we still need to be able to save the response under the question ID in the database. The button component ID can be used for such a situation, we can just call the generator method with arguments, like `generateComponentId(id)`, and will be able to retrieve them back later on. + +The full code for the `handleAskCommand` method is now: +```java +private void handleAskCommand(@NotNull SlashCommandEvent event) { +String id = event.getOption("id").getAsString(); +String question = event.getOption("question").getAsString(); + +event.reply(question) + .addActionRow( + Button.of(ButtonStyle.SUCCESS, generateComponentId(id), "Yes"), + Button.of(ButtonStyle.DANGER, generateComponentId(id), "No")) + .queue(); +} +``` +When trying it out, we can now see the question and two buttons to respond: + +![question add buttons](https://i.imgur.com/KGr9hl6.png) + +However, clicking the buttons still does not trigger anything yet. + +### React to button click + +In order to react to a button click, we have to give an implementation for the `onButtonClick(...)` method, which `SlashCommandAdapter` already implemented, but without any action. The method provides us with the `ButtonClickEvent` and also with a `List` of arguments, which are the optional arguments added to the **component id** earlier. In our case, we added the question id, so we can also retrieve it back now by using `args.get(0)`. Also, we can figure out which button was clicked by using `event.getButton().getStyle()`. + +A minimal setup could now look like: +```java +@Override +public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { + String id = args.get(0); + ButtonStyle buttonStyle = event.getButton().getStyle(); + + boolean clickedYes = switch (buttonStyle) { + case DANGER -> false; + case SUCCESS -> true; + default -> throw new AssertionError("Unexpected button action clicked: " + buttonStyle); + }; + + event.reply("id: " + id + ", clickedYes: " + clickedYes).queue(); +} +``` +Clicking the buttons now works: + +![question react to buttons](https://i.imgur.com/39SjSI2.png) + +### Disable buttons after click + +Right now, the buttons can be clicked as often as wanted and the bot will always be triggered again. To get rid of this, we simply have to disable the buttons after someone clicked. + +We can do so by using `event.getMessage().editMessageComponents(...)` and then providing a new list of components, i.e. the previous buttons but with `button.asDisabled()`. We can get hands on the previous buttons by using `event.getMessage().getButtons()`. + +Long story short, we can simply add: +```java +event.getMessage() + .editMessageComponents(ActionRow + .of(event.getMessage().getButtons().stream().map(Button::asDisabled).toList())) + .queue(); +``` +and the buttons will be disabled after someone clicks them: + +![question ask disable button](https://i.imgur.com/Wf8NQ7U.png) + +### Setup database + +Last but not least for the `ask` command, we have to save the response in the database. Before we can get started with this, we have to create a database table and let Flyway generate the corresponding database code. + +Therefore, we go to the folder `TJ-Bot\application\src\main\resources\db` and add a new database migration script, incrementing the version. For example, if the script with the highest version number is `V1`, we will add `V2` to it. Give the script a nice name, such as `V2__Add_Questions_Table.sql`. The content is simply an SQL statement to create your desired table: +```sql +CREATE TABLE questions +( + id TEXT NOT NULL PRIMARY KEY, + response INTEGER NOT NULL +) +``` +After adding this file, if you build or run the code (or simply execute `gradle database:build`), you will be able to use the database table. + +## Write database + +Thanks to the jOOQ framework, writing to the database is now fairly simple. You can just use `database.write(...)` and make usages of the generated classes revolving around the `questions` table: +```java +try { + database.write(context -> { + QuestionsRecord questionsRecord = context.newRecord(Questions.QUESTIONS) + .setId(id) + .setResponse(clickedYes ? 1 : 0); + if (questionsRecord.update() == 0) { + questionsRecord.insert(); + } + }); + + event.reply("Saved response under '" + id + "'.").queue(); +} catch (DatabaseException e) { + event.reply("Sorry, something went wrong.").queue(); +} +``` +Trying it out, and we get the expected response: + +![question ask write db](https://i.imgur.com/RP4rMGA.png) + +### Add logging + +At this point, we should add logging to the code to simplify debugging. Therefore, just add +```java +private static final Logger logger = LoggerFactory.getLogger(QuestionCommand.class); +``` +to the top, as a new field for our class. + +Now, you can use the logger wherever you want, for example to log a possible error message during writing the database: +```java +} catch (DatabaseException e) { + logger.error("Failed to save response for '{}'", id, e); + event.reply("Sorry, something went wrong.").queue(); +} +``` + +The ask sub-command should now be working correctly. + +## `Get` sub-command + +This command is simpler as we do not have any dialog. We simply have to lookup the database and respond with the result, if found. + +### Read database + +Reading the database revolves around the `database.read(...)` methods and using the generated classes for the `questions` table: +```java +OptionalInt response = database.read(context -> { + return Optional.ofNullable(context.selectFrom(Questions.QUESTIONS) + .where(Questions.QUESTIONS.ID.eq(id)).fetchOne() + ).map(QuestionsRecord::getResponse) + .map(OptionalInt::of) + .orElseGet(OptionalInt::empty); + } +}); +``` + +### Reply + +The last part will be to reply with the saved response: + +```java +if (response.isEmpty()) { + event.reply("There is no response saved for the id '" + id + "'.") + .setEphemeral(true) + .queue(); + return; +} + +boolean clickedYes = response.getAsInt() != 0; +event.reply("The response for '" + id + "' is: " + (clickedYes ? "Yes" : "No")).queue(); +``` + +The full code for the `handleGetCommand` method is now: +```java +String id = event.getOption("id").getAsString(); + +try { + OptionalInt response = database.read(context -> { + return Optional.ofNullable(context.selectFrom(Questions.QUESTIONS) + .where(Questions.QUESTIONS.ID.eq(id)).fetchOne() + ).map(QuestionsRecord::getResponse) + .map(OptionalInt::of) + .orElseGet(OptionalInt::empty); + } + }); + if (response.isEmpty()) { + event.reply("There is no response saved for the id '" + id + "'.") + .setEphemeral(true) + .queue(); + return; + } + + boolean clickedYes = response.getAsInt() != 0; + event.reply("The response for '" + id + "' is: " + (clickedYes ? "Yes" : "No")).queue(); +} catch (DatabaseException e) { + logger.error("Failed to get response for '{}'", id, e); + event.reply("Sorry, something went wrong.").setEphemeral(true).queue(); +} +``` +and if we try it out, we see that the command works: + +![question get](https://i.imgur.com/lQK3eGF.png) + +## Full code + +After some cleanup and minor code improvements, the full code for `QuestionCommand` is: + +```java +package org.togetherjava.tjbot.commands.basic; + +import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.CommandInteraction; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.DatabaseException; +import org.togetherjava.tjbot.db.generated.tables.Questions; +import org.togetherjava.tjbot.db.generated.tables.records.QuestionsRecord; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * This creates a command called {@code /question}, which can ask users questions, save their + * responses and retrieve back responses. + *

+ * For example: + * + *

+ * {@code
+ * /question ask id: noodles question: Do you like noodles?
+ * // User clicks on a 'Yes' button
+ *
+ * /question get id: noodles
+ * // TJ-Bot: The response for 'noodles' is: Yes
+ * }
+ * 
+ */ +public final class QuestionCommand extends SlashCommandAdapter { + private static final Logger logger = LoggerFactory.getLogger(QuestionCommand.class); + private static final String ASK_SUBCOMMAND = "ask"; + private static final String GET_SUBCOMMAND = "get"; + private static final String ID_OPTION = "id"; + private static final String QUESTION_OPTION = "question"; + private static final String NAME = "question"; + private final Database database; + + /** + * Creates a new question command, using the given database. + * + * @param database the database to store the responses in + */ + public QuestionCommand(Database database) { + super(NAME, "Asks users questions, responses are saved and can be retrieved back", + SlashCommandVisibility.GUILD); + this.database = database; + + getData().addSubcommands( + new SubcommandData(ASK_SUBCOMMAND, + "Asks the users a question, responses will be saved") + .addOption(OptionType.STRING, ID_OPTION, + "Unique ID under which the question should be saved", true) + .addOption(OptionType.STRING, QUESTION_OPTION, "Question to ask", true), + new SubcommandData(GET_SUBCOMMAND, "Gets the response to the given question") + .addOption(OptionType.STRING, ID_OPTION, + "Unique ID of the question to retrieve", true)); + } + + private static int clickedYesToInt(boolean clickedYes) { + return clickedYes ? 1 : 0; + } + + private static boolean isClickedYesFromInt(int clickedYes) { + return clickedYes != 0; + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + switch (Objects.requireNonNull(event.getSubcommandName())) { + case ASK_SUBCOMMAND -> handleAskCommand(event); + case GET_SUBCOMMAND -> handleGetCommand(event); + default -> throw new AssertionError(); + } + } + + private void handleAskCommand(@NotNull CommandInteraction event) { + String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); + String question = Objects.requireNonNull(event.getOption(QUESTION_OPTION)).getAsString(); + + event.reply(question) + .addActionRow(Button.of(ButtonStyle.SUCCESS, generateComponentId(id), "Yes"), + Button.of(ButtonStyle.DANGER, generateComponentId(id), "No")) + .queue(); + } + + @Override + public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { + String id = args.get(0); + ButtonStyle buttonStyle = Objects.requireNonNull(event.getButton()).getStyle(); + + boolean clickedYes = switch (buttonStyle) { + case DANGER -> false; + case SUCCESS -> true; + default -> throw new AssertionError("Unexpected button action clicked: " + buttonStyle); + }; + + event.getMessage() + .editMessageComponents(ActionRow + .of(event.getMessage().getButtons().stream().map(Button::asDisabled).toList())) + .queue(); + + try { + database.write(context -> { + QuestionsRecord questionsRecord = context.newRecord(Questions.QUESTIONS) + .setId(id) + .setResponse(clickedYesToInt(clickedYes)); + if (questionsRecord.update() == 0) { + questionsRecord.insert(); + } + }); + + event.reply("Saved response under '" + id + "'.").queue(); + } catch (DatabaseException e) { + logger.error("Failed to save response for '{}'", id, e); + event.reply("Sorry, something went wrong.").queue(); + } + } + + private void handleGetCommand(@NotNull CommandInteraction event) { + String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); + + try { + OptionalInt response = database.read(context -> { + return Optional.ofNullable(context.selectFrom(Questions.QUESTIONS) + .where(Questions.QUESTIONS.ID.eq(id)).fetchOne() + ).map(QuestionsRecord::getResponse) + .map(OptionalInt::of) + .orElseGet(OptionalInt::empty); + } + }); + if (response.isEmpty()) { + event.reply("There is no response saved for the id '" + id + "'.") + .setEphemeral(true) + .queue(); + return; + } + + boolean clickedYes = isClickedYesFromInt(response.getAsInt()); + event.reply("The response for '" + id + "' is: " + (clickedYes ? "Yes" : "No")).queue(); + } catch (DatabaseException e) { + logger.error("Failed to get response for '{}'", id, e); + event.reply("Sorry, something went wrong.").setEphemeral(true).queue(); + } + } +} +``` \ No newline at end of file diff --git a/wiki/Adding-context-commands.md b/wiki/Adding-context-commands.md new file mode 100644 index 0000000000..fff2d0696d --- /dev/null +++ b/wiki/Adding-context-commands.md @@ -0,0 +1,70 @@ +# Overview + +This tutorial shows how to add custom **context command** to the bot. That is, a command that can be selected when **right clicking** an user or message. + +Please read [[Add a new command]] first. + +## What you will learn +* add a custom user context command +* add a custom message context command + +# Tutorial + +## User-context command + +To create a command that can be selected when right clicking a user, we have to implement the `UserContextCommand` interface. The class `BotCommandAdapter` simplifies this process heavily. + +We will create a very simple command that just greets an user: + +![command selection](https://i.imgur.com/IzFvYva.png) +![greet user](https://i.imgur.com/Wo2QzVC.png) + +The code is really simple: +```java +public final class HelloUserCommand extends BotCommandAdapter implements UserContextCommand { + + public HelloUserCommand() { + super(Commands.user("say-hello"), CommandVisibility.GUILD); + } + + @Override + public void onUserContext(UserContextInteractionEvent event) { + event.reply("Hello " + event.getTargetMember().getAsMention()).queue(); + } +} +``` +Finally, we have to add an instance of the class to the system. We do so in the file `Features.java`: + +```java +features.add(new HelloUserCommand()); +``` + +## Message-context command + +To create a command that can be selected when right clicking a message, we have to implement the `MessageContextCommand` interface. `BotCommandAdapter` helps us out here again. + +We will create a very simple command that just repeats the given message: + +![command selection](https://i.imgur.com/Lm5gbqZ.png) +![repeat message](https://i.imgur.com/o4NNcP0.png) + +The code is very similar: +```java +public final class RepeatMessageCommand extends BotCommandAdapter implements MessageContextCommand { + + public RepeatMessageCommand() { + super(Commands.message("repeat"), CommandVisibility.GUILD); + } + + @Override + public void onMessageContext(MessageContextInteractionEvent event) { + String content = event.getTarget().getContentRaw(); + event.reply(content).queue(); + } +} +``` +And we add it to `Features.java` as well: + +```java +features.add(new RepeatMessageCommand()); +``` \ No newline at end of file diff --git a/wiki/Change-log-level.md b/wiki/Change-log-level.md new file mode 100644 index 0000000000..e0093e7781 --- /dev/null +++ b/wiki/Change-log-level.md @@ -0,0 +1,9 @@ +## Overview + +The log level can be changed conveniently from within Discord by using the slash command `/set-log-level`: + +![dropdown](https://i.imgur.com/hAy6LAk.png) + +![changed log level](https://i.imgur.com/x3S6V1m.png) + +Only mods can use this command though. The change is not persisted and will reset on the next restart of the bot. \ No newline at end of file diff --git a/wiki/Code-Guidelines.md b/wiki/Code-Guidelines.md new file mode 100644 index 0000000000..ca7ff2499f --- /dev/null +++ b/wiki/Code-Guidelines.md @@ -0,0 +1,39 @@ +# Overview + +We want the project to be easy to understand and maintain, for anyone, including newcomers. Because of that, we enforce a strict code style based on popular and commonly used configurations. Additionally, we require the code to pass certain checks that analyze it with respect to maintainability, readability, security and more. + +All pull requests have to pass those automated checks before they can be merged to the project. + +Here is a quick glimpse at how our code usually looks like: + +![](https://i.imgur.com/yp6fycB.png) + +## Code style (Spotless) + +The style and layout of the code is checked by **Spotless**. Its configuration is based on the commonly used [Google Java Style](https://google.github.io/styleguide/javaguide.html). The exact configuration being used can be found in the project at +``` +TJ-Bot\meta\formatting\google-style-eclipse.xml +``` + +In order to check your code locally, you can either run **Spotless** or import the style into the formatter of the IDE of your choice. We tested the configuration with: +* IntelliJ +* Eclipse +* Visual Studio Code + +### Run Spotless + +Executing Spotless manually can be done via the Gradle task `spotlessApply`, which will automatically reformat your code according to the style. + +Additionally, Spotless is configured to be executed automatically whenever you compile your code with Gradle, i.e. it is tied to the `compileJava` task. + +## Static code analysis (SonarCloud) + +In order to ensure that code is clean, readable and maintainable, we use static code analysis provided by **SonarCloud**. + +In order to check your code locally, you can either run **SonarCloud** via a Gradle task or install a plugin for your favorite IDE, e.g. the [SonarLint](https://plugins.jetbrains.com/plugin/7973-sonarlint) plugin for IntelliJ. + +### Run SonarCloud + +Executing SonarCloud manually can be done via the Gradle task `sonarqube`, which will check the whole code and explain any issues it found in detail. + +Additionally, SonarCloud is configured to be executed automatically whenever you build your code with Gradle, i.e. it is tied to the `build` task. \ No newline at end of file diff --git a/wiki/Code-in-the-cloud-(codespaces).md b/wiki/Code-in-the-cloud-(codespaces).md new file mode 100644 index 0000000000..e29e468197 --- /dev/null +++ b/wiki/Code-in-the-cloud-(codespaces).md @@ -0,0 +1,80 @@ +# Overview + +This tutorial shows how to code and run the project in GitHubs cloud. + +The service is completely free and allows you to get started in just a few seconds. + +This approach is an alternative to setting the project up on your local machine, as explained in: +* [[Setup project locally]] + +# Tutorial + +## Create codespace + +1. Visit the [landing page](https://github.com/Together-Java/TJ-Bot) +2. Click on `Code > Create codespace on develop` + +![create codespace](https://i.imgur.com/Jg5jiXu.png) + +GitHub now automatically sets up your codespace. The codespace is essentially a virtual machine with everything installed and configured that you need to work with this project. + +Mostly, it will install Java and Gradle for you, and a few other useful plugins and extensions. + +![setup](https://i.imgur.com/8zrXOTc.png) + +This process takes about 2 minutes for the first time. Once the setup is done, it opens an instance of Visual Studio Code in your browser, with the project opened. You can now get started! + +![VSC opened](https://i.imgur.com/Zb6trQb.png) + +## Config + +Before you can run the bot, you have to adjust the configuration file. Therefore, open the file `application/config.json`. + +By default, it will be filled with some example values and most of them are totally okay for now. + +The most important setting you have to change is the bot token. This enables the code to connect to your private bot with which you can then interact from your private server. + +You can find the token at the [Discord Developer Portal](https://discord.com/developers/applications). + +See the following guide if you still have to create a server and a bot first: +* [[Create Discord server and bot]] + +![Discord Developer Portal - Bot Token](https://i.imgur.com/IB5W8vZ.png) + +Replace `` with your bot token; you can also adjust the other settings if you want. + +![replace token](https://i.imgur.com/eCWZHSR.png) + +## Run + +Once done, you are good to go and can run the bot. Just enter `gradle application:run` in your terminal. + +![gradle run](https://i.imgur.com/hQqq6DC.png) + +On the first run, this might take around 3 minutes, because it will first have to download all dependencies, generate the database and compile the code. + +Once the terminal reads `[main] INFO org.togetherjava.tjbot.Application - Bot is ready`, you are done! + +## Have fun + +The bot is now running and connected to your server, hurray πŸŽ‰ + +You can now execute commands and see the bot do its magic: + +![pong](https://i.imgur.com/0x3GsnU.png) + +# IntelliJ instead of VSC + +If you prefer IntelliJ, and have a license, they offer a client called [JetBrains Gateway](https://www.jetbrains.com/remote-development/gateway/). + +While not being all-within your browser, it is essentially an IDE that you can install, which will just remote-connect to your codespace. + +Once installed, you have to get the **GitHub Codespaces** plugin: + +![plugin](https://i.imgur.com/VKzLMd9.png) + +You can then login to your GitHub account and select your codespace: + +![select codespace](https://i.imgur.com/u9OVwXR.png) + +The initial setup takes a few minutes, since it has to install IntelliJ on the codespace first. \ No newline at end of file diff --git a/wiki/Component-ID-Store.md b/wiki/Component-ID-Store.md new file mode 100644 index 0000000000..915a240de7 --- /dev/null +++ b/wiki/Component-ID-Store.md @@ -0,0 +1,49 @@ +# Overview + +[[Component IDs]] are, among other things, used to carry information through an event. + +It does so by storing the actual payload outside of the component ID used by the buttons, in a database table (and in-memory map), associating it to a generated **UUID**, which is then used as actual component ID for JDA. + +# Component ID Store + +The `ComponentIdStore` is the central point for this mechanism, which is exposed via the interfaces: +* `ComponentIdGenerator` - to all commands during setup for id generation +* `ComponentIdParser` - to `CommandSystem` for the routing of events + +The store is basically a 2-layer `Map`: +* first layer: `Cache` (by Caffeine), to speedup lookups, covers probably about 95% of all queries or so +* second layer: a database table `component_ids` + +When an user wants to create a component ID, the store will add the payload to both layers and associate it to a generated UUID. When an user wants to parse back the payload from the UUID, the store looks up the in-memory map first, and if not found, also the database. + +## Eviction + +To prevent the database from just growing indefinitely over the years, the `ComponentIdStore` implements a LRU-mechanism on both, the in-memory map, as well as on the database table. + +For latter, it runs a periodic **eviction-routine**, which will locate records that have not been used for a long time and delete them. + +Each lookup of an UUID **heats** the record in both, the in-memory map and in the database table. That means, its `last_used` timestamp will be updated, making it not targeted for the next evictions. + +Users are able to listen to eviction events, by registering themselves as listener using `ComponentIdStore#addComponentIdRemovedListener`. While not used as of today, this might be interesting in the future, for example to deactivate actions (such as buttons) associated to an expired component ID. + +Component IDs can also be associated with a `Lifespan.PERMANENT` to prevent eviction all together. While this should not be used per default, it can be useful for actions that are not used often but should still be kept alive (for example global role-assignment reactions). + +Eviction details can be configured, but by default they are set to: +- evict every **15 minutes** +- evict entries that have not been used for longer than **20 days** +- in-memory map has a max size of **1_000** + +## Database details + +The table is created as such: +```sql +CREATE TABLE component_ids +( + uuid TEXT NOT NULL UNIQUE PRIMARY KEY, + component_id TEXT NOT NULL, + last_used TIMESTAMP NOT NULL, + lifespan TEXT NOT NULL +) +``` +Content looks for example like this (after popping `/reload` three times): +![table records](https://user-images.githubusercontent.com/13614011/137593105-1cb99a80-ee6d-46c0-8a5b-d1666eb553fb.png) \ No newline at end of file diff --git a/wiki/Component-IDs.md b/wiki/Component-IDs.md new file mode 100644 index 0000000000..f5486e31d7 --- /dev/null +++ b/wiki/Component-IDs.md @@ -0,0 +1,114 @@ +# Overview + +Component IDs are, among other things, used to carry information through an event. + +For example the user who triggered a slash command or details to the query, which are then stored _"inside"_ a button to retrieve the associated information back when it is clicked. By that, it can be implemented for example that a button can only be clicked by the original message author. + +Component IDs in our system have to follow certain rules, which are explained in detail later. + +# Usage + +Our API offers two options to work with component IDs. + +## `SlashCommandAdapter` + +If your command happens to `extend SlashCommandAdapter` (our helper class), you can easily generate valid component IDs using the helper method `generateComponentId`. The method optionally accepts additional strings which can be used to carry information through the event. + +For example, let us suppose you want to create a button that can only be clicked by the user who also triggered the command initially. Then you would create your buttons as such: + +```java +@Override +public void onSlashCommand(@NotNull SlashCommandEvent event) { + event.reply("Do you want to continue?") + .addActionRow( + Button.of(ButtonStyle.SUCCESS, generateComponentId(event.getMember().getId()), "Yes"), + Button.of(ButtonStyle.DANGER, generateComponentId(event.getMember().getId()), "No") + .queue(); +) +``` + +and then later, when retrieving the `ButtonClickEvent`, you get back the list of arguments: + +```java +@Override +public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { + // Ignore if another user clicked the button + String userId = args.get(0); + if (!userId.equals(Objects.requireNonNull(event.getMember()).getId())) { + event.reply("Sorry, but only the user who triggered the command can use these buttons.") + .setEphemeral(true) + .queue(); + return; + } + + event.reply("Nice!").queue(); +} +``` + +## `SlashCommand` + +Alternatively, commands can also implement the interface `SlashCommand` directly, instead of using the helper class `SlashCommandAdapter`. In that case, in order to use component IDs, one has to do some basic setup first. + +Component IDs can be generated by using a `ComponentIdGenerator`, which the command system will provide to the command by calling the `acceptComponentIdGenerator` method, that each command has to implement, once during setup. + +So as first step, you have to memorize this generator: + +```java +public final class MyCommand implements SlashCommand { + private ComponentIdGenerator componentIdGenerator; + + ... + + @Override + public void acceptComponentIdGenerator(@NotNull ComponentIdGenerator generator) { + componentIdGenerator = generator; + } +} +``` + +After that is done, component IDs can be generated easily as well, for example: +```java +componentIdGenerator.generate(new ComponentId(getName(), ...), Lifespan.REGULAR); +``` +where `...` are the optional arguments you want to pass. + +The previous button example would now look like: +```java +@Override +public void onSlashCommand(@NotNull SlashCommandEvent event) { + event.reply("Do you want to continue?") + .addActionRow( + Button.of( + ButtonStyle.SUCCESS, + componentIdGenerator.generate( + new ComponentId(getName(), Arrays.asList(event.getMember().getId()), + Lifespan.REGULAR), + "Yes"), + Button.of( + ButtonStyle.DANGER, + componentIdGenerator.generate( + new ComponentId(getName(), Arrays.asList(event.getMember().getId()), + Lifespan.REGULAR), + "No") + .queue(); +) +``` +# Lifespan + +Component IDs can have different lifespans. If a component ID has expired, associated events can not be used anymore. For example, a button can not be clicked anymore. + +The default lifespan to use is `Lifespan.REGULAR`. IDs associated with this lifespan will generally be valid long enough for most use cases (multiple days). However, in some cases it might be necessary to create IDs that will not expire. `Lifespan.PERMANENT` can be used for that, but do not overuse it. + +Note that the lifetime of a component ID is refreshed each time it is used. Hence, IDs only expire if they have not been used by anyone for a long time. + +# Details + +Technically, for JDA, component IDs could be any text, as long as it is: +* unique among other components in the event +* not longer than 100 characters + +To overcome those limitations and ease the workflow for users, our API generates component IDs as **UUIDs** (which are unique by default). + +In order to attach arbitrary information (that also might be longer than just 100 characters) to the component ID, we store the actual information (the payload) externally in a database instead of stuffing it into the component ID itself. So while the actual component ID is just a UUID, we associate it to the corresponding information in a database. + +See [[Component ID Store]] for more details on the underlying system and the implementation. \ No newline at end of file diff --git a/wiki/Connect-SonarLint-extension-to-our-SonarCloud.md b/wiki/Connect-SonarLint-extension-to-our-SonarCloud.md new file mode 100644 index 0000000000..876a88400d --- /dev/null +++ b/wiki/Connect-SonarLint-extension-to-our-SonarCloud.md @@ -0,0 +1,63 @@ +# Overview + +This tutorial shows how to connect SonarLint extension to our SonarCloud. + +[SonarCloud](https://www.sonarsource.com/products/sonarcloud/) is a cloud-based static analysis tool we integrated in our CI/CD pipeline. It analyses code in each PR for bugs, vulnerabilities and code smells, and reports issues for the contributor to fix. + +If you want to have your code analysed locally, as you write it, you want to install [SonarLint](https://www.sonarsource.com/products/sonarlint/) extension for your IDE. + +Immediate feedback is important, as it increases your productivity. You can see issues immediately as you write code, and you can easily fix them. Having to push to trigger the analysis every time is cumbersome. You have to wait for the results, and then write and push the fix, and what if the fix has some issues as well? + +The issue is, even with SonarLint, you might encounter these workflow issues, since SonarLint is not as powerful as SonarCloud and doesn't have all of our rules enabled. So the goal of this tutorial is to mitigate that as much as possible, and connect the local SonarLint extension to the SonarCloud. + +## Prerequisites +* IDE or code editor supported by SonarLint: [IntelliJ](https://www.jetbrains.com/idea/), [Eclipse](https://eclipseide.org/) or [VSCode](https://code.visualstudio.com/) +* SonarLint extension. You can find them in marketplaces: [IntelliJ extension](https://plugins.jetbrains.com/plugin/7973-sonarlint), [Eclipse extension](https://marketplace.eclipse.org/search/site/SonarLint) + +## What you will learn +* Connect SonarLint to our SonarCloud + +## Benefits + +When SonarLint works in connected mode, it can: + +* use the same quality profile (same rules activation, parameters, severity, ...) +* reuse some settings defined on the server (rule exclusions, analyzer parameters, ...) +* suppress issues that are marked as Won’t Fix or False Positive on the server + +# Setting up Connected Mode + +## Login to SonarCloud + +If you don't have an account, use OAuth with your github account to [login](https://sonarcloud.io/sessions/new). + +## Create a User Token + +For connecting to SonarCloud, we will use a User Token, as it is the most secure way to connect to the SonarCloud. + +Go to your SonarCloud [account security settings](https://sonarcloud.io/sessions/new), and generate new token. + +## Quick IntelliJ guide + +1. Go to: _File | Settings | Tools | SonarLint_ +2. Click on _+_, or press _Alt + Insert_ +3. Enter connection name, for example 'TJ' and click *Next* (or press *return*) +4. Enter the token you just created and click *Next* +5. Click *'Select another organization..'* and enter `togetherjava`, click *OK* and then *Next* +6. Click *Next* again if you are happy with notifications settings +7. Click *Next* once again +8. Go to: _File | Settings | Tools | SonarLint | Project Settings_ +9. Check _'Bind project to SonarQube / SonarCloud'_ +10. Select previously created connection in *'Connection:'* dropdown menu +11. Click *'Search in list...'* button, and click *OK*; or enter project key manually: `Together-Java_TJ-Bot` +12. Click *Apply* + +## IDE-specific instructions + +Follow these official tutorials for your IDE: + +* InteliJ - https://github.com/SonarSource/sonarlint-intellij/wiki/Bind-to-SonarQube-or-SonarCloud + +* Eclipse - https://github.com/SonarSource/sonarlint-eclipse/wiki/Connected-Mode + +* VSCode - https://github.com/SonarSource/sonarlint-vscode/wiki/Connected-mode \ No newline at end of file diff --git a/wiki/Contributing.md b/wiki/Contributing.md new file mode 100644 index 0000000000..c11e3f6fcf --- /dev/null +++ b/wiki/Contributing.md @@ -0,0 +1,95 @@ +# Welcome to the TJ-Bot project! ![](https://i.imgur.com/flystC6.png) + +First off, thank you for considering contributing to TJ-Bot. :tada: + +TJ-Bot is an open-source project, and we love to receive contributions from our community β€” **you**! There are many ways to contribute, from writing tutorials, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into TJ-Bot itself. + +Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open-source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. + +## Ground Rules + +* Create [issues](https://github.com/Together-Java/TJ-Bot/issues) for any major changes and enhancements that you wish to make, as well as for reporting any sort of bugs. For more light-hearted talks, you can use [discussions](https://github.com/Together-Java/TJ-Bot/discussions). Discuss things transparently and get community feedback. +* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. + +## Your First Contribution + +Unsure where to begin contributing to TJ-Bot? You can start by looking through these labels! +* [good first issue](https://github.com/Together-Java/TJ-Bot/issues/?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two. +* [help wanted](https://github.com/Together-Java/TJ-Bot/issues/?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) - issues which should be a bit more involved than good first issues. + +Let us know that you intend to work on the issue by commenting on it, and we will assign it to you. + +Working on your first Pull Request? You can check these resources: +* http://makeapullrequest.com/ +* http://www.firsttimersonly.com/ + +At this point, you're ready to make your changes! Feel free to ask for help; everyone is a beginner at first! :tada: + +# Getting started + +### Create an issue + +Before creating a new issue, make sure to [search](https://github.com/Together-Java/TJ-Bot/issues?q=is%3Aissue) for existing issues first. + +If the issue already exists, comment on it saying that you intend to work on it, and we will assign it to you! + +In case it doesn't, feel free to open a new issue describing what you would like to change, improve or fix. The community will then discuss the issue, and assign it to you. + +Now you are ready to do some work! + +### Create a fork + +Then, you fork the repository. + +The repository has two main branches: +* `master`, a stable branch mostly used for releases that receives changes only occasionally +* `develop`, the branch where the active development takes place; receives changes frequently + +Your work will be based off the `develop` branch. + +To incorporate new commits from `develop` into your feature branch, use `git pull --rebase` or equivalent GUI action. We strongly prefer having linear history, and PRs with merge commits will have to be squashed before the merge, which results in losing all valuable commit history. + +After your first contribution, you will be invited to the contributor team, and you will be able to work on the project directly, without a fork. + +In that case, create a branch like this `feature/name-of-your-feature`, and push directly to the repo! + +### Commit your changes + +After a portion of feature you are working on is done, it's time to commit your changes! + +Each commit should be small, self-contained, and should solve only one problem. + +Each commit name and message should be clear, concise, and informative: Please consider checking these resources: [writing a commit message](https://chris.beams.io/posts/git-commit/) and [writing a good commit message](https://dev.to/chrissiemhrk/git-commit-message-5e21) + +### Create a pull request + +When you are done, you will create a [pull request](https://github.com/Together-Java/TJ-Bot/pulls) to request feedback from the rest of the community. At this point, your code will be automatically tested against our [[code guidelines|Code Guidelines]] (Spotless, SonarCloud, CodeQL, and more). + +Each pull request should be clear, concise, and informative. Please consider checking these resources: [writing a great pull request](https://www.pullrequest.com/blog/writing-a-great-pull-request-description/) and [unwritten guide to pull requests](https://www.atlassian.com/blog/git/written-unwritten-guide-pull-requests). + +A pull request should only implement one feature or bugfix. If you want to add or fix more than one thing, please submit another pull request. + +### Automated checks and review + +After you created a PR, automated checks will be run. PR cannot be merged without all tests passing, so make sure to fix all the issues that are found. + +Your PR will be reviewed, and after being accepted by at least two members of the community, it will get merged to the `develop` branch! :tada: + +From there on, it will lead to an automatic re-deployment of the bot on a test environment, where you can test out your changes live. + +After a while, the `master` branch will be synced with `develop` again, leading to your changes finally being live on the real server! + +# Tutorials + +Make sure to head over to the [Wiki](https://github.com/Together-Java/TJ-Bot/wiki) as a general entry point to the project. It provides lots of tutorials, documentation and other information, for example +* creating a discord bot and a private server; +* setting up the project locally; +* adding your own custom commands; +* a technology overview; +* guidance about how to maintain the bot (e.g., VPS, logs, databases, restart). + +# Community + +You can chat with the TJ-Bot users and devs in our [discord server](https://discord.com/invite/xxfuxzk)! + +Enjoy and have fun πŸ‘ \ No newline at end of file diff --git a/wiki/Create-Discord-server-and-bot.md b/wiki/Create-Discord-server-and-bot.md new file mode 100644 index 0000000000..f919da5810 --- /dev/null +++ b/wiki/Create-Discord-server-and-bot.md @@ -0,0 +1,90 @@ +# Overview + +This tutorial shows how to create your own Discord server and a Discord bot, which can then be connected to a program, like the TJ-Bot. + +## Prerequisites +* a [Discord](https://discord.com/) account + +## What you will learn +* create your own Discord server +* create a Discord bot +* add the bot to your server + +# Tutorial + +## Discord Server + +As first step, you need to create your own Discord server. This is surprisingly easy. +We use Discord's server template feature for this, this way you don't have to create all the channels, roles and more on your own. +You can still modify the servers channels and roles after creation, as it's only a template. + +This can be done using the following [link](https://discord.new/WhtXEUZeFdTg). + +1. Open the URL from above +2. Follow the dialog and enter details + 2.1. Upload a picture + 2.2. Enter a name + 2.3. smack the **Create** button +3. boom! you have your own Discord server πŸŽ‰ + +![Server details](https://user-images.githubusercontent.com/49957334/194017378-c2c2fb65-4235-41d9-ac23-673a9fa178c4.png) +![Server created](https://user-images.githubusercontent.com/49957334/194017750-1e9c1316-fef9-4718-9cd9-5f8dbf8dcaa0.png) + + +## Discord Bot + +Next up, you want to create your own bot. + +1. visit the [Discord Developer Portal](https://discord.com/developers/applications) +2. click on **New Application** + 2.1. enter the name for the bot +3. on the **General Information** tab + 3.1. enter a name, description and upload a picture + 3.2. hit **Save Changes** +4. on the **Bot** tab + 4.1. click on **Add Bot** + 4.2. hit the **Yes, do it!** button + 4.3. you can now see your bots **Token**, you will need this when connecting the bot to a program later + 4.4. enable the **Server Members Intent** + 4.5. enable the **Message Content Intent** +5. on the **OAuth** tab + 5.1. select the `Bot` and `applications.commands` **Scope**s + 5.2. select the desired **Bot permissions**, e.g. `Send Messages`, `Read Message History`, `Add Reactions`, `Use Slash Commands` + 5.3. from the **Scope** section, copy the URL it generated, this is the **bots invite link** + +![New Application](https://i.imgur.com/X1M7F0d.png) +![enter application name](https://i.imgur.com/pxRTzGc.png) +![enter application details](https://i.imgur.com/TvsyJTc.png) +![Add Bot](https://i.imgur.com/8jshb9M.png) +![Confirm add bot](https://i.imgur.com/vps9yLt.png) +![Token](https://i.imgur.com/l0UZPD3.png) +![Enable Intents](https://i.imgur.com/Hi4bkCZ.png) +![scopes](https://i.imgur.com/8x6WjDT.png) +![Bot permissions](https://github.com/Together-Java/TJ-Bot/assets/88111627/0dc2e9ec-3a84-4fef-ad23-d4f22cf2aadc) +![url](https://github.com/Together-Java/TJ-Bot/assets/88111627/7879ae7c-abc2-416c-bb11-50039361ef52) + + +## Add bot to server + +Last but not least, you have to add the bot to the server you just created. + +1. open the bots invite link URL in a browser + 1.1. select your server to add the bot + 1.2. click **Continue** + 1.3. click **Authorize** +2. thats it, your bot was now added to the server! πŸŽ‰ + +![Add bot](https://i.imgur.com/ceaemII.png) +![Authorize](https://i.imgur.com/239LT0n.png) +![bot added](https://i.imgur.com/jjPzxaZ.png) + +# What next? + +Now that have your own server and your own Discord bot and both are connected to each other, you can start to create or run an actual bot-program, such as TJ-Bot and give it your bots token! + +Once the program has your token, it will connect to the bot and you can interact with it from your server. + +You can learn about these steps in the following guide: +* [[Setup project locally]] + +![bot example](https://i.imgur.com/TIewgLt.png) \ No newline at end of file diff --git a/wiki/Create-and-use-modals.md b/wiki/Create-and-use-modals.md new file mode 100644 index 0000000000..ba0b89d892 --- /dev/null +++ b/wiki/Create-and-use-modals.md @@ -0,0 +1,96 @@ +# Overview + +This tutorial shows how to create and use **modals** in commands. That is, a popup message with a form that allows the user to input and submit data. + +Please read [[Add a new command]] first. + +## What you will learn +* create a modal +* react to a modal being submitted + +# Tutorial + +## Create a modal + +To create a modal, all we need is a way to create [[Component IDs]]. The easiest way to do so is by extending `SlashCommandAdapter` or `BotCommandAdapter`. Alternatively, there is also the helper `ComponentIdInteractor`, which can be used directly. + +We will create a very simple slash command that lets the user submit feedback, which is then logged in the console. + +![command selection](https://i.imgur.com/IkNsefS.png) +![modal](https://i.imgur.com/QMV2Vrr.png) +![response](https://i.imgur.com/Pyrc25d.png) +![log](https://i.imgur.com/7z1IeKE.png) + +The core of creating the model would be something like this: +```java +TextInput body = TextInput.create("message", "Message", TextInputStyle.PARAGRAPH) + .setPlaceholder("Put your feedback here") + .setRequiredRange(10, 200) + .build(); + +// we need to use a proper component ID here +Modal modal = Modal.create(generateComponentId(), "Feedback") + .addActionRow(body) // can also have multiple fields + .build(); + +event.replyModal(modal).queue(); +``` + +## React to modal submission + +The system automatically forwards the event based on the generated component ID. To receive it, the class that send it has to implement `UserInteractor`. The easiest way for that is by implementing `BotCommand` or `SlashCommand`, ideally by extending the helpers `BotCommandAdapter` or `SlashCommandAdapter`. + +This gives a method `onModalSubmitted` which will automatically be called by the system and can be used to react to the modal being submitted: + +```java +@Override +public void onModalSubmitted(ModalInteractionEvent event, List args) { + String message = event.getValue("message").getAsString(); + System.out.println("User send feedback: " + message); + + event.reply("Thank you for your feedback!").setEphemeral(true).queue(); +} + +## Add to features +``` +Finally, we have to add an instance of the class to the system. We do so in the file `Features.java`: + +```java +features.add(new SendFeedbackCommand()); +``` + +## Full code + +The full code for the class is +```java +public final class SendFeedbackCommand extends SlashCommandAdapter { + + private static final String MESSAGE_INPUT = "message"; + + public SendFeedbackCommand() { + super("feedback", "Send feedback to the server maintainers", CommandVisibility.GUILD); + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + TextInput body = TextInput.create(MESSAGE_INPUT, "Message", TextInputStyle.PARAGRAPH) + .setPlaceholder("Put your feedback here") + .setRequiredRange(10, 200) + .build(); + + Modal modal = Modal.create(generateComponentId(), "Feedback") + .addActionRow(body) + .build(); + + event.replyModal(modal).queue(); + } + + @Override + public void onModalSubmitted(ModalInteractionEvent event, List args) { + String message = event.getValue(MESSAGE_INPUT).getAsString(); + System.out.println("User send feedback: " + message); + + event.reply("Thank you for your feedback!").setEphemeral(true).queue(); + } +} +``` \ No newline at end of file diff --git a/wiki/Disabling-Features.md b/wiki/Disabling-Features.md new file mode 100644 index 0000000000..74c853c0a0 --- /dev/null +++ b/wiki/Disabling-Features.md @@ -0,0 +1,34 @@ +## Blacklisting a Bot Feature in the Configuration + +If you need to blacklist a specific feature in your bot, follow these steps to exclude it from execution: + +1. **Identify the Feature**: + - Determine the full class name (including the package) of the feature you want to blacklist. + - For example, let's assume the feature is named `ChatGptCommand.java`, located in the package `org.togetherjava.tjbot.features.chatgpt`. + - The full class name would be `org.togetherjava.tjbot.features.chatgpt.ChatGptCommand`. + +2. **Edit the Configuration File (`config.json`)**: + - Open your bot's configuration file (`config.json`). + - Locate the `"featureBlacklist"` section. + +3. **Add the Feature to the Blacklist**: + - Under `"normal"`, add the full class name of the feature you want to blacklist. + - For example: + + ```json + "featureBlacklist": { + "normal": [ + "org.togetherjava.tjbot.features.chatgpt.ChatGptCommand" + ], + "special": [] + } + ``` + + - The `"normal"` section will prevent the specified feature from being executed when added via the `Features.java` file. + +4. **Save and Apply Changes**: + - Save the configuration file. + - If your bot is running, restart it to apply the changes. + +5. **Additional Note**: + - The `"special"` section can be used for features that are not added via `Features.java` for any reason. diff --git a/wiki/Discord's-roadmap.md b/wiki/Discord's-roadmap.md new file mode 100644 index 0000000000..a6e3c5667e --- /dev/null +++ b/wiki/Discord's-roadmap.md @@ -0,0 +1,76 @@ +Discord is working on a ton, and has been working a ton. +I'll explain some of the relavant features really short here, so you can stay up-to-date with ease + +## Slash-commands + +Command's with a `/` as their prefix. +Examples can be the `/thread` command, allows you to create a command + +## Attachment option + +Added 8/9 febuary, so really recent. \ +Just allow you to request a file from the user in a command. + +## Slash-command auto-complete + +Easiest is to just show it tbh + +See a link to the YouTube video [here](https://www.youtube.com/watch?v=kTbCTxZEtZ0) + +Allows us to give the user "options" live, this means you won't be limited to 25 hardcoded options. \ +An example use-case would be the tag command, you can show the user possible tags live. + +## Context Commands + +Command's visible when right clicking a member/user + +![context-commands example](https://user-images.githubusercontent.com/49957334/153312109-74d92dcf-b245-4e7f-9130-9b5c1e2f0850.png) + +## forms/modals + + +Also added the 8th of Feburary! Another recent addition. \ +It's more or less a form, currently only accepts the text input. \ +Selection menu support will be added in the future, unsure about other things that might get added. + +![form](https://user-images.githubusercontent.com/49957334/153312434-8539d6ea-896b-4a01-94ad-bdb0e00c71a9.png) + + +Now let's talk about features that are in WIP, and unfortunately not yet released. + + +## date-picker option for slash-commands + +Explains itself, no new info on this. + +## NSFW commands + +Don't question + +## Slash-commands improved UI + +Current UI isn't that nice, so they're working on the improvements seen below. + +- all required options are selected instead of 1 +- errors are rendered inside the editor +- multi-line support + +![new_slash_command_ux](https://user-images.githubusercontent.com/49957334/153312840-138c29d6-b373-4cda-88d8-c51e6da4c00b.gif) + +This info is a few months old, I don't know anything more recent about it "it'll enable more new features" + +## Permission system rework + +This one is so important, currently you can **only** allow/deny specific roles/users. That's fine for our bot, but just shitty to manage and such. +Discord is now allowing a lot more, you can set permissions instead, or specific roles/users. +And even better, moderators of the server can set everything too! If they only want the command to be used in specific channels by specific people, this is possible easily, see pictures below (might has received some changes) :p + +![specific_bot](https://user-images.githubusercontent.com/49957334/153314174-1b552e2d-11bd-429c-9470-cf890425a6da.png) +![specific_command](https://user-images.githubusercontent.com/49957334/153314171-b793ef2b-e779-4111-8956-fd84f6c6e0de.png) + + +Source(s): +[GitHub Discussions](https://github.com/discord/discord-api-docs/discussions) +[API-Plans discussion](https://github.com/discord/discord-api-docs/discussions/3581) +[Editor upgrades article](https://auralytical.notion.site/Editor-Upgrades-dee51c93462c44b8a0a53fed3d94c4cd) +[Autocomplete article](https://devsnek.notion.site/Application-Command-Option-Autocomplete-Interactions-dacc980320c948768cec5ae3a96a5886) \ No newline at end of file diff --git a/wiki/Discord-Bot-Details.md b/wiki/Discord-Bot-Details.md new file mode 100644 index 0000000000..204ce0beca --- /dev/null +++ b/wiki/Discord-Bot-Details.md @@ -0,0 +1,21 @@ +# Team + +The bots are managed by Discord team called [Together Java](https://discord.com/developers/teams/886331405368438795/information). All [Moderators](https://github.com/orgs/Together-Java/teams/moderators) are members of it. + +The team manages two bots. + +## TJ-Bot (`master`) + +![TJ-Bot logo](https://i.imgur.com/o7oLJuA.png) + +This bot is the real deal. It runs the stable `master` branch and can be used at any time by all members of the [main Discord server](https://discord.com/invite/XXFUXzK). + +It can be managed in the [Developer Portal](https://discord.com/developers/applications/884898473676271646/information). + +## TJ-Bot (`develop`) + +![TJ-Bot (develop) logo](https://i.imgur.com/uBumtEL.png) + +For testing out new commands and features, we have this bot running the `develop` branch. This bot is accessible from a specific [test server](https://discord.com/invite/qDNZNfjbvp). + +It can be managed in the [Developer Portal](https://discord.com/developers/applications/886334503524638751/information). \ No newline at end of file diff --git a/wiki/Edit-the-Config.md b/wiki/Edit-the-Config.md new file mode 100644 index 0000000000..013f29a1d2 --- /dev/null +++ b/wiki/Edit-the-Config.md @@ -0,0 +1,13 @@ +# Overview + +In order to edit the configuration file of the bot, one has to login to the VPS and adjust the config file manually. Only members of the [Moderator](https://github.com/orgs/Together-Java/teams/moderators)-Team can do the following steps. + +See [[Access the VPS]] for details of the login process. + +# Guide + +1. `ssh togetherjava` to login to the VPS +2. Either `cd ~/docker-infra/master-bot` or `cd ~/docker-infra/develop-bot` to go to the directory of the corresponding bot +3. Use `cd config` +4. Edit the `config.json` file, for example `vim config.json` or `nano config.json` +4. Save the file and [[restart the bot|Shutdown or restart the bot]]. \ No newline at end of file diff --git a/wiki/Flyway-guide-with-solutions-to-common-issues.md b/wiki/Flyway-guide-with-solutions-to-common-issues.md new file mode 100644 index 0000000000..74c826eb3b --- /dev/null +++ b/wiki/Flyway-guide-with-solutions-to-common-issues.md @@ -0,0 +1,42 @@ +# Migration + +## Context +Whenever you make changes to state of DB, you have to write a new SQL script in `/resources/db` directory. + +Let's say you wanna modify an existing table in DB, you write a script `V14__Alter_Help_Thread_Metadata.sql`. + +_Let's ignore commented SQL for now, say first iteration of your script only has first two entries_ +![image](https://github.com/Together-Java/TJ-Bot/assets/61616007/1034a3ac-f7ce-45ca-b9c9-6917f5ef61ab) + + when you run the application for first time. Flyway will do the migration, i.e. keep track of that change and verify it stays same. + + On successful run of application, it will create a new entry in table `flyway_schema_history` of your local instance of DB. + +![image](https://github.com/Together-Java/TJ-Bot/assets/61616007/f47961ce-452f-4a5c-a650-fed924ad4f2f) +_screenshot of what `flyway_schema_history` table looks like_ + +Now each time you run your application, it will verify these migrations using `checksum` from this table. +Any changes made after migration, should be done via seperate script otherwise you run into migration errors. + +Now under normal circumstances, once changes are made to production environment you would have to add a new SQL script for any new changes. + + But during development, requirements change frequently. Now you wanna add a new column in your newly created table. so you now add a couple in same SQL script. + +![image](https://github.com/Together-Java/TJ-Bot/assets/61616007/8afb7019-ffb2-4dfd-9654-be7b377f0135) + +_Notice new entries altering state of table_ + +Now if you try and run the application, flyway will throw migration error and you won't be able to run the application. + +![image](https://github.com/Together-Java/TJ-Bot/assets/61616007/e01d6cc7-8399-4e4c-8da1-b6b43ef05195) + +_Error message in logs should look like this_ + +## Solution +1. Open local DB instance, look at table `flyway_schema_history`. +2. Note down the version of last entry(should have name of your sql script). +3. Run this sql command in console for local DB, `delete from flyway_schema_history where version = 'VERSION_NUMBER_HERE';`. +4. Now drop the table/columns that are added via your new sql script using that DB console. +5. Once you revert back to old state of DB, it's safe to rewrite new SQL script with all the statements. +6. Run application, now it will create new entry `flyway_schema_history` and you should be able to run application. + \ No newline at end of file diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000000..377c4288c5 --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,37 @@ +# Overview ![](https://i.imgur.com/Kq68zt9.png) + +TJ-Bot is a **Discord Bot** used on the [Together Java](https://discord.com/invite/xxfuxzk) server. It is maintained by the community, anyone can contribute. + +If you want to join the party, please have a look at: +* [[Contributing]] +* [[Code Guidelines]] +* [[Create Discord server and bot]] +* [[Setup project locally]] +* [[Flyway guide with solutions to common issues]] +* Features + * [[Add a new command]] + * [[Add days command]] + * [[Add question command]] + * [[Adding context commands]] + * [[Create and use modals]] + * [[Disabling Features]] +* [[JDA Tips and Tricks]] +* [[Component IDs]] +* [[Code in the cloud (codespaces)]] +* [[Connect SonarLint extension to our SonarCloud]] + +We also have several guides explaining the infrastructure, architecture and flow of the project: +* [[Tech Stack]] +* [[Component ID Store]] + +As well as some tutorials regarding on how to maintain the project: +* [[Discord Bot Details]] +* [[Access the VPS]] +* Logging + * [[View the logs]] + * [[Change log level]] + * [[Setup Log Viewer]] +* [[Edit the Config]] +* [[Shutdown or restart the bot]] +* [[Release a new version]] +* [[Reset or edit the databases]] \ No newline at end of file diff --git a/wiki/JDA-Tips-and-Tricks.md b/wiki/JDA-Tips-and-Tricks.md new file mode 100644 index 0000000000..8cf0129813 --- /dev/null +++ b/wiki/JDA-Tips-and-Tricks.md @@ -0,0 +1,112 @@ +# Overview + +This guide gives lists some tips and tricks to ease the life of developers working with JDA, the Discord framework used by this project. + +## Tips + +### Use [ISnowflake#getIdLong](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/entities/ISnowflake.html#getIdLong()) instead of [ISnowflake#getId](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/entities/ISnowflake.html#getId()) + +Internally JDA uses `long`s instead of `String`s, so they are faster and still work. + +Example: +```java +long userId = event.getUser().getIdLong(); +``` +However, in some cases using long is sub-optimal, for example when comparing it to a component ID. Component IDs are custom strings allowing storing data within the ID. +Example: +```java +String userThatClickedId = event.getUser().getId(); +String userId = idArgs.get(0); + +if (userThatClickedId.equals(userId)) { + ... +} +``` +If you already have a `long`, you'll need to cast this to a String resulting in less readable and more code, when JDA can also do this for you internally. + +### Don't forget `.queue();` + +Almost all Discord requests do not run automatically and require an explicit `.queue();`. + +Affected requests are called [RestActions](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/requests/restaction/package-summary.html). The rule of thumb is, if your message returns a result it likely has to be queued. Most IDEs can detect such a situation, as seen in the following example: + +![IntelliJ queue warning](https://i.imgur.com/PPkUkdH.png) + +### There are lot of `RestAction` types + +Some of the many `RestAction` types give you more flexibility and additional functionality. + +For example, when editing a message, you can not just add 500 options to the [TextChannel#editMessageById()](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/entities/MessageChannel.html#editMessageById(long,net.dv8tion.jda.api.entities.Message)) method. Instead, it returns a [MessageAction](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/requests/restaction/MessageAction.html) object, which allows you to set all the components and more. + +### Every JDA related object has a `getJDA` method + +Whenever you need an instance of `JDA`, the framework got you covered and offers a general `getJDA()` method available on pretty much any JDA related object. + +### Cast [JDA](https://github.com/discord/discord-api-docs/discussions/3581) to `JDAImpl` for more methods + +This is a dangerous tip and we advise you to not consider it unless there is really no other option. If you are unsure, please ask the other developers for help. + +Internally JDA uses `JDAImpl` instead of `JDA`, which has way more _(internal)_ methods. While almost, if not all, of them are probably not relevant, some might prove useful in very specific use-cases. + +Since this is an internal API, breaking changes can happen with any new version of JDA and it also has no documentation. + +### [EntityBuilder](https://github.com/DV8FromTheWorld/JDA/blob/development/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java) (internal) + +**Note, the entitybuilder isn't meant for serializing and deserializing stored users, it's not backwards compatible.** + +EntityBuilder is an internal class of JDA used to create Discord entities (users, guilds, and more) from their JSON value (DataObject's). Within the TJ-Bot we make usage of this to test command's their logic. + +By creating "fake" JSON's we can make create an event, members and such using an EntityBuilder. +A.e the [EntityBuilder#createUser](https://github.com/DV8FromTheWorld/JDA/blob/development/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java#L331) method. + +To be more precise, you can view the createUser method [here](https://github.com/DV8FromTheWorld/JDA/blob/development/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java#L331). +If we'd give the createUser method a DataObject with the following JSON. +We'd be able to make JDA think Nelly is a real user. +For an up-to-date example, check the [Discord docs](https://discord.com/developers/docs/resources/user#user-object) +```json +/* + Note: this json is from 13/10/2021 + This might be changed at the moment of reading +*/ +{ + "id": "80351110224678912", + "username": "Nelly", + "discriminator": "1337", + "avatar": "8342729096ea3675442027381ff50dfe", + "verified": true, + "email": "nelly@discord.com", + "flags": 64, + "banner": "06c16474723fe537c283b8efa61a30c8", + "accent_color": 16711680, + "premium_type": 1, + "public_flags": 64 +} +``` + +## Tricks + +Due to the complexity of JDA, you might easily run into a situation where you solve a problem in a certain but not optimal way that is either overly complex or just very lengthy. This chapter shows some tricks to help you use JDA correct and better. + +### Method shortcuts + +JDA offers some shortcuts to methods and patterns frequently used: +* [JDA#openPrivateChannelById](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/JDA.html#openPrivateChannelById(long)), instead of manually retrieving the user and calling [User#openPrivateChannel](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/entities/User.html#openPrivateChannel()) +* [JDA#getGuildChannelById](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/JDA.html#getGuildChannelById(long)) also applies to _textchannels_, _storechannels_ and more. So a [Guild](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/entities/Guild.html) instance is not required to get channels. + +### Raw events + +In case you need to inspect an event send by Discord or JDA closely in its raw JSON form, one can enable raw events and inspect the payloads: +```java +// where the JDA instance is created +JDA jda = JDABuilder.createDefault(...) + .setRawEventsEnabled(true) // add this call + ... + .build(); + +// and then add a raw event listener +jda.addEventListener((EventListener) event -> { + if (event instanceof RawGatewayEvent rawEvent) { + System.out.println(rawEvent.getPayload()); + } +}); +``` \ No newline at end of file diff --git a/wiki/Release-a-new-version.md b/wiki/Release-a-new-version.md new file mode 100644 index 0000000000..6d0703f8ea --- /dev/null +++ b/wiki/Release-a-new-version.md @@ -0,0 +1,35 @@ +# Overview + +Thanks to a rich pipeline, releasing a new version of the bot is fairly simple. + +It mainly consists of simply **pushing** `develop` over on `master`, creating an **annotated tag** for the release and possibly adjusting the **configuration** and the Discord environment, thats it. + +## Checklist + +1. Determine the next release version (for example `v1.2.3`) +2. Create a PR to merge `develop` into `master`, call it for example `Release v1.2.3` and tag it as `release`; the PRs only purpose is visibility +3. Ignore the PR(don't merge it via Github) and `rebase` `master` directly onto `develop`, then `force-push`(might not need to do this, try just pushing). As a result, `master` and `develop` are fully identical + 3.1. The PR should now automatically be marked as _merged_ by GitHub + 3.2. In the meantime, the pipeline automatically started deploying the new version to the server + + *Note: for those who are not good with rebase, make sure to have your `develop` branch upto date. Switch to `master`, do `git rebase develop`.* +4. Create and push an **annotated tag** like `v.1.2.3` with a short release description from the state of `master` + 4.1. The pipeline will now create a new release on GitHub + 4.2. Once the release has been created, you can adjust and beautify the description, see [releases](https://github.com/Together-Java/TJ-Bot/releases) + Note: There's two types of tags, annotated and normal tags. We want annotated tags, to create one via intellij follow instructions in given screenshot + + CREATING AN ANNOTATED TAG IN INTELLIJ + + ![image](https://github.com/Together-Java/TJ-Bot/assets/61616007/fcfeec1e-7f72-4d8a-80af-92eed7839d3f) + + PUSHING ANNOTATED TAG + + `git push --follow-tags` + + read more here: https://git-scm.com/docs/git-push + +5. In case the configuration (`config.json`) changed, make sure to update it; see [[Edit the Config]] +6. In case the new version requires changes on Discord, such as other permissions, new channels or roles, make sure to update them as well +7. Verify that the bot works as expected + 7.1. Try `/ping` and see if you get a response + 7.2. Maybe check the logs to see if any error pops up, see [[View the logs]] \ No newline at end of file diff --git a/wiki/Reset-or-edit-the-databases.md b/wiki/Reset-or-edit-the-databases.md new file mode 100644 index 0000000000..05110ea51c --- /dev/null +++ b/wiki/Reset-or-edit-the-databases.md @@ -0,0 +1,45 @@ +# Overview + +In order to reset of edit the databases used by the bot, one has to login to the VPS and navigate to the corresponding directory. Only members of the [Moderator](https://github.com/orgs/Together-Java/teams/moderators)-Team can do the following steps. + +See [[Access the VPS]] for details of the login process. + +# Guide + +1. `ssh togetherjava` to login to the VPS +2. Consider temporarilly shutting down the bot during the database edits (see [[Shutdown or restart the bot]]) +3. Either `cd /var/lib/docker/volumes/tj-bot-master-database/_data` or `cd /var/lib/docker/volumes/tj-bot-develop-database/_data` to go to the directory of the corresponding database +4. Edit the database manually, it is a [SQLite 3](https://www.sqlite.org/index.html) database. + +## Working with the database + +To ease inspecting and editing the database, the `sqlite3` CLI is installed on the VPS. + +Please make sure to either shut down the bot in the meantime or working on a copy of the database instead, to **avoid locking the actual database**: +```bash +cp database.db database_copy.db +``` + +Here are some simple example queries: +* Connect to the database +```bash +sqlite3 database_copy.db +``` +* List all available tables: +```sql +.tables +``` +* Show the structure of the table +```sql +.schema moderation_actions +``` +* Show all against against a user +```sql +SELECT * FROM moderation_actions WHERE author_id = 123456789 +``` +* Exist the database +```sql +.exit +``` + +![example](https://i.imgur.com/zmJtYrD.png) \ No newline at end of file diff --git a/wiki/Setup-Log-Viewer.md b/wiki/Setup-Log-Viewer.md new file mode 100644 index 0000000000..8f3a93e70f --- /dev/null +++ b/wiki/Setup-Log-Viewer.md @@ -0,0 +1,57 @@ +# Logviewer + +The **logviewer** module is a _Spring + Vaadin Web-Application_ in which one can see the logs written by the applications in real-time. + +![logviewer example](https://i.imgur.com/5cuZI85.png) + +In the following, we explain how to set the website up and how to host it. + +## Setup + +1. First you need a _Discord application_, you can use the same also used for the main application. See [[Create Discord server and bot]] for details on how to create one. +2. Open the [Applications Section](https://discord.com/developers/applications) from Discord +3. Open **your Application** +4. Open the **OAuth2 Tab** +5. Add a **Redirect Link**, e.g. `https://localhost:443/login/oauth2/code/github` +6. Save your changes +7. Create a `config.json` file in the directory of the logviewer module `TJ-Bot/logviewer/config.json`. Alternatively you can place it wherever you want and provide the path to the file as a start-argument. The content of the file should be like this (fill in the Discord data): +```json +{ + "clientName": "", + "clientId": "", + "clientSecret": "", + "rootUserName": "", + "rootDiscordID": "", + "logPath": "application/logs", + "databasePath": "logviewer/db/db.db", + "redirectPath": "https://localhost:443/login/oauth2/code/github" +} +``` +#### Explanation for the parameters + +* `clientName` is the name of your Discord Application +* `clientId` is the clientId you can [copy in the OAuth2 Tab](https://i.imgur.com/x7mUyUW.png) from Discord +* `clientSecret` is the secret you can [copy in the OAuth2 Tab](https://i.imgur.com/YEJzMAS.png) +* `rootUserName` is your own Discord username +* `rootDiscordID` is your own Discord ID, enable Developer Mode in your Discord App and [right-click](https://i.imgur.com/z0FjqPC.png) on one of your own posts +* `logPath` is the path to the logs from the Bot, not for this application +* `databasePath` is the path where the database for this Web-Application should be saved +* `redirectPath` is the URL you used in the Discord OAuth2 Settings + +8. You are done, start the application. Open your browser on https://localhost:443 and **accept the Authorization**. + +![your application](https://i.imgur.com/6N6JHDk.png) +![OAuth2 Tab](https://i.imgur.com/XCAvBl1.png) +![redirect URL](https://i.imgur.com/xOhLbSB.png) +![save changes](https://i.imgur.com/bYzMUX5.png) +![accept authorization](https://i.imgur.com/I7s1alf.png) + +## Quick overview + +On the **left side** you can see three views. +* **Logs** displays the actual logfiles as they are in the configured directories. +* **Streamed** displays the log-events as the main application sends them to the web application. +* **User-Management** enables adding or removing users who can access this website and editing their roles (right click the panel). + +![side panel](https://i.imgur.com/RM1tCy6.png) +![user management](https://i.imgur.com/xCygzQZ.png) \ No newline at end of file diff --git a/wiki/Setup-project-locally.md b/wiki/Setup-project-locally.md new file mode 100644 index 0000000000..8b69928eee --- /dev/null +++ b/wiki/Setup-project-locally.md @@ -0,0 +1,129 @@ +# Overview + +This tutorial shows how to download, setup and start the **TJ-Bot** project locally on your machine. + +Alternatively, you can also work directly in the cloud, for free, and get started in just a few seconds. See: +* [[Code in the cloud (codespaces)]] + +## Prerequisites +* [Java 24](https://adoptium.net/temurin/releases?version=24) installed +* your favorite Java IDE or text editor, e.g. [IntelliJ](https://www.jetbrains.com/idea/download/) or [Eclipse](https://www.eclipse.org/downloads/) +* [`git`](https://git-scm.com/downloads) installed (or any GUI or IDE plugin) +* [`gradle`](https://gradle.org/releases/) available (or any GUI or IDE plugin), you can either install it or use our provided wrapper +* your own [Discord](https://discord.com/)-Bot, tied to a server (see [[Create Discord server and bot]]) + * a token of that bot + +## What you will learn +* use git to download the project +* use gradle to download dependencies +* use gradle to build the project +* connect your bot to the program +* use gradle to start the bot +* interact with the bot from your server + +# Tutorial + +## Clone repository + +First of all, you have to download the project to your machine. Visit the projects [GitHub website](https://github.com/Together-Java/TJ-Bot) and copy the `.git` link, which is this +``` +https://github.com/Together-Java/TJ-Bot.git +``` +![.git link](https://i.imgur.com/8jGsr06.png) + +### IntelliJ git plugin + +IntelliJ comes by default with a `git` plugin. You can easily clone repositories to your disk by clicking a few buttons. + +1. open your IntelliJ and select `Get from VCS`. +2. select `Git`, enter the `.git` link and select a directory for the project; smack that `Clone` button +3. IntelliJ will now open the project + +![Get from VSC IntellIJ UI](https://i.imgur.com/uyqWyGF.png) +![.git url IntellIj UI](https://i.imgur.com/AEG0sqg.png) + +### Manual usage of `git` + +To download the project, use the following command: +```bash +git clone https://github.com/Together-Java/TJ-Bot.git TJ-Bot +``` +You now have the project and all its data locally. + +![git clone command line](https://i.imgur.com/EaLmolj.png) +![TJ-Bot folder](https://i.imgur.com/asBubhE.png) + +## Gradle + +Next up, you have to download all the dependencies, generate the database and build the project. + +### IntelliJ Gradle plugin + +IntelliJ comes by default with a `gradle` plugin. If not started already automatically, you can command it to do all of above by clicking a bunch of buttons. + +1. open the Gradle view +2. expand the view and click on `TJ-Bot > Tasks > build > build`, or just click on the elephant icon and enter `gradle build` + +![Gradle tasks IntelliJ UI](https://i.imgur.com/ziFdX9P.png) +![Gradle command IntelliJ UI](https://i.imgur.com/7OuyvMN.png) +![Gradle output](https://i.imgur.com/Q32x2qP.png) + +
+ℹ️ If you get any gradle errors... +Make sure that your project and gradle is setup to use the latest Java version. Sometimes IntelliJ might guess it wrong and mess up, leading to nasty issues. + +Therefore, review your **Project Structure** settings and the **Gradle** settings: +![project settings](https://i.imgur.com/2hPB4ga.png) +![gradle settings](https://i.imgur.com/O8FGHK0.png) +
+ +### Manual usage of `gradle` + +You can also just execute Gradle from the command line. + +1. open a command line in the root directory of the project +2. execute `gradle build` + +![Gradle command line start](https://i.imgur.com/YcVjVxZ.png) +![Gradle command line end](https://i.imgur.com/WGextPN.png) + +## Start the bot + +Last but not least, you want to start the bot with your bot token and let it connect to your private bot with which you can interact from one of your servers. + +For this step, you need to hold your bot token ready, you can find it at the [Discord Developer Portal](https://discord.com/developers/applications). + +See the following guide if you still have to create a server and a bot first: +* [[Create Discord server and bot]] + +![Discord Developer Portal - Bot Token](https://i.imgur.com/IB5W8vZ.png) + +To run the bot, you will need a `config.json` file with specific content. You can find a template for this file, with meaningful default values, in `application/config.json.template`. + +Replace `` with your bot token; you can also adjust the other settings if you want. + +### IntelliJ + +1. put the configuration file to `TJ-Bot\application\config.json` or run the program with a single argument, the path to your config file +2. in the Gradle view, click the `run` task and start it + +![Bot runs](https://i.imgur.com/KdsSsx0.png) + +### Command line, runnable jar + +1. build a runnable jar of the project by executing `gradle shadowJar` + 1.1. the jar can now be found at `TJ-Bot\application\build\libs` +2. unless you move the jar around, you have to adjust the database path in the config to `../../../build/database.db` +3. put the configuration file right next to the jar or run the program with a single argument, the path to your config file +4. run `java -jar TJ-Bot.jar` + +![shadowJar](https://i.imgur.com/jGMVAv4.png) +![jar](https://i.imgur.com/Xv6HIFG.png) + +### Have fun + +The bot is now running and connected to your server, hurray πŸŽ‰ + +You can now execute commands and see the bot do its magic: + +![Bot command](https://user-images.githubusercontent.com/73871477/194744735-562b70a6-62ff-4675-9f04-e4327b38b2f6.png) diff --git a/wiki/Shutdown-or-restart-the-bot.md b/wiki/Shutdown-or-restart-the-bot.md new file mode 100644 index 0000000000..110d556cd1 --- /dev/null +++ b/wiki/Shutdown-or-restart-the-bot.md @@ -0,0 +1,14 @@ +# Overview + +In order to shutdown or restart any of the bots, one has to login to the VPS and command Docker to execute the corresponding task. Only members of the [Moderator](https://github.com/orgs/Together-Java/teams/moderators)-Team can do the following steps. + +See [[Access the VPS]] for details of the login process. + +# Guide + +1. `ssh togetherjava` to login to the VPS +2. Either `cd ~/docker-infra/master-bot` or `cd ~/docker-infra/develop-bot` to go to the directory of the corresponding bot +3. Execute the corresponding `docker-compose` command + a. To issue a graceful shutdown, execute `docker-compose down` + b. To shutdown the bot forcefully, use `docker-compose kill` + c. To command a restart of the bot, put `docker-compose restart` \ No newline at end of file diff --git a/wiki/Tech-Stack.md b/wiki/Tech-Stack.md new file mode 100644 index 0000000000..f1296c9c36 --- /dev/null +++ b/wiki/Tech-Stack.md @@ -0,0 +1,47 @@ +# Overview + +TJ-Bot is a classic Discord bot with a slim but modern set of dependencies. + +![Tech Stack Diagram](https://i.imgur.com/yp9dixn.png) + +## Core + +The project stays up to date with the latest Java version. + +We use [JDA](https://github.com/DV8FromTheWorld/JDA) to communicate with the Discord API and [Gradle](https://gradle.org/) to manage the project and its dependencies. + +## Database + +The bot uses a single [SQLite](https://www.sqlite.org/index.html) database, which is generated automatically by [Flyway](https://flywaydb.org/) based on the scripts found in +``` +TJ-Bot/application/src/main/resources/db/ +``` +Interaction with the database is then done using [jOOQ](https://www.jooq.org/). + +## Logging + +We rely on [SLF4J](http://www.slf4j.org/) for logging, backed by [Log4j 2](https://logging.apache.org/log4j/2.x/). + +The configuration can be found at +``` +TJ-Bot/application/src/main/resources/log4j2.xml +``` + +## Testing + +For testing the project, we use [JUnit 5](https://junit.org/junit5/docs/current/user-guide/). + +## Code Quality + +The quality of the code is ensured by [Spotless](https://github.com/diffplug/spotless), using a strict style based on the commonly used [Google Java Style](https://google.github.io/styleguide/javaguide.html). The exact style definition can be found at: +``` +TJ-Bot/meta/formatting/google-style-eclipse.xml +``` + +Additionally, we use static code analyse by [SonarCloud](https://sonarcloud.io/dashboard?id=Together-Java_TJ-Bot). + +Further, the code is checked automatically by [CodeQL](https://codeql.github.com/docs/) and dependencies are kept up to date with the aid of [Dependabot](https://dependabot.com/) + +## Deployment + +In order for the bot to actually go live, it is deployed as [Docker](https://www.docker.com/) image, build by [jib](https://github.com/GoogleContainerTools/jib), to a VPS provided by [Hetzner](https://www.hetzner.com/) (see [[Access the VPS]] for details). \ No newline at end of file diff --git a/wiki/View-the-logs.md b/wiki/View-the-logs.md new file mode 100644 index 0000000000..3fd44e0617 --- /dev/null +++ b/wiki/View-the-logs.md @@ -0,0 +1,31 @@ +# Overview + +There are two ways to read the logs of the bots: +* by reading the forwarded messages in **Discord** +* by manually logging in to the **VPS** and looking up the log files + +The log level can be changed temporarily using the command `/set-log-level`. + +## Discord + +All log messages, with a few sensitive exceptions, are forwarded to Discord via webhooks. You can read them in the two channels: + +* **tjbot_log_info** - contains all `INFO`, `DEBUG`, `TRACE` messages +* **tjbot_log_error** - contains all `WARN`, `ERROR`, `FATAL` messages + +![log channels](https://i.imgur.com/nkvy80n.png) + +## Manually viewing the files + +In order to read the log files of the bots directly, one has to login to the VPS and command Docker to execute the corresponding task. Only members of the [Moderator](https://github.com/orgs/Together-Java/teams/moderators)-Team can do the following steps. + +See [[Access the VPS]] for details of the login process. + +# Guide + +1. `ssh togetherjava` to login to the VPS +2. Either `cd ~/docker-infra/master-bot` or `cd ~/docker-infra/develop-bot` to go to the directory of the corresponding bot +3. Execute `docker-compose logs -f` +4. Hit Ctrl + C to stop + +![cmd logs](https://i.imgur.com/TTciCaY.png) \ No newline at end of file diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md new file mode 100644 index 0000000000..765dee1aa9 --- /dev/null +++ b/wiki/_Sidebar.md @@ -0,0 +1,39 @@ +#### Home + +* [[Overview|Home]] + +#### Join the party + +* [[Contributing]] +* [[Code Guidelines]] +* [[Create Discord server and bot]] +* [[Setup project locally]] +* [[Flyway guide with solutions to common issues]] +* Features + * [[Add a new command]] + * [[Add days command]] + * [[Add question command]] + * [[Adding context commands]] + * [[Create and use modals]] +* [[JDA Tips and Tricks]] +* [[Component IDs]] +* [[Code in the cloud (codespaces)]] +* [[Connect SonarLint extension to our SonarCloud]] + +#### Project documentation + +* [[Tech Stack]] +* [[Component ID Store]] + +#### Maintenance + +* [[Discord Bot Details]] +* [[Access the VPS]] +* Logging + * [[View the logs]] + * [[Change log level]] + * [[Setup Log Viewer]] +* [[Edit the Config]] +* [[Shutdown or restart the bot]] +* [[Release a new version]] +* [[Reset or edit the databases]] \ No newline at end of file