In einem anderen Post wurde ja bereits gezeigt, wie mit Hilfe von discord.js ein Discord Bot programmiert werden kann.
In diesem Post möchte ich hingegen zeigen, wie es ohne großen Aufwand möglich ist, einen Discord Bot ohne Bibliotheken wie discord.py oder discord.js zu programmieren. Dabei werden wir die neuen (und zu diesem Zeitpunkt sich noch in der Beta-Phase befindenden) Slash Commands verwenden.
Wir werden zwei Befehle implementieren: Einen /ping
Befehl, welcher einfach nur mit dem Text “Pong!” antwortet und einen /echo text
, welcher den vom Nutzer gegebenen Text als Nachricht wiederholt.

Vorausgesetzt für dieses Tutorial sind Grundkenntnisse in Python und der aiohttp Bibliothek, welche es uns sowohl ermöglicht einen simplen Webserver in Python zu implementieren, als auch HTTP-Request an die Discord-API zu stellen. Außerdem wären Erfahrung mit der Discord-API und Webservern hilfreich.
Vorbereitung
Bevor wir mit dem Programmieren beginnen können, müssen die benötigten Bibliotheken installiert werden. Ich gehe davon aus, dass Python >=3.5 und pip bereits installiert sind.
Zum installieren der Bibliotheken werden müssen die folgenden Befehle ausgeführt werden:
pip install pynacl
pip install aiohttp
Nun muss noch ein Verzeichnis und eine Python-Datei (.py) für den Bot erstellt werden. Dies kann an einem beliebigen Ort passieren.
Implementierung
Nun kann es an die Implementierung gehen. Ich werde die Implementierung in einzelne Teile unterteilen und am Ende nochmal den gesamte Ergebnis zeigen.
Das Grundgerüst
Im folgenden Code werden alle benötigten Bibliotheken importiert. Viele davon finden jetzt noch keine Anwendung, werden aber später wichtig.
Nun werden vier globale Variablen definiert. Diese können später durch Umgebungsvariablen gesetzt werden. Außerdem werden vier Funktioniert definiert, welche wir in den folgenden Teilen füllen werden.
In den letzten vier Zeilen passiert einiges. Zuerst erstellen wir eine Instanz der web.Application
Klasse, welche zum erstellen des Webservers dient. Nun registrieren wir eine Route, welche durch eine POST request nach /entry
ausgeführt werden soll. Außerdem sagen wir der App, dass create_commands
ausgeführt werden soll, wenn die App gestartet wird.
Als letztes wird der Webserver gestartet und wartet nun unter 127.0.0.1:8080
auf Anfragen.
from aiohttp import web, ClientSession
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
import json
from os import environ
PUBLIC_KEY = VerifyKey(bytes.fromhex(environ["PUBLIC_KEY"]))
CLIENT_ID = environ["CLIENT_ID"]
TOKEN = environ["TOKEN"]
GUILD_ID = environ["GUILD_ID"]
async def ping_command(data):
pass
async def echo_command(data):
pass
async def command_entry(request):
pass
async def create_commands(app):
pass
app = web.Application() # Create a web application
app.add_routes([web.post("/entry", command_entry)]) # Add the command route at /entry
app.on_startup.append(create_commands) # Run create_commands before starting up the app
web.run_app(app, host="127.0.0.1", port=8080) # Start the app and bind to 127.0.0.1:8080
create_commands()
Die create_commands
Funktion wird vor dem starten des Webservers ausgeführt und dient zur Registrierung der Befehle.
Um die Befehle zu registrieren, müssen wir eine HTTP-Anfrage an die Discord API stellen. Wir verwenden dafür den PUT /applications/{client_id}/guilds/{guild_id}/commands
Endpoint, welcher es uns ermöglicht mehrere Befehle für einen Discord Server zu registrieren.
Als erstes erstellen wir eine Liste an Befehlen, welche wir registrieren wollen. Diese Liste muss der Struktur folgen, welche hier beschrieben ist. Am wichtigstes zu wissen ist, dass wir hier zwei Befehle registrieren (“ping” und “echo”). Der zweite Befehl hat ein extra Argument, welches vom Nutzer später gefüllt werden kann.
Um mit Hilfe von aiohttp eine HTTP-Anfrage zu stellen, muss als erstes eine ClientSession
erstellt werden. Mit Hilfe dieser könne wir nun die HTTP-Anfrage stellen. Danach können wir die Session wieder schließen, da wir sich nicht mehr benötigen.
# The JSON-Struktur der Befehle
commands = [
{
"name": "ping",
"description": "Ping? Pong!",
"options": [] # The command doesn't need any arguments
},
{
"name": "echo",
"description": "Let the bot repeat the given text",
"options": [
{
"type": 3, # It's a text argument,
"name": "text",
"description": "The text to repeat",
"required": True,
}
]
}
]
# Wir brauchen eine Session um HTTP-Anfragen zu machen
session = ClientSession()
# Stelle die Anfrage an die discord API
async with session.put(
f"https://discord.com/api/v8/applications/{CLIENT_ID}/guilds/{GUILD_ID}/commands",
headers={"Authorization": f"Bot {TOKEN}"},
json=commands
) as resp:
# Wenn etwas schief läuft, werfe einen Fehler
resp.raise_for_status()
# Wir brauchen die Session nicht mehr
await session.close()
command_entry()
Diese Funktion ist mit Abstand die komplexeste, und wird für jeden eingehenden Befehl aufgerufen. Ihre Aufgabe ist es, die Anfrage zu validieren und ggf. an eine der Befehl-Funktionen (ping_command
/ echo_command
) weiterzuleiten.
Ich werde hier nicht weiter auf die Validierung der Anfrage eingehen. Wichtig zu wissen ist nur, dass wir damit validieren ob die Anfrage tatsächlich von discord kommt. Sollten wir diese Validierung nicht korrekt durchführen, wird discord unseren Bot nicht annehmen.
Nach der Validierung wird der der Inhalt der Anfrage als JSON gelesen. In diesem JSON-Objekt können wir alle wichtigen Information inklusive des Befehlsnamen und Argumente finden. Die genaue Struktur ist hier beschrieben.
Relativ am Ende der Funktion, entscheiden wir anhand des Befehlsnamen, welche Funktion ausgeführt werden. Beim Ausführen der Funktionen geben wir das JSON-Objekt weiter, damit die Funktion Zugriff auf alle Information hat.
# Read the request body as text (str)
raw_data = await request.text()
# Get our header values
signature = request.headers.get("x-signature-ed25519")
timestamp = request.headers.get("x-signature-timestamp")
if signature is None or timestamp is None:
# We can't verify the request without the signature and timestamp
return web.HTTPUnauthorized()
try:
# Verify the signature with our public key
PUBLIC_KEY.verify(f"{timestamp}{raw_data}".encode(), bytes.fromhex(signature))
except BadSignatureError:
# The signature is wrong
return web.HTTPUnauthorized()
# Parse the request body as json
data = json.loads(raw_data)
# Check which interaction type this request is
if data["type"] == 1:
# It's a PING request -> respond with PONG (type 1)
return web.json_response({"type": 1})
elif data["type"] == 2:
# It's a command request -> run the correct command
command = data["data"]
if command["name"] == "ping":
return await ping_command(data)
if command["name"] == "echo":
return await echo_command(data)
else:
# We don't know this command
return web.HTTPNotFound()
else:
# We don't know what to do with this
return web.HTTPBadRequest()
ping_command()
Dies ist wahrscheinlich der einfachste Befehl, welchen man implementieren kann. Das einzige was wir hier machen, ist auf den /ping
mit einer “Pong!” Nachricht zu antworten.
Dabei können wir noch zwei kleine Einstellungen vornehmen: type: 3
teilt discord mit, dass die Befehlsnachricht des Nutzers (/ping
in diesem Fall) nicht angezeigt werden soll und data.flags: 1 << 6
teilt discord mit, dass die “Pong!” Nachricht nur für den Nutzer sichtbar sein soll.
Die Struktur der JSON-Antwort ist hier genauer beschrieben. Nicht wundern, data.flags
ist zu diesem Zeitpunkt noch nicht dokumentiert. 
return web.json_response({
"type": 3, # send a response without showing the command message
"data": {
"content": "Pong!",
"flags": 1 << 6 # Make the respond message only visible to the user than ran the command
}
})
echo_command():
Der Echo-Befehl ist ähnlich einfach. Der Hauptunterschied ist, dass wir den, vom Nutzer übergebenen, Wert aus dem ersten Argument lesen. Dieses haben wir vorher in regsiter_commands
definiert.
Außerdem verwenden wir nun type: 4
und keine message flags. Dies teilt discord mit, dass wir die Befehlsnachricht des Nutzers anzeigen wollen und die Antwort-Nachricht für alle sichtbar sein soll.
command = data["data"]
text_option = command["options"][0] # We know the command always has one option, so it's safe to do this
return web.json_response({
"type": 4, # send a response and show the command message
"data": {
"content": text_option["value"],
}
})
Kofiguration und Testen
Um unseren neuen Discord-Bot nutzen zu können, müssen wir unter https://discord.com/developers/applications eine neue Applikation erstellen. Wie dies im Detail funktioniert, wurde hier gut erklärt.
Nun müssen wir unter “OAuth2” einen Invite-Link erstellen um den bot zu unserem server einzuladen. Wichtig ist hier, dass das applications.commands
scope ausgewählt ist.
Nun müssen die einzelnen Daten wie Token, Publick Key und Client ID von dem Dev Portal in die entsprechenden Umgebungsvariablen übertragen werden. Dies geht ganz einfach mit dem export
befehl. Zum Beispiel: export CLIENT_ID=726526093967884340
.
Außerdem müssen wir die ID des servers, auf welchen wir den Bot eingeladen haben, kopieren und mit Hilfe von export GUILD_ID=...
in die Umgebungsvariablen übertragen.
Nun kann unser Python-Programm das erste mal gestartet werden. Wenn alles glatt geht tauchen keine Fehler auf und das folgende erscheint in der Konsole:
======== Running on http://127.0.0.1:8080 ========
(Press CTRL+C to quit)
Nun muss der Webserver noch richtig konfiguriert werden um HTTPS-Anfragen verarbeiten zu können. Discord setzt ein valides SSL-Zertifikat für die Kommunikation voraus. Dafür gibt es viele verschiedene Möglichkeiten. In den meisten Fällen ist eine Konfiguration hinter einem Reverse-Proxy wie nginx
sinnvoll, welcher die Verschlüsselung übernimmt. Darauf kann ich hier aber nicht genauer eingehen.
Als letztes müsst ihr nun die öffentliche URL unter welcher der Webserver erreichbar ist unter Interactions Endpoint URL
im Devportal eintragen. Wenn alles glatt geht, sollte discord die URL validieren und annehmen.
Nun sind wir endlich fertig. Wenn ihr auf eurem, server /ping
oder /repeat
in die Chatbox eintippt, sollte Discord bereits die Befehle vorschlagen. Fragen gerne in die Kommentare 
Vollständiger Source-Code:
https://github.com/merlinfuchs/slash-command-example