| import sys |
| import os |
|
|
| from flask import Flask, request, jsonify, send_file |
| from flask_cors import CORS |
| from werkzeug.utils import secure_filename |
| import tempfile |
| import uuid |
| import io |
| import base64 |
| import time |
| import json |
| import hashlib |
| import qrcode |
| from PIL import Image |
| import requests |
| import random |
| import string |
| from flask_socketio import SocketIO, emit, join_room, leave_room |
| import threading |
| from database import init_supabase, get_supabase |
| from auth import signup_user, login_user, check_username_exists, verify_token |
| from functools import wraps |
|
|
| app = Flask(__name__) |
| CORS(app) |
|
|
| print("Initializing Supabase...") |
| init_supabase() |
|
|
| |
| if not os.getenv("SUPABASE_URL") or not os.getenv("SUPABASE_KEY"): |
| print("β οΈ WARNING: SUPABASE_URL or SUPABASE_KEY not set!") |
| print("β οΈ Authentication features will not work properly") |
| else: |
| print(f"β
Supabase URL configured: {os.getenv('SUPABASE_URL')[:30]}...") |
| |
| |
| socketio = SocketIO( |
| app, |
| cors_allowed_origins="*", |
| async_mode='eventlet', |
| logger=True, |
| engineio_logger=True, |
| ping_timeout=60, |
| ping_interval=25, |
| transports=['websocket', 'polling'] |
| ) |
|
|
|
|
| |
|
|
| |
| SECRETS = {} |
| SHORT_LINKS = {} |
| ANALYTICS = {} |
| SCREEN_SHARE_ROOMS = {} |
| CURSOR_STATES = {} |
| CHAT_ROOMS = {} |
|
|
| |
| MAX_FILE_SIZE = 5 * 1024 * 1024 |
| ALLOWED_EXTENSIONS = { |
| 'image': ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'], |
| 'video': ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'], |
| 'audio': ['mp3', 'wav', 'ogg', 'aac', 'm4a'], |
| 'document': ['pdf', 'txt', 'doc', 'docx', 'rtf', 'odt'] |
| } |
|
|
| ONLINE_USERS = {} |
| FILE_TRANSFERS = {} |
|
|
| def assign_cursor_color(existing_participants): |
| """Assign unique cursor color to new participant""" |
| import random |
| colors = ['blue', 'green', 'red', 'yellow', 'purple', 'pink', 'orange', 'cyan'] |
| used_colors = [p.get('cursor_color') for p in existing_participants if p.get('cursor_color')] |
| available_colors = [c for c in colors if c not in used_colors] |
| |
| if available_colors: |
| return available_colors[0] |
| else: |
| return random.choice(colors) |
|
|
| def get_file_type(filename): |
| """Determine file type based on extension""" |
| if not filename: |
| return 'unknown' |
| |
| ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' |
| |
| for file_type, extensions in ALLOWED_EXTENSIONS.items(): |
| if ext in extensions: |
| return file_type |
| |
| return 'unknown' |
|
|
| def generate_short_id(): |
| """Generate a short, unique ID""" |
| return ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) |
|
|
| def get_client_ip(request): |
| """Get client IP address""" |
| if request.headers.get('X-Forwarded-For'): |
| return request.headers.get('X-Forwarded-For').split(',')[0].strip() |
| elif request.headers.get('X-Real-IP'): |
| return request.headers.get('X-Real-IP') |
| else: |
| return request.remote_addr |
|
|
| def get_location_info(ip): |
| """Get location information from IP (mock implementation)""" |
| |
| try: |
| |
| if ip == '127.0.0.1' or ip.startswith('192.168.'): |
| return { |
| 'country': 'Local', |
| 'city': 'Local', |
| 'region': 'Local', |
| 'timezone': 'Local' |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| return { |
| 'country': 'Unknown', |
| 'city': 'Unknown', |
| 'region': 'Unknown', |
| 'timezone': 'Unknown' |
| } |
| except: |
| return { |
| 'country': 'Unknown', |
| 'city': 'Unknown', |
| 'region': 'Unknown', |
| 'timezone': 'Unknown' |
| } |
|
|
| def generate_qr_code(data): |
| """Generate QR code for the given data""" |
| qr = qrcode.QRCode( |
| version=1, |
| error_correction=qrcode.constants.ERROR_CORRECT_L, |
| box_size=10, |
| border=4, |
| ) |
| qr.add_data(data) |
| qr.make(fit=True) |
| |
| img = qr.make_image(fill_color="black", back_color="white") |
| |
| |
| buffer = io.BytesIO() |
| img.save(buffer, format='PNG') |
| img_str = base64.b64encode(buffer.getvalue()).decode() |
| |
| return f"data:image/png;base64,{img_str}" |
|
|
| def record_access(secret_id, request): |
| """Record access analytics""" |
| ip = get_client_ip(request) |
| location = get_location_info(ip) |
| user_agent = request.headers.get('User-Agent', '') |
| |
| |
| device_type = 'desktop' |
| if any(mobile in user_agent.lower() for mobile in ['mobile', 'android', 'iphone', 'ipad']): |
| device_type = 'mobile' |
| |
| analytics_entry = { |
| 'timestamp': time.time(), |
| 'ip': ip, |
| 'location': location, |
| 'user_agent': user_agent, |
| 'device_type': device_type, |
| 'referer': request.headers.get('Referer', ''), |
| 'accept_language': request.headers.get('Accept-Language', '') |
| } |
| |
| if secret_id not in ANALYTICS: |
| ANALYTICS[secret_id] = [] |
| |
| ANALYTICS[secret_id].append(analytics_entry) |
| |
| return analytics_entry |
|
|
| def require_auth(f): |
| """Decorator to require authentication""" |
| @wraps(f) |
| def decorated_function(*args, **kwargs): |
| token = request.headers.get('Authorization') |
| if not token: |
| return jsonify({'error': 'No token provided'}), 401 |
| |
| user = verify_token(token) |
| |
| if not user: |
| return jsonify({'error': 'Invalid token'}), 401 |
| |
| |
| request.current_user = user |
| return f(*args, **kwargs) |
| |
| return decorated_function |
|
|
|
|
|
|
| @app.route("/") |
| def index(): |
| """Health check endpoint""" |
| return jsonify({ |
| "status": "running", |
| "service": "Sharelock Backend", |
| "version": "2.0.0", |
| "features": [ |
| "End-to-end encryption", |
| "File uploads (5MB max)", |
| "QR code generation", |
| "Analytics tracking", |
| "Short URLs", |
| "Self-destruct messages", |
| "Real-time chat rooms" |
| ] |
| }) |
|
|
| @app.route("/api/store", methods=["POST"]) |
| def store(): |
| """Store encrypted secret with enhanced features""" |
| try: |
| form = request.form |
| data = form.get("data") |
| |
| if not data: |
| return jsonify({"error": "Data is required"}), 400 |
| |
| |
| ttl = int(form.get("ttl", 300)) |
| view_once = form.get("view_once", "false").lower() == "true" |
| delay_seconds = int(form.get("delay_seconds", 0)) |
| theme = form.get("theme", "default") |
| password_hint = form.get("password_hint", "") |
| |
| |
| file_data = None |
| file_type = None |
| file_name = None |
| |
| if 'file' in request.files: |
| file = request.files['file'] |
| if file and file.filename: |
| |
| file.seek(0, os.SEEK_END) |
| file_size = file.tell() |
| file.seek(0) |
| |
| if file_size > MAX_FILE_SIZE: |
| return jsonify({"error": f"File too large. Max size: {MAX_FILE_SIZE/1024/1024:.1f}MB"}), 400 |
| |
| |
| file_name = secure_filename(file.filename) |
| file_type = get_file_type(file_name) |
| |
| if file_type == 'unknown': |
| return jsonify({"error": "File type not supported"}), 400 |
| |
| |
| file_content = file.read() |
| file_data = base64.b64encode(file_content).decode('utf-8') |
| |
| |
| secret_id = str(uuid.uuid4()) |
| short_id = generate_short_id() |
| |
| |
| while short_id in SHORT_LINKS: |
| short_id = generate_short_id() |
| |
| |
| SECRETS[secret_id] = { |
| "data": data, |
| "file_data": file_data, |
| "file_type": file_type, |
| "file_name": file_name, |
| "expire_at": time.time() + ttl, |
| "view_once": view_once, |
| "delay_seconds": delay_seconds, |
| "theme": theme, |
| "password_hint": password_hint, |
| "created_at": time.time(), |
| "creator_ip": get_client_ip(request), |
| "access_count": 0 |
| } |
| |
| |
| SHORT_LINKS[short_id] = secret_id |
| |
| |
| base_url = request.host_url.rstrip('/') |
| secret_url = f"{base_url}/tools/sharelock?id={secret_id}" |
| qr_code = generate_qr_code(secret_url) |
| |
| return jsonify({ |
| "id": secret_id, |
| "short_id": short_id, |
| "short_url": f"{base_url}/s/{short_id}", |
| "qr_code": qr_code, |
| "expires_at": SECRETS[secret_id]["expire_at"], |
| "has_file": file_data is not None |
| }) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/fetch/<secret_id>") |
| def fetch(secret_id): |
| """Fetch and decrypt secret with analytics - MODIFIED TO HANDLE verify_only""" |
| try: |
| |
| if secret_id in SHORT_LINKS: |
| secret_id = SHORT_LINKS[secret_id] |
| |
| secret = SECRETS.get(secret_id) |
| if not secret: |
| return jsonify({"error": "Secret not found"}), 404 |
| |
| |
| if time.time() > secret["expire_at"]: |
| |
| if secret_id in SECRETS: |
| del SECRETS[secret_id] |
| |
| for short_id, full_id in list(SHORT_LINKS.items()): |
| if full_id == secret_id: |
| del SHORT_LINKS[short_id] |
| return jsonify({"error": "Secret has expired"}), 410 |
| |
| |
| verify_only = request.args.get('verify_only', 'false').lower() == 'true' |
| |
| |
| if not verify_only: |
| |
| analytics_entry = record_access(secret_id, request) |
| |
| |
| secret["access_count"] += 1 |
| |
| |
| response = { |
| "data": secret["data"], |
| "theme": secret.get("theme", "default"), |
| "delay_seconds": secret.get("delay_seconds", 0), |
| "password_hint": secret.get("password_hint", ""), |
| "access_count": secret["access_count"] |
| } |
| |
| |
| if secret.get("file_data"): |
| response["file_data"] = secret["file_data"] |
| response["file_type"] = secret.get("file_type", "unknown") |
| response["file_name"] = secret.get("file_name", "unknown") |
| |
| |
| if secret["view_once"] and not verify_only: |
| |
| del SECRETS[secret_id] |
| |
| |
| for short_id, full_id in list(SHORT_LINKS.items()): |
| if full_id == secret_id: |
| del SHORT_LINKS[short_id] |
| break |
| |
| return jsonify(response) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/analytics/<secret_id>") |
| def get_analytics(secret_id): |
| """Get analytics for a specific secret - MODIFIED TO HANDLE verify_only""" |
| try: |
| |
| verify_only = request.args.get('verify_only', 'false').lower() == 'true' |
| |
| |
| if secret_id not in SECRETS and secret_id not in ANALYTICS: |
| return jsonify({"error": "Secret not found"}), 404 |
| |
| |
| if not verify_only: |
| |
| record_access(secret_id, request) |
| |
| analytics_data = ANALYTICS.get(secret_id, []) |
| |
| |
| formatted_analytics = [] |
| for entry in analytics_data: |
| formatted_analytics.append({ |
| "timestamp": entry["timestamp"], |
| "datetime": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(entry["timestamp"])), |
| "ip": entry["ip"], |
| "location": entry["location"], |
| "device_type": entry["device_type"], |
| "user_agent": entry["user_agent"][:100] + "..." if len(entry["user_agent"]) > 100 else entry["user_agent"] |
| }) |
| |
| return jsonify({ |
| "secret_id": secret_id, |
| "total_accesses": len(formatted_analytics), |
| "analytics": formatted_analytics |
| }) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/secrets") |
| def list_secrets(): |
| """List all active secrets (for dashboard)""" |
| try: |
| current_time = time.time() |
| active_secrets = [] |
| |
| for secret_id, secret in SECRETS.items(): |
| if current_time <= secret["expire_at"]: |
| |
| short_id = None |
| for s_id, full_id in SHORT_LINKS.items(): |
| if full_id == secret_id: |
| short_id = s_id |
| break |
| |
| active_secrets.append({ |
| "id": secret_id, |
| "short_id": short_id, |
| "created_at": secret["created_at"], |
| "expires_at": secret["expire_at"], |
| "view_once": secret["view_once"], |
| "has_file": secret.get("file_data") is not None, |
| "file_type": secret.get("file_type"), |
| "theme": secret.get("theme", "default"), |
| "access_count": secret.get("access_count", 0), |
| "preview": secret["data"][:100] + "..." if len(secret["data"]) > 100 else secret["data"] |
| }) |
| |
| return jsonify({ |
| "secrets": active_secrets, |
| "total": len(active_secrets) |
| }) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/delete/<secret_id>", methods=["DELETE"]) |
| def delete_secret(secret_id): |
| """Manually delete a secret - MODIFIED TO HANDLE verify_only""" |
| try: |
| |
| verify_only = request.args.get('verify_only', 'false').lower() == 'true' |
| |
| if secret_id not in SECRETS: |
| return jsonify({"error": "Secret not found"}), 404 |
| |
| |
| if not verify_only: |
| |
| record_access(secret_id, request) |
| |
| |
| del SECRETS[secret_id] |
| |
| |
| for short_id, full_id in list(SHORT_LINKS.items()): |
| if full_id == secret_id: |
| del SHORT_LINKS[short_id] |
| break |
| |
| return jsonify({"message": "Secret deleted successfully"}) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/s/<short_id>") |
| def redirect_short_link(short_id): |
| """Redirect short link to full URL""" |
| if short_id not in SHORT_LINKS: |
| return jsonify({"error": "Short link not found"}), 404 |
| |
| secret_id = SHORT_LINKS[short_id] |
| base_url = request.host_url.rstrip('/') |
| return f""" |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>Sharelock - Redirecting...</title> |
| <meta http-equiv="refresh" content="0;url={base_url}/tools/sharelock?id={secret_id}"> |
| </head> |
| <body> |
| <p>Redirecting to secure message...</p> |
| <p>If you are not redirected automatically, <a href="{base_url}/tools/sharelock?id={secret_id}">click here</a>.</p> |
| </body> |
| </html> |
| """ |
|
|
| @app.route("/api/qr/<secret_id>") |
| def get_qr_code(secret_id): |
| """Generate QR code for a secret""" |
| try: |
| if secret_id not in SECRETS: |
| return jsonify({"error": "Secret not found"}), 404 |
| |
| base_url = request.host_url.rstrip('/') |
| secret_url = f"{base_url}/tools/sharelock?id={secret_id}" |
| qr_code = generate_qr_code(secret_url) |
| |
| return jsonify({"qr_code": qr_code}) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/stats") |
| def get_stats(): |
| """Get overall statistics""" |
| try: |
| total_secrets = len(SECRETS) |
| total_accesses = sum(len(analytics) for analytics in ANALYTICS.values()) |
| |
| |
| file_types = {} |
| for secret in SECRETS.values(): |
| file_type = secret.get("file_type", "text") |
| file_types[file_type] = file_types.get(file_type, 0) + 1 |
| |
| |
| themes = {} |
| for secret in SECRETS.values(): |
| theme = secret.get("theme", "default") |
| themes[theme] = themes.get(theme, 0) + 1 |
| |
| return jsonify({ |
| "total_secrets": total_secrets, |
| "total_accesses": total_accesses, |
| "file_types": file_types, |
| "themes": themes, |
| "active_short_links": len(SHORT_LINKS) |
| }) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/cleanup", methods=["POST"]) |
| def cleanup_expired(): |
| """Clean up expired secrets""" |
| try: |
| current_time = time.time() |
| expired_count = 0 |
| |
| |
| expired_secrets = [] |
| for secret_id, secret in SECRETS.items(): |
| if current_time > secret["expire_at"]: |
| expired_secrets.append(secret_id) |
| |
| |
| for secret_id in expired_secrets: |
| del SECRETS[secret_id] |
| expired_count += 1 |
| |
| |
| for short_id, full_id in list(SHORT_LINKS.items()): |
| if full_id == secret_id: |
| del SHORT_LINKS[short_id] |
| break |
| |
| return jsonify({ |
| "message": f"Cleaned up {expired_count} expired secrets", |
| "expired_count": expired_count |
| }) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| |
|
|
| @app.route("/api/chat/create", methods=["POST"]) |
| def create_chat_room(): |
| try: |
| form = request.form |
| ttl = int(form.get("ttl", 3600)) |
| max_receivers = int(form.get("max_receivers", 5)) |
| password = form.get("password", "") |
| allow_files = form.get("allow_files", "true").lower() == "true" |
| |
| room_id = str(uuid.uuid4()) |
| admin_session = str(uuid.uuid4()) |
| |
| CHAT_ROOMS[room_id] = { |
| "admin_session": admin_session, |
| "created_at": time.time(), |
| "expires_at": time.time() + ttl, |
| "settings": { |
| "max_receivers": max_receivers, |
| "password": password, |
| "allow_files": allow_files, |
| "burn_on_admin_exit": True |
| }, |
| "active_sessions": {}, |
| "receiver_counter": 0 |
| } |
| |
| |
| |
| |
| return jsonify({ |
| "room_id": room_id, |
| "admin_session": admin_session, |
| "expires_at": CHAT_ROOMS[room_id]["expires_at"] |
| }) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/chat/join/<room_id>") |
| def join_chat_room(room_id): |
| try: |
| password = request.args.get("password", "") |
| admin_session = request.args.get("admin", "") |
| |
| if room_id not in CHAT_ROOMS: |
| return jsonify({"error": "Chat room not found"}), 404 |
| |
| room = CHAT_ROOMS[room_id] |
| |
| |
| if time.time() > room["expires_at"]: |
| return jsonify({"error": "Chat room has expired"}), 410 |
| |
| |
| if room["settings"]["password"] and password != room["settings"]["password"]: |
| return jsonify({"error": "Wrong password"}), 403 |
| |
| |
| if admin_session and admin_session == room["admin_session"]: |
| |
| role = "admin" |
| session_id = admin_session |
| receiver_number = None |
| else: |
| |
| active_receivers = sum(1 for s in room["active_sessions"].values() if s["role"] == "receiver") |
| if active_receivers >= room["settings"]["max_receivers"]: |
| return jsonify({"error": "Chat room is full"}), 403 |
| |
| role = "receiver" |
| session_id = str(uuid.uuid4()) |
| room["receiver_counter"] += 1 |
| receiver_number = room["receiver_counter"] |
| |
| return jsonify({ |
| "session_id": session_id, |
| "role": role, |
| "receiver_number": receiver_number, |
| "room_settings": room["settings"], |
| "expires_at": room["expires_at"] |
| }) |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| |
| @socketio.on('join_chat') |
| def handle_join_chat(data): |
| room_id = data['room_id'] |
| session_id = data['session_id'] |
| role = data['role'] |
| public_key = data.get('public_key') |
| |
| if room_id not in CHAT_ROOMS: |
| return |
| |
| join_room(room_id) |
| |
| |
| CHAT_ROOMS[room_id]["active_sessions"][session_id] = { |
| "role": role, |
| "receiver_number": data.get('receiver_number'), |
| "public_key": public_key, |
| "joined_at": time.time(), |
| "last_seen": time.time() |
| } |
| |
| |
| peer_list = [] |
| for sid, session in CHAT_ROOMS[room_id]["active_sessions"].items(): |
| if sid != session_id: |
| peer_list.append({ |
| 'session_id': sid, |
| 'role': session['role'], |
| 'receiver_number': session.get('receiver_number'), |
| 'public_key': session.get('public_key') |
| }) |
| |
| emit('peer_list', {'peers': peer_list}) |
| |
| |
| emit('peer_joined', { |
| 'session_id': session_id, |
| 'role': role, |
| 'receiver_number': data.get('receiver_number'), |
| 'public_key': public_key, |
| 'active_count': len(CHAT_ROOMS[room_id]["active_sessions"]) |
| }, room=room_id, include_self=False) |
|
|
| @socketio.on('start_typing') |
| def handle_start_typing(data): |
| room_id = data['room_id'] |
| session_id = data['session_id'] |
| |
| if room_id in CHAT_ROOMS and session_id in CHAT_ROOMS[room_id]["active_sessions"]: |
| session = CHAT_ROOMS[room_id]["active_sessions"][session_id] |
| emit('user_typing', { |
| 'role': session["role"], |
| 'receiver_number': session.get("receiver_number") |
| }, room=room_id, include_self=False) |
|
|
| @socketio.on('stop_typing') |
| def handle_stop_typing(data): |
| room_id = data['room_id'] |
| session_id = data['session_id'] |
| |
| if room_id in CHAT_ROOMS and session_id in CHAT_ROOMS[room_id]["active_sessions"]: |
| session = CHAT_ROOMS[room_id]["active_sessions"][session_id] |
| emit('user_stopped_typing', { |
| 'role': session["role"], |
| 'receiver_number': session.get("receiver_number") |
| }, room=room_id, include_self=False) |
|
|
| @socketio.on('chat_peer_offer') |
| def handle_chat_peer_offer(data): |
| """Relay WebRTC offer for chat P2P connection""" |
| room_id = data.get('room_id') |
| to_session = data.get('to_session') |
| from_session = data.get('from_session') |
| offer = data.get('offer') |
| |
| if room_id not in CHAT_ROOMS: |
| return |
| |
| print(f"π€ Relaying offer: {from_session[:8]}... β {to_session[:8]}...") |
| |
| |
| emit('chat_peer_offer', { |
| 'from_session': from_session, |
| 'offer': offer |
| }, to=room_id, skip_sid=request.sid) |
|
|
| @socketio.on('chat_peer_answer') |
| def handle_chat_peer_answer(data): |
| """Relay WebRTC answer for chat P2P connection""" |
| room_id = data.get('room_id') |
| to_session = data.get('to_session') |
| from_session = data.get('from_session') |
| answer = data.get('answer') |
| |
| if room_id not in CHAT_ROOMS: |
| return |
| |
| print(f"π€ Relaying answer: {from_session[:8]}... β {to_session[:8]}...") |
| |
| |
| emit('chat_peer_answer', { |
| 'from_session': from_session, |
| 'answer': answer |
| }, to=room_id, skip_sid=request.sid) |
|
|
| @socketio.on('chat_ice_candidate') |
| def handle_chat_ice_candidate(data): |
| """Relay ICE candidates for chat connections""" |
| room_id = data.get('room_id') |
| to_session = data.get('to_session') |
| from_session = data.get('from_session') |
| candidate = data.get('candidate') |
| |
| if room_id not in CHAT_ROOMS: |
| return |
| |
| |
| emit('chat_ice_candidate', { |
| 'from_session': from_session, |
| 'candidate': candidate |
| }, to=room_id, skip_sid=request.sid) |
|
|
| @socketio.on('leave_chat') |
| def handle_leave_chat(data): |
| room_id = data['room_id'] |
| session_id = data['session_id'] |
| |
| if room_id in CHAT_ROOMS and session_id in CHAT_ROOMS[room_id]["active_sessions"]: |
| session = CHAT_ROOMS[room_id]["active_sessions"][session_id] |
| del CHAT_ROOMS[room_id]["active_sessions"][session_id] |
| |
| |
| if session["role"] == "admin" and CHAT_ROOMS[room_id]["settings"]["burn_on_admin_exit"]: |
| emit('room_closing', {'reason': 'Admin left the room'}, room=room_id) |
| del CHAT_ROOMS[room_id] |
| if room_id in CHAT_MESSAGES: |
| del CHAT_MESSAGES[room_id] |
| else: |
| emit('user_left', { |
| 'role': session["role"], |
| 'receiver_number': session.get("receiver_number"), |
| 'active_count': len(CHAT_ROOMS[room_id]["active_sessions"]) |
| }, room=room_id) |
| |
| leave_room(room_id) |
|
|
| @app.route("/api/auth/signup", methods=["POST"]) |
| def api_signup(): |
| """Register new user""" |
| try: |
| data = request.get_json() |
| username = data.get("username") |
| password = data.get("password") |
| |
| if not username or not password: |
| return jsonify({"error": "Username and password required"}), 400 |
| |
| result, status_code = signup_user(username, password) |
| return jsonify(result), status_code |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/auth/login", methods=["POST"]) |
| def api_login(): |
| """Login existing user""" |
| try: |
| data = request.get_json() |
| username = data.get("username") |
| password = data.get("password") |
| |
| if not username or not password: |
| return jsonify({"error": "Username and password required"}), 400 |
| |
| result, status_code = login_user(username, password) |
| return jsonify(result), status_code |
| |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/auth/check-username", methods=["POST"]) |
| def api_check_username(): |
| """Check if username exists""" |
| try: |
| data = request.get_json() |
| username = data.get("username") |
| |
| if not username: |
| return jsonify({"error": "Username required"}), 400 |
| |
| |
| supabase = get_supabase() |
| if supabase is None: |
| print("β οΈ Supabase not available, allowing all usernames") |
| |
| return jsonify({"exists": True, "username": username}), 200 |
| |
| result, status_code = check_username_exists(username) |
| return jsonify(result), status_code |
| |
| except Exception as e: |
| print(f"β Check username error: {e}") |
| import traceback |
| traceback.print_exc() |
| |
| return jsonify({"exists": True, "username": username}), 200 |
|
|
| @app.route("/api/auth/me", methods=["GET"]) |
| @require_auth |
| def api_get_current_user(): |
| """Get current user from JWT token""" |
| try: |
| user = request.current_user |
| return jsonify({ |
| "user_id": user['user_id'], |
| "username_hash": user['username_hash'] |
| }) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/api/auth/logout", methods=["POST"]) |
| def api_logout(): |
| """Logout user (client-side token deletion)""" |
| return jsonify({"message": "Logged out successfully"}) |
|
|
| @socketio.on('user_online') |
| def handle_user_online(data): |
| """Track user coming online""" |
| username_hash = data.get('username_hash') |
| if username_hash: |
| ONLINE_USERS[username_hash] = request.sid |
| print(f"π€ User online: {username_hash[:8]}... (SID: {request.sid})") |
| print(f"π Total online users: {len(ONLINE_USERS)}") |
| print(f"π Online users: {[h[:8] for h in ONLINE_USERS.keys()]}") |
| |
| |
| notify_friends_of_online_status(username_hash, 'online') |
|
|
| def notify_friends_of_online_status(username_hash, status): |
| """Notify all online friends that a user came online/offline""" |
| print(f"\nπ Notifying friends of {username_hash[:8]} status: {status}") |
| |
| |
| for other_user_hash, other_sid in ONLINE_USERS.items(): |
| if other_user_hash == username_hash: |
| continue |
| |
| |
| emit('friend_status_changed', { |
| 'username_hash': username_hash, |
| 'status': status |
| }, room=other_sid) |
| |
| print(f" βοΈ Notified {other_user_hash[:8]} about {username_hash[:8]}") |
|
|
| @socketio.on('user_offline') |
| def handle_user_offline(data): |
| """Track user going offline""" |
| username_hash = data.get('username_hash') |
| if username_hash and username_hash in ONLINE_USERS: |
| del ONLINE_USERS[username_hash] |
| print(f"π€ User offline: {username_hash[:8]}...") |
| print(f"π Total online users: {len(ONLINE_USERS)}") |
| |
| |
| notify_friends_of_online_status(username_hash, 'offline') |
|
|
| @socketio.on('check_friends_online') |
| def handle_check_friends_online(data): |
| """Check which friends are online""" |
| friend_hashes = data.get('friend_hashes', []) |
| |
| print(f"\n=== FRIENDS ONLINE CHECK ===") |
| print(f"π¨ Received from SID: {request.sid}") |
| print(f"π Checking {len(friend_hashes)} friends") |
| print(f"π Friend hashes to check: {[h[:8] for h in friend_hashes]}") |
| print(f"π Currently online: {[h[:8] for h in ONLINE_USERS.keys()]}") |
| |
| online_friends = [] |
| for friend_hash in friend_hashes: |
| if friend_hash in ONLINE_USERS: |
| online_friends.append({ |
| 'username_hash': friend_hash, |
| 'status': 'online' |
| }) |
| print(f"β
Friend {friend_hash[:8]} is ONLINE") |
| else: |
| print(f"β Friend {friend_hash[:8]} is OFFLINE") |
| |
| print(f"π€ Sending back {len(online_friends)} online friends") |
| print(f"=========================\n") |
| |
| |
| emit('friends_status', {'friends': online_friends}) |
|
|
| |
|
|
| @socketio.on('screen_share_invite') |
| def handle_screen_share_invite(data): |
| """Host sends screen share invite to viewer""" |
| room_id = data.get('room_id') |
| to_hash = data.get('to_hash') |
| from_hash = data.get('from_hash') |
| |
| print(f"\nπΊ Screen Share Invite") |
| print(f"From: {from_hash[:8]}...") |
| print(f"To: {to_hash[:8]}...") |
| print(f"Room: {room_id[:8] if room_id else 'None'}...") |
| |
| |
| if to_hash not in ONLINE_USERS: |
| print(f"β Recipient {to_hash[:8]} is OFFLINE") |
| emit('error', {'message': 'Friend is offline'}) |
| return |
| |
| recipient_sid = ONLINE_USERS[to_hash] |
| print(f"β
Recipient is ONLINE (SID: {recipient_sid})") |
| |
| |
| emit('screen_share_invite', { |
| 'room_id': room_id, |
| 'from_hash': from_hash |
| }, room=recipient_sid) |
| |
| print(f"β
Invite forwarded to {to_hash[:8]}") |
|
|
|
|
| @socketio.on('screen_share_offer') |
| def handle_screen_share_offer(data): |
| """Viewer sends WebRTC offer to host""" |
| room_id = data.get('room_id') |
| to_hash = data.get('to_hash') |
| from_hash = data.get('from_hash') |
| offer = data.get('offer') |
| |
| print(f"\nπ‘ WebRTC Offer") |
| print(f"From: {from_hash[:8]}... (viewer)") |
| print(f"To: {to_hash[:8]}... (host)") |
| print(f"Room: {room_id[:8] if room_id else 'None'}...") |
| |
| |
| if to_hash not in ONLINE_USERS: |
| print(f"β Host {to_hash[:8]} is OFFLINE") |
| emit('error', {'message': 'Host is offline'}) |
| return |
| |
| host_sid = ONLINE_USERS[to_hash] |
| print(f"β
Host is ONLINE (SID: {host_sid})") |
| |
| |
| emit('screen_share_offer', { |
| 'room_id': room_id, |
| 'from_hash': from_hash, |
| 'offer': offer |
| }, room=host_sid) |
| |
| print(f"β
Offer forwarded to host {to_hash[:8]}") |
|
|
| @socketio.on('screen_share_answer') |
| def handle_screen_share_answer(data): |
| """Host sends WebRTC answer back to viewer""" |
| room_id = data.get('room_id') |
| to_hash = data.get('to_hash') |
| from_hash = data.get('from_hash') |
| answer = data.get('answer') |
| |
| print(f"\nπ‘ WebRTC Answer") |
| print(f"From: {from_hash[:8]}... (host)") |
| print(f"To: {to_hash[:8]}... (viewer)") |
| |
| |
| if to_hash not in ONLINE_USERS: |
| print(f"β Viewer {to_hash[:8]} is OFFLINE") |
| return |
| |
| viewer_sid = ONLINE_USERS[to_hash] |
| print(f"β
Viewer is ONLINE (SID: {viewer_sid})") |
| |
| |
| emit('screen_share_answer', { |
| 'room_id': room_id, |
| 'from_hash': from_hash, |
| 'answer': answer |
| }, room=viewer_sid) |
| |
| print(f"β
Answer forwarded to viewer {to_hash[:8]}") |
|
|
| @socketio.on('ice_candidate') |
| def handle_ice_candidate(data): |
| """Forward ICE candidates between peers""" |
| to_hash = data.get('to_hash') |
| from_hash = data.get('from_hash') |
| candidate = data.get('candidate') |
| |
| print(f"π§ ICE candidate: {from_hash[:8]} β {to_hash[:8]}") |
| |
| if to_hash in ONLINE_USERS: |
| recipient_sid = ONLINE_USERS[to_hash] |
| emit('ice_candidate', { |
| 'from_hash': from_hash, |
| 'candidate': candidate |
| }, room=recipient_sid) |
|
|
| @socketio.on('join_screen_room') |
| def handle_join_screen_room(data): |
| """User joins screen share room""" |
| room_id = data.get('room_id') |
| username_hash = data.get('username_hash') |
| |
| print(f"\nπͺ User joining screen room") |
| print(f"User: {username_hash[:8]}...") |
| print(f"Room: {room_id[:8] if room_id else 'None'}...") |
| |
| if not room_id: |
| return |
| |
| join_room(room_id) |
| |
| |
| if room_id not in SCREEN_SHARE_ROOMS: |
| SCREEN_SHARE_ROOMS[room_id] = { |
| 'host_hash': username_hash, |
| 'participants': [], |
| 'settings': { |
| 'allow_files': True, |
| 'allow_cursors': True |
| }, |
| 'created_at': time.time(), |
| 'expires_at': time.time() + 3600 |
| } |
| print(f"β
Room created (host)") |
| else: |
| |
| room = SCREEN_SHARE_ROOMS[room_id] |
| if username_hash != room['host_hash']: |
| if not any(p['username_hash'] == username_hash for p in room['participants']): |
| room['participants'].append({ |
| 'username_hash': username_hash, |
| 'socket_id': request.sid, |
| 'cursor_enabled': False, |
| 'cursor_color': assign_cursor_color(room['participants']), |
| 'joined_at': time.time() |
| }) |
| print(f"β
User added as participant") |
| |
| |
| socketio.emit('user_joined_room', { |
| 'username_hash': username_hash, |
| 'participant_count': len(room['participants']) + 1 |
| }, room=room_id, include_self=False) |
|
|
| @socketio.on('leave_screen_room') |
| def handle_leave_screen_room(data): |
| """User leaves screen share room""" |
| room_id = data.get('room_id') |
| username_hash = data.get('username_hash') |
| |
| print(f"πͺ User leaving room: {username_hash[:8]}...") |
| |
| if room_id in SCREEN_SHARE_ROOMS: |
| room = SCREEN_SHARE_ROOMS[room_id] |
| |
| |
| room['participants'] = [p for p in room['participants'] if p['username_hash'] != username_hash] |
| |
| |
| socketio.emit('user_left_room', { |
| 'username_hash': username_hash, |
| 'participant_count': len(room['participants']) |
| }, room=room_id) |
| |
| leave_room(room_id) |
|
|
| @socketio.on('screen_share_end') |
| def handle_screen_share_end(data): |
| """Host ends screen share""" |
| room_id = data.get('room_id') |
| |
| print(f"π Screen share ending: {room_id[:8] if room_id else 'None'}...") |
| |
| if room_id in SCREEN_SHARE_ROOMS: |
| |
| socketio.emit('screen_share_ended', { |
| 'room_id': room_id, |
| 'reason': 'Host ended the session' |
| }, room=room_id) |
| |
| |
| del SCREEN_SHARE_ROOMS[room_id] |
| if room_id in CURSOR_STATES: |
| del CURSOR_STATES[room_id] |
|
|
| |
|
|
| @socketio.on('cursor_enable_request') |
| def handle_cursor_enable_request(data): |
| """Viewer requests to enable cursor""" |
| room_id = data.get('room_id') |
| username_hash = data.get('username_hash') |
| |
| room = SCREEN_SHARE_ROOMS.get(room_id) |
| if not room: |
| emit('error', {'message': 'Room not found'}) |
| return |
| |
| host_hash = room['host_hash'] |
| |
| |
| if host_hash in ONLINE_USERS: |
| host_sid = ONLINE_USERS[host_hash] |
| emit('cursor_enable_request', { |
| 'room_id': room_id, |
| 'username_hash': username_hash, |
| 'cursor_color': data.get('cursor_color', 'blue') |
| }, room=host_sid) |
| print(f"π±οΈ Cursor request: {username_hash[:8]} in room {room_id[:8]}") |
|
|
| @socketio.on('cursor_enable_response') |
| def handle_cursor_enable_response(data): |
| """Host approves/denies cursor""" |
| room_id = data.get('room_id') |
| username_hash = data.get('username_hash') |
| approved = data.get('approved', False) |
| |
| room = SCREEN_SHARE_ROOMS.get(room_id) |
| if not room: |
| return |
| |
| |
| if approved: |
| for participant in room['participants']: |
| if participant['username_hash'] == username_hash: |
| participant['cursor_enabled'] = True |
| break |
| |
| |
| if username_hash in ONLINE_USERS: |
| user_sid = ONLINE_USERS[username_hash] |
| emit('cursor_enabled', { |
| 'room_id': room_id, |
| 'approved': approved |
| }, room=user_sid) |
| print(f"π±οΈ Cursor {'approved' if approved else 'denied'}: {username_hash[:8]}") |
|
|
|
|
|
|
| |
|
|
| @socketio.on('file_transfer_start') |
| def handle_file_transfer_start(data): |
| """Initiator starts file transfer""" |
| room_id = data.get('room_id') |
| file_id = data.get('file_id') |
| from_hash = data.get('from_hash') |
| to_hashes = data.get('to_hashes', []) |
| file_size = data.get('file_size', 0) |
| |
| |
| if file_size > 104857600: |
| emit('file_transfer_error', {'error': 'File too large (max 100 MB)'}) |
| return |
| |
| room = SCREEN_SHARE_ROOMS.get(room_id) |
| if not room or not room['settings']['allow_files']: |
| emit('file_transfer_error', {'error': 'File sharing not allowed'}) |
| return |
| |
| |
| if to_hashes == ["all"]: |
| to_hashes = [p['username_hash'] for p in room['participants'] |
| if p['username_hash'] != from_hash] |
| |
| |
| FILE_TRANSFERS[file_id] = { |
| 'from_hash': from_hash, |
| 'to_hashes': to_hashes, |
| 'file_name': data.get('file_name'), |
| 'file_size': file_size, |
| 'mime_type': data.get('mime_type'), |
| 'chunks_total': data.get('chunks_total', 0), |
| 'chunks_received': {h: 0 for h in to_hashes}, |
| 'status': 'transferring', |
| 'started_at': time.time() |
| } |
| |
| |
| for to_hash in to_hashes: |
| if to_hash in ONLINE_USERS: |
| recipient_sid = ONLINE_USERS[to_hash] |
| emit('file_transfer_incoming', { |
| 'file_id': file_id, |
| 'from_hash': from_hash, |
| 'file_name': data.get('file_name'), |
| 'file_size': file_size, |
| 'mime_type': data.get('mime_type'), |
| 'chunks_total': data.get('chunks_total') |
| }, room=recipient_sid) |
| |
| print(f"π File transfer started: {data.get('file_name')} ({file_size} bytes)") |
|
|
| @socketio.on('file_transfer_complete') |
| def handle_file_transfer_complete(data): |
| """Recipient confirms complete transfer""" |
| file_id = data.get('file_id') |
| to_hash = data.get('to_hash') |
| |
| if file_id in FILE_TRANSFERS: |
| transfer = FILE_TRANSFERS[file_id] |
| |
| |
| from_hash = transfer['from_hash'] |
| if from_hash in ONLINE_USERS: |
| sender_sid = ONLINE_USERS[from_hash] |
| emit('file_transfer_complete', { |
| 'file_id': file_id, |
| 'to_hash': to_hash, |
| 'file_name': transfer['file_name'] |
| }, room=sender_sid) |
| |
| print(f"β
File transfer complete: {transfer['file_name']} to {to_hash[:8]}") |
|
|
|
|
|
|
| @socketio.on('disconnect') |
| def handle_disconnect(): |
| username_hash = None |
| |
| print(f"\nπ Disconnect event - SID: {request.sid}") |
| |
| |
| for user_hash, socket_id in list(ONLINE_USERS.items()): |
| if socket_id == request.sid: |
| username_hash = user_hash |
| del ONLINE_USERS[user_hash] |
| print(f"π€ User disconnected: {username_hash[:8]}...") |
| print(f"π Remaining online: {len(ONLINE_USERS)} users") |
| |
| |
| notify_friends_of_online_status(username_hash, 'offline') |
| break |
| |
| if not username_hash: |
| print(f"β οΈ Unknown user disconnected (SID: {request.sid})") |
| |
| if username_hash: |
| print(f"π€ User disconnected: {username_hash[:8]}...") |
| |
| |
| for room_id, room in list(CHAT_ROOMS.items()): |
| if username_hash in [s for s_id, s in room['active_sessions'].items()]: |
| |
| for session_id, session in list(room['active_sessions'].items()): |
| if session.get('username_hash') == username_hash or \ |
| (session['role'] == 'admin' and room['admin_session'] == session_id): |
| |
| |
| if session['role'] == 'admin' and room['settings'].get('burn_on_admin_exit', True): |
| socketio.emit('room_closing', {'reason': 'Admin disconnected'}, room=room_id) |
| if room_id in CHAT_MESSAGES: |
| del CHAT_MESSAGES[room_id] |
| del CHAT_ROOMS[room_id] |
| else: |
| del room['active_sessions'][session_id] |
| socketio.emit('user_left', { |
| 'role': session['role'], |
| 'receiver_number': session.get('receiver_number'), |
| 'active_count': len(room['active_sessions']) |
| }, room=room_id) |
| |
| |
| for room_id, room in list(SCREEN_SHARE_ROOMS.items()): |
| |
| room['participants'] = [ |
| p for p in room['participants'] |
| if p.get('socket_id') != request.sid and p.get('username_hash') != username_hash |
| ] |
| |
| |
| if room.get('host_hash') == username_hash: |
| socketio.emit('screen_share_ended', { |
| 'room_id': room_id, |
| 'reason': 'Host disconnected' |
| }, room=room_id) |
| del SCREEN_SHARE_ROOMS[room_id] |
| if room_id in CURSOR_STATES: |
| del CURSOR_STATES[room_id] |
| else: |
| |
| socketio.emit('user_left_room', { |
| 'username_hash': username_hash, |
| 'participant_count': len(room['participants']) |
| }, room=room_id) |
|
|
| |
|
|
| @socketio.on('user_speaking') |
| def handle_user_speaking(data): |
| """User started speaking""" |
| room_id = data.get('room_id') |
| username_hash = data.get('username_hash') |
| |
| socketio.emit('user_speaking', { |
| 'username_hash': username_hash |
| }, room=room_id, include_self=False) |
|
|
| @socketio.on('user_stopped_speaking') |
| def handle_user_stopped_speaking(data): |
| """User stopped speaking""" |
| room_id = data.get('room_id') |
| username_hash = data.get('username_hash') |
| |
| socketio.emit('user_stopped_speaking', { |
| 'username_hash': username_hash |
| }, room=room_id, include_self=False) |
|
|
| @socketio.on('user_mic_state') |
| def handle_user_mic_state(data): |
| """User muted/unmuted mic""" |
| room_id = data.get('room_id') |
| username_hash = data.get('username_hash') |
| muted = data.get('muted') |
| |
| socketio.emit('user_mic_state', { |
| 'username_hash': username_hash, |
| 'muted': muted |
| }, room=room_id, include_self=False) |
|
|
| |
|
|
| @socketio.on('request_viewer_list') |
| def handle_request_viewer_list(data): |
| """Send list of all viewers in room to requester""" |
| room_id = data.get('room_id') |
| requester_hash = data.get('username_hash') |
| |
| print(f"\nπ Viewer list requested") |
| print(f"Room: {room_id[:8] if room_id else 'None'}...") |
| print(f"Requester: {requester_hash[:8]}...") |
| |
| if room_id not in SCREEN_SHARE_ROOMS: |
| emit('viewer_list', {'viewers': []}) |
| return |
| |
| room = SCREEN_SHARE_ROOMS[room_id] |
| |
| |
| viewers = [ |
| p['username_hash'] |
| for p in room['participants'] |
| if p['username_hash'] != requester_hash |
| ] |
| |
| |
| if room['host_hash'] in viewers: |
| viewers.remove(room['host_hash']) |
| |
| print(f"β
Sending {len(viewers)} viewers: {[v[:8] for v in viewers]}") |
| |
| emit('viewer_list', {'viewers': viewers}) |
|
|
|
|
| @socketio.on('notify_audio_peer_join') |
| def handle_notify_audio_peer_join(data): |
| """Notify existing viewers that a new viewer joined (for audio mesh)""" |
| room_id = data.get('room_id') |
| new_viewer_hash = data.get('username_hash') |
| |
| print(f"\nπ Notifying room of new audio peer") |
| print(f"Room: {room_id[:8] if room_id else 'None'}...") |
| print(f"New viewer: {new_viewer_hash[:8]}...") |
| |
| if room_id not in SCREEN_SHARE_ROOMS: |
| return |
| |
| room = SCREEN_SHARE_ROOMS[room_id] |
| host_hash = room['host_hash'] |
| |
| |
| for participant in room['participants']: |
| p_hash = participant['username_hash'] |
| |
| if p_hash == new_viewer_hash or p_hash == host_hash: |
| continue |
| |
| if p_hash in ONLINE_USERS: |
| recipient_sid = ONLINE_USERS[p_hash] |
| emit('audio_peer_joined', { |
| 'viewer_hash': new_viewer_hash |
| }, room=recipient_sid) |
| |
| print(f" βοΈ Notified {p_hash[:8]}") |
|
|
|
|
| @socketio.on('audio_peer_offer') |
| def handle_audio_peer_offer(data): |
| """Relay WebRTC offer between two viewers (audio only)""" |
| room_id = data.get('room_id') |
| to_hash = data.get('to_hash') |
| from_hash = data.get('from_hash') |
| offer = data.get('offer') |
| |
| print(f"\nπ€ Audio Offer") |
| print(f"From: {from_hash[:8]}...") |
| print(f"To: {to_hash[:8]}...") |
| |
| |
| if to_hash in ONLINE_USERS: |
| recipient_sid = ONLINE_USERS[to_hash] |
| emit('audio_peer_offer', { |
| 'from_hash': from_hash, |
| 'offer': offer |
| }, room=recipient_sid) |
| |
| print(f"β
Audio offer relayed") |
| else: |
| print(f"β Recipient {to_hash[:8]} not online") |
|
|
|
|
| @socketio.on('audio_peer_answer') |
| def handle_audio_peer_answer(data): |
| """Relay WebRTC answer between two viewers""" |
| room_id = data.get('room_id') |
| to_hash = data.get('to_hash') |
| from_hash = data.get('from_hash') |
| answer = data.get('answer') |
| |
| print(f"\nπ€ Audio Answer") |
| print(f"From: {from_hash[:8]}...") |
| print(f"To: {to_hash[:8]}...") |
| |
| if to_hash in ONLINE_USERS: |
| recipient_sid = ONLINE_USERS[to_hash] |
| emit('audio_peer_answer', { |
| 'from_hash': from_hash, |
| 'answer': answer |
| }, room=recipient_sid) |
| |
| print(f"β
Audio answer relayed") |
| else: |
| print(f"β Recipient {to_hash[:8]} not online") |
|
|
|
|
| @socketio.on('audio_ice_candidate') |
| def handle_audio_ice_candidate(data): |
| """Relay ICE candidates between two viewers (audio connections)""" |
| room_id = data.get('room_id') |
| to_hash = data.get('to_hash') |
| from_hash = data.get('from_hash') |
| candidate = data.get('candidate') |
| |
| print(f"π§ Audio ICE: {from_hash[:8]} β {to_hash[:8]}") |
| |
| if to_hash in ONLINE_USERS: |
| recipient_sid = ONLINE_USERS[to_hash] |
| emit('audio_ice_candidate', { |
| 'from_hash': from_hash, |
| 'candidate': candidate |
| }, room=recipient_sid) |
|
|
| |
| @app.errorhandler(404) |
| def not_found(error): |
| return jsonify({"error": "Endpoint not found"}), 404 |
|
|
| @app.errorhandler(500) |
| def internal_error(error): |
| return jsonify({"error": "Internal server error"}), 500 |
|
|
| def periodic_cleanup(): |
| """Run periodic cleanup of expired data""" |
| while True: |
| time.sleep(300) |
| |
| try: |
| now = time.time() |
| |
| |
| expired_rooms = [] |
| for room_id, room in list(SCREEN_SHARE_ROOMS.items()): |
| if now > room['expires_at']: |
| expired_rooms.append(room_id) |
| |
| |
| socketio.emit('room_expired', { |
| 'room_id': room_id, |
| 'reason': 'Room expired' |
| }, room=room_id) |
| |
| |
| del SCREEN_SHARE_ROOMS[room_id] |
| if room_id in CURSOR_STATES: |
| del CURSOR_STATES[room_id] |
| |
| |
| stale_transfers = [] |
| for file_id, transfer in FILE_TRANSFERS.items(): |
| if transfer['status'] == 'transferring': |
| if now - transfer['started_at'] > 300: |
| transfer['status'] = 'timeout' |
| stale_transfers.append(file_id) |
| |
| |
| cursor_cleaned = 0 |
| for room_id in list(CURSOR_STATES.keys()): |
| for username_hash in list(CURSOR_STATES[room_id].keys()): |
| state = CURSOR_STATES[room_id][username_hash] |
| if now - state['last_update'] > 30: |
| del CURSOR_STATES[room_id][username_hash] |
| cursor_cleaned += 1 |
| |
| |
| if not CURSOR_STATES[room_id]: |
| del CURSOR_STATES[room_id] |
| |
| |
| |
| |
| if expired_rooms or stale_transfers or cursor_cleaned: |
| print(f"[Cleanup] Expired rooms: {len(expired_rooms)}, " |
| f"Stale transfers: {len(stale_transfers)}, " |
| f"Stale cursors: {cursor_cleaned}") |
| |
| except Exception as e: |
| print(f"[Cleanup] Error: {e}") |
|
|
| |
| cleanup_thread = threading.Thread(target=periodic_cleanup, daemon=True) |
| cleanup_thread.start() |
|
|
| |
| if __name__ == "__main__": |
| print("π Sharelock Backend Starting...") |
| print("π Features enabled:") |
| print(" β
End-to-end encryption") |
| print(" β
File uploads (5MB max)") |
| print(" β
QR code generation") |
| print(" β
Analytics tracking") |
| print(" β
Short URLs") |
| print(" β
Self-destruct messages") |
| print(" β
Multiple themes") |
| print(" β
Password hints") |
| print(" β
verify_only parameter support") |
| print(" β
Real-time chat rooms") |
| print("π Server running on http://0.0.0.0:7860") |
| |
| |
| socketio.run(app, host="0.0.0.0", port=7860, debug=False, allow_unsafe_werkzeug=True) |