Compare commits

...

8 Commits

Author SHA1 Message Date
Clemens Klug c5f5b151bd Merge branch 'matrix' into 'master'
Matrix

See merge request server/doorbot!1
2018-04-16 11:00:56 +00:00
Clemens Klug c7cb219584 Merge branch 'master' into 'matrix'
# Conflicts:
#   bot.py
2018-04-16 11:00:36 +00:00
Clemens Klug eb4bde8b58 add docker 2018-04-16 12:57:15 +02:00
agp8x e624296fd2 move functions to nive places 2018-04-11 22:09:38 +02:00
agp8x 4b962c4bb5 refactoring complete :)
Beware: semantics of `-l` options have changed: Now it prevents looping
2018-04-11 21:38:57 +02:00
agp8x 39fbb813ae Merge branch 'master' of clkl.de:wiai/doorbot 2018-04-11 19:19:18 +02:00
agp8x df11aef895 load text from config 2018-04-11 19:16:59 +02:00
Clemens Klug 40a08bfb15 add targets 2018-04-11 18:14:11 +02:00
7 changed files with 229 additions and 88 deletions

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM python:3.6-alpine3.7
ADD [".", "/app"]
WORKDIR /app
RUN apk add --update g++ gfortran openblas-dev libpng-dev musl-dev freetype-dev
RUN pip install -r requirements.txt --no-cache-dir
CMD ["python", "bot.py"]

128
bot.py
View File

@ -1,116 +1,76 @@
import argparse import argparse
import datetime
import json import json
import time
from tempfile import NamedTemporaryFile
import logging import logging
import time
from tempfile import NamedTemporaryFile
from collections import namedtuple
import requests
import schedule import schedule
STATUS_URL = "https://isfswiaiopen.wiai.de?json" from plot import get_plot
MESSAGE_URL = "https://api.telegram.org/bot{token}/sendMessage" from sources import IsFsWIAIopen
IMAGE_URL = "https://api.telegram.org/bot{token}/sendPhoto" 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") 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__) log = logging.getLogger(__name__)
def parse_time(string): Config = namedtuple("Config", ['sleep', 'plot_interval', 'clients', 'source', 'loop'])
return datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S")
def get_status(): def publish(clients, fn, **kwargs):
status = requests.get(STATUS_URL).json() for client in clients:
status["timestamp"] = parse_time(status['timestamp']) fn(client, **kwargs)
return status
def get_status_text(config, src=get_status): def post_plot(config):
return config["texts"][str(get_status()["doorstate"])] with NamedTemporaryFile() as target:
plot_file, last = get_plot(target)
def post(chats, text, token): plot_file.seek(0)
url = MESSAGE_URL.format(token=token) publish(config.clients, Client.post_image, file=plot_file, name="")
for chat in chats:
response = requests.post(url, data={'chat_id': chats[chat], "text": text})
log.info("post message: %s", response.status_code)
def has_argument(args, key): def has_argument(args, key):
return key in args and args[key] return key in args and args[key]
def get_config(args): def get_config(args):
config = json.load(open(args['config'])) settings = json.load(open(args['config']))
if has_argument(args, "interval"): clients = []
log.info("Overwrite sleep value by argument…") for client in (MatrixClient, TelegramClient):
config["sleep"] = args["interval"] 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 return config
def main(args={"config": "settings.json"}): def setup(config):
log.info("run once") schedule.every(config.plot_interval).seconds.do(lambda: post_plot(config))
config = get_config(args)
text = get_status_text(config)
post(config['groups'], text, config['token'])
post_plot(config)
def loop(args={"config": "settings.json"}): def main(args={'config': "settings.json"}):
log.info("prepare loop")
config = get_config(args) config = get_config(args)
status = config.source.get_status()
publish(config.clients, Client.post_text, text=status.text)
setup(config) setup(config)
while True:
while config.loop:
try: 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: except Exception as e:
log.exception(e) log.exception(e)
time.sleep(config['sleep']) if not config.loop:
post_plot(config)
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
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)
def setup(config):
schedule.every(config['photo_interval']).seconds.do(lambda: post_plot(config))
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="DoorStateBot") parser = argparse.ArgumentParser(description="DoorStateBot")
parser.add_argument("--config", "-c", default="settings.json", help="Configuration file") 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") parser.add_argument("--interval", "-i", help="Interval")
args = parser.parse_args() args = parser.parse_args()
if args.loop:
loop(vars(args))
else:
main(vars(args)) main(vars(args))

8
docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
version: "2"
services:
doorbot:
build: .
image: fswiai/doorbot:0.2
volumes:
- ./settings.json:/app/settings.json

View File

@ -2,3 +2,4 @@ requests==2.18.4
schedule==0.4.3 schedule==0.4.3
matplotlib==2.0.2 matplotlib==2.0.2
numpy==1.13.1 numpy==1.13.1
matrix_client==0.1.0

View File

@ -1,9 +1,17 @@
{ {
"token": "BOTID:TOKEN",
"sleep": 30, "sleep": 30,
"photo_interval": 30, "plot_interval": 30,
"groups":{ "groups":{
"matrix": {
"host": "…",
"username": "…",
"password": "…",
"doorstate": "..."
},
"telegram": {
"token": "BOTID:TOKEN",
"wiaidoor": -12357567 "wiaidoor": -12357567
}
}, },
"disabled":{ "disabled":{
"WIAIdoorTest": -234502, "WIAIdoorTest": -234502,

35
sources.py Normal file
View File

@ -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()

121
targets.py Normal file
View File

@ -0,0 +1,121 @@
import logging
import requests
from matrix_client.client import MatrixClient as MatrixApiClient
from matrix_client.errors import MatrixError
class Client:
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):
"""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):
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:
try:
self.client.join_room(target)
except MatrixError as e:
self.log.error("could not join room '" + target + "'")
self.log.exception(e)
return None
return self.client.rooms[target]
def _upload_image(self, file, content_type):
try:
data = file.read()
file.seek(0)
return self.client.api.media_upload(data, content_type)
except MatrixError as e:
self.log.exception(e)
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 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, **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 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 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)