• Discord Bots

    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.

    result

    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

  • Discord Bots

    Sorry für potentielle typos, ich bin fucking Müde 😄

Ähnliche Themen

  • 10
  • 1
  • 1
  • 1
  • 1