Einen Brot Bot programmieren

Wir lieben Brot!

Und wir wollen wissen, wie es um die Welt des Brotes bestellt ist. Dafür haben wir einen Telegram Chatbot programmiert, welcher auf einem ESP32 läuft und wenn man ihn bittet, das Deutsche Brotinstitut nach der Anzahl der aktuell registrierten Brotsorten fragt.

Programmiert ist das ganze in C++ in VS Code in der PlatformIO Umgebung.

Dazu muss zuerst ein Bot Token bei Telegram angefordert werden. Eine Anleitung dazu findet ihr hier.

Dann muss eine Telegram Bot C++ Library heruntergeladen werden. Wir verwenden hier die Universal Arduino Telegram Bot Library von Brian Lough, welche hier zu finden ist.

Jetzt zum Code:

Zuerst werden alle Libraries importiert:
#include "WiFi.h"
#include "WiFiClientSecure.h"
#include "UniversalTelegramBot.h"
#include "time.h"
#include "HTTPClient.h"
#include "string.h"
#include "regex"

Dann ein paar Variablen festgelegt:
// network settings
#define WIFI_SSID "XXX"
#define WIFI_PASSWORD "XXX"
#define BOT_TOKEN "XXX"
WiFiClientSecure secured_client;
UniversalTelegramBot bot(BOT_TOKEN, secured_client);
char* BrotURL = "https://www.brotinstitut.de/brotkultur";
//time settings
const unsigned long BOT_MTBS = 1000;
unsigned long bot_lasttime;
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3600;
const int daylightOffset_sec = 3600;
int counts;
std::string now;

Die erste Funktion fragt nach der aktuellen Uhrzeit.
std::string getTime () {
//get current time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo;
if(!getLocalTime(&timeinfo)){
Serial.println("Failed to obtain time");
std::string timePrint = "NA";
return(timePrint);
} else {
char buffer[80];
strftime(buffer,sizeof(buffer),"%F %H:%M:%S",&timeinfo);
std::string timePrint(buffer);
//timePrint = (&timeinfo, "%Y-%B-%d %H:%M:%S");
return(timePrint);
}
}

Und die nächste sucht nach der Zahl für die aktuell registrierten Brotsorten, wenn sie mit dem Seitenquelltext gefüttert wird.
std::string regexChar(char* bufferIn, int c) {
std::string regIn;
regIn.assign(bufferIn, c);
std::regex aSearch("class="\"counter\"">([0-9]{1,4})<");
std::smatch am;
if (std::regex_search(regIn, am, aSearch)) {
std::smatch bm;
std::regex bSearch("([0-9]{1,4})");
std::string search2 = am.str();
if (std::regex_search(search2, bm, bSearch)) {
std::string out = bm.str();
return(out);
}
return("NA");
}
return("NA");
}

Dazu wird deine Funktion benötigt, die den Quellcode abfragt und stückchenweise an die regex-Funktion übergibt (der Quellcode der Zielseite ist zu groß, als dass der ESP ihn am Stück verarbeiten kann).
std::string grabHTML(char* URL) {
bool regHit = false;
HTTPClient http;
http.begin(URL);
int httpCode = http.GET();
if(httpCode == HTTP_CODE_OK) {
int len = http.getSize();
char buff[2048] = { 0 };
WiFiClient * stream = http.getStreamPtr();
// read all data from server
while(http.connected() && (len > 0 || len == -1)) {
size_t size = stream->available();
if(size) {
int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size));
//do regex things
std::string regExResult = regexChar(buff, c);
if(regExResult != "NA"){
regHit = true;
http.end();
return(regExResult);
}
if(len > 0) {
len -= c;
}
}
delay(1);
}
}
http.end();
std::string noRegExResult = "NA";
return(noRegExResult);
}

