Fix/pylint cleanup (#8)
* Fix pylint warnings across all 24 Python files in web_server - Add module, class, and function docstrings (C0114, C0115, C0116) - Fix import ordering: stdlib before third-party before local (C0411) - Replace wildcard imports with explicit named imports (W0401) - Remove trailing whitespace and add missing final newlines (C0303, C0304) - Replace dict() with dict literals (R1735) - Remove unused imports and variables (W0611, W0612) - Narrow broad Exception catches to specific exceptions (W0718) - Replace f-string logging with lazy % formatting (W1203) - Fix variable naming: UPPER_CASE for constants, snake_case for locals (C0103) - Add pylint disable comments for necessary global statements (W0603) - Fix no-else-return, simplifiable-if-expression, singleton-comparison - Fix bad indentation in stripe.py (W0311) - Add encoding="utf-8" to open() calls (W1514) - Add check=True to subprocess.run() calls (W1510) - Register Celery task modules via conf.include * Update `package-lock.json` add peer dependencies
This commit is contained in:
committed by
GitHub
parent
fed1a2f288
commit
2758be8680
24
frontend/package-lock.json
generated
24
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.5.0",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.5.0",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@stripe/react-stripe-js": "^3.1.1",
|
||||
"@stripe/stripe-js": "^5.5.0",
|
||||
@@ -97,6 +97,7 @@
|
||||
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.26.0",
|
||||
@@ -1393,6 +1394,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.5.0.tgz",
|
||||
"integrity": "sha512-lkfjyAd34aeMpTKKcEVfy8IUyEsjuAT3t9EXr5yZDtdIUncnZpedl/xLV16Dkd4z+fQwixScsCCDxSMNtBOgpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
@@ -1482,6 +1484,7 @@
|
||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -1539,6 +1542,7 @@
|
||||
"integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.20.0",
|
||||
"@typescript-eslint/types": "8.20.0",
|
||||
@@ -1805,6 +1809,7 @@
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2018,6 +2023,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
@@ -2051,9 +2057,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001695",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz",
|
||||
"integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==",
|
||||
"version": "1.0.30001769",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
|
||||
"integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2386,6 +2392,7 @@
|
||||
"integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3534,6 +3541,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -3723,6 +3731,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -3741,6 +3750,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -4233,6 +4243,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -4351,6 +4362,7 @@
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4443,6 +4455,7 @@
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.21.0.tgz",
|
||||
"integrity": "sha512-zcwerRb257QAuWfi8NH9yEX7vrGKFthjfcONmOQ4lxFRpDAbAi+u5LAjCjMWqhJda6zEmxkgdDpOMW3Y21QpXA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/http-streaming": "^3.16.2",
|
||||
@@ -4495,6 +4508,7 @@
|
||||
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.24.2",
|
||||
"postcss": "^8.4.49",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"""Flask application factory and blueprint registration."""
|
||||
|
||||
from os import getenv
|
||||
|
||||
from flask import Flask
|
||||
from flask_session import Session
|
||||
from flask_cors import CORS
|
||||
@@ -13,10 +17,8 @@ from blueprints.oauth import oauth_bp, init_oauth
|
||||
from blueprints.socket import socketio
|
||||
from blueprints.search_bar import search_bp
|
||||
|
||||
from celery import Celery
|
||||
from celery_tasks import celery_init_app
|
||||
|
||||
from os import getenv
|
||||
|
||||
def create_app():
|
||||
"""
|
||||
@@ -34,16 +36,15 @@ def create_app():
|
||||
app.config['GOOGLE_CLIENT_SECRET'] = getenv("GOOGLE_CLIENT_SECRET")
|
||||
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||
|
||||
|
||||
app.config.from_mapping(
|
||||
CELERY=dict(
|
||||
broker_url="redis://redis:6379/0",
|
||||
result_backend="redis://redis:6379/0",
|
||||
task_ignore_result=True,
|
||||
),
|
||||
CELERY={
|
||||
"broker_url": "redis://redis:6379/0",
|
||||
"result_backend": "redis://redis:6379/0",
|
||||
"task_ignore_result": True,
|
||||
},
|
||||
)
|
||||
app.config.from_prefixed_env()
|
||||
celery = celery_init_app(app)
|
||||
celery_init_app(app)
|
||||
|
||||
#! ↓↓↓ For development purposes only - Allow cross-origin requests for the frontend
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Admin blueprint for user management operations."""
|
||||
|
||||
from flask import Blueprint, session
|
||||
from utils.utils import sanitize
|
||||
from utils.admin_utils import *
|
||||
from utils.admin_utils import check_if_admin, check_if_user_exists, ban_user
|
||||
|
||||
admin_bp = Blueprint("admin", __name__)
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""Authentication blueprint for user signup, login, and logout."""
|
||||
|
||||
import logging
|
||||
from secrets import token_hex
|
||||
|
||||
from flask import Blueprint, session, request, jsonify
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask_cors import cross_origin
|
||||
@@ -5,18 +10,21 @@ from database.database import Database
|
||||
from blueprints.middleware import login_required
|
||||
from utils.user_utils import get_user_id
|
||||
from utils.utils import sanitize
|
||||
from secrets import token_hex
|
||||
from utils.path_manager import PathManager
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
path_manager = PathManager()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@auth_bp.route("/signup", methods=["POST"])
|
||||
@cross_origin(supports_credentials=True)
|
||||
def signup():
|
||||
"""
|
||||
Route that allows a user to sign up by providing a `username`, `email` and `password`.
|
||||
Route that allows a user to sign up by providing a
|
||||
`username`, `email` and `password`.
|
||||
"""
|
||||
# ensure a JSON request is made to contact this route
|
||||
if not request.is_json:
|
||||
@@ -30,7 +38,9 @@ def signup():
|
||||
|
||||
# Validation - ensure all fields exist, users cannot have an empty field
|
||||
if not all([username, email, password]):
|
||||
error_fields = get_error_fields([username, email, password]) #!←← find the error_fields, to highlight them in red to the user on the frontend
|
||||
error_fields = get_error_fields(
|
||||
[username, email, password]
|
||||
)
|
||||
return jsonify({
|
||||
"account_created": False,
|
||||
"error_fields": error_fields,
|
||||
@@ -42,7 +52,7 @@ def signup():
|
||||
username = sanitize(username, "username")
|
||||
email = sanitize(email, "email")
|
||||
password = sanitize(password, "password")
|
||||
except ValueError as e:
|
||||
except ValueError:
|
||||
error_fields = get_error_fields([username, email, password])
|
||||
return jsonify({
|
||||
"account_created": False,
|
||||
@@ -100,11 +110,11 @@ def signup():
|
||||
"message": "Account created successfully"
|
||||
}), 201
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during signup: {e}") # Log the error
|
||||
except (ValueError, TypeError, KeyError) as exc:
|
||||
logger.error("Error during signup: %s", exc)
|
||||
return jsonify({
|
||||
"account_created": False,
|
||||
"message": "Server error occurred: " + str(e)
|
||||
"message": "Server error occurred: " + str(exc)
|
||||
}), 500
|
||||
|
||||
finally:
|
||||
@@ -138,7 +148,7 @@ def login():
|
||||
try:
|
||||
username = sanitize(username, "username")
|
||||
password = sanitize(password, "password")
|
||||
except ValueError as e:
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"account_created": False,
|
||||
"error_fields": ["username", "password"],
|
||||
@@ -177,7 +187,10 @@ def login():
|
||||
session.clear()
|
||||
session["username"] = username
|
||||
session["user_id"] = get_user_id(username)
|
||||
print(f"Logged in as {username}. session: {session.get('username')}. user_id: {session.get('user_id')}", flush=True)
|
||||
logger.info(
|
||||
"Logged in as %s. session: %s. user_id: %s",
|
||||
username, session.get('username'), session.get('user_id')
|
||||
)
|
||||
|
||||
# User has been logged in, let frontend know that
|
||||
return jsonify({
|
||||
@@ -186,8 +199,8 @@ def login():
|
||||
"username": username
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during login: {e}") # Log the error
|
||||
except (ValueError, TypeError, KeyError) as exc:
|
||||
logger.error("Error during login: %s", exc)
|
||||
return jsonify({
|
||||
"logged_in": False,
|
||||
"message": "Server error occurred"
|
||||
@@ -206,7 +219,6 @@ def logout() -> dict:
|
||||
If the user is currently streaming, end their stream first.
|
||||
Can only be accessed by a logged in user.
|
||||
"""
|
||||
from database.database import Database
|
||||
from utils.stream_utils import end_user_stream
|
||||
|
||||
# Check if user is currently streaming
|
||||
@@ -214,12 +226,21 @@ def logout() -> dict:
|
||||
username = session.get("username")
|
||||
|
||||
with Database() as db:
|
||||
is_streaming = db.fetchone("""SELECT is_live FROM users WHERE user_id = ?""", (user_id,))
|
||||
is_streaming = db.fetchone(
|
||||
"""SELECT is_live FROM users WHERE user_id = ?""",
|
||||
(user_id,)
|
||||
)
|
||||
|
||||
if is_streaming and is_streaming.get("is_live") == 1:
|
||||
# Get the user's stream key
|
||||
stream_key_info = db.fetchone("""SELECT stream_key FROM users WHERE user_id = ?""", (user_id,))
|
||||
stream_key = stream_key_info.get("stream_key") if stream_key_info else None
|
||||
stream_key_info = db.fetchone(
|
||||
"""SELECT stream_key FROM users WHERE user_id = ?""",
|
||||
(user_id,)
|
||||
)
|
||||
stream_key = (
|
||||
stream_key_info.get("stream_key") if stream_key_info
|
||||
else None
|
||||
)
|
||||
|
||||
if stream_key:
|
||||
# End the stream
|
||||
@@ -227,6 +248,8 @@ def logout() -> dict:
|
||||
session.clear()
|
||||
return {"logged_in": False}
|
||||
|
||||
|
||||
def get_error_fields(values: list):
|
||||
"""Return field names for empty values."""
|
||||
fields = ["username", "email", "password"]
|
||||
return [fields[i] for i, v in enumerate(values) if not v]
|
||||
@@ -1,14 +1,17 @@
|
||||
"""Chat blueprint for WebSocket-based real-time messaging."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
from database.database import Database
|
||||
from .socket import socketio
|
||||
from flask_socketio import emit, join_room, leave_room
|
||||
from datetime import datetime
|
||||
from utils.user_utils import get_user_id, is_subscribed
|
||||
from utils.user_utils import is_subscribed
|
||||
import redis
|
||||
import json
|
||||
|
||||
redis_url = "redis://redis:6379/1"
|
||||
r = redis.from_url(redis_url, decode_responses=True)
|
||||
REDIS_URL = "redis://redis:6379/1"
|
||||
r = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
chat_bp = Blueprint("chat", __name__)
|
||||
|
||||
@socketio.on("connect")
|
||||
@@ -125,7 +128,13 @@ def send_chat(data) -> None:
|
||||
|
||||
# Input validation - chatter is logged in, message is not empty, stream exists
|
||||
if not all([chatter_name, message, stream_id]):
|
||||
emit("error", {"error": f"Unable to send a chat. The following info was given: chatter_name={chatter_name}, message={message}, stream_id={stream_id}"}, broadcast=False)
|
||||
emit("error", {
|
||||
"error": (
|
||||
f"Unable to send a chat. The following info was given: "
|
||||
f"chatter_name={chatter_name}, message={message}, "
|
||||
f"stream_id={stream_id}"
|
||||
)
|
||||
}, broadcast=False)
|
||||
return
|
||||
subscribed = is_subscribed(chatter_id, stream_id)
|
||||
# Send the chat message to the client so it can be displayed
|
||||
@@ -161,9 +170,8 @@ def update_viewers(user_id, num_viewers):
|
||||
SET num_viewers = ?
|
||||
WHERE user_id = ?;
|
||||
""", (num_viewers, user_id))
|
||||
db.close_connection
|
||||
db.close_connection()
|
||||
|
||||
#TODO: Make sure that users entry within Redis is removed if they disconnect from socket
|
||||
def add_favourability_entry(user_id, stream_id):
|
||||
"""
|
||||
Adds entry to Redis that user is watching a streamer
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from flask import redirect, g, session
|
||||
from functools import wraps
|
||||
"""Authentication middleware and error handler registration."""
|
||||
|
||||
import logging
|
||||
from functools import wraps
|
||||
from os import getenv
|
||||
|
||||
from flask import redirect, g, session
|
||||
from dotenv import load_dotenv
|
||||
from database.database import Database
|
||||
|
||||
@@ -57,5 +60,5 @@ def register_error_handlers(app):
|
||||
for code, message in error_responses.items():
|
||||
@app.errorhandler(code)
|
||||
def handle_error(error, message=message, code=code):
|
||||
logging.error(f"Error {code}: {str(error)}")
|
||||
logging.error("Error %d: %s", code, str(error))
|
||||
return {"error": message}, code
|
||||
@@ -1,15 +1,18 @@
|
||||
"""OAuth blueprint for Google authentication."""
|
||||
|
||||
from os import getenv
|
||||
from secrets import token_hex, token_urlsafe
|
||||
from random import randint
|
||||
|
||||
from authlib.integrations.flask_client import OAuth, OAuthError
|
||||
from flask import Blueprint, jsonify, session, redirect, request
|
||||
from blueprints.user import get_session_info_email
|
||||
from database.database import Database
|
||||
from dotenv import load_dotenv
|
||||
from secrets import token_hex, token_urlsafe
|
||||
from random import randint
|
||||
from utils.path_manager import PathManager
|
||||
|
||||
oauth_bp = Blueprint("oauth", __name__)
|
||||
google = None
|
||||
_google = None
|
||||
|
||||
load_dotenv()
|
||||
url_api = getenv("VITE_API_URL")
|
||||
@@ -23,8 +26,8 @@ def init_oauth(app):
|
||||
Initialise the OAuth functionality.
|
||||
"""
|
||||
oauth = OAuth(app)
|
||||
global google
|
||||
google = oauth.register(
|
||||
global _google # pylint: disable=global-statement
|
||||
_google = oauth.register(
|
||||
'google',
|
||||
client_id=app.config['GOOGLE_CLIENT_ID'],
|
||||
client_secret=app.config['GOOGLE_CLIENT_SECRET'],
|
||||
@@ -54,7 +57,7 @@ def login_google():
|
||||
# Make sure session is saved before redirect
|
||||
session.modified = True
|
||||
|
||||
return google.authorize_redirect(
|
||||
return _google.authorize_redirect(
|
||||
redirect_uri=f'{url}/api/google_auth',
|
||||
nonce=session['nonce'],
|
||||
state=session['state']
|
||||
@@ -72,21 +75,25 @@ def google_auth():
|
||||
stored_state = session.get('state')
|
||||
|
||||
if not stored_state or stored_state != returned_state:
|
||||
print(f"State mismatch: stored={stored_state}, returned={returned_state}", flush=True)
|
||||
print(
|
||||
f"State mismatch: stored={stored_state}, "
|
||||
f"returned={returned_state}", flush=True
|
||||
)
|
||||
return jsonify({
|
||||
'error': f"mismatching_state: CSRF Warning! State not equal in request and response.",
|
||||
'error': "mismatching_state: CSRF Warning! "
|
||||
"State not equal in request and response.",
|
||||
'message': 'Authentication failed'
|
||||
}), 400
|
||||
|
||||
# State matched, proceed with token authorization
|
||||
token = google.authorize_access_token()
|
||||
token = _google.authorize_access_token()
|
||||
|
||||
# Verify nonce
|
||||
nonce = session.get('nonce')
|
||||
if not nonce:
|
||||
return jsonify({'error': 'Missing nonce in session'}), 400
|
||||
|
||||
user = google.parse_id_token(token, nonce=nonce)
|
||||
user = _google.parse_id_token(token, nonce=nonce)
|
||||
|
||||
# Check if email exists to login else create a database entry
|
||||
user_email = user.get("email")
|
||||
@@ -133,7 +140,10 @@ def google_auth():
|
||||
# Ensure session is saved
|
||||
session.modified = True
|
||||
|
||||
print(f"session: {session.get('username')}. user_id: {session.get('user_id')}", flush=True)
|
||||
print(
|
||||
f"session: {session.get('username')}. "
|
||||
f"user_id: {session.get('user_id')}", flush=True
|
||||
)
|
||||
|
||||
return redirect(origin)
|
||||
|
||||
@@ -144,7 +154,7 @@ def google_auth():
|
||||
'error': str(e)
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
except (ValueError, TypeError, KeyError) as e:
|
||||
print(f"Unexpected Error: {str(e)}", flush=True)
|
||||
return jsonify({
|
||||
'message': 'An unexpected error occurred',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Search bar blueprint for querying categories, users, streams, and VODs."""
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from database.database import Database
|
||||
from utils.utils import sanitize
|
||||
@@ -20,7 +22,7 @@ def rank_results(query, result):
|
||||
# Assign a score based on the level of the match
|
||||
if query in result:
|
||||
return 0
|
||||
elif all(c in charset for c in query):
|
||||
if all(c in charset for c in query):
|
||||
return 1
|
||||
return 2
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""WebSocket configuration using Flask-SocketIO."""
|
||||
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
socketio = SocketIO(
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
"""Stream and VOD management blueprint."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, session, jsonify, request, redirect
|
||||
from utils.stream_utils import *
|
||||
from utils.recommendation_utils import *
|
||||
from utils.stream_utils import (
|
||||
get_category_id, get_current_stream_data, get_streamer_live_status,
|
||||
get_latest_vod, get_vod, get_user_vods, end_user_stream
|
||||
)
|
||||
from utils.recommendation_utils import (
|
||||
get_user_preferred_category, get_highest_view_streams,
|
||||
get_streams_based_on_category, get_highest_view_categories,
|
||||
get_user_category_recommendations, get_followed_categories_recommendations
|
||||
)
|
||||
from utils.user_utils import get_user_id
|
||||
from blueprints.middleware import login_required
|
||||
from database.database import Database
|
||||
from datetime import datetime
|
||||
from celery_tasks.streaming import update_thumbnail, combine_ts_stream
|
||||
from dateutil import parser
|
||||
from celery_tasks.streaming import update_thumbnail
|
||||
from utils.path_manager import PathManager
|
||||
from PIL import Image
|
||||
import json
|
||||
|
||||
stream_bp = Blueprint("stream", __name__)
|
||||
|
||||
@@ -28,12 +37,12 @@ def popular_streams(no_streams) -> list[dict]:
|
||||
Returns a list of streams live now with the highest viewers
|
||||
"""
|
||||
|
||||
# Limit the number of streams to MAX_STREAMS
|
||||
MAX_STREAMS = 100
|
||||
# Limit the number of streams to max_streams
|
||||
max_streams = 100
|
||||
if no_streams < 1:
|
||||
return jsonify([])
|
||||
elif no_streams > MAX_STREAMS:
|
||||
no_streams = MAX_STREAMS
|
||||
if no_streams > max_streams:
|
||||
no_streams = max_streams
|
||||
|
||||
# Get the highest viewed streams
|
||||
streams = get_highest_view_streams(no_streams)
|
||||
@@ -101,7 +110,7 @@ def popular_categories(no_categories=4, offset=0) -> list[dict]:
|
||||
# Limit the number of categories to 100
|
||||
if no_categories < 1:
|
||||
return jsonify([])
|
||||
elif no_categories > 100:
|
||||
if no_categories > 100:
|
||||
no_categories = 100
|
||||
|
||||
category_data = get_highest_view_categories(no_categories, offset)
|
||||
@@ -135,7 +144,8 @@ def following_categories_streams():
|
||||
@stream_bp.route('/user/<string:username>/status')
|
||||
def user_live_status(username):
|
||||
"""
|
||||
Returns a streamer's status, if they are live or not and their most recent stream (as a vod) (their current stream if live)
|
||||
Returns a streamer's status, if they are live or not and their
|
||||
most recent stream (as a vod) (their current stream if live)
|
||||
Returns:
|
||||
{
|
||||
"is_live": bool,
|
||||
@@ -146,7 +156,7 @@ def user_live_status(username):
|
||||
user_id = get_user_id(username)
|
||||
|
||||
# Check if streamer is live and get their most recent vod
|
||||
is_live = True if get_streamer_live_status(user_id)['is_live'] else False
|
||||
is_live = bool(get_streamer_live_status(user_id)['is_live'])
|
||||
most_recent_vod = get_latest_vod(user_id)
|
||||
|
||||
# If there is no most recent vod, set it to None
|
||||
@@ -171,25 +181,24 @@ def user_live_status_direct(username):
|
||||
"""
|
||||
|
||||
user_id = get_user_id(username)
|
||||
is_live = True if get_streamer_live_status(user_id)['is_live'] else False
|
||||
is_live = bool(get_streamer_live_status(user_id)['is_live'])
|
||||
|
||||
if is_live:
|
||||
return 'ok', 200
|
||||
else:
|
||||
return 'not live', 404
|
||||
|
||||
|
||||
# VOD Routes
|
||||
@stream_bp.route('/vods/<int:vod_id>')
|
||||
def vod(vod_id):
|
||||
def single_vod(vod_id):
|
||||
"""
|
||||
Returns details about a specific vod
|
||||
"""
|
||||
vod = get_vod(vod_id)
|
||||
return jsonify(vod)
|
||||
vod_data = get_vod(vod_id)
|
||||
return jsonify(vod_data)
|
||||
|
||||
@stream_bp.route('/vods/<string:username>')
|
||||
def vods(username):
|
||||
def user_vods(username):
|
||||
"""
|
||||
Returns a JSON of all the vods of a streamer
|
||||
Returns:
|
||||
@@ -207,8 +216,8 @@ def vods(username):
|
||||
|
||||
"""
|
||||
user_id = get_user_id(username)
|
||||
vods = get_user_vods(user_id)
|
||||
return jsonify(vods)
|
||||
vod_list = get_user_vods(user_id)
|
||||
return jsonify(vod_list)
|
||||
|
||||
@stream_bp.route('/vods/all')
|
||||
def get_all_vods():
|
||||
@@ -216,9 +225,14 @@ def get_all_vods():
|
||||
Returns data of all VODs by all streamers in a JSON-compatible format
|
||||
"""
|
||||
with Database() as db:
|
||||
vods = db.fetchall("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id;""")
|
||||
all_vods = db.fetchall(
|
||||
"""SELECT vods.*, username, category_name
|
||||
FROM vods
|
||||
JOIN users ON vods.user_id = users.user_id
|
||||
JOIN categories ON vods.category_id = categories.category_id;"""
|
||||
)
|
||||
|
||||
return jsonify(vods)
|
||||
return jsonify(all_vods)
|
||||
|
||||
# RTMP Server Routes
|
||||
|
||||
@@ -232,7 +246,8 @@ def init_stream():
|
||||
|
||||
with Database() as db:
|
||||
# Check if valid stream key and user is allowed to stream
|
||||
user_info = db.fetchone("""SELECT user_id, username, is_live
|
||||
user_info = db.fetchone(
|
||||
"""SELECT user_id, username, is_live
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
|
||||
@@ -280,24 +295,26 @@ def publish_stream():
|
||||
username = None
|
||||
|
||||
with Database() as db:
|
||||
user_info = db.fetchone("""SELECT user_id, username, is_live
|
||||
user_info = db.fetchone(
|
||||
"""SELECT user_id, username, is_live
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
|
||||
if not user_info or user_info.get("is_live"):
|
||||
print(
|
||||
"Unauthorized. No user found from Stream key or user is already streaming.", flush=True)
|
||||
"Unauthorized. No user found from Stream key "
|
||||
"or user is already streaming.", flush=True)
|
||||
return "Unauthorized", 403
|
||||
|
||||
user_id = user_info.get("user_id")
|
||||
username = user_info.get("username")
|
||||
|
||||
# Insert stream into database
|
||||
db.execute("""INSERT INTO streams (user_id, title, start_time, num_viewers, category_id)
|
||||
VALUES (?, ?, ?, ?, ?)""", (user_id,
|
||||
stream_title,
|
||||
datetime.now(),
|
||||
0,
|
||||
db.execute(
|
||||
"""INSERT INTO streams
|
||||
(user_id, title, start_time, num_viewers, category_id)
|
||||
VALUES (?, ?, ?, ?, ?)""",
|
||||
(user_id, stream_title, datetime.now(), 0,
|
||||
get_category_id(stream_category)))
|
||||
|
||||
# Set user as streaming
|
||||
@@ -306,10 +323,12 @@ def publish_stream():
|
||||
|
||||
# Update thumbnail periodically only if a custom thumbnail is not provided
|
||||
if not stream_thumbnail:
|
||||
update_thumbnail.apply_async((user_id,
|
||||
update_thumbnail.apply_async(
|
||||
(user_id,
|
||||
path_manager.get_stream_file_path(username),
|
||||
path_manager.get_current_stream_thumbnail_file_path(username),
|
||||
THUMBNAIL_GENERATION_INTERVAL), countdown=10)
|
||||
THUMBNAIL_GENERATION_INTERVAL),
|
||||
countdown=10)
|
||||
|
||||
return "OK", 200
|
||||
|
||||
@@ -332,28 +351,33 @@ def update_stream():
|
||||
|
||||
|
||||
with Database() as db:
|
||||
user_info = db.fetchone("""SELECT user_id, username, is_live
|
||||
user_info = db.fetchone(
|
||||
"""SELECT user_id, username, is_live
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
|
||||
if not user_info or not user_info.get("is_live"):
|
||||
print(
|
||||
"Unauthorized - No user found from stream key or user is not streaming", flush=True)
|
||||
"Unauthorized - No user found from stream key "
|
||||
"or user is not streaming", flush=True)
|
||||
return "Unauthorized", 403
|
||||
|
||||
user_id = user_info.get("user_id")
|
||||
username = user_info.get("username")
|
||||
|
||||
# TODO: Add update to thumbnail here
|
||||
db.execute("""UPDATE streams
|
||||
db.execute(
|
||||
"""UPDATE streams
|
||||
SET title = ?, category_id = ?
|
||||
WHERE user_id = ?""", (stream_title, get_category_id(stream_category), user_id))
|
||||
WHERE user_id = ?""",
|
||||
(stream_title, get_category_id(stream_category), user_id))
|
||||
|
||||
print("GOT: " + stream_thumbnail, flush=True)
|
||||
|
||||
if stream_thumbnail:
|
||||
# Set custom thumbnail status to true
|
||||
db.execute("""UPDATE streams
|
||||
db.execute(
|
||||
"""UPDATE streams
|
||||
SET custom_thumbnail = ?
|
||||
WHERE user_id = ?""", (True, user_id))
|
||||
|
||||
@@ -390,7 +414,8 @@ def end_stream():
|
||||
|
||||
# Get user info from stream key
|
||||
with Database() as db:
|
||||
user_info = db.fetchone("""SELECT user_id, username
|
||||
user_info = db.fetchone(
|
||||
"""SELECT user_id, username
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Stripe payment integration blueprint."""
|
||||
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify, session as s
|
||||
from blueprints.middleware import login_required
|
||||
from utils.user_utils import subscribe
|
||||
import os, stripe
|
||||
import stripe
|
||||
|
||||
stripe_bp = Blueprint("stripe", __name__)
|
||||
|
||||
@@ -20,7 +24,7 @@ def create_checkout_session():
|
||||
# Checks to see who is subscribing to who
|
||||
user_id = s.get("user_id")
|
||||
streamer_id = request.args.get("streamer_id")
|
||||
session = stripe.checkout.Session.create(
|
||||
checkout_session = stripe.checkout.Session.create(
|
||||
ui_mode = 'embedded',
|
||||
payment_method_types=['card'],
|
||||
line_items=[
|
||||
@@ -33,19 +37,24 @@ def create_checkout_session():
|
||||
redirect_on_completion = 'never',
|
||||
client_reference_id = f"{user_id}-{streamer_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
except (ValueError, TypeError, KeyError) as e:
|
||||
return str(e), 500
|
||||
|
||||
return jsonify(clientSecret=session.client_secret)
|
||||
return jsonify(clientSecret=checkout_session.client_secret)
|
||||
|
||||
@stripe_bp.route('/session-status') # check for payment status
|
||||
def session_status():
|
||||
"""
|
||||
Used to query payment status
|
||||
"""
|
||||
session = stripe.checkout.Session.retrieve(request.args.get('session_id'))
|
||||
checkout_session = stripe.checkout.Session.retrieve(
|
||||
request.args.get('session_id')
|
||||
)
|
||||
|
||||
return jsonify(status=session.status, customer_email=session.customer_details.email)
|
||||
return jsonify(
|
||||
status=checkout_session.status,
|
||||
customer_email=checkout_session.customer_details.email
|
||||
)
|
||||
|
||||
@stripe_bp.route('/stripe/webhook', methods=['POST'])
|
||||
def stripe_webhook():
|
||||
@@ -66,12 +75,19 @@ def stripe_webhook():
|
||||
except stripe.error.SignatureVerificationError as e:
|
||||
raise e
|
||||
|
||||
if event['type'] == "checkout.session.completed": # Handles payment success webhook
|
||||
session = event['data']['object']
|
||||
product_id = stripe.checkout.Session.list_line_items(session['id'])['data'][0]['price']['product']
|
||||
# Handles payment success webhook
|
||||
if event['type'] == "checkout.session.completed":
|
||||
checkout_session = event['data']['object']
|
||||
product_id = stripe.checkout.Session.list_line_items(
|
||||
checkout_session['id']
|
||||
)['data'][0]['price']['product']
|
||||
if product_id == subscription:
|
||||
client_reference_id = session.get("client_reference_id")
|
||||
user_id, streamer_id = map(int, client_reference_id.split("-"))
|
||||
client_reference_id = checkout_session.get(
|
||||
"client_reference_id"
|
||||
)
|
||||
user_id, streamer_id = map(
|
||||
int, client_reference_id.split("-")
|
||||
)
|
||||
subscribe(user_id, streamer_id)
|
||||
|
||||
return "Success", 200
|
||||
@@ -1,17 +1,28 @@
|
||||
"""User profile and account management blueprint."""
|
||||
|
||||
from flask import Blueprint, jsonify, session, request
|
||||
from utils.user_utils import *
|
||||
from utils.auth import *
|
||||
from utils.user_utils import (
|
||||
get_user_id, get_user, update_bio, is_subscribed,
|
||||
subscription_expiration, delete_subscription, is_following,
|
||||
follow, unfollow, get_followed_streamers, get_followed_categories,
|
||||
is_following_category, follow_category, unfollow_category,
|
||||
has_password, get_session_info_email
|
||||
)
|
||||
from database.database import Database
|
||||
from utils.auth import verify_token, reset_password
|
||||
from utils.utils import get_category_id
|
||||
from blueprints.middleware import login_required
|
||||
from utils.email import send_email, forgot_password_body, newsletter_conf, remove_from_newsletter, email_exists
|
||||
from utils.email import (
|
||||
send_email, forgot_password_body, newsletter_conf,
|
||||
remove_from_newsletter, email_exists
|
||||
)
|
||||
from utils.path_manager import PathManager
|
||||
from celery_tasks.streaming import convert_image_to_png
|
||||
import redis
|
||||
|
||||
from PIL import Image
|
||||
|
||||
redis_url = "redis://redis:6379/1"
|
||||
r = redis.from_url(redis_url, decode_responses=True)
|
||||
REDIS_URL = "redis://redis:6379/1"
|
||||
r = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
|
||||
user_bp = Blueprint("user", __name__)
|
||||
|
||||
@@ -24,7 +35,7 @@ def user_data(username: str):
|
||||
"""
|
||||
user_id = get_user_id(username)
|
||||
if not user_id:
|
||||
jsonify({"error": "User not found from username"}), 404
|
||||
return jsonify({"error": "User not found from username"}), 404
|
||||
data = get_user(user_id)
|
||||
return jsonify(data)
|
||||
|
||||
@@ -73,7 +84,7 @@ def user_change_bio():
|
||||
bio = data.get("bio")
|
||||
update_bio(user_id, bio)
|
||||
return jsonify({"status": "Success"}), 200
|
||||
except Exception as e:
|
||||
except (ValueError, TypeError, KeyError) as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
## Subscription Routes
|
||||
@@ -198,13 +209,14 @@ def user_forgot_password(email):
|
||||
exists = email_exists(email)
|
||||
password = has_password(email)
|
||||
# Checks if password exists and is not a Google OAuth account
|
||||
if(exists and password):
|
||||
if exists and password:
|
||||
send_email(email, lambda: forgot_password_body(email))
|
||||
return email
|
||||
return jsonify({"error": "Invalid email or not found"}), 404
|
||||
|
||||
@user_bp.route("/send_newsletter/<string:email>", methods=["POST"])
|
||||
def send_newsletter(email):
|
||||
"""Sends a newsletter confirmation email."""
|
||||
send_email(email, lambda: newsletter_conf(email))
|
||||
return email
|
||||
|
||||
@@ -226,6 +238,7 @@ def user_reset_password(token, new_password):
|
||||
|
||||
@user_bp.route("/user/unsubscribe/<string:token>", methods=["POST"])
|
||||
def unsubscribe(token):
|
||||
"""Unsubscribes a user from the newsletter."""
|
||||
salt = r.get(token)
|
||||
if salt:
|
||||
r.delete(token)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
from celery import Celery, shared_task, Task
|
||||
"""Celery configuration and Flask app context setup for async tasks."""
|
||||
|
||||
from celery import Celery, Task
|
||||
|
||||
|
||||
def celery_init_app(app) -> Celery:
|
||||
"""Initialize Celery with Flask application context."""
|
||||
class FlaskTask(Task):
|
||||
"""Celery task that runs within Flask app context."""
|
||||
def __call__(self, *args: object, **kwargs: object) -> object:
|
||||
with app.app_context():
|
||||
return self.run(*args, **kwargs)
|
||||
@@ -14,6 +19,10 @@ def celery_init_app(app) -> Celery:
|
||||
'schedule': 30.0,
|
||||
},
|
||||
}
|
||||
celery_app.conf.include = [
|
||||
'celery_tasks.preferences',
|
||||
'celery_tasks.streaming',
|
||||
]
|
||||
celery_app.set_default()
|
||||
app.extensions["celery"] = celery_app
|
||||
return celery_app
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Celery app initialization with Flask."""
|
||||
|
||||
from blueprints import create_app
|
||||
|
||||
flask_app = create_app()
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
"""Scheduled task for updating user preferences based on stream viewing."""
|
||||
|
||||
import json
|
||||
|
||||
from celery import shared_task
|
||||
from database.database import Database
|
||||
import redis
|
||||
import json
|
||||
|
||||
redis_url = "redis://redis:6379/1"
|
||||
r = redis.from_url(redis_url, decode_responses=True)
|
||||
REDIS_URL = "redis://redis:6379/1"
|
||||
r = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
|
||||
@shared_task
|
||||
def user_preferences():
|
||||
"""
|
||||
Updates users preferences on different stream categories based on the streams they are currently watching
|
||||
Updates users preferences on different stream categories
|
||||
based on the streams they are currently watching
|
||||
"""
|
||||
stats = r.hget("current_viewers", "viewers")
|
||||
# If there are any current viewers
|
||||
@@ -21,13 +25,19 @@ def user_preferences():
|
||||
# For each user and stream combination
|
||||
for stream_id in stream_ids:
|
||||
# Retrieves category associated with stream
|
||||
current_category = db.fetchone("""SELECT category_id FROM streams
|
||||
current_category = db.fetchone(
|
||||
"""SELECT category_id FROM streams
|
||||
WHERE user_id = ?
|
||||
""", (stream_id,))
|
||||
# If stream is still live then update the user_preferences table to reflect their preferences
|
||||
# If stream is still live then update the
|
||||
# user_preferences table to reflect their preferences
|
||||
if current_category:
|
||||
db.execute("""INSERT INTO user_preferences (user_id,category_id,favourability)
|
||||
db.execute(
|
||||
"""INSERT INTO user_preferences
|
||||
(user_id,category_id,favourability)
|
||||
VALUES (?,?,?)
|
||||
ON CONFLICT(user_id, category_id)
|
||||
DO UPDATE SET favourability = favourability + 1
|
||||
""", (user_id, current_category["category_id"], 1))
|
||||
DO UPDATE SET
|
||||
favourability = favourability + 1
|
||||
""", (user_id,
|
||||
current_category["category_id"], 1))
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from celery import Celery, shared_task, Task
|
||||
from datetime import datetime
|
||||
from celery_tasks.preferences import user_preferences
|
||||
from utils.stream_utils import generate_thumbnail, get_streamer_live_status, get_custom_thumbnail_status, remove_hls_files, get_video_duration
|
||||
from time import sleep
|
||||
from os import listdir, remove
|
||||
from utils.path_manager import PathManager
|
||||
"""Async tasks for stream thumbnail updates, VOD creation, and image conversion."""
|
||||
|
||||
import subprocess
|
||||
from os import listdir
|
||||
|
||||
from celery import shared_task
|
||||
from utils.stream_utils import (
|
||||
generate_thumbnail, get_streamer_live_status,
|
||||
get_custom_thumbnail_status, remove_hls_files, get_video_duration
|
||||
)
|
||||
from utils.path_manager import PathManager
|
||||
|
||||
path_manager = PathManager()
|
||||
|
||||
@@ -16,10 +19,13 @@ def update_thumbnail(user_id, stream_file, thumbnail_file, sleep_time, second_ca
|
||||
"""
|
||||
|
||||
# Check if stream is still live and custom thumbnail has not been set
|
||||
if get_streamer_live_status(user_id)['is_live'] and not get_custom_thumbnail_status(user_id)['custom_thumbnail']:
|
||||
if (get_streamer_live_status(user_id)['is_live']
|
||||
and not get_custom_thumbnail_status(user_id)['custom_thumbnail']):
|
||||
print("Updating thumbnail...")
|
||||
generate_thumbnail(stream_file, thumbnail_file)
|
||||
update_thumbnail.apply_async((user_id, stream_file, thumbnail_file, sleep_time, second_capture), countdown=sleep_time)
|
||||
update_thumbnail.apply_async(
|
||||
(user_id, stream_file, thumbnail_file, sleep_time, second_capture),
|
||||
countdown=sleep_time)
|
||||
else:
|
||||
print(f"Stopping thumbnail updates for stream of {user_id}")
|
||||
|
||||
@@ -34,7 +40,7 @@ def combine_ts_stream(stream_path, vods_path, vod_file_name, thumbnail_file) ->
|
||||
ts_files.sort()
|
||||
|
||||
# Create temp file listing all ts files
|
||||
with open(f"{stream_path}/list.txt", "w") as f:
|
||||
with open(f"{stream_path}/list.txt", "w", encoding="utf-8") as f:
|
||||
for ts_file in ts_files:
|
||||
f.write(f"file '{ts_file}'\n")
|
||||
|
||||
@@ -53,7 +59,7 @@ def combine_ts_stream(stream_path, vods_path, vod_file_name, thumbnail_file) ->
|
||||
vod_file_path
|
||||
]
|
||||
|
||||
subprocess.run(vod_command)
|
||||
subprocess.run(vod_command, check=True)
|
||||
|
||||
# Remove HLS files, even if user is not streaming
|
||||
remove_hls_files(stream_path)
|
||||
@@ -78,4 +84,4 @@ def convert_image_to_png(image_path, png_path):
|
||||
png_path
|
||||
]
|
||||
|
||||
subprocess.run(image_command)
|
||||
subprocess.run(image_command, check=True)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""SQLite database connection management with context manager support."""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
|
||||
class Database:
|
||||
"""Database wrapper providing connection management and query execution."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._db = os.path.join(os.path.abspath(os.path.dirname(__file__)), "app.db")
|
||||
self._conn = None
|
||||
@@ -63,4 +68,6 @@ class Database:
|
||||
if not result:
|
||||
return []
|
||||
columns = [desc[0] for desc in self.cursor.description]
|
||||
return [dict(zip(columns, row)) for row in result] if isinstance(result, list) else dict(zip(columns, result))
|
||||
if isinstance(result, list):
|
||||
return [dict(zip(columns, row)) for row in result]
|
||||
return dict(zip(columns, result))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Admin utility functions for user management."""
|
||||
|
||||
from database.database import Database
|
||||
|
||||
def check_if_admin(username):
|
||||
@@ -34,5 +36,4 @@ def ban_user(banned_user):
|
||||
db.execute("""
|
||||
DELETE FROM users
|
||||
WHERE username = ?;""",
|
||||
(banned_user)
|
||||
)
|
||||
(banned_user,))
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"""Token generation and verification for password resets."""
|
||||
|
||||
from typing import Optional
|
||||
from os import getenv
|
||||
|
||||
from database.database import Database
|
||||
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
|
||||
from typing import Optional
|
||||
from dotenv import load_dotenv
|
||||
from os import getenv
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
load_dotenv()
|
||||
|
||||
serializer = URLSafeTimedSerializer(getenv("AUTH_SECRET_KEY"))
|
||||
|
||||
def generate_token(email, salt_value) -> str:
|
||||
"""
|
||||
Creates a token for password reset
|
||||
@@ -19,7 +23,6 @@ def verify_token(token: str, salt_value) -> Optional[str]:
|
||||
"""
|
||||
Given a token, verifies and decodes it into an email
|
||||
"""
|
||||
|
||||
try:
|
||||
email = serializer.loads(token, salt=salt_value, max_age=3600)
|
||||
return email
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
"""Email sending utilities for password reset, account confirmation, and newsletters."""
|
||||
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from os import getenv
|
||||
from secrets import token_hex
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from utils.auth import generate_token
|
||||
from secrets import token_hex
|
||||
from .user_utils import get_session_info_email
|
||||
import redis
|
||||
from database.database import Database
|
||||
|
||||
redis_url = "redis://redis:6379/1"
|
||||
r = redis.from_url(redis_url, decode_responses=True)
|
||||
REDIS_URL = "redis://redis:6379/1"
|
||||
r = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -23,31 +25,31 @@ def send_email(email, func) -> None:
|
||||
"""
|
||||
|
||||
# Setup the sender email details
|
||||
SMTP_SERVER = "smtp.gmail.com"
|
||||
SMTP_PORT = 587
|
||||
SMTP_EMAIL = getenv("EMAIL")
|
||||
SMTP_PASSWORD = getenv("EMAIL_PASSWORD")
|
||||
smtp_server = "smtp.gmail.com"
|
||||
smtp_port = 587
|
||||
smtp_email = getenv("EMAIL")
|
||||
smtp_password = getenv("EMAIL_PASSWORD")
|
||||
|
||||
# Setup up the receiver details
|
||||
body, subject = func()
|
||||
|
||||
msg = MIMEText(body, "html")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = SMTP_EMAIL
|
||||
msg["From"] = smtp_email
|
||||
msg["To"] = email
|
||||
|
||||
# Send the email using smtplib
|
||||
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp:
|
||||
with smtplib.SMTP(smtp_server, smtp_port) as smtp:
|
||||
try:
|
||||
smtp.starttls() # TLS handshake to start the connection
|
||||
smtp.login(SMTP_EMAIL, SMTP_PASSWORD)
|
||||
smtp.login(smtp_email, smtp_password)
|
||||
smtp.ehlo()
|
||||
smtp.send_message(msg)
|
||||
|
||||
except TimeoutError:
|
||||
print("Server timed out", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
except smtplib.SMTPException as e:
|
||||
print("Error: ", e, flush=True)
|
||||
|
||||
def forgot_password_body(email) -> str:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Description: This file contains the PathManager class which is responsible for managing the paths of the stream data."""
|
||||
"""File system path management for user streams, VODs, and profile pictures."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class PathManager():
|
||||
"""Manages paths for user stream data, VODs, and profile pictures."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.root_path = "user_data"
|
||||
self.vod_directory_name = "vods"
|
||||
@@ -39,25 +42,33 @@ class PathManager():
|
||||
os.rmdir(user_path)
|
||||
|
||||
def get_user_path(self, username):
|
||||
"""Returns the base path for a user's data directory."""
|
||||
return os.path.join(self.root_path, username)
|
||||
|
||||
def get_vods_path(self, username):
|
||||
"""Returns the path to a user's VODs directory."""
|
||||
return os.path.join(self.root_path, username, self.vod_directory_name)
|
||||
|
||||
def get_stream_path(self, username):
|
||||
"""Returns the path to a user's stream directory."""
|
||||
return os.path.join(self.root_path, username, self.stream_directory_name)
|
||||
|
||||
def get_stream_file_path(self, username):
|
||||
"""Returns the path to a user's stream index file."""
|
||||
return os.path.join(self.get_stream_path(username), "index.m3u8")
|
||||
|
||||
def get_current_stream_thumbnail_file_path(self, username):
|
||||
"""Returns the path to a user's current stream thumbnail."""
|
||||
return os.path.join(self.get_stream_path(username), "index.png")
|
||||
|
||||
def get_vod_file_path(self, username, vod_id):
|
||||
"""Returns the path to a specific VOD file."""
|
||||
return os.path.join(self.get_vods_path(username), f"{vod_id}.mp4")
|
||||
|
||||
def get_vod_thumbnail_file_path(self, username, vod_id):
|
||||
"""Returns the path to a specific VOD thumbnail."""
|
||||
return os.path.join(self.get_vods_path(username), f"{vod_id}.png")
|
||||
|
||||
def get_profile_picture_file_path(self, username):
|
||||
"""Returns the path to a user's profile picture."""
|
||||
return os.path.join(self.root_path, username, self.profile_picture_name)
|
||||
@@ -1,9 +1,13 @@
|
||||
from database.database import Database
|
||||
"""Personalized stream and category recommendations based on user preferences."""
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
from database.database import Database
|
||||
|
||||
def get_user_preferred_category(user_id: int) -> Optional[int]:
|
||||
"""
|
||||
Queries user_preferences database to find users favourite streaming category and returns the category
|
||||
Queries user_preferences database to find users favourite
|
||||
streaming category and returns the category
|
||||
"""
|
||||
with Database() as db:
|
||||
category = db.fetchone("""
|
||||
@@ -16,17 +20,23 @@ def get_user_preferred_category(user_id: int) -> Optional[int]:
|
||||
return category["category_id"] if category else None
|
||||
|
||||
|
||||
def get_followed_categories_recommendations(user_id: int, no_streams: int = 4) -> Optional[List[dict]]:
|
||||
def get_followed_categories_recommendations(
|
||||
user_id: int, no_streams: int = 4
|
||||
) -> Optional[List[dict]]:
|
||||
"""
|
||||
Returns top streams given a user's category following
|
||||
"""
|
||||
with Database() as db:
|
||||
streams = db.fetchall("""
|
||||
SELECT u.user_id, title, u.username, num_viewers, category_name
|
||||
SELECT u.user_id, title, u.username, num_viewers,
|
||||
category_name
|
||||
FROM streams
|
||||
JOIN users u ON streams.user_id = u.user_id
|
||||
JOIN categories ON streams.category_id = categories.category_id
|
||||
WHERE categories.category_id IN (SELECT category_id FROM followed_categories WHERE user_id = ?)
|
||||
JOIN categories
|
||||
ON streams.category_id = categories.category_id
|
||||
WHERE categories.category_id IN
|
||||
(SELECT category_id
|
||||
FROM followed_categories WHERE user_id = ?)
|
||||
ORDER BY num_viewers DESC
|
||||
LIMIT ?;
|
||||
""", (user_id, no_streams))
|
||||
@@ -47,13 +57,17 @@ def get_followed_your_categories(user_id: int) -> Optional[List[dict]]:
|
||||
return categories
|
||||
|
||||
|
||||
def get_streams_based_on_category(category_id: int, no_streams: int = 4, offset: int = 0) -> Optional[List[dict]]:
|
||||
def get_streams_based_on_category(
|
||||
category_id: int, no_streams: int = 4, offset: int = 0
|
||||
) -> Optional[List[dict]]:
|
||||
"""
|
||||
Queries stream database to get top most viewed streams based on given category
|
||||
Queries stream database to get top most viewed streams
|
||||
based on given category
|
||||
"""
|
||||
with Database() as db:
|
||||
streams = db.fetchall("""
|
||||
SELECT u.user_id, title, username, num_viewers, c.category_name
|
||||
SELECT u.user_id, title, username, num_viewers,
|
||||
c.category_name
|
||||
FROM streams s
|
||||
JOIN users u ON s.user_id = u.user_id
|
||||
JOIN categories c ON s.category_id = c.category_id
|
||||
@@ -70,42 +84,62 @@ def get_highest_view_streams(no_streams: int = 4) -> Optional[List[dict]]:
|
||||
"""
|
||||
with Database() as db:
|
||||
data = db.fetchall("""
|
||||
SELECT u.user_id, username, title, num_viewers, category_name
|
||||
SELECT u.user_id, username, title, num_viewers,
|
||||
category_name
|
||||
FROM streams
|
||||
JOIN users u ON streams.user_id = u.user_id
|
||||
JOIN categories ON streams.category_id = categories.category_id
|
||||
JOIN categories
|
||||
ON streams.category_id = categories.category_id
|
||||
ORDER BY num_viewers DESC
|
||||
LIMIT ?;
|
||||
""", (no_streams,))
|
||||
return data
|
||||
|
||||
def get_highest_view_categories(no_categories: int = 4, offset: int = 0) -> Optional[List[dict]]:
|
||||
def get_highest_view_categories(
|
||||
no_categories: int = 4, offset: int = 0
|
||||
) -> Optional[List[dict]]:
|
||||
"""
|
||||
Returns a list of top most popular categories given offset
|
||||
"""
|
||||
with Database() as db:
|
||||
categories = db.fetchall("""
|
||||
SELECT categories.category_id, categories.category_name, COALESCE(SUM(streams.num_viewers), 0) AS num_viewers
|
||||
SELECT categories.category_id,
|
||||
categories.category_name,
|
||||
COALESCE(SUM(streams.num_viewers), 0)
|
||||
AS num_viewers
|
||||
FROM categories
|
||||
LEFT JOIN streams ON streams.category_id = categories.category_id
|
||||
GROUP BY categories.category_id, categories.category_name
|
||||
LEFT JOIN streams
|
||||
ON streams.category_id = categories.category_id
|
||||
GROUP BY categories.category_id,
|
||||
categories.category_name
|
||||
ORDER BY num_viewers DESC
|
||||
LIMIT ? OFFSET ?;
|
||||
""", (no_categories, offset))
|
||||
return categories
|
||||
|
||||
def get_user_category_recommendations(user_id = 1, no_categories: int = 4) -> Optional[List[dict]]:
|
||||
def get_user_category_recommendations(
|
||||
user_id=1, no_categories: int = 4
|
||||
) -> Optional[List[dict]]:
|
||||
"""
|
||||
Queries user_preferences database to find users top favourite streaming category and returns the category
|
||||
Queries user_preferences database to find users top favourite
|
||||
streaming category and returns the category
|
||||
"""
|
||||
with Database() as db:
|
||||
categories = db.fetchall("""
|
||||
SELECT categories.category_id, categories.category_name, COALESCE(SUM(streams.num_viewers), 0) AS num_viewers
|
||||
SELECT categories.category_id,
|
||||
categories.category_name,
|
||||
COALESCE(SUM(streams.num_viewers), 0)
|
||||
AS num_viewers
|
||||
FROM categories
|
||||
JOIN user_preferences ON categories.category_id = user_preferences.category_id
|
||||
LEFT JOIN streams ON categories.category_id = streams.category_id
|
||||
JOIN user_preferences
|
||||
ON categories.category_id
|
||||
= user_preferences.category_id
|
||||
LEFT JOIN streams
|
||||
ON categories.category_id
|
||||
= streams.category_id
|
||||
WHERE user_preferences.user_id = ?
|
||||
GROUP BY categories.category_id, categories.category_name
|
||||
GROUP BY categories.category_id,
|
||||
categories.category_name
|
||||
ORDER BY user_preferences.favourability DESC
|
||||
LIMIT ?
|
||||
""", (user_id, no_categories))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from database.database import Database
|
||||
from typing import Optional
|
||||
import os, subprocess
|
||||
"""Stream data retrieval and management utilities."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Optional, List
|
||||
from time import sleep
|
||||
from utils.path_manager import PathManager
|
||||
|
||||
from database.database import Database
|
||||
|
||||
|
||||
def get_streamer_live_status(user_id: int):
|
||||
"""
|
||||
@@ -21,11 +23,13 @@ def get_streamer_live_status(user_id: int):
|
||||
def get_followed_live_streams(user_id: int) -> Optional[List[dict]]:
|
||||
"""
|
||||
Searches for streamers who the user followed which are currently live
|
||||
Returns a list of live streams with the streamer's user id, stream title, and number of viewers
|
||||
Returns a list of live streams with the streamer's user id,
|
||||
stream title, and number of viewers
|
||||
"""
|
||||
with Database() as db:
|
||||
live_streams = db.fetchall("""
|
||||
SELECT users.user_id, streams.title, streams.num_viewers, users.username
|
||||
SELECT users.user_id, streams.title,
|
||||
streams.num_viewers, users.username
|
||||
FROM streams JOIN users
|
||||
ON streams.user_id = users.user_id
|
||||
WHERE users.user_id IN
|
||||
@@ -40,7 +44,8 @@ def get_current_stream_data(user_id: int) -> Optional[dict]:
|
||||
"""
|
||||
with Database() as db:
|
||||
most_recent_stream = db.fetchone("""
|
||||
SELECT s.user_id, u.username, s.title, s.start_time, s.num_viewers, c.category_name, c.category_id
|
||||
SELECT s.user_id, u.username, s.title, s.start_time,
|
||||
s.num_viewers, c.category_name, c.category_id
|
||||
FROM streams AS s
|
||||
JOIN categories AS c ON s.category_id = c.category_id
|
||||
JOIN users AS u ON s.user_id = u.user_id
|
||||
@@ -60,42 +65,51 @@ def end_user_stream(stream_key, user_id, username):
|
||||
Returns:
|
||||
bool: True if stream was ended successfully, False otherwise
|
||||
"""
|
||||
from flask import current_app
|
||||
from datetime import datetime
|
||||
from dateutil import parser
|
||||
from celery_tasks.streaming import combine_ts_stream
|
||||
from utils.path_manager import PathManager
|
||||
|
||||
path_manager = PathManager()
|
||||
pm = PathManager()
|
||||
print(f"Ending stream for user {username} (ID: {user_id})", flush=True)
|
||||
|
||||
if not stream_key or not user_id or not username:
|
||||
print("Cannot end stream - missing required information", flush=True)
|
||||
return False
|
||||
return False, "Missing required information"
|
||||
|
||||
try:
|
||||
# Open database connection
|
||||
with Database() as db:
|
||||
# Get stream info
|
||||
stream_info = db.fetchone("""SELECT *
|
||||
stream_info = db.fetchone(
|
||||
"""SELECT *
|
||||
FROM streams
|
||||
WHERE user_id = ?""", (user_id,))
|
||||
|
||||
# If user is not streaming, just return
|
||||
if not stream_info:
|
||||
print(f"User {username} (ID: {user_id}) is not streaming", flush=True)
|
||||
print(
|
||||
f"User {username} (ID: {user_id}) "
|
||||
"is not streaming", flush=True)
|
||||
return True, "User is not streaming"
|
||||
|
||||
# Remove stream from database
|
||||
db.execute("""DELETE FROM streams
|
||||
db.execute(
|
||||
"""DELETE FROM streams
|
||||
WHERE user_id = ?""", (user_id,))
|
||||
|
||||
# Move stream to vod table
|
||||
stream_length = int(
|
||||
(datetime.now() - parser.parse(stream_info.get("start_time"))).total_seconds())
|
||||
(datetime.now() - parser.parse(
|
||||
stream_info.get("start_time")
|
||||
)).total_seconds())
|
||||
|
||||
db.execute("""INSERT INTO vods (user_id, title, datetime, category_id, length, views)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""", (user_id,
|
||||
db.execute(
|
||||
"""INSERT INTO vods
|
||||
(user_id, title, datetime, category_id,
|
||||
length, views)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(user_id,
|
||||
stream_info.get("title"),
|
||||
stream_info.get("start_time"),
|
||||
stream_info.get("category_id"),
|
||||
@@ -105,24 +119,29 @@ def end_user_stream(stream_key, user_id, username):
|
||||
vod_id = db.get_last_insert_id()
|
||||
|
||||
# Set user as not streaming
|
||||
db.execute("""UPDATE users
|
||||
db.execute(
|
||||
"""UPDATE users
|
||||
SET is_live = 0
|
||||
WHERE user_id = ?""", (user_id,))
|
||||
|
||||
# Queue task to combine TS files into MP4
|
||||
combine_ts_stream.delay(
|
||||
path_manager.get_stream_path(username),
|
||||
path_manager.get_vods_path(username),
|
||||
pm.get_stream_path(username),
|
||||
pm.get_vods_path(username),
|
||||
vod_id,
|
||||
path_manager.get_vod_thumbnail_file_path(username, vod_id)
|
||||
pm.get_vod_thumbnail_file_path(username, vod_id)
|
||||
)
|
||||
|
||||
print(f"Stream ended for user {username} (ID: {user_id})", flush=True)
|
||||
print(
|
||||
f"Stream ended for user {username} (ID: {user_id})",
|
||||
flush=True)
|
||||
return True, "Stream ended successfully"
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error ending stream for user {username}: {str(e)}", flush=True)
|
||||
return False, f"Error ending stream: {str(e)}"
|
||||
except (ValueError, TypeError, KeyError) as exc:
|
||||
print(
|
||||
f"Error ending stream for user {username}: {str(exc)}",
|
||||
flush=True)
|
||||
return False, f"Error ending stream: {str(exc)}"
|
||||
|
||||
def get_category_id(category_name: str) -> Optional[int]:
|
||||
"""
|
||||
@@ -153,7 +172,13 @@ def get_vod(vod_id: int) -> dict:
|
||||
Returns data of a streamers vod
|
||||
"""
|
||||
with Database() as db:
|
||||
vod = db.fetchone("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vod_id = ?;""", (vod_id,))
|
||||
vod = db.fetchone(
|
||||
"""SELECT vods.*, username, category_name
|
||||
FROM vods
|
||||
JOIN users ON vods.user_id = users.user_id
|
||||
JOIN categories
|
||||
ON vods.category_id = categories.category_id
|
||||
WHERE vod_id = ?;""", (vod_id,))
|
||||
return vod
|
||||
|
||||
def get_latest_vod(user_id: int):
|
||||
@@ -161,7 +186,14 @@ def get_latest_vod(user_id: int):
|
||||
Returns data of the most recent stream by a streamer
|
||||
"""
|
||||
with Database() as db:
|
||||
latest_vod = db.fetchone("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vods.user_id = ? ORDER BY vod_id DESC;""", (user_id,))
|
||||
latest_vod = db.fetchone(
|
||||
"""SELECT vods.*, username, category_name
|
||||
FROM vods
|
||||
JOIN users ON vods.user_id = users.user_id
|
||||
JOIN categories
|
||||
ON vods.category_id = categories.category_id
|
||||
WHERE vods.user_id = ?
|
||||
ORDER BY vod_id DESC;""", (user_id,))
|
||||
return latest_vod
|
||||
|
||||
def get_user_vods(user_id: int):
|
||||
@@ -169,10 +201,17 @@ def get_user_vods(user_id: int):
|
||||
Returns data of all vods by a streamer
|
||||
"""
|
||||
with Database() as db:
|
||||
vods = db.fetchall("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vods.user_id = ? ORDER BY vod_id DESC;""", (user_id,))
|
||||
vods = db.fetchall(
|
||||
"""SELECT vods.*, username, category_name
|
||||
FROM vods
|
||||
JOIN users ON vods.user_id = users.user_id
|
||||
JOIN categories
|
||||
ON vods.category_id = categories.category_id
|
||||
WHERE vods.user_id = ?
|
||||
ORDER BY vod_id DESC;""", (user_id,))
|
||||
return vods
|
||||
|
||||
def generate_thumbnail(stream_file: str, thumbnail_file: str, second_capture) -> None:
|
||||
def generate_thumbnail(stream_file: str, thumbnail_file: str, second_capture=0) -> None:
|
||||
"""
|
||||
Generates the thumbnail of a stream
|
||||
"""
|
||||
@@ -192,10 +231,16 @@ def generate_thumbnail(stream_file: str, thumbnail_file: str, second_capture) ->
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(thumbnail_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
subprocess.run(
|
||||
thumbnail_command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True)
|
||||
print(f"Thumbnail generated for {stream_file}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"No information available for {stream_file}, aborting thumbnail generation")
|
||||
except subprocess.CalledProcessError:
|
||||
print(
|
||||
f"No information available for {stream_file}, "
|
||||
"aborting thumbnail generation")
|
||||
|
||||
def remove_hls_files(stream_path: str) -> None:
|
||||
"""
|
||||
@@ -221,11 +266,13 @@ def get_video_duration(video_path: str) -> int:
|
||||
]
|
||||
|
||||
try:
|
||||
video_length = subprocess.check_output(video_length_command).decode("utf-8")
|
||||
video_length = subprocess.check_output(
|
||||
video_length_command
|
||||
).decode("utf-8")
|
||||
print(f"Video length: {video_length}")
|
||||
return int(float(video_length))
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error getting video length: {e}")
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error getting video length")
|
||||
return 0
|
||||
|
||||
def get_stream_tags(user_id: int) -> Optional[List[str]]:
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from database.database import Database
|
||||
"""User profile management, following, and subscription utilities."""
|
||||
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from database.database import Database
|
||||
from dateutil import parser
|
||||
|
||||
|
||||
def get_user_id(username: str) -> Optional[int]:
|
||||
"""
|
||||
Returns user_id associated with given username
|
||||
@@ -40,7 +44,8 @@ def update_bio(user_id: int, bio: str):
|
||||
|
||||
def has_password(email: str):
|
||||
"""
|
||||
Returns if account associated with this email has password, i.e is account from Google OAuth
|
||||
Returns if account associated with this email has password,
|
||||
i.e is account from Google OAuth
|
||||
"""
|
||||
with Database() as db:
|
||||
data = db.fetchone("""
|
||||
@@ -48,7 +53,7 @@ def has_password(email: str):
|
||||
FROM users
|
||||
WHERE email = ?
|
||||
""", (email,))
|
||||
return False if data["password"] == None else True
|
||||
return data["password"] is not None
|
||||
|
||||
def get_session_info_email(email: str) -> dict:
|
||||
"""
|
||||
@@ -227,6 +232,7 @@ def subscription_expiration(user_id: int, subscribed_id: int) -> int:
|
||||
return 0
|
||||
|
||||
def get_email(user_id: int) -> Optional[str]:
|
||||
"""Returns the email address for a given user_id."""
|
||||
with Database() as db:
|
||||
email = db.fetchone("""
|
||||
SELECT email
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from database.database import Database
|
||||
"""Input sanitization and validation utilities."""
|
||||
|
||||
from typing import Optional, List
|
||||
from re import match
|
||||
|
||||
from database.database import Database
|
||||
|
||||
|
||||
def get_all_categories() -> Optional[List[dict]]:
|
||||
"""
|
||||
Returns all possible streaming categories
|
||||
@@ -84,10 +88,10 @@ def sanitize(user_input: str, input_type="default") -> str:
|
||||
}
|
||||
|
||||
# Get the validation rules for the specified type
|
||||
r = rules.get(input_type)
|
||||
if not r or \
|
||||
not (r["min_length"] <= len(sanitised_input) <= r["max_length"]) or \
|
||||
not match(r["pattern"], sanitised_input):
|
||||
rule = rules.get(input_type)
|
||||
if (not rule
|
||||
or not (rule["min_length"] <= len(sanitised_input) <= rule["max_length"])
|
||||
or not match(rule["pattern"], sanitised_input)):
|
||||
raise ValueError("Unaccepted character or length in input")
|
||||
|
||||
return sanitised_input
|
||||
Reference in New Issue
Block a user