MQTT, Discord und Python gehen in eine Bar! - Teil 2

MQTT, Discord und Python gehen in eine Bar! - Teil 2

Überblick

Ziel dieser Reihe ist das Steuern von MQTT-fähigen Geräten zuhause über Discord-Befehle, sodass keine Cloud-Lösung verwendet werden muss oder irgendwelche Ports nach außen freigegeben werden. ACHTUNG! Was nicht gesteuert werden sollte: Geräte die unbeaufsichtigt Brände verursachen können oder Türen öffnen.
In Teil 2 zeige ich dir die nötigen Schritte um einen Discord-Bot in in Python zu programmieren. Teil 1 findest du hier.

Dauer: ~1 Stunde
Themen: Python, Discord-Bots
Anwendungsgebiet: Lesen, Reagieren auf Kommandos und Antworten in einem Discord-Channel aus Python heraus.
Anmerkung: Es schadet nicht ein paar Vorkenntnisse in Python mitzubringen. Wenn das bei dir nicht der Fall ist, empfehle ich den Code wie Prosa zu lesen und sich nicht zu viele Gedanken über die Details zu machen.

Voraussetzungen

Du musst bereits einen Discord-Account haben, die Registrierung wird hier nicht besprochen. Wie du einen eigenen Server einrichtest, wird weiter unten erklärt.

Python-Umgebung einrichten

Zunächst benötigst du die Python-Umgebung aus dem vorherigen Blogpost. Dazu installierst du als nächstes ein Discord-Paket, namens discord.py,  das dir erlaubt deinen Bot in Python zu programmieren. Achte darauf, dass das Virtual Environment bei der Installation von discord.py aktiviert ist. Den Befehl habe ich vorsichtshalber nochmal aufgeführt:

# windows
.\env\Scripts\activate.ps1

# unix
source env/bin/activate

pip install discord.py

Discord Server anlegen

In deiner Discord-App (oder auf der Website) gibt es ganz unten in der Serverleiste einen Plus-Button, den du drücken kannst, um einen neuen Server zu erstellen. In dem Erstellen-Dialog wählst du "selber erstellen" und danach "Für mich und meine Freunde". Im letzten Schritt gibst du dem Server einen Namen und optional schon ein Server-Logo. Das war schon alles, du hast jetzt deinen eigenen Server, auf dem du das Oberhaupt (mit Krone) bist.

Discord-Bot registrieren

Für den Bot musst du als allererstes eine neue Discord-Anwendung registrieren. Gehe dazu im Discord Developer Portal auf die Seite für Applications. Klicke oben rechts auf New Application und gib deiner Anwendung im modalen Fenster, das aufgeht, einen Namen, ich habe z.B. "MqttBridge" gewählt.

Innerhalb der Anwendung ist der erste Schritt einen Bot anzulegen. Gehe dafür im Menü auf Bot und klicke im Fenster dann auf "Add Bot". Dieser Bot bekommt jetzt den gleichen Namen wie die Anwendung. Der Name kann noch geändert werden und dem Bot kann hier auch ein Aussehen (Avatar) zur Verfügung gestellt werden. Ein Discord Bot muss (wenn er nicht öffentlich bzw. public ist) auf einen Server eingeladen werden. Dafür gehst du unter OAuth2 im Menü auf URL Generator und klickst die folgenden Boxen an:

  • [scopes] bot
  • [bot permissions] Read Messages / View Channels
  • [bot permissions] Send Messages
  • [bot permissions] Send Messages in Threads
  • [bot permissions] Add Reactions

Im unteren Bereich wird in Echtzeit die URL aktualisiert, die du, wenn du alle Berechtigungen gesetzt hast und den Discord-Server im letzten Schritt schon angelegt hast, aufrufen kannst. Auf der Website wird dann gefragt, ob du mit deinem Benutzer dafür bürgst, dass der Bot einem Server beitritt, den du im Dropdown Menü auswählen kannst. Es werden nur Server angezeigt, die dir gehören oder bei denen du die nötigen Berechtigungen hast (z.B. Admin sein).

Sobald der Bot dem Server beigetreten ist, kann es mit der Programmierung losgehen. Deshalb ist hier ein perfekter Moment für Boxenstopp und Trinkpause.

Der kleinste überlebensfähige Discord-Bot

Auf der Developer-Seite, wo du die URL generiert hast, kannst du noch fix im Menü auf Oauth2->General klicken und dort auf "Reset Secret". Du bekommst dann einmalig ein Token, dass du im folgenden Code brauchen wirst.

Den folgenden Code kannst du in eine Datei mit dem Namen bot.py kopieren:

import discord

client = discord.Client()

DISCORD_TOKEN = "......"
command = "!hello"