Zu guter Letzt brauchen wir eine Funktion, die sich mit den Telgram-Dingen befasst.
Serial.println("handleNewMessages");
Serial.println(String(numNewMessages));
for (int i = 0; i < numNewMessages; i++) {
String chat_id = bot.messages[i].chat_id;
String text = bot.messages[i].text;
String from_name = bot.messages[i].from_name;
if (from_name == "")
from_name = "Guest";
if (text == "/start" || text == "/help") {
String welcome = "Howdy, " + from_name + "!\n";
welcome += "I am BobBrotBot, the friendly Brot Bot.\n";
welcome += "I am here to give you the current number of breads registered at the German Bread Institute (Brotinstitut.de).\n";
welcome += "You can access this data with the command /brot\n";
welcome += "Access statistics with /info\n\n ";
welcome += "I am currently in version 0.0.3 and I am being developed by Moronaut (moronaut.de)";
bot.sendMessage(chat_id, welcome);
}
if (text == "/brot") {
std::string brotZeit= getTime();
std::string brotCount = grabHTML(BrotURL);
std::string brotmsg = "Hey!\nAt the moment there are " + brotCount + " breads registered!\nTime checked was: " + brotZeit + ".";
String outmsg = brotmsg.c_str();
bot.sendMessage(chat_id, outmsg);
counts += 1;
}
if (text == "/info") {
std::string countStr = std::to_string(counts);
std::string infoStr = "I was booted at " + now + ". And since then I was asked about bread for " + countStr + " times.";
String outStr = infoStr.c_str();
bot.sendMessage(chat_id, outStr);
}
}
}

Im setup dann mit dem WLAN verbinden und die aktuelle Zeit abfragen (für den Befehl /info des Bots).
void setup()
{
Serial.begin(115200);
Serial.println();
// attempt to connect to Wifi network:
Serial.print("Connecting to Wifi SSID ");
Serial.print(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
secured_client.setCACert(TELEGRAM_CERTIFICATE_ROOT); // Add root certificate for api.telegram.org
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.print("\nWiFi connected. IP address: ");
Serial.println(WiFi.localIP());
now = getTime();
}

Zum Schluss in festgelegten Abständen nach neuen Nachrichten fragen.
void loop()
{
if (millis() - bot_lasttime > BOT_MTBS)
{
int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
while (numNewMessages)
{
Serial.println("got response");
handleNewMessages(numNewMessages);
numNewMessages = bot.getUpdates(bot.last_message_received + 1);
}
bot_lasttime = millis();
}
}

Nachdem der Code fertig ist, braucht der Brot Bot noch ein freundliches Gesicht. Also wurde mittels Stable Diffusion ein freundlicher, brotliebender Roboter erstellt.

Fertig ist ein simpler Chatbot für Telegram und so sieht er in Aktion aus:

Den gleichen Bot kann man auch in Python für den PC programmieren, was den Code wesentlich simpler macht. Hier eine etwas abgespeckte Version, die zum Testen der Bot-Tokens verwendet wurde:


import os
import telebot
import urllib.request
import re
from datetime import datetime
BOT_TOKEN="XXX"
bot = telebot.TeleBot(BOT_TOKEN)
@bot.message_handler(commands=['info', 'help', 'hallo', 'hello'])
def send_welcome(message):
bot.reply_to(message, "Howdy! I am BobBrotBot, the nice Brot Bot\n I am here to give you the current number of breads registered at the German Bread Institute (Brotinstitut.de)\n you can access this data with the command brot\n\n\n I am currently in version 0.0.1 and i am being developed by Moronaut (moronaut.de)\n ")
@bot.message_handler(commands=['brot'])
def brotinfo(message):
page = urllib.request.urlopen('https://www.brotinstitut.de/brotkultur')
xml = page.read()
page.close()
text = xml.decode("utf-8")
result = re.search('>\d+<', text).group()
number = result[1:5]
now = datetime.now()
current_time = now.strftime("%Y-%m-%d %H:%M:%S")
s = 'at ' + current_time + ', there are ' + number + ' breads registered.'
bot.reply_to(message, s)
bot.infinity_polling()

Hier gibt es beide Dateien und das Profilbild zum Download