summaryrefslogtreecommitdiff
path: root/src/home/web_api/web_api.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/home/web_api/web_api.py')
-rw-r--r--src/home/web_api/web_api.py213
1 files changed, 213 insertions, 0 deletions
diff --git a/src/home/web_api/web_api.py b/src/home/web_api/web_api.py
new file mode 100644
index 0000000..c75c031
--- /dev/null
+++ b/src/home/web_api/web_api.py
@@ -0,0 +1,213 @@
+import logging
+import json
+import os.path
+
+from datetime import datetime, timedelta
+from typing import Optional
+
+from werkzeug.exceptions import HTTPException
+from flask import Flask, request, Response
+
+from ..config import config, is_development_mode
+from ..database import BotsDatabase, SensorsDatabase
+from ..util import stringify, format_tb
+from ..api.types import BotType, TemperatureSensorLocation, SoundSensorLocation
+from ..sound import RecordStorage
+
+db: Optional[BotsDatabase] = None
+sensors_db: Optional[SensorsDatabase] = None
+app = Flask(__name__)
+logger = logging.getLogger(__name__)
+
+
+class AuthError(Exception):
+ def __init__(self, message: str):
+ super().__init__()
+ self.message = message
+
+
+# api methods
+# -----------
+
+@app.route("/")
+def hello():
+ message = "nothing here, keep lurking"
+ if is_development_mode():
+ message += ' (dev mode)'
+ return message
+
+
+@app.route('/api/sensors/data/', methods=['GET'])
+def sensors_data():
+ hours = request.args.get('hours', type=int, default=1)
+ sensor = TemperatureSensorLocation(request.args.get('sensor', type=int))
+
+ if hours < 1 or hours > 24:
+ raise ValueError('invalid hours value')
+
+ dt_to = datetime.now()
+ dt_from = dt_to - timedelta(hours=hours)
+
+ data = sensors_db.get_temperature_recordings(sensor, (dt_from, dt_to))
+ return ok(data)
+
+
+@app.route('/api/sound_sensors/hits/', methods=['GET'])
+def get_sound_sensors_hits():
+ location = SoundSensorLocation(request.args.get('location', type=int))
+
+ after = request.args.get('after', type=int)
+ kwargs = {}
+ if after is None:
+ last = request.args.get('last', type=int)
+ 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 = db.get_sound_hits(location, **kwargs)
+ return ok(data)
+
+
+@app.route('/api/sound_sensors/hits/', methods=['POST'])
+def post_sound_sensors_hits():
+ hits = []
+ for hit, count in json.loads(request.form.get('hits', type=str)):
+ 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))
+
+ db.add_sound_hits(hits, datetime.now())
+ return ok()
+
+
+@app.route('/api/logs/bot-request/', methods=['POST'])
+def log_bot_request():
+ user_id = request.form.get('user_id', type=int, default=0)
+ message = request.form.get('message', type=str, default='')
+ bot = BotType(request.form.get('bot', type=int))
+
+ # validate message
+ if message.strip() == '':
+ raise ValueError('message can\'t be empty')
+
+ # add record to the database
+ db.add_request(bot, user_id, message)
+
+ return ok()
+
+
+@app.route('/api/logs/openwrt/', methods=['POST'])
+def log_openwrt():
+ logs = request.form.get('logs', type=str, default='')
+
+ # 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]
+ ))
+
+ db.add_openwrt_logs(lines)
+ return ok()
+
+
+@app.route('/api/recordings/list/', methods=['GET'])
+def recordings_list():
+ extended = request.args.get('extended', type=bool, default=False)
+ node = request.args.get('node', type=str)
+
+ root = os.path.join(config['recordings']['directory'], node)
+ if not os.path.isdir(root):
+ raise ValueError(f'invalid node {node}: no such directory')
+
+ storage = RecordStorage(root)
+ files = storage.getfiles(as_objects=extended)
+ if extended:
+ files = list(map(lambda file: file.__dict__(), files))
+
+ return ok(files)
+
+
+# internal functions
+# ------------------
+
+def ok(data=None) -> Response:
+ response = {'result': 'ok'}
+ if data is not None:
+ response['data'] = data
+ return Response(stringify(response),
+ mimetype='application/json')
+
+
+def err(e) -> Response:
+ error = {
+ 'type': e.__class__.__name__,
+ 'message': e.message if hasattr(e, 'message') else str(e)
+ }
+ if is_development_mode():
+ tb = format_tb(e)
+ if tb:
+ error['stacktrace'] = tb
+ data = {
+ 'result': 'error',
+ 'error': error
+ }
+ return Response(stringify(data), mimetype='application/json')
+
+
+def get_token() -> Optional[str]:
+ name = 'X-Token'
+ if name in request.headers:
+ return request.headers[name]
+
+ token = request.args.get('token', default='', type=str)
+ if token != '':
+ return token
+
+ return None
+
+
+@app.errorhandler(Exception)
+def handle_exception(e):
+ if isinstance(e, HTTPException):
+ return e
+ return err(e), 500
+
+
+@app.before_request
+def validate_token() -> None:
+ if request.path.startswith('/api/') and not is_development_mode():
+ token = get_token()
+ if not token:
+ raise AuthError(f'token is missing')
+
+ if token != config['api']['token']:
+ raise AuthError('invalid token')
+
+
+def get_app():
+ global db, sensors_db
+
+ config.load('web_api')
+ app.config.from_mapping(**config['flask'])
+
+ db = BotsDatabase()
+ sensors_db = SensorsDatabase()
+
+ return app