Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

10 changed files with 106 additions and 279 deletions

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
settings.json
__pycache__/
*.pyc
*.png
.venv/

View File

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

94
bot.py Normal file
View File

@ -0,0 +1,94 @@
import argparse
import datetime
import json
import time
from tempfile import NamedTemporaryFile
import logging
import requests
import schedule
logging.basicConfig(format='%(levelname)s %(name)s:%(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
def parse_time(string):
return datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S")
def get_status():
status = requests.get("https://isfswiaiopen.wiai.de?json").json()
status["timestamp"] = parse_time(status['timestamp'])
return status
def get_status_text(src=get_status):
if get_status()["doorstate"]:
text = "fs WIAI is open :)"
else:
text = "fs WIAI is closed :("
return text
def post(chats, text, token):
url = "https://api.telegram.org/bot{token}/sendMessage".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 main(args={"config": "settings.json"}):
log.info("run once")
config = json.load(open(args['config']))
text = get_status_text()
post(config['groups'], text, config['token'])
post_plot(config)
def loop(args={"config": "settings.json"}):
log.info("prepare loop")
config = json.load(open(args['config']))
setup(config)
while True:
try:
do_loop(config)
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 = False
new_state = get_status()
if last_state is None:
changed = True
elif not last_state["doorstate"] == new_state["doorstate"]:
changed = True
if changed:
last_state = new_state
text = get_status_text(lambda: last_state)
post(config["groups"], text, config["token"])
log.info("run pending tasks")
schedule.run_pending()
log.info("sleep")
time.sleep(config['sleep'])
def post_plot(config):
from plot import get_plot
with NamedTemporaryFile() as target:
image_url = 'https://api.telegram.org/bot{token}/sendPhoto'.format(token=config['token'])
files = {'photo': get_plot(target)}
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__":
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")
args = parser.parse_args()
if args.loop:
loop(vars(args))
else:
main(vars(args))

View File

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

View File

@ -6,6 +6,7 @@ import requests
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
@ -40,13 +41,15 @@ def plot(raw, target="wiai.png"):
ax.set_yticks(np.arange(7))
ax.set_yticklabels(DAYS_ABBR)
ax.set_xticks(np.arange(24))
colors = [ im.cmap(im.norm(value)) for value in values ]
patches = [ mpatches.Patch(color=colors[i], label=str(values[i])) for i in range(len(values))]
plt.legend(handles=patches, bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.title("Aggregated opening count FS WIAI")
plt.figtext(0.5, 0.25, "created: " + str(date.today()), ha="center")
plt.figtext(0.5, 0.2, str(first[0]) + "" + str(last[0]), ha="center")
cax = plt.axes((0.95, 0.15, 0.05, 0.5))
plt.colorbar(im, cax=cax)
plt.figtext(0.5, 0.25, "created: "+str(date.today()), ha="center")
plt.figtext(0.5, 0.2, str(first[0])+""+str(last[0]), ha="center")
plt.savefig(target, format="PNG", transparent=True, bbox_inches="tight")
return target, last[0]
return target
def local(target):
@ -56,7 +59,7 @@ def local(target):
def prod(target):
return plot(requests.get('https://isfswiaiopen.wiai.de/log').json(), target=target)
plot(requests.get('https://isfswiaiopen.wiai.de/log').json(), target=target)
def get_plot(target):
return plot(requests.get('https://isfswiaiopen.wiai.de/log').json(), target=target)

View File

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

View File

@ -1,26 +1,13 @@
{
"token": "BOTID:TOKEN",
"sleep": 30,
"plot_interval": 30,
"photo_interval": 30,
"groups":{
"matrix": {
"host": "…",
"username": "…",
"password": "…",
"doorstate": "..."
},
"telegram": {
"token": "BOTID:TOKEN",
"wiaidoor": -12357567
}
"wiaidoor": -12357567
},
"disabled":{
"WIAIdoorTest": -234502,
"name": -1333,
"fswiai main": -1001
},
"texts":{
"-1": "fs WIAI is undefined ¯\_(ツ)_/¯",
"0": "fs WIAI is closed :(",
"1": "fs WIAI is open :)"
}
}

View File

@ -1,76 +0,0 @@
import argparse
import json
import logging
import time
from tempfile import NamedTemporaryFile
from collections import namedtuple
import schedule
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__)
Config = namedtuple("Config", ['sleep', 'plot_interval', 'clients', 'source', 'loop'])
def publish(clients, fn, **kwargs):
for client in clients:
fn(client, **kwargs)
def post_plot(config):
with NamedTemporaryFile() as target:
plot_file, last = get_plot(target)
plot_file.seek(0)
publish(config.clients, Client.post_image, file=plot_file, name="")
def has_argument(args, key):
return key in args and args[key]
def get_config(args):
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 setup(config):
schedule.every(config.plot_interval).seconds.do(lambda: post_plot(config))
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 config.loop:
try:
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)
if not config.loop:
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_false", help="Loop")
parser.add_argument("--interval", "-i", help="Interval")
args = parser.parse_args()
main(vars(args))

View File

@ -1,35 +0,0 @@
import datetime
from abc import ABCMeta, abstractmethod
from collections import namedtuple
import requests
Status = namedtuple("Status", ['doorstate', 'timestamp', 'text'])
class Source(metaclass=ABCMeta):
@abstractmethod
def get_status(self):
pass
def is_recent(self, status, **kwargs):
return status.timestamp + datetime.timedelta(days=1) < datetime.datetime.today()
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'])))

View File

@ -1,124 +0,0 @@
import logging
from abc import ABCMeta, abstractmethod
import requests
from matrix_client.client import MatrixClient as MatrixApiClient
from matrix_client.errors import MatrixError
class Client(metaclass=ABCMeta):
@abstractmethod
def __init__(self):
pass
def post_image(self, file, name, content_type="image/png", targets=None):
"""Push to all targets"""
self.post_image(file, name, content_type, targets)
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
file (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)
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)