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
@@ -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__)
|
||||
|
||||
@@ -18,10 +20,10 @@ def admin_delete_user(banned_user):
|
||||
# Check if the user is an admin
|
||||
username = session.get("username")
|
||||
is_admin = check_if_admin(username)
|
||||
|
||||
|
||||
# Check if the user exists
|
||||
user_exists = check_if_user_exists(banned_user)
|
||||
|
||||
# If the user is an admin, try to delete the account
|
||||
if is_admin and user_exists:
|
||||
ban_user(banned_user)
|
||||
ban_user(banned_user)
|
||||
|
||||
@@ -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,19 +38,21 @@ 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,
|
||||
"message": "Missing required fields"
|
||||
}), 400
|
||||
|
||||
|
||||
# Sanitize the inputs - helps to prevent SQL injection
|
||||
try:
|
||||
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,
|
||||
@@ -81,7 +91,7 @@ def signup():
|
||||
|
||||
# Create new user once input is validated
|
||||
db.execute(
|
||||
"""INSERT INTO users
|
||||
"""INSERT INTO users
|
||||
(username, password, email, stream_key)
|
||||
VALUES (?, ?, ?, ?)""",
|
||||
(
|
||||
@@ -100,16 +110,16 @@ 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:
|
||||
db.close_connection()
|
||||
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["POST"])
|
||||
@cross_origin(supports_credentials=True)
|
||||
@@ -127,24 +137,24 @@ def login():
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
# Validation - ensure all fields exist, users cannot have an empty field
|
||||
# Validation - ensure all fields exist, users cannot have an empty field
|
||||
if not all([username, password]):
|
||||
return jsonify({
|
||||
"logged_in": False,
|
||||
"message": "Missing required fields"
|
||||
}), 400
|
||||
|
||||
|
||||
# Sanitize the inputs - helps to prevent SQL injection
|
||||
try:
|
||||
username = sanitize(username, "username")
|
||||
password = sanitize(password, "password")
|
||||
except ValueError as e:
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"account_created": False,
|
||||
"error_fields": ["username", "password"],
|
||||
"message": "Invalid input received"
|
||||
}), 400
|
||||
|
||||
|
||||
# Create a connection to the database
|
||||
db = Database()
|
||||
|
||||
@@ -169,7 +179,7 @@ def login():
|
||||
"error_fields": ["username", "password"],
|
||||
"message": "Invalid username or password"
|
||||
}), 401
|
||||
|
||||
|
||||
# Add user directories for stream data in case they don't exist
|
||||
path_manager.create_user(username)
|
||||
|
||||
@@ -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"
|
||||
@@ -202,31 +215,41 @@ def login():
|
||||
def logout() -> dict:
|
||||
"""
|
||||
Log out and clear the users session.
|
||||
|
||||
|
||||
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
|
||||
user_id = session.get("user_id")
|
||||
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
|
||||
end_user_stream(stream_key, user_id, username)
|
||||
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]
|
||||
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")
|
||||
@@ -32,7 +35,7 @@ def handle_join(data) -> None:
|
||||
join_room(stream_id)
|
||||
num_viewers = len(list(socketio.server.manager.get_participants("/", stream_id)))
|
||||
update_viewers(stream_id, num_viewers)
|
||||
emit("status",
|
||||
emit("status",
|
||||
{
|
||||
"message": f"Welcome to the chat, stream_id: {stream_id}",
|
||||
"num_viewers": num_viewers
|
||||
@@ -53,7 +56,7 @@ def handle_leave(data) -> None:
|
||||
remove_favourability_entry(str(data["user_id"]), str(stream_id))
|
||||
num_viewers = len(list(socketio.server.manager.get_participants("/", stream_id)))
|
||||
update_viewers(stream_id, num_viewers)
|
||||
emit("status",
|
||||
emit("status",
|
||||
{
|
||||
"message": f"Welcome to the chat, stream_id: {stream_id}",
|
||||
"num_viewers": num_viewers
|
||||
@@ -78,10 +81,10 @@ def get_past_chat(stream_id: int):
|
||||
all_chats = db.fetchall("""
|
||||
SELECT user_id, username, message, time_sent, is_subscribed
|
||||
FROM (
|
||||
SELECT
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.username,
|
||||
c.message,
|
||||
u.username,
|
||||
c.message,
|
||||
c.time_sent,
|
||||
CASE
|
||||
WHEN s.user_id IS NOT NULL AND s.expires > CURRENT_TIMESTAMP THEN 1
|
||||
@@ -101,8 +104,8 @@ def get_past_chat(stream_id: int):
|
||||
|
||||
# Create JSON output of chat_history to pass through NGINX proxy
|
||||
chat_history = [{"chatter_id": chat["user_id"],
|
||||
"chatter_username": chat["username"],
|
||||
"message": chat["message"],
|
||||
"chatter_username": chat["username"],
|
||||
"message": chat["message"],
|
||||
"time_sent": chat["time_sent"],
|
||||
"is_subscribed": bool(chat["is_subscribed"])} for chat in all_chats]
|
||||
print(chat_history)
|
||||
@@ -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
|
||||
|
||||
#TODO: Make sure that users entry within Redis is removed if they disconnect from socket
|
||||
db.close_connection()
|
||||
|
||||
def add_favourability_entry(user_id, stream_id):
|
||||
"""
|
||||
Adds entry to Redis that user is watching a streamer
|
||||
@@ -183,7 +191,7 @@ def add_favourability_entry(user_id, stream_id):
|
||||
else:
|
||||
# Creates new entry for user and stream
|
||||
current_viewers[user_id] = [stream_id]
|
||||
|
||||
|
||||
r.hset("current_viewers", "viewers", json.dumps(current_viewers))
|
||||
|
||||
def remove_favourability_entry(user_id, stream_id):
|
||||
@@ -202,9 +210,9 @@ def remove_favourability_entry(user_id, stream_id):
|
||||
if user_id in current_viewers:
|
||||
# Removes specific stream from user
|
||||
current_viewers[user_id] = [stream for stream in current_viewers[user_id] if stream != stream_id]
|
||||
|
||||
|
||||
# If user is no longer watching any streams
|
||||
if not current_viewers[user_id]:
|
||||
del current_viewers[user_id]
|
||||
|
||||
r.hset("current_viewers", "viewers", json.dumps(current_viewers))
|
||||
r.hset("current_viewers", "viewers", json.dumps(current_viewers))
|
||||
|
||||
@@ -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)}")
|
||||
return {"error": message}, code
|
||||
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'],
|
||||
@@ -50,11 +53,11 @@ def login_google():
|
||||
session["nonce"] = token_urlsafe(16)
|
||||
session["state"] = token_urlsafe(32)
|
||||
session["origin"] = request.args.get("next")
|
||||
|
||||
|
||||
# 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']
|
||||
@@ -70,23 +73,27 @@ def google_auth():
|
||||
# Check state parameter before authorizing
|
||||
returned_state = request.args.get('state')
|
||||
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")
|
||||
@@ -108,7 +115,7 @@ def google_auth():
|
||||
break
|
||||
|
||||
db.execute(
|
||||
"""INSERT INTO users
|
||||
"""INSERT INTO users
|
||||
(username, email, stream_key)
|
||||
VALUES (?, ?, ?)""",
|
||||
(
|
||||
@@ -124,16 +131,19 @@ def google_auth():
|
||||
origin = session.get("origin", f"{url.replace('/api', '')}")
|
||||
username = user_data["username"]
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
|
||||
# Clear session and set new data
|
||||
session.clear()
|
||||
session["username"] = username
|
||||
session["user_id"] = user_id
|
||||
|
||||
|
||||
# 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,9 +154,9 @@ 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',
|
||||
'error': str(e)
|
||||
}), 500
|
||||
}), 500
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,7 +52,7 @@ def search_results():
|
||||
res_dict.append(c)
|
||||
categories = sorted(res_dict, key=lambda d: d["score"])
|
||||
categories = categories[:4]
|
||||
|
||||
|
||||
# 3 users
|
||||
res_dict = []
|
||||
users = db.fetchall("SELECT user_id, username, is_live FROM users")
|
||||
@@ -63,7 +65,7 @@ def search_results():
|
||||
users = sorted(res_dict, key=lambda d: d["score"])
|
||||
users = users[:4]
|
||||
|
||||
# 3 streams
|
||||
# 3 streams
|
||||
res_dict = []
|
||||
streams = db.fetchall("""SELECT s.user_id, s.title, s.num_viewers, c.category_name, u.username
|
||||
FROM streams AS s
|
||||
@@ -71,7 +73,7 @@ def search_results():
|
||||
INNER JOIN users AS u ON s.user_id = u.user_id
|
||||
INNER JOIN categories AS c ON s.category_id = c.category_id
|
||||
""")
|
||||
|
||||
|
||||
for s in streams:
|
||||
key = s.get("title")
|
||||
score = rank_results(query.lower(), key.lower())
|
||||
@@ -83,7 +85,7 @@ def search_results():
|
||||
|
||||
# 3 VODs
|
||||
res_dict = []
|
||||
vods = db.fetchall("""SELECT v.vod_id, v.title, u.user_id, u.username
|
||||
vods = db.fetchall("""SELECT v.vod_id, v.title, u.user_id, u.username
|
||||
FROM vods as v JOIN users as u
|
||||
ON v.user_id = u.user_id""")
|
||||
for v in vods:
|
||||
@@ -98,5 +100,5 @@ def search_results():
|
||||
db.close_connection()
|
||||
|
||||
print(query, streams, users, categories, vods, flush=True)
|
||||
|
||||
return jsonify({"streams": streams, "categories": categories, "users": users, "vods": vods})
|
||||
|
||||
return jsonify({"streams": streams, "categories": categories, "users": users, "vods": vods})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""WebSocket configuration using Flask-SocketIO."""
|
||||
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
socketio = SocketIO(
|
||||
cors_allowed_origins="*",
|
||||
cors_allowed_origins="*",
|
||||
async_mode='gevent',
|
||||
logger=False, # Reduce logging
|
||||
engineio_logger=False, # Reduce logging
|
||||
ping_timeout=5000,
|
||||
ping_interval=25000
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
@@ -204,11 +213,11 @@ def vods(username):
|
||||
"views": int
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
"""
|
||||
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;""")
|
||||
|
||||
return jsonify(vods)
|
||||
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(all_vods)
|
||||
|
||||
# RTMP Server Routes
|
||||
|
||||
@@ -232,9 +246,10 @@ 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
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
user_info = db.fetchone(
|
||||
"""SELECT user_id, username, is_live
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
|
||||
# No user found from stream key
|
||||
if not user_info:
|
||||
@@ -280,25 +295,27 @@ def publish_stream():
|
||||
username = None
|
||||
|
||||
with Database() as db:
|
||||
user_info = db.fetchone("""SELECT user_id, username, is_live
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
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,
|
||||
get_category_id(stream_category)))
|
||||
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
|
||||
db.execute("""UPDATE users SET is_live = 1 WHERE user_id = ?""",
|
||||
@@ -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,
|
||||
path_manager.get_stream_file_path(username),
|
||||
path_manager.get_current_stream_thumbnail_file_path(username),
|
||||
THUMBNAIL_GENERATION_INTERVAL), countdown=10)
|
||||
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)
|
||||
|
||||
return "OK", 200
|
||||
|
||||
@@ -332,31 +351,36 @@ def update_stream():
|
||||
|
||||
|
||||
with Database() as db:
|
||||
user_info = db.fetchone("""SELECT user_id, username, is_live
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
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
|
||||
SET title = ?, category_id = ?
|
||||
WHERE user_id = ?""", (stream_title, get_category_id(stream_category), user_id))
|
||||
|
||||
db.execute(
|
||||
"""UPDATE streams
|
||||
SET title = ?, category_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
|
||||
SET custom_thumbnail = ?
|
||||
WHERE user_id = ?""", (True, user_id))
|
||||
|
||||
db.execute(
|
||||
"""UPDATE streams
|
||||
SET custom_thumbnail = ?
|
||||
WHERE user_id = ?""", (True, user_id))
|
||||
|
||||
# Get thumbnail path
|
||||
thumbnail_path = path_manager.get_current_stream_thumbnail_file_path(username)
|
||||
|
||||
@@ -390,26 +414,27 @@ def end_stream():
|
||||
|
||||
# Get user info from stream key
|
||||
with Database() as db:
|
||||
user_info = db.fetchone("""SELECT user_id, username
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
|
||||
user_info = db.fetchone(
|
||||
"""SELECT user_id, username
|
||||
FROM users
|
||||
WHERE stream_key = ?""", (stream_key,))
|
||||
|
||||
# Return unauthorized if no user found
|
||||
if not user_info:
|
||||
print("Unauthorized - No user found from stream key", flush=True)
|
||||
return "Unauthorized", 403
|
||||
|
||||
|
||||
# Get user info
|
||||
user_id = user_info["user_id"]
|
||||
username = user_info["username"]
|
||||
|
||||
|
||||
# End stream
|
||||
result, message = end_user_stream(stream_key, user_id, username)
|
||||
|
||||
|
||||
# Return error if stream could not be ended
|
||||
if not result:
|
||||
print(f"Error ending stream: {message}", flush=True)
|
||||
return "Error ending stream", 500
|
||||
|
||||
|
||||
print(f"Stream ended: {message}", flush=True)
|
||||
return "Stream ended", 200
|
||||
|
||||
@@ -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
|
||||
@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'))
|
||||
"""
|
||||
Used to query payment status
|
||||
"""
|
||||
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():
|
||||
@@ -65,13 +74,20 @@ def stripe_webhook():
|
||||
raise e
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -48,11 +59,11 @@ def user_profile_picture_save():
|
||||
"""
|
||||
username = session.get("username")
|
||||
thumbnail_path = path_manager.get_profile_picture_file_path(username)
|
||||
|
||||
|
||||
# Check if the post request has the file part
|
||||
if 'image' not in request.files:
|
||||
return jsonify({"error": "No image found in request"}), 400
|
||||
|
||||
|
||||
# Fetch image, convert to png, and save
|
||||
image = Image.open(request.files['image'])
|
||||
image.convert('RGB')
|
||||
@@ -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
|
||||
@@ -186,7 +197,7 @@ def user_login_status():
|
||||
"""
|
||||
username = session.get("username")
|
||||
user_id = session.get("user_id")
|
||||
return jsonify({'status': username is not None,
|
||||
return jsonify({'status': username is not None,
|
||||
'username': username,
|
||||
'user_id': user_id})
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
@@ -237,4 +250,4 @@ def unsubscribe(token):
|
||||
if email:
|
||||
remove_from_newsletter(email)
|
||||
return jsonify({"message": "unsubscribed from newsletter"}), 200
|
||||
return jsonify({"error": "Invalid token"}), 400
|
||||
return jsonify({"error": "Invalid token"}), 400
|
||||
|
||||
Reference in New Issue
Block a user