diff --git a/.gitignore b/.gitignore index 559e0a0..e6b99fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Persistence -persistent_vars.json +prst.db.* # API-Key (keep secret at all times) keys.py diff --git a/bot/api/telegram.py b/bot/api/telegram.py index 88002f4..bb6b96b 100644 --- a/bot/api/telegram.py +++ b/bot/api/telegram.py @@ -1,6 +1,7 @@ import emoji import requests import Levenshtein as lev +import datetime import bot.api.keys @@ -44,7 +45,7 @@ class TelegramIO(): """Inspects the message and reacts accordingly. Can easily be extended""" message_data = result[0] - self.persistence.increment("messages_read") + self.persistence["bot"]["messages_read"] += 1 self.offset = message_data["update_id"] + 1 if "edited_message" in message_data: @@ -55,7 +56,7 @@ class TelegramIO(): self.chat_id = message["chat"]["id"] author = message["from"] - chat_members = self.persistence.read("chat_members") + chat_members = self.persistence["bot"]["chat_members"] if str(author["id"]) not in chat_members: name = "" if "first_name" in author: @@ -65,7 +66,7 @@ class TelegramIO(): if len(name) == 0: name += "anonymous" chat_members[author["id"]] = name - self.persistence.write("chat_members", chat_members) + self.persistence["bot"]["chat_members"] = chat_members self.send_message("Welcome to this chat " + name + "!") if "text" in message: @@ -90,7 +91,7 @@ class TelegramIO(): command = self.fuzzy_match_command(full[0]) if len(command) != 1: if command[0] == "EXACT": - self.persistence.increment("commands_executed") + self.persistence["bot"]["commands_executed"] += 1 return command[1], full[1:] else: send = "Did you mean " + command[1] + "" @@ -129,11 +130,12 @@ class TelegramIO(): def send_message(self, message): - if message == "": + if message == "" or message == None: return - print("SENDING: " + emoji.demojize(message)) - + print("SENDING: " + message) + # message = message.replace("<","<").replace(">", ">") + # TODO: sanitize input but keep relevant tags data = { 'chat_id': self.chat_id, 'text': emoji.emojize(message), @@ -144,13 +146,14 @@ class TelegramIO(): send_url = self.base_url + "sendMessage" try: r = requests.post(send_url, data=data) - print(r.status_code) - self.persistence.increment("messages_sent") + if (r.status_code != 200): + raise Exception + self.persistence["bot"]["messages_sent"] except: out = datetime.datetime.now().strftime("%d.%m.%y - %H:%M") out += " @ " + "telegram.send_message" out += " --> " + "did not send:\n" + message - self.persistence.append_list("log", out) + self.persistence["bot"]["log"] += [out] def send_photo(self, url, caption): @@ -165,9 +168,9 @@ class TelegramIO(): send_url = self.base_url + "sendPhoto" try: r = requests.post(send_url, data=data) - self.persistence.increment("photos_sent") + self.persistence["bot"]["photos_sent"] += 1 except: out = datetime.datetime.now().strftime("%d.%m.%y - %H:%M") out += " @ " + "telegram.send_photo" out += " --> " + "did not send:\n" + url - self.persistence.append_list("log", out) + self.persistence["bot"]["log"] += [out] diff --git a/bot/main.py b/bot/main.py index cff0519..612e608 100644 --- a/bot/main.py +++ b/bot/main.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from bot.api import telegram, google, weather, reddit -from persistence import rw as pvars import requests import time @@ -11,21 +10,21 @@ import emoji class ChatBot(): """""" - def __init__(self, name, version): + def __init__(self, name, version, prst): """Inits the Bot with a few conf. vars Args: -> name:str - Name of the bot - -> api_key:str - t.me api-key -> version:str - Version number + -> prst:shelveObj - persistence """ self.version = version self.name = name # Persistent variable - self.persistence = pvars.Variables("bot") + self.persistence = prst # Uptime counter self.start_time = datetime.datetime.now() - self.persistence.increment("reboots") + self.persistence["bot"]["reboots"] += 1 # Available commands. Must be manually updated! self.commands = { @@ -43,7 +42,7 @@ class ChatBot(): "joke" : self.bot_tell_joke, "meme" : self.bot_send_meme, "news" : self.bot_send_news, - "shopping" : self.bot_shopping_list, + "list" : self.bot_list, } @@ -118,7 +117,7 @@ class ChatBot(): out = datetime.datetime.now().strftime("%d.%m.%y - %H:%M") out += " @ " + function_name out += " --> " + error_message - self.persistence.append_list("log", out) + self.persistence["bot"]["log"] += [out] ############################################################################ @@ -144,11 +143,11 @@ class ChatBot(): ip = "not fetchable" message = "
Status: Running :green_circle:\n"
         message += "Uptime: " + delta[:delta.rfind(".")] + "\n"