@client.event
async def on_ready():
    print(f"{client.user} has connected to {client.guilds[0].name}")


@client.event
async def on_message(message):
    if not message.content.startswith("!"):
        return
    if message.content.startswith(command):
        await message.channel.send("Hallo zurück!")
    else:
        await message.channel.send(f"> {message.content}? \nDas kenne ich nicht!")

client.run(DISCORD_TOKEN)

Starte den Bot auf der Kommandozeile python bot.py, wenn der Bot sich verbinden kann, wird auf der Kommandozeile so etwas ausgegeben wie: "MqttBridge#0000 has connected to MeinServer". Ist das der Fall, kannst du jetzt auf deinem Server den Befehl !hello absenden.

Wann immer du den Bot stoppen möchtest, drücke im Terminal Strg+C (bzw. Ctrl+C). Das steht für "cancel", was so viel heißt wie "abbrechen". Wenn du den Bot nur stoppst um ihn gleich wieder neuzustarten (z.B. nach Code-Änderungen, kannst du ganz bequem nach Strg+C einmal die Pfeil-Hoch-Taste und Enter drücken. Dadurch wird der vorhergehende Befehl (Bot starten) nochmal ausgeführt.

Der Code besteht aus 4 Teilen. Der obere Teil importiert die Python-Bibliothek um einen Discord-Bot zu programmieren und definiert Token sowie dein erstes Kommando.

Der zweite Teil definiert in der Funktion "on_ready" was passieren soll, wenn der Bot sich zum Discord-Server verbunden hat. Damit die Python-Bibliothek diese Funktion im Rahmen des Bots aufrufen kann, wird sie mit einem sogenannten Dekorator @client.event registriert. Da diese Bot-Funktionen auf Ereignisse reagieren können sollen, müssen sie mit dem Schlüsselwort async definiert werden. Dadurch können sie sozusagen im Hintergrund gestartet werden.

Der dritte Teil definiert, was passiert, wenn der Bot eine Nachricht liest. Dementsprechend heißt die Funktion on_message. Zeilen 16&17 lassen den Bot einfach nichts tun (und per return die Funktion frühzeitig beenden), wenn der Nachrichteninhalt nicht mit ! beginnt. So kannst du verhindern, dass der Bot auf alle Nachrichten antwortet (auch auf seine eigenen, was zu einer Endlosschleife von Nachrichten führen kann). Zeilen 18&19 überprüfen, ob das oben genannte Kommando gesendet wurde und reagieren mit einer Nachricht in den Channel (in diesem Fall ein "Hallo zurück!"). Im letzten Fall (die Nachricht beginnt mit "!" entspricht aber nicht "!hello") meldet der Bot, dass er diesen Befehl nicht kennt. Hier ist auch ein guter Ort um ggfs. eine Benutzungs-Hilfe auszugeben.

Der vierte Teil besteht nur aus Zeile 23 und startet den eigentlichen Discord-Bot.

Einfache Konfigurierbarkeit von Frage-Antwort-Kommandos

Für bessere Übersicht und eine leichtere Integration wirst du im nächsten Schritt ein paar Konfigurationen in eine separate Datei auslagern. Im gleichen Zug ist es dann auch mit zwei kleinen Änderungen möglich beliebig viele Kommandos (mit vordefinierter Antwort) zu definieren.

Erstelle eine Datei config.py mit folgendem Inhalt

DISCORD_TOKEN = "....."
COMMANDS = {
    "!hello": "Hallo zurück!",
    "!bye" : "Tschüsseldorf!",
    "!rules": "Hier auf dem Server darfst du:\n1. Dinge lernen!\n2. Spaß haben!"
}

Der Unterschied zu der Konfiguration in bot.py ist, dass COMMANDS jetzt ein Dictionary ist. Das ist ein Liste aus Schlüssel-Wert-Paaren, wobei der Schlüssel jeweils das Kommando und der Wert nach dem Doppelpunkt die gewünschte Antwort des Bots ist.

In der Datei bot.py musst du als erstes ganz oben import config einfügen. Dadurch werden die Einträge dort in den aktuellen Kontext geladen. Das heißt du kannst die Zeile in bot.py, die den DISCORD_TOKEN definiert jetzt löschen. Die Zeile command = ... benötigst du auch nicht mehr. Stattdessen lassen wir den Bot bei einer eingehenden Nachricht auf alle Kommandos überprüfen.

Ersetze dafür die beiden Zeilen, die vorhin das Kommando beantwortet haben

    if message.content.startswith(command):
        await message.channel.send("Hallo zurück!")

mit einer Schleife, die die aktuelle Nachricht mit allen bekannten Kommandos vergleicht

    for command,reply in config.COMMANDS.items():
        if message.content.startswith(command):
            await message.channel.send(reply)
            return

Diese Schleife schaut sich jetzt alle in der config.py definierten Kommandos und deren entsprechende Antwort an und sendet letztere, wenn das Kommando übereinstimmt. Wenn ein Kommando erkannt wird, hört der Vergleich hier auch auf (sodass nur eine Antwortnachricht geschickt wird)

Außerdem kannst du das else: entfernen und die Einrückung der nächsten Zeile verringern, weil jetzt alle richtigen Codepfade frühzeitig per return enden und nur im Sonst-Fall die "Fehlernachricht" gesendet wird.

Als letztes musst du das DISCORD_TOKEN (letzte Zeile) jetzt durch config.DISCORD_TOKEN ersetzen.

Wenn du den Bot jetzt startest, sollte er wieder verbinden und die neuen Kommandos !bye und !rules zur Verfügung haben. Wenn du die Config änderst, musst du den Bot anhalten und neustarten, damit die Änderungen in Discord sichtbar sind.

Einschränkung

Dadurch, dass oben .startswith() verwendet wird, kann von zwei Kommandos "!help" und "!help_me" nur eins ausgeführt werden, wenn "!help" gesendet wird. Welches erkannt wird hängt von ein paar verschiedenen Faktoren ab. Als Faustregel kannst du dir also merken, dass kein vollständiges Kommando Teil eines anderen, längeren Kommandos sein sollte.

Das Paket discord.py erlaubt einen schnellen einfachen Einstieg in discord bots in Python, dafür ist es nicht immer auf dem aktuellsten Stand was Funktionalität angeht. Dropdowns und Buttons wurden der Discord API beispielweise hinzugefügt, sind aber in dem Paket noch nicht verfügbar. (Stand 7. Juli 2022)

[Bonus] Systemd Service Datei, um den Bot laufen zu lassen

Um den Bot automatisch auf deinem Raspberry Pi (derselbe der in dieser Reihe den MQTT-Broker stellt) laufen zu lassen, kopiere den Code auf deinen Pi, z.B. nach /home/pi/Services/MqttDiscordBot und lege ein virtuelles Environment an, wie vorher beschrieben.

Unter /etc/systemd/system/mqtt_discord_bot.service kannst du dann eine Datei mit folgendem Inhalt anlegen:

[Unit]
Description=Starts discord bot that reads discord messages and replies with pre-defined answers
After=network.target

[Service]
Type=simple
Restart=on-failure
WorkingDirectory=/home/pi/Services/MqttDiscordBot/
User=pi
ExecStart=/home/pi/Services/MqttDiscordBot/env/bin/python /home/pi/Services/MqttDiscordBot/bot.py

[Install]
WantedBy=multi-user.target

Nachdem das passiert ist, aktivierst du den Autostart und startest den Service das erste Mal mit den folgenden zwei Befehlen

sudo systemctl enable mqtt_discord_bot.service
sudo systemctl start mqtt_discord_bot.service

Diese Datei muss im Teil 3 gegebenenfalls nochmal angepasst werden, sodass in Zeile 10 nicht bot.py aufgerufen wird, sondern die dann relevante Einstiegsdatei, vermutlich main.py.

Zusammenfassung

Discord-Bots zu schreiben ist nicht wirklich schwierig, gerade wenn es darum geht, vordefinierte Antworten zu vordefinierten Anfragen zu schicken. Für alles was darüber hinaus geht, liegt der Teufel im Detail. Bei Interesse lohnt sich ein Blick in die offizielle Dokumentation. Das Paket discord.py erlaubt einen schnellen einfachen Einstieg.

Im 3. Teil wird es neben der Verknüpfung von MQTT-Client und Discord Bot auch um weitere Aufräumarbeiten gehen. Am Ende soll ein Python-Projekt stehen, das nicht mehr programmiert, sondern nur noch konfiguriert werden muss, und dann auf einem deiner Discord-Server Frage-Antwort-Funktion, Sensorwerte und Steuerung für dein Smarthome zur Verfügung stellt. Es geht wieder in ungefähr 2 - 3 Wochen weiter mit dem nächsten Teil.

Wenn es dir bis hierhin Spaß gemacht hat, teile den Post gerne mit Freunden und Bekannten, die sich auch für das Thema interessieren könnten. Wenn nicht, teile es mit deinen ärgsten Feinden :D Um diese kostenlosen Anleitungen zu unterstützen, kauf deine Hardware gerne über meine Affiliate-Links in Teil 1 oder gib mir einen Kaffee auf ko-fi aus (Link im Menü). Bei Fragen schreib mir eine E-Mail an info@ oder auf Twitter.