import time from sanic import Blueprint, response from .decorators import auth_route from .streaming import OP from ..snowflake import get_snowflake bp = Blueprint(__name__) # since sqlite uses bigints, we make sure # we get the same type being sent over. # (yes, I know sqlite can accept floats # in a bigint column, but having a float # would cause API inconsistency). _time = lambda: int(time.time()) def fetch_incident(conn, incident_id: dict) -> tuple: """Fetch a single incident's row.""" cur = conn.cursor() cur.execute(""" SELECT id, incident_type, title, content, ongoing, start_timestamp, end_timestamp FROM incidents WHERE id = ? """, (incident_id,)) return cur.fetchone() def fetch_stages(conn, incident_id: int) -> list: """Fetch all the stages for an incident""" cur = conn.cursor() cur.execute(""" SELECT title, content, timestamp FROM incident_stages WHERE parent_id = ? ORDER BY timestamp ASC """, (incident_id,)) stage_rows = cur.fetchall() def stage_obj(stage_row) -> dict: """give a stage dict, given the stage row.""" return { 'title': stage_row[0], 'content': stage_row[1], 'created_at': stage_row[2], } return list(map(stage_obj, stage_rows)) def publish_update(manager, incident_id: int): """Publish an update to an incident. This makes sure the data being published is the latest by requering the database. """ full = fetch_dict(manager.conn, incident_id) manager.publish_incident(OP.INC_UPDATE, full) def incident_dict(conn, row) -> dict: """make an incident dict, given incident row.""" return { 'id': str(row[0]), 'type': row[1], 'title': row[2], 'content': row[3], 'ongoing': row[4], 'start_date': row[5], 'end_date': row[6], 'stages': fetch_stages(conn, row[0]) } def fetch_dict(conn, incident_id) -> dict: """Fetch an incident and return a dictionary.""" row = fetch_incident(conn, incident_id) return incident_dict(conn, row) @bp.get('/api/incidents/current') async def get_current_incident(request): """Get the current incident, if any.""" manager = request.app.manager cur = manager.conn.cursor() cur.execute(""" SELECT id, incident_type, title, content, ongoing, start_timestamp, end_timestamp FROM incidents ORDER BY id ASC LIMIT 1 """) rows = cur.fetchall() try: row = next(iter(rows)) drow = incident_dict(manager.conn, row) except StopIteration: row = None drow = {} return response.json({ 'all_good': not drow.get('ongoing'), 'current_incident': None if drow == {} else drow }) @bp.get('/api/incidents/') async def get_incidents(request, page: int): """Get a list of incidents.""" manager = request.app.manager cur = manager.conn.cursor() cur.execute(f""" SELECT id, incident_type, title, content, ongoing, start_timestamp, end_timestamp FROM incidents ORDER BY id DESC LIMIT 10 OFFSET ({page} * 10) """) rows = cur.fetchall() res = [] for row in rows: res.append(incident_dict(manager.conn, row)) return response.json(res) @bp.put('/api/incidents') @auth_route async def create_incident(request): """Create a new incident and put it as ongoing by default.""" incident = request.json manager = request.app.manager incident_id = get_snowflake() start_timestamp = _time() manager.conn.execute(""" INSERT INTO incidents (id, incident_type, title, content, ongoing, start_timestamp, end_timestamp) VALUES (?, ?, ?, ?, true, ?, NULL) """, ( incident_id, incident['type'], incident['title'], incident['content'], start_timestamp, )) manager.conn.commit() # refetch so we know we have the good stuff incident = fetch_dict(manager.conn, incident_id) manager.publish_incident(OP.INCIDENT_NEW, incident) return response.json(incident) @bp.patch('/api/incidents/') @auth_route async def patch_incident(request, incident_id): """Patch an existing incident.""" incident = request.json manager = request.app.manager if 'end_timestamp' not in incident and not incident['ongoing']: incident['end_timestamp'] = _time() orig = fetch_dict(manager.conn, incident_id) def _get(field): return incident.get(field, orig[field]) manager.conn.execute(""" UPDATE incidents SET incident_type = ?, title = ?, content = ?, ongoing = ?, start_timestamp = ?, end_timestamp = ? WHERE id = ? """, ( _get('type'), _get('title'), _get('content'), _get('ongoing'), _get('start_date'), _get('end_date'), incident_id, )) manager.conn.commit() if incident['ongoing']: publish_update(manager, incident_id) else: manager.publish_incident(OP.INCIDENT_CLOSE, {**incident, **{ 'id': str(incident_id) }}) return response.text('', status=204) @bp.post('/api/incidents//stages') @auth_route async def new_stage(request, incident_id): """Create a new stage in an incident.""" stage = request.json manager = request.app.manager timestamp = _time() manager.conn.execute(""" INSERT INTO incident_stages (parent_id, timestamp, title, content) VALUES (?, ?, ?, ?) """, (incident_id, timestamp, stage['title'], stage['content'])) manager.conn.commit() publish_update(manager, incident_id) return response.json({**{ 'parent_id': str(incident_id), 'created_at': timestamp, }, **stage})