commit b93b745cf07901500d15ca008ba261c7b0efcc2d Author: klux2 Date: Sun Mar 17 18:48:05 2019 +0100 initial commit Signed-off-by: klux2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3b8857 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ + +# Created by https://www.gitignore.io/api/pycharm+iml +# Edit at https://www.gitignore.io/?templates=pycharm+iml + +### PyCharm+iml ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+iml Patch ### +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# End of https://www.gitignore.io/api/pycharm+iml + +*.log \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..15a15b2 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Daemon.py b/Daemon.py new file mode 100644 index 0000000..534525d --- /dev/null +++ b/Daemon.py @@ -0,0 +1,127 @@ +"""Generic linux daemon base class for python 3.x.""" + +import atexit +import os +import signal +import sys +import time + + +# noinspection SpellCheckingInspection +class Daemon: + """A generic daemon class. + + Usage: subclass the daemon class and override the run() method.""" + + def __init__(self, pidfile): + self.pidfile = pidfile + + def daemonize(self): + """Deamonize class. UNIX double fork mechanism.""" + + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #1 failed: {0}\n'.format(err)) + sys.exit(1) + + # decouple from parent environment + os.chdir('/') + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #2 failed: {0}\n'.format(err)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + + pid = str(os.getpid()) + with open(self.pidfile, 'w+') as f: + f.write(pid + '\n') + + def delpid(self): + os.remove(self.pidfile) + + def start(self): + """Start the daemon.""" + + # Check for a pidfile to see if the daemon already runs + try: + with open(self.pidfile, 'r') as pf: + + pid = int(pf.read().strip()) + except IOError: + pid = None + + if pid: + message = "pidfile {0} already exist. " + \ + "Daemon already running?\n" + sys.stderr.write(message.format(self.pidfile)) + sys.exit(1) + + # Start the daemon + self.daemonize() + self.run() + + def stop(self): + """Stop the daemon.""" + + # Get the pid from the pidfile + try: + with open(self.pidfile, 'r') as pf: + pid = int(pf.read().strip()) + except IOError: + pid = None + + if not pid: + message = "pidfile {0} does not exist. " + \ + "Daemon not running?\n" + sys.stderr.write(message.format(self.pidfile)) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, signal.SIGTERM) + time.sleep(0.1) + except OSError as err: + e = str(err.args) + if e.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print(str(err.args)) + sys.exit(1) + + def restart(self): + """Restart the daemon.""" + self.stop() + self.start() + + def run(self): + """You should override this method when you subclass Daemon. + + It will be called after the process has been daemonized by + start() or restart().""" diff --git a/HSMensaW_bot.py b/HSMensaW_bot.py new file mode 100644 index 0000000..9c41ee7 --- /dev/null +++ b/HSMensaW_bot.py @@ -0,0 +1,202 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import datetime +import json +import signal +import ssl +import sys +import urllib.request +import xml.etree.ElementTree as ET +from time import sleep +from urllib.error import HTTPError + +import telepot +from telepot.loop import MessageLoop + + +class Essen: + def __init__(self, name, preis, kategorie): + self.name = name + self.preis = preis + self.kategorie = kategorie + + def __str__(self): + return str("*%s*: %s (%.2f €)" % (self.kategorie, self.name, self.preis)) + + +config_filename = "config.json" +monate = ["Januar", "Februar", "März", "April", "Mai", "Juni", "August", "September", "Oktober", "November", "Dezember"] +wochentage = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] +status = "" + + +def handle(msg): + global essen, status + + content_type, chat_type, chat_id = telepot.glance(msg) + if content_type == "text": + text = str(msg["text"]).lower() + if text.startswith("/start"): + if chat_id not in ids: + ids.append(chat_id) + bot.sendMessage(chat_id, "Bot wurde aktiviert") + + chat = get_chat_name(msg) + send_status("Bot aktiviert für Chat %s (ID: %i)" % (chat, chat_id)) + + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + else: + bot.sendMessage(chat_id, "Bot war bereits aktiviert") + elif text.startswith("/stop"): + if chat_id in ids: + ids.remove(chat_id) + bot.sendMessage(chat_id, "Bot wurde deaktiviert") + + chat = get_chat_name(msg) + send_status("Bot deaktiviert für Chat %s (ID: %i)" % (chat, chat_id)) + + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + else: + bot.sendMessage(chat_id, "Bot war nicht aktiv") + elif text.startswith("/essen") or text.startswith("/mensa") or text.startswith("/speiseplan"): + + chat = get_chat_name(msg) + send_status("Essen angefordert für Chat %s (ID: %i)" % (chat, chat_id)) + + var = get_essen() + if len(essen) == 0: + if var: + bot.sendMessage(chat_id, "Es ist ein Fehler aufgetreten. Bitte später erneut versuchen.") + else: + bot.sendMessage(chat_id, "Für heute ist leider kein Speiseplan verfügbar.") + else: + send_essen(chat_id) + send_status("Essen versendet für Chat %s (ID: %i)" % (chat, chat_id)) + elif text.startswith("/status") and chat_id in config_ids: + bot.sendMessage(chat_id, status) + elif content_type == "new_chat_member": + if msg["new_chat_participant"]["id"] == bot.getMe()["id"]: + bot.sendMessage(chat_id, "*Mensa-Bot der Hochschule Mittweida (beta)*\nDieser Bot versendet jeden Tag um " + "8 Uhr den aktuellen Mensa-Speiseplan. Er wird über /start für den aktuellen Chat " + "oder die aktuelle Gruppe gestartet, /stop beendet ihn wieder. Mit /essen, /mensa " + "und /speiseplan kann der aktuelle Speiseplan manuell abgerufen werden.\n\n" + "_Haftungsausschluss: Dieser Bot steht in keiner Verbindung mit der Hochschule " + "Mittweida oder dem Studentenwerk Freiberg. Alle Angaben ohne Gewähr._", + "markdown") + elif content_type == "left_chat_member": + if msg["left_chat_participant"]["id"] == bot.getMe()["id"]: + if chat_id in ids: + ids.remove(chat_id) + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + + +def send_essen(chat_id): + global datum, essen + nachricht = "Speiseplan am WOCHENTAG, den %s:\n" % datum.strftime("%d. MONAT %Y") + nachricht = nachricht.replace("MONAT", monate[(datum.month - 1) % 12]) + nachricht = nachricht.replace("WOCHENTAG", wochentage[datum.weekday()]) + for i in essen: + nachricht += "- " + str(i).replace(".", ",") + "\n\n" + bot.sendMessage(chat_id, nachricht, "markdown") + + +def send_status(text): + global config_ids + for chat_id in config_ids: + bot.sendMessage(chat_id, text) + + +def get_essen(): + global datum, essen, ctx + essen = [] + try: + # response = urllib.request.urlopen(url, context=ctx) + response = urllib.request.urlopen(url) + except HTTPError: + return False + except Exception as expt: + send_status("Es ist ein Fehler aufgetreten:\n%s" % str(expt)) + print("Fehler:\n%s" % str(expt)) + return True + data = response.read() + response.close() + text = data.decode('utf-8') + + et = ET.fromstring(text) + date = et.findall("./menus/day/date")[0].text + datum = datetime.date(int(date[:4]), int(date[5:7]), int(date[8:10])) + menus = et.findall("./menus/day/menu") + + for i in menus: + kategorie = i.findall("type")[0].text + name = i.findall("description")[0].text.strip("()1234567890, ") + preis = float(i.findall("./prices/price[label='Studenten']/value")[0].text.replace(",", ".")) + essen.append(Essen(name, preis, kategorie)) + + +def get_chat_name(msg): + if msg["chat"]["type"] == "group": + chat = msg["chat"]["title"] + " (g)" + elif msg["chat"]["type"] == "private": + chat = msg["chat"]["first_name"] + " " + msg["chat"]["last_name"] + " (p)" + else: + chat = "null" + return chat + + +def shutdown(signum, frame): + print('Signal handler called with signal', signum) + sys.stdout.close() + sys.stderr.close() + sys.exit(-1) + + +sys.stdout = open("out.log", "a") +sys.stderr = open("err.log", "a") +signal.signal(signal.SIGTERM, shutdown) + +try: + with open(config_filename, 'r') as config_file: + config = json.load(config_file) +except FileNotFoundError as e: + print('Error: config file "{}" not found: {}'.format(config_filename, e)) + sys.exit(-1) +except ValueError as e: + print('Error: invalid config file "{}": {}'.format(config_filename, e)) + sys.exit(-1) + +telegram_bot_token = config.get("telegram_bot_token") +url = config.get("url") +ids = config.get("ids") +config_ids = config.get("config_ids") + +bot = telepot.Bot(telegram_bot_token) + +MessageLoop(bot, handle).run_as_thread() + +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE + +while True: + now = datetime.datetime.today() + nextDay = datetime.datetime(now.year, now.month, now.day) + datetime.timedelta(1, 28800) + send_status("Wartezeit: %i Sekunden" % (nextDay - now).seconds) + status = "Warten bis %s" % nextDay + sleep((nextDay - now).seconds) + send_status("Aufwachen um 8 Uhr") + status = "Essen abrufen" + get_essen() + send_status("%i Essen gefunden" % len(essen)) + status = "Essen senden" + if len(essen) > 0: + send_status("Essen werden gesendet") + for i in ids: + send_essen(i) + send_status("Abgeschlossen, warte 30 Sekunden") + status = "Warte 30 Sekunden" + sleep(30) diff --git a/HSMensaW_botA.py b/HSMensaW_botA.py new file mode 100644 index 0000000..2b93c09 --- /dev/null +++ b/HSMensaW_botA.py @@ -0,0 +1,252 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import datetime +import json +import signal +import sys +import urllib.request +import xml.etree.ElementTree as ET +from urllib.error import HTTPError + +import telepot +from telepot.aio.delegate import per_chat_id, create_open, pave_event_space +from telepot.aio.loop import MessageLoop + + +class Essen: + def __init__(self, name, preis, kategorie): + self.name = name + self.preis = preis + self.kategorie = kategorie + + def __str__(self): + if self.preis > 0: + return str("*%s*: %s (%.2f €)" % (self.kategorie, self.name, self.preis)) + else: + return str("*%s*: %s" % (self.kategorie, self.name)) + + +config_filename = "config.json" +monate = ["Januar", "Februar", "März", "April", "Mai", "Juni", "August", "September", "Oktober", "November", "Dezember"] +wochentage = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] +info_str = "*Mensa-Bot der Hochschule Mittweida (beta)*\nDieser Bot versendet jeden Tag um 10 Uhr den aktuellen " \ + "Mensa-Speiseplan. Er wird über /start für den aktuellen Chat oder die aktuelle Gruppe gestartet, /stop " \ + "beendet ihn wieder. Mit /essen, /mensa und /speiseplan kann der aktuelle Speiseplan manuell abgerufen " \ + "werden.\n\n_Haftungsausschluss: Dieser Bot steht in keiner Verbindung mit der Hochschule Mittweida oder" \ + " dem Studentenwerk Freiberg. Alle Angaben ohne Gewähr._" +status = "" + + +class HSMensaW(telepot.aio.helper.ChatHandler): + async def on_chat_message(self, msg): + global status, config + + content_type, chat_type, chat_id = telepot.glance(msg) + if content_type == "text": + text = str(msg["text"]).lower() + + if text.startswith("/start"): + if chat_id not in ids: + ids.append(chat_id) + await bot.sendMessage(chat_id, "Bot wurde aktiviert") + + chat = get_chat_name(msg) + await send_status("Bot aktiviert für Chat %s (ID: %i)" % (chat, chat_id)) + + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + else: + await bot.sendMessage(chat_id, "Bot war bereits aktiviert") + + elif text.startswith("/stop"): + if chat_id in ids: + ids.remove(chat_id) + await bot.sendMessage(chat_id, "Bot wurde deaktiviert") + + chat = get_chat_name(msg) + await send_status("Bot deaktiviert für Chat %s (ID: %i)" % (chat, chat_id)) + + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + else: + await bot.sendMessage(chat_id, "Bot war nicht aktiv") + + elif text.startswith("/essen") or text.startswith("/mensa") or text.startswith("/speiseplan"): + chat = get_chat_name(msg) + await send_status("Essen angefordert für Chat %s (ID: %i)" % (chat, chat_id)) + + loop = asyncio.get_event_loop() + fut = asyncio.Future() + loop.run_until_complete(get_essen(fut)) + (datum, essen) = fut.result() + # datum, essen = loop.run_until_complete(get_essen()) + if type(essen) == bool: + if essen: + await bot.sendMessage(chat_id, "Es ist ein Fehler aufgetreten. Bitte später erneut versuchen.") + else: + await bot.sendMessage(chat_id, "Für heute ist leider kein Speiseplan verfügbar.") + else: + await send_essen(chat_id, datum, essen) + await send_status("Essen versendet für Chat %s (ID: %i)" % (chat, chat_id)) + + elif text.startswith("/status") and chat_id in config_ids: + await bot.sendMessage(chat_id, status) + + elif text.startswith("/info"): + await bot.sendMessage(chat_id, info_str, "markdown") + + elif content_type == "new_chat_member": + if msg["new_chat_participant"]["id"] == get_bot_id(): + await bot.sendMessage(chat_id, info_str, "markdown") + + elif content_type == "left_chat_member": + if msg["left_chat_participant"]["id"] == get_bot_id(): + if chat_id in ids: + ids.remove(chat_id) + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + + +async def send_essen(chat_id, datum, essen): + nachricht = "Speiseplan am WOCHENTAG, den %s:\n" % datum.strftime("%d. MONAT %Y") + nachricht = nachricht.replace("MONAT", monate[(datum.month - 1) % 12]) + nachricht = nachricht.replace("WOCHENTAG", wochentage[datum.weekday()]) + for i in essen: + nachricht += "- " + str(i).replace(".", ",") + "\n\n" + await bot.sendMessage(chat_id, nachricht, "markdown") + + +async def send_status(text): + global config_ids + for chat_id in config_ids: + await bot.sendMessage(chat_id, text) + + +async def get_essen(future): + # global ctx + essen = [] + datum = datetime.date.today() + try: + # response = urllib.request.urlopen(url, context=ctx) + response = urllib.request.urlopen(url) + except HTTPError: + future.set_result((datum, False)) + return + except Exception as expt: + await send_status("Es ist ein Fehler aufgetreten:\n%s" % str(expt)) + print("Fehler:\n%s" % str(expt)) + future.set_result((datum, True)) + return + data = response.read() + response.close() + text = data.decode('utf-8') + + et = ET.fromstring(text) + date = et.findall("./menus/day/date")[0].text + datum = datetime.date(int(date[:4]), int(date[5:7]), int(date[8:10])) + menus = et.findall("./menus/day/menu") + + for i in menus: + kategorie = i.findall("type")[0].text + name = i.findall("description")[0].text.strip("()1234567890, ") + try: + preis = float(i.findall("./prices/price[label='Studenten']/value")[0].text.replace(",", ".")) + except ValueError: + preis = -1 + essen.append(Essen(name, preis, kategorie)) + + future.set_result((datum, essen)) + return + + +def get_bot_id(): + api_url = "https://api.telegram.org/bot" + telegram_bot_token + "/getMe" + with urllib.request.urlopen(api_url) as response: + data = json.load(response) + bot_id = data["result"]["id"] + return bot_id + + +def get_chat_name(msg): + if msg["chat"]["type"] == "group": + chat = msg["chat"]["title"] + " (g)" + elif msg["chat"]["type"] == "private": + chat = msg["chat"]["first_name"] + " " + msg["chat"]["last_name"] + " (p)" + else: + chat = "null" + return chat + + +def shutdown(signum, frame): + print('Signal handler called with signal', signum) + ml.close() + loop.stop() + loop.close() + sys.stdout.close() + sys.stderr.close() + sys.exit(-1) + + +async def essen_loop(): + global status + while True: + now = datetime.datetime.today() + next_day = datetime.datetime(now.year, now.month, now.day) + datetime.timedelta(1, 36000) + await send_status("Wartezeit: %i Sekunden" % (next_day - now).seconds) + status = "Warten bis %s" % next_day + await asyncio.sleep((next_day - now).seconds) + await send_status("Aufwachen um 10 Uhr") + status = "Essen abrufen" + datum, essen = get_essen() + await send_status("%i Essen gefunden" % len(essen)) + status = "Essen senden" + if type(essen) == list and len(essen) > 0: + await send_status("Essen werden gesendet") + for i in ids: + await send_essen(i, datum, essen) + await send_status("Abgeschlossen, warte 30 Sekunden") + status = "Warte 30 Sekunden" + await asyncio.sleep(30) + + +sys.stdout = open("out.log", "a") +sys.stderr = open("err.log", "a") +signal.signal(signal.SIGTERM, shutdown) +signal.signal(signal.SIGINT, shutdown) + +try: + with open(config_filename, 'r') as config_file: + config = json.load(config_file) +except FileNotFoundError as e: + print('Error: config file "{}" not found: {}'.format(config_filename, e)) + sys.exit(-1) +except ValueError as e: + print('Error: invalid config file "{}": {}'.format(config_filename, e)) + sys.exit(-1) + + +telegram_bot_token = config.get("telegram_bot_token") +url = config.get("url") +ids = config.get("ids") +config_ids = config.get("config_ids") + +bot = telepot.aio.DelegatorBot(telegram_bot_token, [ + pave_event_space()( + per_chat_id(), create_open, HSMensaW, timeout=10 + ), +]) + +loop = asyncio.get_event_loop() + +# ml = MessageLoop(bot, handle).run_forever() +ml = MessageLoop(bot).run_forever() + +loop.create_task(ml) +loop.create_task(essen_loop()) + +loop.run_forever() + +# ctx = ssl.create_default_context() +# ctx.check_hostname = False +# ctx.verify_mode = ssl.CERT_NONE diff --git a/HSMensaW_botA.py.BAK b/HSMensaW_botA.py.BAK new file mode 100644 index 0000000..6d07a81 --- /dev/null +++ b/HSMensaW_botA.py.BAK @@ -0,0 +1,249 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import datetime +import json +import signal +import sys +import urllib.request +import xml.etree.ElementTree as ET +from urllib.error import HTTPError + +import telepot +from telepot.aio.delegate import per_chat_id, create_open, pave_event_space +from telepot.aio.loop import MessageLoop + + +class Essen: + def __init__(self, name, preis, kategorie): + self.name = name + self.preis = preis + self.kategorie = kategorie + + def __str__(self): + if self.preis > 0: + return str("*%s*: %s (%.2f €)" % (self.kategorie, self.name, self.preis)) + else: + return str("*%s*: %s" % (self.kategorie, self.name)) + + +config_filename = "config.json" +monate = ["Januar", "Februar", "März", "April", "Mai", "Juni", "August", "September", "Oktober", "November", "Dezember"] +wochentage = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] +info_str = "*Mensa-Bot der Hochschule Mittweida (beta)*\nDieser Bot versendet jeden Tag um 10 Uhr den aktuellen " \ + "Mensa-Speiseplan. Er wird über /start für den aktuellen Chat oder die aktuelle Gruppe gestartet, /stop " \ + "beendet ihn wieder. Mit /essen, /mensa und /speiseplan kann der aktuelle Speiseplan manuell abgerufen " \ + "werden.\n\n_Haftungsausschluss: Dieser Bot steht in keiner Verbindung mit der Hochschule Mittweida oder" \ + " dem Studentenwerk Freiberg. Alle Angaben ohne Gewähr._" +status = "" +essen = [] +var = True + + +class HSMensaW(telepot.aio.helper.ChatHandler): + async def on_chat_message(self, msg): + global config, essen, status, var + + content_type, chat_type, chat_id = telepot.glance(msg) + if content_type == "text": + text = str(msg["text"]).lower() + + if text.startswith("/start"): + if chat_id not in ids: + ids.append(chat_id) + config['ids'] = ids + await bot.sendMessage(chat_id, "Bot wurde aktiviert") + + chat = get_chat_name(msg) + await send_status("Bot aktiviert für Chat %s (ID: %i)" % (chat, chat_id)) + + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + else: + await bot.sendMessage(chat_id, "Bot war bereits aktiviert") + + elif text.startswith("/stop"): + if chat_id in ids: + ids.remove(chat_id) + config['ids'] = ids + await bot.sendMessage(chat_id, "Bot wurde deaktiviert") + + chat = get_chat_name(msg) + await send_status("Bot deaktiviert für Chat %s (ID: %i)" % (chat, chat_id)) + + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + else: + await bot.sendMessage(chat_id, "Bot war nicht aktiv") + + elif text.startswith("/essen") or text.startswith("/mensa") or text.startswith("/speiseplan"): + chat = get_chat_name(msg) + await send_status("Essen angefordert für Chat %s (ID: %i)" % (chat, chat_id)) + + await get_essen() + if len(essen) == 0: + if var: + await bot.sendMessage(chat_id, "Es ist ein Fehler aufgetreten. Bitte später erneut versuchen.") + else: + await bot.sendMessage(chat_id, "Für heute ist leider kein Speiseplan verfügbar.") + else: + await send_essen(chat_id) + await send_status("Essen versendet für Chat %s (ID: %i)" % (chat, chat_id)) + + elif text.startswith("/status") and chat_id in config_ids: + await bot.sendMessage(chat_id, status) + + elif text.startswith("/info"): + await bot.sendMessage(chat_id, info_str, "markdown") + + elif content_type == "new_chat_member": + if msg["new_chat_participant"]["id"] == get_bot_id(): + await bot.sendMessage(chat_id, info_str, "markdown") + + elif content_type == "left_chat_member": + if msg["left_chat_participant"]["id"] == get_bot_id(): + if chat_id in ids: + ids.remove(chat_id) + config['ids'] = ids + with open(config_filename, 'w') as outfile: + json.dump(config, outfile) + + +async def send_essen(chat_id): + global datum, essen + nachricht = "Speiseplan am WOCHENTAG, den %s:\n" % datum.strftime("%d. MONAT %Y") + nachricht = nachricht.replace("MONAT", monate[(datum.month - 1) % 12]) + nachricht = nachricht.replace("WOCHENTAG", wochentage[datum.weekday()]) + for i in essen: + nachricht += "- " + str(i).replace(".", ",") + "\n\n" + await bot.sendMessage(chat_id, nachricht, "markdown") + + +async def send_status(text): + global config_ids + for chat_id in config_ids: + await bot.sendMessage(chat_id, text) + + +async def get_essen(): + global datum, essen, var # , ctx + essen = [] + try: + # response = urllib.request.urlopen(url, context=ctx) + response = urllib.request.urlopen(url) + except HTTPError: + var = False + return + except Exception as expt: + await send_status("Es ist ein Fehler aufgetreten:\n%s" % str(expt)) + print("Fehler:\n%s" % str(expt)) + var = True + return + data = response.read() + response.close() + text = data.decode('utf-8') + + et = ET.fromstring(text) + date = et.findall("./menus/day/date")[0].text + datum = datetime.date(int(date[:4]), int(date[5:7]), int(date[8:10])) + menus = et.findall("./menus/day/menu") + + for i in menus: + kategorie = i.findall("type")[0].text + name = i.findall("description")[0].text.strip("()1234567890, ") + try: + preis = float(i.findall("./prices/price[label='Studenten']/value")[0].text.replace(",", ".")) + except ValueError: + preis = -1 + essen.append(Essen(name, preis, kategorie)) + + +def get_bot_id(): + api_url = "https://api.telegram.org/bot" + telegram_bot_token + "/getMe" + with urllib.request.urlopen(api_url) as response: + data = json.load(response) + bot_id = data["result"]["id"] + return bot_id + + +def get_chat_name(msg): + if msg["chat"]["type"] == "group": + chat = msg["chat"]["title"] + " (g)" + elif msg["chat"]["type"] == "private": + chat = msg["chat"]["first_name"] + " " + msg["chat"]["last_name"] + " (p)" + else: + chat = "null" + return chat + + +def shutdown(signum, frame): + print('Signal handler called with signal', signum) + ml.close() + loop.stop() + loop.close() + sys.stdout.close() + sys.stderr.close() + sys.exit(-1) + + +async def essen_loop(): + global status, essen, ids + while True: + now = datetime.datetime.today() + next_day = datetime.datetime(now.year, now.month, now.day) + datetime.timedelta(1, 36000) + await send_status("Wartezeit: %i Sekunden" % (next_day - now).seconds) + status = "Warten bis %s" % next_day + await asyncio.sleep((next_day - now).seconds) + await send_status("Aufwachen um 10 Uhr") + status = "Essen abrufen" + await get_essen() + await send_status("%i Essen gefunden" % len(essen)) + status = "Essen senden" + if len(essen) > 0: + await send_status("Essen werden gesendet") + for i in ids: + await send_essen(i) + await send_status("Abgeschlossen, warte 30 Sekunden") + status = "Warte 30 Sekunden" + await asyncio.sleep(30) + + +sys.stdout = open("out.log", "a") +sys.stderr = open("err.log", "a") +signal.signal(signal.SIGTERM, shutdown) +signal.signal(signal.SIGINT, shutdown) + +try: + with open(config_filename, 'r') as config_file: + config = json.load(config_file) +except FileNotFoundError as e: + print('Error: config file "{}" not found: {}'.format(config_filename, e)) + sys.exit(-1) +except ValueError as e: + print('Error: invalid config file "{}": {}'.format(config_filename, e)) + sys.exit(-1) + +telegram_bot_token = config.get("telegram_bot_token") +url = config.get("url") +ids = config.get("ids") +config_ids = config.get("config_ids") + +bot = telepot.aio.DelegatorBot(telegram_bot_token, [ + pave_event_space()( + per_chat_id(), create_open, HSMensaW, timeout=10 + ), +]) + +loop = asyncio.get_event_loop() + +# ml = MessageLoop(bot, handle).run_forever() +ml = MessageLoop(bot).run_forever() + +loop.create_task(ml) +loop.create_task(essen_loop()) + +loop.run_forever() + +# ctx = ssl.create_default_context() +# ctx.check_hostname = False +# ctx.verify_mode = ssl.CERT_NONE diff --git a/HSMensaW_daemon.py b/HSMensaW_daemon.py new file mode 100644 index 0000000..9599ccb --- /dev/null +++ b/HSMensaW_daemon.py @@ -0,0 +1,156 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import datetime +import json +import sys +import urllib.request +import xml.etree.ElementTree as ET +from time import sleep +from urllib.error import HTTPError + +import telepot +from telepot.loop import MessageLoop + +from Daemon import Daemon + + +class Essen: + def __init__(self, name, preis, kategorie): + self.name = name + self.preis = preis + self.kategorie = kategorie + + def __str__(self): + return str("%s: *%s* (%.2f €)" % (self.kategorie, self.name, self.preis)) + + +class HSMensaW_daemon(Daemon): + def run(self): + while True: + now = datetime.datetime.today() + nextDay = datetime.datetime(now.year, now.month, now.day) + datetime.timedelta(1, 36000) + print("Sleeping: " + str((nextDay - now).seconds)) + sleep((nextDay - now).seconds) + get_essen() + if len(essen) > 0: + for i in ids: + send_essen(i) + sleep(30) + + +config_filename = "config.json" +monate = ["Januar", "Februar", "März", "April", "Mai", "Juni", "August", "September", "Oktober", "November", "Dezember"] +wochentage = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] + + +def handle(msg): + global essen + + content_type, chat_type, chat_id = telepot.glance(msg) + if content_type == "text": + text = str(msg["text"]).lower() + if msg["text"].startswith("/start"): + ids.append(chat_id) + bot.sendMessage(chat_id, "Bot wurde aktiviert") + with open('config.json', 'w') as outfile: + json.dump(config, outfile) + elif msg["text"].startswith("/stop"): + try: + ids.remove(chat_id) + bot.sendMessage(chat_id, "Bot wurde deaktiviert") + with open('config.json', 'w') as outfile: + json.dump(config, outfile) + except ValueError: + bot.sendMessage(chat_id, "Bot war nicht aktiv") + elif msg["text"].startswith("/essen") or msg["text"].startswith("/mensa") or \ + msg["text"].startswith("/speiseplan"): + get_essen() + if len(essen) == 0: + bot.sendMessage(chat_id, "Für heute ist leider kein Speiseplan verfügbar.") + else: + send_essen(chat_id) + elif content_type == "new_chat_member": + if msg["new_chat_participant"]["id"] == bot.getMe()["id"]: + bot.sendMessage(chat_id, "*Mensa-Bot der Hochschule Mittweida*\nDieser Bot versendet jeden Tag um 10 Uhr " + "den aktuellen Mensa-Speiseplan. Er wird über /start für den aktuellen Chat oder " + "die aktuelle Gruppe gestartet, /stop beendet ihn wieder. Mit /essen, /mensa und " + "/speiseplan kann der aktuelle Speiseplan manuell abgerufen werden.\n\n" + "_Haftungsausschluss: Dieser Bot steht in keiner Verbindung mit der Hochschule " + "Mittweida oder dem Studentenwerk Freiberg. Alle Angaben ohne Gewähr._", + "markdown") + elif content_type == "left_chat_member": + if msg["left_chat_participant"]["id"] == bot.getMe()["id"]: + if chat_id in ids: + ids.remove(chat_id) + with open('config.json', 'w') as outfile: + json.dump(config, outfile) + + +def send_essen(chat_id): + global datum, essen + nachricht = "Speiseplan am WOCHENTAG, den %s:\n" % datum.strftime("%d. MONAT %Y") + nachricht = nachricht.replace("MONAT", monate[datum.month]) + nachricht = nachricht.replace("WOCHENTAG", wochentage[datum.weekday()]) + for i in essen: + nachricht += str(i).replace(".", ",") + "\n" + bot.sendMessage(chat_id, nachricht, "markdown") + + +def get_essen(): + global datum, essen + essen = [] + try: + response = urllib.request.urlopen(url) + except HTTPError: + return + data = response.read() + response.close() + text = data.decode('utf-8') + + et = ET.fromstring(text) + date = et.findall("./menus/day/date")[0].text + datum = datetime.date(int(date[:4]), int(date[5:7]), int(date[8:10])) + menus = et.findall("./menus/day/menu") + + for i in menus: + kategorie = i.findall("type")[0].text + name = i.findall("description")[0].text.strip("()1234567890, ") + preis = float(i.findall("./prices/price[label='Studenten']/value")[0].text.replace(",", ".")) + essen.append(Essen(name, preis, kategorie)) + + +if __name__ == "__main__": + try: + with open(config_filename, 'r') as config_file: + config = json.load(config_file) + except FileNotFoundError as e: + print('Error: config file "{}" not found: {}'.format(config_filename, e)) + sys.exit(-1) + except ValueError as e: + print('Error: invalid config file "{}": {}'.format(config_filename, e)) + sys.exit(-1) + + telegram_bot_token = config.get("telegram_bot_token") + url = config.get("url") + ids = config.get("ids") + + bot = telepot.Bot(telegram_bot_token) + MessageLoop(bot, handle).run_as_thread() + + daemon = HSMensaW_daemon("daemon.pid") + + if len(sys.argv) == 2: + if 'start' == sys.argv[1]: + daemon.start() + elif 'stop' == sys.argv[1]: + daemon.stop() + elif 'restart' == sys.argv[1]: + daemon.restart() + else: + print("Unknown command") + sys.exit(2) + sys.exit(0) + else: + print("usage: %s start|stop|restart" % sys.argv[0]) + sys.exit(2)