-        message += "Reboots: " + str(self.persistence.read("reboots")) + "\n"
+        message += "Reboots: " + str(self.persistence["bot"]["reboots"]) + "\n"
         message += "IP-Adress: " + ip + "\n"
-        message += "Messages read: " + str(self.persistence.read("messages_read")) + "\n"
-        message += "Messages sent: " + str(self.persistence.read("messages_sent")) + "\n"
-        message += "Commands executed " + str(self.persistence.read("commands_executed")) + "
" + message += "Messages read: " + str(self.persistence["bot"]["messages_read"]) + "\n" + message += "Messages sent: " + str(self.persistence["bot"]["messages_sent"]) + "\n" + message += "Commands executed " + str(self.persistence["bot"]["commands_executed"]) + "" return message @@ -245,9 +244,15 @@ class ChatBot(): self.persistence.write("log",[]) return "Log cleared" elif "system" in args: - send_text = self.persistence.read_ext_file("persistence/log.txt") - return send_text - + path="persistence/log.txt" + try: + file = open(path,"r") + content = file.read() + file.close() + return content + except: + return "could not read File" + send_text = "" for event in self.persistence.read("log"): send_text += event + "\n" @@ -279,7 +284,7 @@ class ChatBot(): def bot_zvv(self, *args): """Uses the swiss travel api to return the best route between a start- and endpoint. usage: 'start' to 'finish'""" - if len(args) <= 3: + if len(args) < 3: return "Please specify a start- and endpoint as well as a separator (the 'to')" url = "http://transport.opendata.ch/v1/connections" @@ -421,33 +426,49 @@ class ChatBot(): return text - def bot_shopping_list(self, *args): - """Shows a shopping list. Usage - add - print - clear - remove + def bot_list(self, *args): + """Shows and interacts with a list. Usage + list <name> <action> {object} + actions are: create, delete, print, clear, add, remove + example: + list new shopping : creates list name shopping + list shopping add bread : adds bread to the list + list shopping print + list shopping clear """ output = "" # args = list(args) - if len(args) == 0: - return "Missing parameter(s)" - - if args[0] == "print": - sl = self.persistence.global_action("read", "shopping_list") - for ind,thing in enumerate(sl): - output += str(ind+1) + ". " + thing + "\n" - elif args[0] == "clear": - self.persistence.global_action("write", "shopping_list", value=[]) - output = "Cleared list." - elif args[0] == "add": - if len(args) == 1: - output = "Missing parameter" - add = " ".join(args[1:]) - self.persistence.global_action("append_list", "shopping_list", value=add) - output = "Added " + add + "." - elif args[0] == "remove": - output = "Removed test." + if len(args) < 2: + return "Missing parameters" + try: + if args[0] == "create": + lname = " ".join(args[1:]) + self.persistence["global"]["lists"][lname] = [] + output = "Created list " + lname + elif args[0] == "delete": + lname = " ".join(args[1:]) + self.persistence["global"]["lists"].pop(lname, None) # no error if key doesnt exist + output = "Deleted list " + lname + else: + lname = args[0] + act = args[1] + if act == "print": + sl = self.persistence["global"]["lists"][lname] + for ind,thing in enumerate(sl): + output += str(ind+1) + ". " + thing + "\n" + elif act == "clear": + self.persistence["global"]["lists"][lname] = [] + output = "Cleared list " + lname + elif act == "add": + if len(args) < 3: + return "Missing paramaeter" + add = " ".join(args[2:]) + self.persistence["global"]["lists"][lname] += [add] + return "Added " + add + "." + elif act == "remove": + return "Not working yet" + except: + output = "Could not handle your request. Maybe check the keys?" return output diff --git a/bot_wrapper.py b/bot_wrapper.py deleted file mode 100644 index f2acda9..0000000 --- a/bot_wrapper.py +++ /dev/null @@ -1,45 +0,0 @@ -import time -import datetime - - -class ModuleWrapper(): - """Wrapper for the BOT-functionality""" - def __init__(self, bot_module, clock_module): - """""" - print("Initializing bot-functionality") - ####################################################################### - - self.bot = bot_module - self.clock = clock_module - - # available hw-commands. Must be updated manually! - self.hw_commands = { - "blink" : self.clock.alarm_blink, - "wakeup" : self.clock.wake_light, - "showmessage" : self.clock.show_message, - - } - self.bot.add_commands(self.hw_commands) - - self.message_loop() - - - def message_loop(self): - """Calls the telegram entity regularly to check for activity""" - print("Starting bot mainloop") - while(True): - result = self.bot.telegram.fetch_updates() - if len(result) != 0: - command, params = self.bot.telegram.handle_result(result) - if command != "nothing": - if command in self.hw_commands: - self.react_hw_command(command,params) # hw-level - else: - self.bot.react_command(command,params) # sw-level - time.sleep(5) - - - def react_hw_command(self, command, params): - """""" - # so params is a list, and so, to pass the commands, we need to unpack it: - self.hw_commands[command](*params) diff --git a/clock/main.py b/clock/main.py index 47d437e..96f2f91 100644 --- a/clock/main.py +++ b/clock/main.py @@ -6,15 +6,13 @@ import numpy from clock.api import led -import persistence.rw - ################################################################################ #start of actual programm. class ClockFace(object): """Actual functions one might need for a clock""" - def __init__(self, text_speed=18): + def __init__(self, text_speed=18, prst=""): self.IO = led.OutputHandler(32,16) self.tspeed = text_speed @@ -136,5 +134,5 @@ class ClockFace(object): """Runs a text message over the screen. Obviously needs the text""" # keep in mind, in this case args is a tuple of all words message_str = " ".join(args) - print("SENDING: " + message_str) + print("SHOWING (CLOCK): " + message_str) self.text_scroll(message_str) diff --git a/clock_wrapper.py b/clock_wrapper.py deleted file mode 100644 index af81e02..0000000 --- a/clock_wrapper.py +++ /dev/null @@ -1,58 +0,0 @@ -import wrapper -import datetime - - -class ClockWrapper(wrapper.Wrapper): - """Wrapper for the CLOCK-functionality""" - def __init__(self, own_module, *other_modules): - """""" - super().__init__(own_module, *other_modules) - print("Initializing clock-functionality") - self.weather = {"weather":"", "high":"", "low":"", "show":"temps"} - self.mainloop(15) - - - - def mainloop(self, sleep_delta): - """Runs the showing of the clock-face periodically (better way?)""" - print("Starting clock mainloop") - self.prev_time = 0 - self.prev_weather_time = datetime.datetime.fromtimestamp(0) - - def perform_loop(): - if self.prev_time != datetime.datetime.now().strftime("%H:%M"): - d = datetime.datetime.now() - self.prev_weather_time - mins_elapsed = int(d.total_seconds()/60) - - if mins_elapsed >= 3*60: - # fetch new weather every 3 hours (hard coded) - prev_weather_time = datetime.datetime.now() - weather = self.others[0].bot_show_weather("zurich") - if not (":sad:" in weather): - l1 = weather[weather.find("")+5:weather.find("\n")].replace (":","") - # current weather situation (icon): we pick the first line, remove the start string, remove :: indicating an emoji - - temps_today = weather.splitlines()[4] - low = temps_today[temps_today.find("button")+8:temps_today.find("°")] - temps_today = temps_today[temps_today.find("°") + 1:] - high = temps_today[temps_today.find("button")+8:temps_today.find("°")] - self.weather["weather"] = l1 - self.weather["high"] = high - self.weather["low"] = low - else: - self.weather["weather"] = "error" - self.weather["high"] = "error" - self.weather["low"] = "error" - - if mins_elapsed % 5 == 0: - if self.weather["show"] == "weather": - next = "temps" - else: - next = "weather" - self.weather["show"] = next - - prev_time = datetime.datetime.now().strftime("%H:%M") - - self.own.set_face(self.weather) - - super().mainloop(sleep_delta,perform_loop) \ No newline at end of file diff --git a/launcher.py b/launcher.py index 3d409c0..fc30143 100644 --- a/launcher.py +++ b/launcher.py @@ -4,13 +4,11 @@ import clock.main import dashboard.main # wrapper -import clock_wrapper -import bot_wrapper -import dashboard_wrapper +import wrapper # misc. from threading import Thread - +import shelve class Launcher(): @@ -18,8 +16,11 @@ class Launcher(): def __init__(self): """""" - self.bot_module = bot.main.ChatBot("ChatterBot", "2.0") - self.clock_module = clock.main.ClockFace() + self.persistence = shelve.open('persistence/prst.db', writeback=True) + self.init_persistence() + # TODO populate the persistence + self.bot_module = bot.main.ChatBot(name="ChatterBot", version="2.1", prst=self.persistence) + self.clock_module = clock.main.ClockFace(prst=self.persistence) self.threads = [] self.threads.append(Thread(target=self.chatbot)) @@ -30,18 +31,32 @@ class Launcher(): def clock(self): - print("Launching clock-functionality") - self.clock = clock_wrapper.ClockWrapper(self.clock_module, self.bot_module) + self.clock = wrapper.ClockWrapper(self.clock_module, self.bot_module) def chatbot(self): - print("Launching bot-functionality") - self.bot = bot_wrapper.ModuleWrapper(self.bot_module, self.clock_module) + self.bot = wrapper.BotWrapper(self.bot_module, self.clock_module) def dashboard(self): - print("Launching dashboard-functionality") - self.dashboard = dashboard_wrapper.DashBoardWrapper(self.dashboard_module, self.bot_module) + self.dashboard = wrapper.DashBoardWrapper(self.dashboard_module, self.bot_module) + + def init_persistence(self): + self.persistence["bot"] = { + "messages_read": 0, + "messages_sent": 0, + "commands_executed": 0, + "photos_sent": 0, + "log": [], + "chat_members": {}, + "reboots": 0 + } + self.persistence["clock"] = {} + self.persistence["dashboard"] = {} + self.persistence["global"] = { + "lists" : {} + } + ######################################################################## ## Aand liftoff! diff --git a/persistence/__init__.py b/persistence/__init__.py deleted file mode 100644 index dcf2c80..0000000 --- a/persistence/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Placeholder diff --git a/persistence/persistent_init.json b/persistence/persistent_init.json deleted file mode 100644 index 6987f6c..0000000 --- a/persistence/persistent_init.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "bot" : { - "messages_read": 0, - "messages_sent": 0, - "commands_executed": 0, - "photos_sent": 0, - "log": [], - "chat_members": {}, - "reboots": 0 - }, - - "clock" : { - "test":"" - }, - "dashboard" : { - "test":"" - }, - "global" : { - "shopping_list" : [] - } -} diff --git a/persistence/rw.py b/persistence/rw.py deleted file mode 100644 index e658ea5..0000000 --- a/persistence/rw.py +++ /dev/null @@ -1,83 +0,0 @@ -import json -import time -import os -import shutil - -class Variables(): - """""" - - def __init__(self, module_name, file_name="persistence/persistent_vars.json", init_name="persistence/persistent_init.json", ): - self.path = file_name - self.init_path = init_name - - self.module = module_name - - self.last_action = "" - # last performed action, if only reads are made, then the underlying var has not been changed - # and doesn't need to be read again - self.savefile = {} - - if not os.path.exists(self.path): - shutil.copy(self.init_path, self.path) - - - def global_action(self, action, name, value=""): - old = self.module - self.module = "global" - action = getattr(self, action) - if value != "": - ret = action(name,value) - else: - ret = action(name) - self.module = old - return ret - - def write(self, name, value): - pre = self.read("") - pre[self.module][name] = value - try: - file = open(self.path,"w") - json.dump(pre, file) - file.close() - self.last_action = "write" - except: - print("Config not written - critical") - - - def read(self, name): - if self.last_action == "read": - vars = self.savefile - else: - file = open(self.path,"r") - vars = json.load(file) - file.close() - self.savefile = vars - self.last_action = "read" - - if name != "": - vars = vars[self.module][name] - return vars - - - def increment(self, name, inc=1): - pre = self.read(name) - if pre: - self.write(name, pre + inc) - else: - self.write(name, inc) - - - def append_list(self, name, value): - pre = self.read(name) - pre.append(value) - self.write(name, pre) - - - def read_ext_file(self, path): - """returns content of given file""" - if not os.path.exists(path): - return "File does not exist" - file = open(path,"r") - content = file.read() - file.close() - return content \ No newline at end of file diff --git a/wrapper.py b/wrapper.py index 7692c9d..1cda615 100644 --- a/wrapper.py +++ b/wrapper.py @@ -1,4 +1,6 @@ import time +import datetime + class Wrapper(): """Wrapper skeleton for the modules (bot, clock dashboard...)""" @@ -6,12 +8,113 @@ class Wrapper(): def __init__(self, own_module, *other_modules): self.own = own_module self.others = other_modules + print("Starting " + self.__class__.__name__ + " functionality") def mainloop(self, sleep_delta, action): """sleep_delta in seconds sets the sleep period of the loop action is a function that is performed every * seconds""" + print("Launching " + self.__class__.__name__ + " mainloop") while True: action() time.sleep(sleep_delta) + + + +class ClockWrapper(Wrapper): + """Wrapper for the CLOCK-functionality""" + def __init__(self, own_module, *other_modules): + """""" + super().__init__(own_module, *other_modules) + self.weather = {"weather":"", "high":"", "low":"", "show":"temps"} + self.mainloop(15) + + + + def mainloop(self, sleep_delta): + """Runs the showing of the clock-face periodically (better way?)""" + + self.prev_time = 0 + self.prev_weather_time = datetime.datetime.fromtimestamp(0) + + def perform_loop(): + if self.prev_time != datetime.datetime.now().strftime("%H:%M"): + d = datetime.datetime.now() - self.prev_weather_time + mins_elapsed = int(d.total_seconds()/60) + + if mins_elapsed >= 3*60: + # fetch new weather every 3 hours (hard coded) + self.prev_weather_time = datetime.datetime.now() + weather = self.others[0].bot_show_weather("zurich") + if not (":sad:" in weather): + l1 = weather[weather.find("")+5:weather.find("\n")].replace (":","") + # current weather situation (icon): we pick the first line, remove the start string, remove :: indicating an emoji + + temps_today = weather.splitlines()[4] + low = temps_today[temps_today.find("button")+8:temps_today.find("°")] + temps_today = temps_today[temps_today.find("°") + 1:] + high = temps_today[temps_today.find("button")+8:temps_today.find("°")] + self.weather["weather"] = l1 + self.weather["high"] = high + self.weather["low"] = low + else: + self.weather["weather"] = "error" + self.weather["high"] = "error" + self.weather["low"] = "error" + + if mins_elapsed % 5 == 0: + if self.weather["show"] == "weather": + next = "temps" + else: + next = "weather" + self.weather["show"] = next + + self.prev_time = datetime.datetime.now().strftime("%H:%M") + + self.own.set_face(self.weather) + + super().mainloop(sleep_delta,perform_loop) + + + +class BotWrapper(Wrapper): + """Wrapper for the BOT-functionality""" + def __init__(self, own_module, *other_modules): + """""" + super().__init__(own_module, *other_modules) + + self.bot = own_module + self.clock = other_modules[0] + + + # available hw-commands. Must be updated manually! + self.hw_commands = { + "blink" : self.clock.alarm_blink, + "wakeup" : self.clock.wake_light, + "showmessage" : self.clock.show_message, + + } + self.bot.add_commands(self.hw_commands) + + self.mainloop(10) + + + def mainloop(self, sleep_delta): + """Calls the telegram entity regularly to check for activity""" + def perform_loop(): + result = self.bot.telegram.fetch_updates() + if len(result) != 0: + command, params = self.bot.telegram.handle_result(result) + if command != "nothing": + if command in self.hw_commands: + self.react_hw_command(command,params) # hw-level + else: + self.bot.react_command(command,params) # sw-level + + super().mainloop(sleep_delta, perform_loop) + + def react_hw_command(self, command, params): + """""" + # so params is a list, and so, to pass the commands, we need to unpack it: + self.hw_commands[command](*params)