summaryrefslogtreecommitdiff
path: root/src/web_api.py
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2022-10-25 02:41:46 +0300
committerEvgeny Zinoviev <me@ch1p.io>2022-10-30 01:37:35 +0300
commitdb784dc98ba0c4e15ee7b501d909425c79c825fb (patch)
tree68270179f77c95dc49ec4ab62f9305aa651c4cd4 /src/web_api.py
parent0fce6c52516aba239acc81fd528dcb5051c04f68 (diff)
web_api: rewrite to aiohttp, drop flask
Diffstat (limited to 'src/web_api.py')
-rwxr-xr-xsrc/web_api.py199
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()