Den Bot einsperren
Damit der Bot nur eingeschränkte Rechte auf dem System erhält, muss das Programm unter einer eigenen Benutzer-ID laufen. Dazu legen Sie zuerst einen neuen Account an, etwa mybot (Listing 2, erste zwei Zeilen). Das Programm (im Beispiel /usr/local/sbin/rpi-bot.py) starten Sie anschließend über einen Systemd-Service mit der Unit-Datei /etc/systemd/system/rpi-bot.service (Listing 3) über die Kommandos aus den beiden letzten Zeilen von Listing 2.
Planen Sie, den Bot Kommandos ans System absetzen zu lassen, geben Sie ihm auf keinen Fall volle Sudo-Rechte: Es genügt, einzelne Kommandos freizuschalten. Dazu legen Sie die Datei /etc/sudoers.d/rpi-bot mit dem Inhalt aus Listing 4 an. Damit darf der Bot per Sudo genau die genannten Befehle ausführen. Der Name der Datei spielt dabei keine Rolle, entscheidend ist der Inhalt. Je nach Sicherungsbedarf bieten sich noch weitere Verfahren an, wie etwa eine Chroot-Umgebung oder ein via Docker teilvirtualisiertes System.
Listing 2
$ sudo groupadd -r mybot $ sudo adduser --disabled-login --gecos "" --system --group --home /var/lib/mybot mybot [... Unit-Datei anlegen ...] $ sudo systemctl enable rpi-bot.service $ sudo systemctl start rpi-bot.service
Listing 3
# Unit-Datei /etc/systemd/system/rpi-bot.service [Unit] Description=Servicedefinition für rpi-bot After=multi-user.target [Service] Type=simple User=rpi-bot ExecStart=/usr/local/sbin/rpi-bot.py [Install] WantedBy=multi-user.target
Listing 4
rpi-bot ALL=(ALL) NOPASSWD: /sbin/halt, /sbin/reboot
Texterkennung
Das Listing 1 stellt das absolute Minimum für einen Bot dar, für den echten Einsatz eignet es sich in dieser Einfachheit aber nicht. Primär fehlt eine Logik, die je nach Nachricht anders reagiert. Die Methode handle() fällt deshalb in realen Anwendungen etwas ausgefeilter aus. Der Bot arbeitet nicht wie ein generischer Assistent vom Schlag Alexa und Co., sondern agiert vielmehr als für einen spezifischen Einsatzzweck maßgeschneidertes Programm. Dazu genügt es, die Zeichenketten mittels regulärer Ausdrücke zu untersuchen.
Die Zeilen 39 bis 46 und 96 bis 100 aus Listing 5 verdeutlichen das Prinzip. Das Programm namens callbackbot.py erstellt eine Liste mit Paaren von regulären Ausdrücken und dazugehörigen Funktionen (Zeilen 39 bis 46). Aus Leistungsgründen kompiliert das Programm dabei die Ausdrücke gleich, wandelt sie also in eine effiziente interne Form um. Das abgedruckte Listing zeigt einen Ausschnitt des Skripts, auf das sich die Zeilennummern im Text beziehen. Das vollständige Skript finden Sie auf der Heft-DVD.
Die Befehle ab Zeile 96 verarbeiten jede eingehende Nachricht. Dabei vergleicht das Skript den extrahierten Text der Reihe nach mit allen Ausdrücken in der Liste. Bei einem Treffer ruft Zeile 98 dann die zugeordnete Funktion auf. Falls keiner der regulären Ausdrücke passt, führt das Programm in Zeile 100 eine weitere Methode aus.
Listing 5
#!/usr/bin/python
# -*- coding: utf-8 -*-
import re, signal, os, pprint, json
import telepot
from telepot.loop import MessageLoop
from telepot.namedtuple import InlineKeyboardMarkup, InlineKeyboardButton
# Konstanten (IDs vom Bot und von mir)
BOT_ID = 'xxx'
MY_ID = 'yyyy'
# --- Verarbeitungsfunktionen ---
def do_start(chat_id,text):
bot.sendMessage(chat_id,"/start gewählt")
[...]
def do_reboot(chat_id,text):
zeilen = []
zeile = [InlineKeyboardButton(text='Nein',
callback_data='{"name": "do_reboot", "wert": "nein"}'),
InlineKeyboardButton(text='Ja',
callback_data='{"name": "do_reboot", "wert": "ja"}'),
]
zeilen.append(zeile)
keyboard = InlineKeyboardMarkup(inline_keyboard=zeilen)
bot.sendMessage(chat_id,"Wirklich neu starten?",reply_markup=keyboard)
# --- Zuordnung Text->Funktion ---
HANDLER = [
(re.compile("/start"),do_start),
[...]
(re.compile("/reboot"),do_reboot),
]
# --- Callback-Handler für Reboot ---
def do_reboot_callback(query_id,chat_id,wert):
bot.answerCallbackQuery(query_id)
if wert == 'nein':
bot.sendMessage(chat_id,"Reboot abgebrochen")
else:
bot.sendMessage(chat_id,"Neustart ausgeführt")
os.system("sudo /sbin/reboot &")
os.kill(os.getpid(), signal.SIGINT)
def do_fallback(chat_id,text):
bot.sendMessage(chat_id,"fallback-Handler")
# --- Eingehende Callback-Messages verarbeiten ---
def handle_callback(query):
if query['from']['id'] != MY_ID:
return
query_id = query['id']
chat_id = query['message']['chat']['id']
callback_data = query['data']
# Daten aus Callback extrahieren
cb_info = json.loads(callback_data)
# Funktionsname erstellen (im Attribut "name")
func_name = "%s_callback" % cb_info['name']
func = globals()[func_name]
# Funktion aufrufen
func(query_id,chat_id,cb_info['wert'])
# --- Eingehende Text-Messages verarbeiten ---
def handle_text(msg):
pprint.pprint(msg)
chat = msg['chat']
# Prüfen, ob die Meldung privat ist und von "mir" kommt
if chat['type'] != "private" or msg['from']['id'] != MY_ID:
print "Message wird ignoriert!"
return
chat_id = chat['id']
text = msg['text']
# Meldungen mit vordefinierten Regex vergleichen
for (regex,handler) in HANDLER:
if regex.match(text):
handler(chat_id,text)
return
do_fallback(chat_id,text)
# --- Hauptprogramm ---
bot = telepot.Bot(BOT_ID)
MessageLoop(bot, {'chat': handle_text,
'callback_query': handle_callback}).run_as_thread()
signal.pause()
Mit diesem einfachen Mittel kommen Sie schon sehr weit. Komplizierte Dialoge erfordern aber, dass der Bot den aktuellen Status (“Wo stehe ich im Dialog?”) speichert. Hierfür eignet sich eine Implementation als Klasse am besten.
Der einfache Loop aus Listing 1 kann auch nicht mit mehreren Benutzern parallel chatten. Zwar unterstützt die Telepot-Bibliothek diesen Fall, bei dem pro Gegenseite dann ein eigener Thread startet, für den vorliegenden Einsatz (das Steuern eines Rechners) ist das aber nicht notwendig.
Kommandos
In den einleitenden Screenshots und im Listing 5 tauchen “Chat-Nachrichten” auf, die mit einem Schrägstrich beginnen, die sogenannten Kommandos. Der BotFather erlaubt, Kommandos für den eigenen Bot zu registrieren, auch nachträglich. Dazu stellt er das Kommando /setcommands bereit. Sie müssen allerdings sämtliche Kommandos auf einmal registrieren: Der Befehl /setcommands fragt nach einer Liste mit je einem Kommando pro Zeile.
Versteht der eigene Bot dann Kommandos, fügt Telegram einen Button mit / rechts ins Telegram-Eingabefeld ein. Tippen Sie darauf oder geben den Schrägstrich ins Eingabefeld des Chats ein, zeigt der Server alle definierten Kommandos an (Abbildung 4). Kommandos in Nachrichten markiert der Dienst ebenfalls als aktive Flächen. Beides spart viel Tipparbeit.
Eine Reihe von Kommandos sollte jeder Bot implementieren, wie etwa /start, /help und /settings. Aber Vorsicht: Alle registrierten Kommandos stehen öffentlich bereit. Wer zufällig auf einen Bot stößt, der ein Reboot-Kommando anbietet, mag sich dazu verleitet sehen, diesen auszulösen. Die Prüfung der Benutzer-ID, wie oben beschrieben, gehört hier also zum absoluten Minimum an Schutzmaßnahmen.
Callbacks und Tastaturen
Telegram bietet noch eine zweite Möglichkeit, dem Anwender Tipparbeit zu ersparen: Statt eines Texts als Antwort schickt der Bot eine virtuelle Tastatur mit Schaltflächen. Der Bot im Beispiel übernimmt die Aufgabe, das System herunterzufahren oder neu zu starten. Auf die entsprechenden Befehle reagiert er mit einer Rückfrage, die Sie mit Ja oder Nein bestätigen (Abbildung 5).






