Diese Repository enthält den Quellcode für den SlimeBall
Bot auf dem SlimeCloud Discord Server.
Der Bot steht unter ständiger Entwicklung, falls du Interesse daran hast mitzuwirken, schau dir doch die Contributing-Sektion an.
Wenn du Fragen hast oder dich mit anderen Entwicklern austauschen möchtest, kannst du in der #👾│tüftlerecke auf dem SlimeCloud Discord Server vorbeischauen.
Dieses Projekt steht unter der GNU Affero General Public License v3.0 Lizenz!
Wir verwenden GitHub issues um Fehler und Feature-Anfragen zu verwalten. Auch wenn du nicht selbst programmieren kannst, kannst du gerne einen Issue erstellen, wenn du eine Idee für ein Feature hast oder einen Fehler melden möchtest. Die Entwickler suchen sich regelmäßig die dringendsten Issues, um diese umzusetzen.
In GitHub Issues und Pull-Requests gibt es oft ToDo-Listen. Diese haben folgendes Format:
- [ ] Item A
- [ ] Item B
- [x] Item C
Achte darauf, zum Abhaken ein einfaches 'x' zwischen die eckigen Klammern zu setzten. Bitte füge keine weiteren Leerzeichen ein, da die Formatierung von GitHub ansonsten nicht korrekt erkannt wird!
Wenn du die Programmiersprache Java selbst beherrschst und dich mit der JDA-Bibliothek auskennst, kannst du gerne neue Features programmieren oder Fehler reparieren.
Suche dir entweder einen Issue aus, den du bearbeiten möchtest (und gib ihn in der Pull-request an) oder ändere etwas, das du unabhängig von einem bestehenden Issue ändern möchtest.
Beachte jedoch, dass wenn bei einem Issue bereits eine Person als Beauftragter markiert ist, diese Person für den Issue zuständig ist und du dich nicht um diesen Issue kümmern solltest!
Ein Grund dafür ist zum Beispiel, dass interne Besprechungen mit einem Teammitglied stattgefunden haben, bei deinen Details zur Umsetzung vereinbart wurden.
Du kannst unter solchen Issues oder dazugehörigen Pull-requests trotzdem gerne kommentieren und deine Vorschläge einbringen.
Dazu kannst du die Repository forken und in deiner eigenen Kopie einen neuen Branch für deine Änderungen anlegen (Halte dich dabei an die Styleguides!).
Sobald du mit deinen Änderungen begonnen hast, kannst du eine Pull-request erstellen.
Wenn die Änderungen noch nicht fertig sind, solltest du sie als Draft erstellen, um zu zeigen, dass du noch nicht fertig bist.
Im Text der Pull-request oder des Drafts gibst du an, was du verändert hast.
Durch der erstellen der Pull-request zeigst du anderen Entwicklern woran du arbeitest und die Maintainer können dir Hinweise geben, wenn du etwas anders angehen solltest.
Sobald deine Änderungen fertig sind, kannst du den Draft als "Ready for Review" markieren, um einen Maintainer der Repository darum zu bitten, deine Änderungen zu verifizieren und letztendlich in den master
-Branch zu übernehmen.
Dieser Bot verwendet java 17! Wenn du den bot selber verwenden oder an der Entwicklung teilnehmen möchtest, wird vorausgesetzt, dass du bereits ein JRE/JDK 17 installiert hast, und weißt, wie du es verwendest!
Für die Entwicklung empfehlen wir IntelliJ (Community Version reicht aus) als Entwicklungsumgebung. Andere IDE's können auch verwendet werden, folgende Erklärungen beziehen sich jedoch ausschließlich auf IntelliJ.
Beginne damit, eines Projekt zu erstellen. Nutze dazu das Menü File->New->Poject from Version Control
.
Gib dort als URL die URL deines Forks der Repository an. Dadurch wird ein Projekt erstellt, das den Sourcecode sowie die Buildscripts der aktuellen Version des SlimeBallBots enthält.
In der Project Structure
des Projekts muss das JDK 17 angeben werden.
Das Projekt hat Start-Konfigurationen vordefiniert, die verwendet werden können, um den Bot mit IntelliJ zu starten.
Um runtime daten vom Code zu trennen, führt die Konfiguration den Code im Ordner run
aus.
Im Projekt befindet sich ein Ordner run_template
.
In diesem Ordner findest du eine standard config
Datei sowie eine .env
Datei.
Bevor du startest, muss der run_template
Order kopiert und die Kopie in run
umbenannt werden.
In diesem run
Ordner kannst du die Konfiguration nach deinen wünschen anpassen.
Trage dazu zunächst dein Bot-Token in der .env
Datei als TEST_TOKEN
ein.
Es wird vorausgesetzt, dass du bereits einen Bot Account im Developer Portal erstellt hast, und weißt, wie du das Token kopieren kannst.
In der config
Datei kannst du nun die Werte so anpassen, dass sie für deine Test-Umgebung funktionieren.
Falls du Beispielsweise keine Datenbank hast, kannst du das Element database
einfach löschen oder zu -database
umbenennen, um keine Datenbank zu verwenden.
Dieses Vorgehen gilt auch für andere Konfigurationseinträge.
Beachte jedoch, dass durch das Entfernen von Konfigurationen einige Funktionen deaktiviert werden.
Dazu bekommt du auch eine Informationsnachricht in den Logs beim Starten.
Du kannst nun die Run Konfiguration Run
auswählen und starten. Der SlimeBallBot sollte starten und auf deine Befehle reagieren.
Wenn du den Bot außerhalb deiner IDE verwenden möchtest, musst du ihn als jar
exportieren.
Verwende dazu die Package
Run Konfiguration. Es sollte eine Datei in build/libs
erstellt werden. Diese kannst du nun mit java -jar
ausführen.
Um den Code übersichtlich und einheitlich zu halten, sollten sich alle an einen Codestyle halten. Im Folgenden werden die wichtigsten Richtlinien aufgezählt.
- Einrückung: Immer wenn ein neuer Codeblock eröffnet wird, wird um einen Tab weiter eingerückt. Diese Einrückungen werden mit
Tab
Zeichen vorgenommen und nicht mit Leerzeichen! - Neue Zeilen: Generell starten Blöcke in der neuen Zeile nach dem Steuerblock.
Die geöffnete geschweifte Klammer steht dabei noch in der Zeile des Kontrollblocks.
Wenn ein Block nur aus einem weiteren Steuerblock wie
return
oderbreak
besteht, wird dieser in er Zeile des Kontrollblocks ohne geschweifte Klammern geschrieben:if (false) return; if (true) { System.out.println("Test"); int x = 0; }
- Abstände: Um den Code nicht gequetscht wirken zu lassen, werden zwischen einzelnen Teilen in einer Zeile Leerzeichen eingefügt:
for (int i = 0; i < 10; i++) { System.out.println((i + 1) + ". Test"); }
- Java API: Wir verwenden die Java Stream- und Optional API. Das hat auch die starke Verwendung von Lambda-Ausdrücken zur Folge.
- Namen & Sprache: Alle Variablen, Klassen und Methoden im Code werden englisch benannt. Variablen und Methoden in lowerCamelCase und Klassen in UpperCamelCase. Alle Texte, die Nutzer sehen, sollen jedoch auf Deutsch verfasst werden.
- Kommentare: Mit Kommentare könnt ihr gerne Stellen beschreiben, bei denen nicht sofort klar wird, was der Code warum tut. Ihr müsst jedoch nicht allen Code kommentieren. Wenn ihr markieren möchtet, dass etwas in Zukunft geändert
werden soll, könnt ihr
//TODO
oder//FIXME
Kommentare verwenden.
Als gute Richtlinie lassen sich die intelliJ Standard-Vorgaben verwenden. Wenn du deinen Code mit der intelliJ Funktion "Reformat Code" formatierst, werden die groben Formatierungen bereits automatisch angewendet.
Allgemeine Konfiguration für den Bot wird in der config
-Datei im gleichen Ordner wie der Bot durchgeführt.
Eine Vorlage für die Konfiguration ist in der config_template
-Datei zu finden.
Zum Lesen der Konfiguration verwenden wir Gson.
Im code sind die Konfigurationsfelder in der Config
-Klasse lesbar.
Eine Instanz dieser Klasse, die verwendet werden sollte befindet sich in Main.config
.
Um selbst Konfigurationsfelder hinzuzufügen, kann einfach eine Variable in der Config
-Klasse erstellt werden.
Beim Lesen der Konfiguration wird das Feld automatisch mitgelesen, ohne dass du zusätzlichen code schreiben musst.
Bei Konfigurationsfeldern, die für neue Funktionen im bot benötigt werden, sollte vor dem Initialisieren der Funktion überprüft werden, ob das Konfigurationsfeld in der Config
-Klasse einen Wert hat, und falls kein Wert vorhanden ist,
statt dem Initialisieren der Funktion eine Warnung ausgeben.
Wir verwenden eine PostgreSQL Datenbank, um große Datenmengen zu speichern.
Zur Interaktion mit der Datenbank verwenden wir JDBI-Bibliothek.
Um mit der Datenbank zu interagieren wird eine sogenannte DataClass
benötigt. Diese Klasse enthält alle daten die gespeichert werden sollen als Variabeln (es werden nur primitive typen und Strings unterstützt).
public class TestData extends DataClass {
private long guild;
private long user;
private String name;
private boolean vip;
}
Die Primary keys für den Datensatz werden mit @Key
markiert und sollten final sein.
...
@Key
private final long guild;
@Key
private final long user;
...
Um einen Konstruktor für die Klasse zu erstellen, kann @RequiredArgsConstructor
von lombok verwendet werden
@RequiredArgsConstructor
public class TestData extends DataClass {
...
}
Als Tabellen name wird automatisch der Klassen name in klein genutzt.
In diesem fall testdata
.
Der Tabellen name kann aber auch selbst gesetzt werden, durch die nutzung von @Table
.
In diesem beispiel wird der name auf mytable
gesetzt.
@Table(name = "mytable")
@RequiredArgsConstructor
public class TestData extends DataClass {
...
Wenn daten in der DataClass geändert wurden, können diese durch die save()
methode gespeichert werden.
@Table(name = "mytable")
@RequiredArgsConstructor
@Setter
public class TestData extends DataClass {
@Key
private final long guild;
@Key
private final long user;
private String name;
private boolean vip;
}
public class Main {
public static void main(String[] args) {
TestData td = new TestData(123L, 1234L);
td.setName("Paul");
td.save();
}
}
Um die DataClass aus der Datenbank zu laden kann DataClass.load()
verwendet werden.
Als erster parameter wird eine instanz der DataClass über ein Interface übergeben.
Was der DataClass für parameter übergeben werden ist in diesem fall egal.
public class Main {
public static void main(String[] args) {
TestData td = DataClass.load(() -> new DataClass(64236L, 4214L), ...);
}
}
Als zweiter parameter wird eine Map<String, Object>
übergeben.
In dieser Map ist der String der name des key parameters und das Object der Wert, mit dem die Daten gesucht werden sollen.
public class Main {
public static void main(String[] args) {
TestData td = DataClass.load(..., Map.of("guild", 123L, "user", 1234L));
}
}
um aus dem Optional welcher von der load()
methode zurück gegeben wird eine DataClass zu machen, kann die methode orElseGet()
verwendet werden.
Dieser Methode wird einfach eine Instanz der DataClass über ein Interface übergeben, welche zurückgegeben werden soll, wenn keine passenden Daten gefunden wurden.
public class Main {
public static void main(String[] args) {
TestData td = DataClass.load(..., ...).orElseGet(() -> new TestData(123L, 1234L));
}
}
Um kleinere Datenmengen - wie zum Beispiel für Server Konfigurationen - verwenden wir json-files im guild
Ordner.
In diesem Ordner gibt es für jeden Server eine Datei <server id>.json
.
Sie enthält jegliche Konfiguration für den Server.
Um die Daten in Java zu verwenden wird - ähnlich wie bei der Bot Konfiguration - eine Java Klasse mit der gleichen Struktur wie die Datei erstellt, die dann mit den Daten aus der Datei befüllt wird.
Wenn du selbst ein neues Konfigurationsfeld benötigst, kannst du einfach eine Java Variable in der GuildConfig
Klasse erstellen.
Zusätzlich sollte eine getter-Methode erstellt werden, die ein Optional zurückgibt. Dadurch wird das Handhaben von nicht-gesetzten Konfigurationsfeldern vereinfacht.
Wenn du eine neue Konfigurationskategorie hinzufügst, kannst du eine neue Klasse im package com.slimebot.main.config.guild
erstellen und ein Feld mit dieser Klasse als Typ in GuildConfig
hinzufügen.
Dieses Feld sollte die Annotation ConfigCategory
haben, damit automatisch ein Konfigurationsbefehl erstellt wird.
Die tatsächlichen Konfigurationsfelder, entweder in einer Kategorie oder in GuildConfig
selber, sollten die Annotation ConfigField
haben, ebenfalls um automatisch die Konfigurationsbefehle zu erstellen.
Um auf die Konfiguration eines Servers zuzugreifen, kann die Methode GuildConfig#getConfig
verwendet werden.
Discord Befehle erstellen und verarbeiten wir mit der DiscordUtils Bibliothek. Die Bibliothek wird zum Registrieren und Bearbeiten der Befehle verwendet, aber auch um zugehörige Events zu handhaben. Im Folgenden werden die für diesen Bot nötigen Grundlagen erklärt, für detaillierte Informationen kannst du im DiscordUtils Wiki nachschauen.
Einen einfachen Befehl zu erstellen ist recht einfach. So sieht due Grundstruktur aus:
@ApplicationCommand(name = "test", description = "Test Command")
public class TestCommand {
@ApplicationCommandMethod
public void performCommand(SlashCommandInteractionEvent event) {
//Diese Methode wird bei Interaktion mit dem Befehl aufgerufen
}
}
Der Befehl muss anschließend im CommandManager
registriert werden:
.useCommandManager(
...,
config -> {
//...
config.registerCommand(TestCommand.class);
}
)
Um Optionen hinzuzufügen, können parameter mit der @Option
-Annotation zur performCommand
-Methode hinzugefügt werden. Wenn Optionen optional sind und kein Wert angegeben wurde, haben sie beim Aufrufen der Methode der Wert null
.
Optionen mit primitiven typen (wie int
oder boolean
) muss bei optionalen Optionen daher die Wrapper Klasse verwendet werden (int
-> Integer
, boolean
-> Boolean
).
@ApplicationCommand(name = "test", description = "Test Command")
public class TestCommand {
@ApplicationCommandMethod
public void performCommand(SlashCommandInteractionEvent event,
@Option(name = "text", description = "Ein Text") String text,
@Option(name = "anzahl", description = "Anzahl der Wiederholungen", required = false) Integer amount //Optionale Option
) {
if(amount == null) {
amount = 1;
}
event.reply(text.repeat(amount)).setEphemeral(true).queue();
}
}
Sowohl in der performCommand
-Methode, als auch in der setup
-Methode haben Parameter mit den typen DiscordUtils
und CommandManager
Werte mit den aktuellen Instanzen dieser Klassen.
In der performCommand
-Methode können zusätzlich Parameter mit den Typen SlashCommandInteractionEvent
, CommandContext
sowie Parameter mit der @Option
-Annotation verwendet werden.
Der CommandContext wird in diesem Projekt jedoch aktuell nicht verwendet.
Mit den Annotationsparametern guildOnly
und feature
in @ApplicationCommand
kann bestimmt werden, wann und wo Befehle sichtbar sind.
Standardmäßig sind alle Befehle Globalcommands
.
Das bedeutet, dass sie auf allen Servern und in Privatnachrichten verwendet werden können.
Mit dem Parameter guildOnly
werden Befehle auf Server beschränkt und sind in Privatnachrichten Kanälen nicht mehr verwendbar.
Wenn der feature
parameter einen nicht-leeren String als Wert hat, wird der Befehl als Guildcommand
erstellt.
Solche Befehle werden für jeden Server einzeln erstellt, und pro Server nur dann, wenn in der Methode Main#updateGuildCommands
true
in der Map mit dem Wert des feature
Parameters als Key hat.
Dadurch werden Befehle auf Servern nur dann aktiviert, wenn die notwendige Konfiguration für diesen Befehl auf dem Server vorhanden ist.
Wichtig zu beachten ist, dass die Methode Main#updateGuildCommands
bei Guildcommands
immer aufgerufen werden muss, wenn die entsprechende Konfiguration geändert wird,
um daraus hervorgehende Änderungen in der Liste der Befehle auch an Discord zu senden.
Wenn du nach dem Registrieren des Befehls Setup-Code für den Befehl ausführen möchtest, kannst du eine Methode mit der @WhenFinished
-Annotation (mit dem Namen setup
) erstellen.
Dies kann zum Beispiel verwendet werden, um ListComand
s hinzuzufügen. Da diese inherited
Commands sind, können sie nicht ohne weiteres als Subcommands der annotaed
Commands hinzugefügt werden, in der setup Methode können sie
jedoch manuell registriert werden.
Um Interaktions-Events für deine Befehle zu handhaben, können Methoden mit der @Listener
-Annotation erstellt werden.
Beispiel:
@ApplicationCommand(name = "test", description = "Test Command")
public class TestCommand {
@ApplicationCommandMethod
public void performCommand(SlashCommandInteractionEvent event) {
event.replyModal(
Modal.create("test:modal", "Titel")
.addActionRow(TextInput.create("test", "Test", TextInputStyle.SHORT).build())
.build()
).queue();
}
@Listener(type = ModalHandler.class, filter = "test:modal") //Methode wird ausgeführt, wenn ein Nutzer mit einem Modal mit ID "test:modal" interagiert
public void handleModal(ModalInteractionEvent event) {
event.reply(event.getValue("test").getAsString()).setEphemeral(true).queue();
}
}
Für Event Listener, die unabhängig von Befehlen sind, verwenden wir Klassen, die ListenerAdapter
extenden und in der Main
-Klasse als Listener registriert werden.
Für jeden "Themenbereich" wird ein eigener Listener erstellt, in dem die Methoden für die jeweiligen Events überschrieben werden können.