diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2022-10-25 02:41:46 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2022-10-30 01:37:35 +0300 |
commit | db784dc98ba0c4e15ee7b501d909425c79c825fb (patch) | |
tree | 68270179f77c95dc49ec4ab62f9305aa651c4cd4 /src/web_api.py | |
parent | 0fce6c52516aba239acc81fd528dcb5051c04f68 (diff) |
web_api: rewrite to aiohttp, drop flask
Diffstat (limited to 'src/web_api.py')
-rwxr-xr-x | src/web_api.py | 199 |
1 files changed, 192 insertions, 7 deletions
diff --git a/src/web_api.py b/src/web_api.py index beaab57..2a3dfcd 100755 --- a/src/web_api.py +++ b/src/web_api.py @@ -1,13 +1,198 @@ #!/usr/bin/env python3 -from home.web_api import get_app -from typing import Optional -from flask import Flask +import asyncio +import json +import os -app: Optional[Flask] = None +from datetime import datetime, timedelta +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from aiohttp import web +from home import http +from home.util import parse_addr +from home.config import config, is_development_mode +from home.database import BotsDatabase, SensorsDatabase, InverterDatabase +from home.api.types import BotType, TemperatureSensorLocation, SoundSensorLocation +from home.media import SoundRecordStorage -if __name__ in ('__main__', 'app'): - app = get_app() + +class AuthError(Exception): + def __init__(self, message: str): + super().__init__() + self.message = message + + +class WebAPIServer(http.HTTPServer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.app.middlewares.append(self.validate_auth) + + self.get('/', self.get_index) + self.get('/sensors/data/', self.GET_sensors_data) + self.get('/sound-sensors/hits/', self.GET_sound_sensors_hits) + self.post('/sound-sensors/hits/', self.POST_sound_sensors_hits) + + self.post('/log/bot-request/', self.POST_bot_request_log) + self.post('/log/openwrt/', self.POST_openwrt_log) + + self.get('/recordings/list/', self.GET_recordings_list) + + @staticmethod + @web.middleware + async def validate_auth(req: http.Request, handler): + def get_token() -> str: + name = 'X-Token' + if name in req.headers: + return req.headers[name] + + return req.query['token'] + + try: + token = get_token() + except KeyError: + raise AuthError('no token') + + if token != config['api']['token']: + raise AuthError('invalid token') + + return await handler(req) + + @staticmethod + async def get_index(req: http.Request): + message = "nothing here, keep lurking" + if is_development_mode(): + message += ' (dev mode)' + return http.Response(text=message, content_type='text/plain') + + async def GET_sensors_data(self, req: http.Request): + try: + hours = int(req.query['hours']) + if hours < 1 or hours > 24: + raise ValueError('invalid hours value') + except KeyError: + hours = 1 + + sensor = TemperatureSensorLocation(int(req.query['sensor'])) + + dt_to = datetime.now() + dt_from = dt_to - timedelta(hours=hours) + + db = SensorsDatabase() + data = db.get_temperature_recordings(sensor, (dt_from, dt_to)) + return self.ok(data) + + async def GET_sound_sensors_hits(self, req: http.Request): + location = SoundSensorLocation(int(req.query['location'])) + + after = int(req.query['after']) + kwargs = {} + if after is None: + last = int(req.query['last']) + if last is None: + raise ValueError('you must pass `after` or `last` params') + else: + if not 0 < last < 100: + raise ValueError('invalid last value: must be between 0 and 100') + kwargs['last'] = last + else: + kwargs['after'] = datetime.fromtimestamp(after) + + data = BotsDatabase().get_sound_hits(location, **kwargs) + return self.ok(data) + + async def POST_sound_sensors_hits(self, req: http.Request): + hits = [] + data = await req.post() + for hit, count in json.loads(data['hits']): + if not hasattr(SoundSensorLocation, hit.upper()): + raise ValueError('invalid sensor location') + if count < 1: + raise ValueError(f'invalid count: {count}') + hits.append((SoundSensorLocation[hit.upper()], count)) + + BotsDatabase().add_sound_hits(hits, datetime.now()) + return self.ok() + + async def POST_bot_request_log(self, req: http.Request): + data = await req.post() + + try: + user_id = int(data['user_id']) + except KeyError: + user_id = 0 + + try: + message = data['message'] + except KeyError: + message = '' + + bot = BotType(int(data['bot'])) + + # validate message + if message.strip() == '': + raise ValueError('message can\'t be empty') + + # add record to the database + BotsDatabase().add_request(bot, user_id, message) + + return self.ok() + + async def POST_openwrt_log(self, req: http.Request): + data = await req.post() + + try: + logs = data['logs'] + except KeyError: + logs = '' + + # validate it + logs = json.loads(logs) + assert type(logs) is list, "invalid json data (list expected)" + + lines = [] + for line in logs: + assert type(line) is list, "invalid line type (list expected)" + assert len(line) == 2, f"expected 2 items in line, got {len(line)}" + assert type(line[0]) is int, "invalid line[0] type (int expected)" + assert type(line[1]) is str, "invalid line[1] type (str expected)" + + lines.append(( + datetime.fromtimestamp(line[0]), + line[1] + )) + + BotsDatabase().add_openwrt_logs(lines) + return self.ok() + + async def GET_recordings_list(self, req: http.Request): + data = await req.post() + + try: + extended = bool(int(data['extended'])) + except KeyError: + extended = False + + node = data['node'] + + root = os.path.join(config['recordings']['directory'], node) + if not os.path.isdir(root): + raise ValueError(f'invalid node {node}: no such directory') + + storage = SoundRecordStorage(root) + files = storage.getfiles(as_objects=extended) + if extended: + files = list(map(lambda file: file.__dict__(), files)) + + return self.ok(files) + + +# start of the program +# -------------------- if __name__ == '__main__': - app.run(host='0.0.0.0') + config.load('web_api') + + loop = asyncio.get_event_loop() + + server = WebAPIServer(parse_addr(config['server']['listen'])) + server.run() |