diff --git a/bot.py b/bot.py index bd8afb6..801f241 100644 --- a/bot.py +++ b/bot.py @@ -1,116 +1,76 @@ import argparse -import datetime import json -import time -from tempfile import NamedTemporaryFile import logging +import time + +from tempfile import NamedTemporaryFile +from collections import namedtuple -import requests import schedule -STATUS_URL = "https://isfswiaiopen.wiai.de?json" -MESSAGE_URL = "https://api.telegram.org/bot{token}/sendMessage" -IMAGE_URL = "https://api.telegram.org/bot{token}/sendPhoto" +from plot import get_plot +from sources import IsFsWIAIopen +from targets import Client, MatrixClient, TelegramClient logging.basicConfig(format='%(asctime)s %(levelname)s %(name)s:%(message)s', level=logging.DEBUG, datefmt="%Y.%m.%d %H:%M:%S") log = logging.getLogger(__name__) -def parse_time(string): - return datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") +Config = namedtuple("Config", ['sleep', 'plot_interval', 'clients', 'source', 'loop']) -def get_status(): - status = requests.get(STATUS_URL).json() - status["timestamp"] = parse_time(status['timestamp']) - return status - -def get_status_text(config, src=get_status): - return config["texts"][str(get_status()["doorstate"])] - -def post(chats, text, token): - url = MESSAGE_URL.format(token=token) - for chat in chats: - response = requests.post(url, data={'chat_id': chats[chat], "text": text}) - log.info("post message: %s", response.status_code) +def publish(clients, fn, **kwargs): + for client in clients: + fn(client, **kwargs) def has_argument(args, key): return key in args and args[key] def get_config(args): - config = json.load(open(args['config'])) - if has_argument(args, "interval"): - log.info("Overwrite sleep value by argument…") - config["sleep"] = args["interval"] + settings = json.load(open(args['config'])) + clients = [] + for client in (MatrixClient, TelegramClient): + if client.key in settings['groups']: + clients.append(client(**settings['groups'][client.key])) + config = Config( + sleep=args['interval'] if has_argument(args, 'interval') else settings['sleep'], + plot_interval=settings['plot_interval'], + clients=clients, + source=IsFsWIAIopen(texts=settings['texts']), + loop=args['loop']) return config -def main(args={"config": "settings.json"}): - log.info("run once") - config = get_config(args) - text = get_status_text(config) - post(config['groups'], text, config['token']) - post_plot(config) - -def loop(args={"config": "settings.json"}): - log.info("prepare loop") +def main(args={'config': "settings.json"}): config = get_config(args) + + status = config.source.get_status() + publish(config.clients, Client.post_text, text=status.text) setup(config) - while True: + + while config.loop: try: - do_loop(config) + schedule.run_pending() + new_status = config.source.get_status() + if not new_status.doorstate == status.doorstate: + status = new_status + publish(config.clients, Client.post_text, text=status.text) + time.sleep(config.sleep) except Exception as e: log.exception(e) - time.sleep(config['sleep']) - -def do_loop(config): - last_state = None - while True: - log.info("enter loop") - changed = has_changed(last_state) - if changed: - last_state = update(new_state) - log.info("run pending tasks") - schedule.run_pending() - log.info("sleep") - time.sleep(config['sleep']) - -def has_changed(last_state): - changed = False - new_state = get_status() - if last_state is None: - changed = True - elif not last_state["doorstate"] == new_state["doorstate"]: - changed = True - return changed - -def update(state): - text = get_status_text(config, lambda: state) - post(config["groups"], text, config["token"]) - return state + if not config.loop: + post_plot(config) def post_plot(config): - from plot import get_plot with NamedTemporaryFile() as target: - image_url = IMAGE_URL.format(token=config['token']) - photo, last = get_plot(target) - files = {'photo': photo} - if last + datetime.timedelta(days=1) < datetime.datetime.today(): - log.info("skipping image, no new updates...") - return - for chat in config['groups']: - files['photo'].seek(0) - values = {"chat_id": config['groups'][chat]} - r = requests.post(image_url, files=files, data=values) - log.info("post photo: %s", r.status_code) + plot_file, last = get_plot(target) + plot_file.seek(0) + publish(config.clients, Client.post_image, file=plot_file, name="") def setup(config): - schedule.every(config['photo_interval']).seconds.do(lambda: post_plot(config)) + schedule.every(config.plot_interval).seconds.do(lambda: post_plot(config)) if __name__ == "__main__": parser = argparse.ArgumentParser(description="DoorStateBot") parser.add_argument("--config", "-c", default="settings.json", help="Configuration file") - parser.add_argument("--loop", "-l", action="store_true", help="Loop") + parser.add_argument("--loop", "-l", action="store_false", help="Loop") parser.add_argument("--interval", "-i", help="Interval") args = parser.parse_args() - if args.loop: - loop(vars(args)) - else: - main(vars(args)) + main(vars(args)) diff --git a/settings.sample.json b/settings.sample.json index beed094..08c2619 100644 --- a/settings.sample.json +++ b/settings.sample.json @@ -1,9 +1,17 @@ { - "token": "BOTID:TOKEN", "sleep": 30, - "photo_interval": 30, + "plot_interval": 30, "groups":{ - "wiaidoor": -12357567 + "matrix": { + "host": "…", + "username": "…", + "password": "…", + "doorstate": "..." + }, + "telegram": { + "token": "BOTID:TOKEN", + "wiaidoor": -12357567 + } }, "disabled":{ "WIAIdoorTest": -234502, diff --git a/sources.py b/sources.py new file mode 100644 index 0000000..d2bd8a4 --- /dev/null +++ b/sources.py @@ -0,0 +1,35 @@ +import datetime +import requests + +from collections import namedtuple + +Status = namedtuple("Status", ['doorstate', 'timestamp', 'text']) + +class Source: + def get_status(self): + raise NotImplementedError() + + def is_recent(self, status, **kwargs): + raise NotImplementedError() + +class IsFsWIAIopen(Source): + url = "https://isfswiaiopen.wiai.de?json" + + def __init__(self, texts=None): + self.texts = texts + + def _parse_time(self, string): + return datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") + + def _get_text(self, state): + return self.texts[state] if self.texts else "" + + def get_status(self): + status = requests.get(self.url).json() + return Status( + doorstate=str(status['doorstate']), + timestamp=self._parse_time(status['timestamp']), + text=self._get_text(str(status['doorstate']))) + + def is_recent(self, status, **kwargs): + return status.timestamp + datetime.timedelta(days=1) < datetime.datetime.today() \ No newline at end of file diff --git a/targets.py b/targets.py index cb553cf..3df4708 100644 --- a/targets.py +++ b/targets.py @@ -5,20 +5,54 @@ import requests from matrix_client.client import MatrixClient as MatrixApiClient from matrix_client.errors import MatrixError + class Client: - def send_image(self, target, path, name, content_type="image/png"): - raise NotImplementedError() + def post_image(self, file, name, content_type="image/png", targets=None): + """Push to all targets""" + self.post_image(file, name, content_type, targets) + #raise NotImplementedError() + + def send_image(self, target, file, name, content_type="image/png"): + """Send an image to a room + + Args: + target (str): The internal room id to post into + path (file): The image file object + name (str): The name for the file in the room + content_type (str): Content-type of the image + """ + self.post_image(path, name, content_type, targets=[target]) + + def post_text(self, text, targets=None): + """Push to all targets""" + self.post_text(text, targets) + #raise NotImplementedError() def send_text(self, target, text): - raise NotImplementedError() + """Send a text to a room + + Args: + target (str): The internal room id to post into + text (str): The text to post + """ + self.post_text(text, targets=[target]) + + def _get_targets(self, targets): + if not targets: + targets = self.targets + if not targets: + self.log.info("no targets…") + targets = [] + return targets class MatrixClient(Client): - - def __init__(self, host, username, password): + key = "matrix" + def __init__(self, host, username, password, **kwargs): self.client = MatrixApiClient(host) self.client.login_with_password_no_sync(username=username, password=password) self.log = logging.getLogger(__name__) + self.targets = kwargs.values() def _get_room(self, target): if target not in self.client.rooms: @@ -30,47 +64,58 @@ class MatrixClient(Client): return None return self.client.rooms[target] - def send_image(self, target, path, name, content_type="image/png"): - """Send an image to a room - - Args: - target (str): The internal room id to post into - path (str/Path-like): The path to the image file - name (str): The name for the file in the room - content_type (str): Content-type of the image - """ - with open(path, "rb") as src: - data = src.read() + def _upload_image(self, file, content_type): try: - mxc = self.client.api.media_upload(data, content_type) + data = file.read() + file.seek(0) + return self.client.api.media_upload(data, content_type) except MatrixError as e: self.log.exception(e) - return - room = self._get_room(target) - if room: + return None + + def post_image(self, file, name, content_type="image/png", targets=None): + targets = self._get_targets(targets) + mxc = None + for target in targets: + room = self._get_room(target) + if not room: + self.log.error("could not get room '" + target + "'") + continue + if not mxc: + mxc = self._upload_image(file, content_type) + if not mxc: + return room.send_image(mxc['content_uri'], name) - def send_text(self, target, text): - room = self._get_room(target) - if room: - room.send_text(text) + def post_text(self, text, targets=None): + targets = self._get_targets(targets) + for target in targets: + room = self._get_room(target) + if room: + room.send_text(text) class TelegramClient(Client): - + key = "telegram" MESSAGE_URL = "https://api.telegram.org/bot{token}/sendMessage" IMAGE_URL = "https://api.telegram.org/bot{token}/sendPhoto" - def __init__(self, token): + def __init__(self, token, **kwargs): self.text_url = self.MESSAGE_URL.format(token=token) self.image_url = self.IMAGE_URL.format(token=token) self.log = logging.getLogger(__name__) + self.targets = kwargs.values() - def send_image(self, target, path, name, content_type="image/png"): - files = {'photo': open(path, "rb")} - values = {"chat_id": target} - r = requests.post(self.image_url, files=files, data=values) - self.log.info("post photo: %s", r.status_code) + def post_image(self, file, name, content_type="image/png", targets=None): + targets = self._get_targets(targets) + for target in targets: + files = {'photo': file} + values = {'chat_id': target} + response = requests.post(self.image_url, files=files, data=values) + self.log.info("post message: %s -- %s", response.status_code, response.text) + file.seek(0) - def send_text(self, target, text): - response = requests.post(self.text_url, data={'chat_id': target, "text": text}) - self.log.info("post message: %s", response.status_code) \ No newline at end of file + def post_text(self, text, targets=None): + targets = self._get_targets(targets) + for target in targets: + response = requests.post(self.text_url, data={'chat_id': target, 'text': text}) + self.log.info("post message: %s -- %s", response.status_code, response.text) \ No newline at end of file