import re import urllib.parse from functools import wraps from random import randrange import flask import gevent from common import database from flask import jsonify, request, copy_current_request_context from gevent import sleep from psycopg2.extras import execute_values from google.oauth2 import id_token from google.auth.transport import requests app = flask.Flask('buscribe') def authenticate(f): """Authenticate a token against the database. Reference: https://developers.google.com/identity/sign-in/web/backend-auth https://developers.google.com/identity/gsi/web/guides/verify-google-id-token#using-a-google-api-client-library""" @wraps(f) def auth_wrapper(*args, **kwargs): try: user_token = request.cookies.get("credentials") print(user_token) except (KeyError, TypeError): return 'User token required', 401 try: idinfo = id_token.verify_oauth2_token(user_token, requests.Request(), "164084252563-kaks3no7muqb82suvbubg7r0o87aip7n.apps.googleusercontent.com") if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: raise ValueError('Wrong issuer.') except ValueError: return 'Invalid token. Access denied.', 403 # check whether user is in the database email = idinfo['email'].lower() conn = app.db_manager.get_conn() results = database.query(conn, """ SELECT email FROM buscribe_verifiers WHERE lower(email) = %s""", email) row = results.fetchone() if row is None: return 'Unknown user. Access denied.', 403 return f(*args, editor=email, **kwargs) return auth_wrapper @app.route('/professor/line/', methods=["GET"]) def get_line(line_id): db_conn = app.db_manager.get_conn() line = database.query(db_conn, "SELECT * FROM buscribe_transcriptions WHERE id = %(id)s;", id=line_id).fetchone() if line is None: return "Line not found.", 404 else: return {"id": line.id, "start_time": line.start_time.isoformat(), "end_time": line.end_time.isoformat(), "line_data": line.transcription_json} @app.route('/professor/line/random', methods=["GET"]) def get_random_line(): db_conn = app.db_manager.get_conn() n_lines = database.query(db_conn, "SELECT count(*) AS n_lines FROM buscribe_transcriptions;").fetchone().n_lines row = randrange(n_lines) line = database.query(db_conn, "SELECT * FROM buscribe_transcriptions OFFSET %(row)s LIMIT 1;", row=row).fetchone() if line is None: return "Line not found.", 404 else: return {"id": line.id, "start_time": line.start_time.isoformat(), "end_time": line.end_time.isoformat(), "line_data": line.transcription_json} @app.route('/professor/line//playlist.m3u8', methods=["GET"]) def get_playlist(line_id): db_conn = app.db_manager.get_conn() line = database.query(db_conn, "SELECT * FROM buscribe_transcriptions WHERE id = %(id)s;", id=line_id).fetchone() if line is None: return "Line not found.", 404 else: start_time_iso = line.start_time.isoformat() end_time_iso = line.end_time.isoformat() duration = line.end_time - line.start_time return f"""#EXTM3U #EXT-X-PLAYLIST-TYPE:vod #EXT-X-TARGETDURATION:{duration.total_seconds()} #EXT-X-PROGRAM-DATE-TIME:{start_time_iso} #EXTINF:{duration.total_seconds()} /cut/desertbus/source.ts?start={urllib.parse.quote_plus(start_time_iso)}&end={urllib.parse.quote_plus(end_time_iso)}&type=rough&allow_holes=true #EXT-X-ENDLIST""" @app.route('/professor/line/', methods=["POST"]) @authenticate def update_line(line_id, editor): db_conn = app.db_manager.get_conn() if "speakers" in request.json and \ isinstance(request.json["speakers"], list) and \ request.json["speakers"] != []: # Simpler than dealing with uniqueness database.query(db_conn, "DELETE FROM buscribe_line_speakers WHERE line = %(line_id)s AND verifier = %(verifier)s;", line_id=line_id, verifier=editor) execute_values(db_conn.cursor(), "INSERT INTO buscribe_line_speakers(line, speaker, verifier) " "VALUES %s;", [(line_id, speaker, editor) for speaker in request.json["speakers"]]) if "transcription" in request.json and \ isinstance(request.json["transcription"], str) and \ request.json["transcription"] != "": verified_line = request.json["transcription"].lower() verified_line = re.sub(r"[^[a-z]\s']]", "", verified_line) database.query(db_conn, "DELETE FROM buscribe_verified_lines WHERE line = %(line_id)s AND verifier = %(verifier)s;", line_id=line_id, verifier=editor) database.query(db_conn, "INSERT INTO buscribe_verified_lines(line, verified_line, verifier) " "VALUES (%(line)s, %(verified_line)s, %(verifier)s)", line=line_id, verified_line=verified_line, verifier=editor) return "", 204 @app.route('/professor/speaker', methods=["GET"]) def get_speakers(): db_conn = app.db_manager.get_conn() speakers = database.query(db_conn, "SELECT id, name FROM buscribe_speakers;") return jsonify([{"id": speaker.id, "name": speaker.name} for speaker in speakers]) @app.route('/professor/speaker/', methods=["GET"]) def get_speaker(speaker_id): db_conn = app.db_manager.get_conn() speaker = database.query(db_conn, "SELECT name FROM buscribe_speakers WHERE id = %(id)s;", id=speaker_id).fetchone() if speaker is None: return "Speaker not found.", 404 else: return jsonify(speaker.name) @app.route('/professor/speaker', methods=["PUT"]) @authenticate def new_speaker(editor=None): name = request.json if not isinstance(name, str): return "Invalid name!", 400 name = name.lower() name = re.sub(r"[^\w\s']", "", name) db_conn = app.db_manager.get_conn() speakers = database.query(db_conn, "INSERT INTO buscribe_speakers(name) " "VALUES (%(name)s) " "ON CONFLICT (name) DO UPDATE SET name=EXCLUDED.name " "RETURNING id;", name=name) return "", 200, {"Content-Location": f"/professor/speaker/{speakers.fetchone().id}"}