Fix/pylint cleanup (#8)
Some checks are pending
CI / build (3.10) (push) Waiting to run
CI / build (3.8) (push) Waiting to run
CI / build (3.9) (push) Waiting to run

* 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:
Christopher Ahern
2026-02-07 20:57:28 +00:00
committed by GitHub
parent fed1a2f288
commit 2758be8680
25 changed files with 680 additions and 419 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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})

View File

@@ -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
)
)

View File

@@ -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

View File

@@ -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

View File

@@ -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