Spaces:
Running
Running
| import random | |
| from funcs import * | |
| the_reachable_void() | |
| import asyncio | |
| import json | |
| import os | |
| import uuid | |
| import bcrypt | |
| import time | |
| import secrets | |
| import hashlib | |
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect | |
| from fastapi.responses import Response | |
| from fastapi.middleware.cors import CORSMiddleware | |
| import urllib.request | |
| try: | |
| from huggingface_hub import HfApi, hf_hub_url, hf_hub_download | |
| except ImportError: | |
| HfApi = None | |
| hf_hub_url = None | |
| hf_hub_download = None | |
| # Environment variables configuration for Hugging Face storage | |
| HF_TOKEN = os.getenv("database_token") | |
| DATASET_REPO_ID = os.getenv("dataset_link") | |
| USERS_FILENAME = "users.json" | |
| CONVERSATIONS_FILENAME = "conversations.json" | |
| SALTS_FILENAME = "remember_me_salts.json" | |
| RECENTS_FILENAME = "users_recent_chats.json" | |
| OFFLINE_QUEUES_FILENAME = "offline_queues.json" | |
| os.makedirs("/data", exist_ok=True) | |
| LOCAL_USERS_PATH = os.path.join("/data", USERS_FILENAME) | |
| LOCAL_CONVERSATIONS_PATH = os.path.join("/data", CONVERSATIONS_FILENAME) | |
| LOCAL_SALTS_PATH = os.path.join("/data", SALTS_FILENAME) | |
| LOCAL_RECENTS_PATH = os.path.join("/data", RECENTS_FILENAME) | |
| LOCAL_OFFLINE_QUEUES_PATH = os.path.join("/data", OFFLINE_QUEUES_FILENAME) | |
| hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN and HfApi else None | |
| app = FastAPI() | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| MAINTENANCE_FILENAME = "maintenance.json" | |
| LOCAL_MAINTENANCE_PATH = os.path.join("/data", MAINTENANCE_FILENAME) | |
| PENDING_USERS_SYNC = False | |
| PENDING_CONVERSATIONS_SYNC = False | |
| PENDING_SALTS_SYNC = False | |
| PENDING_RECENTS_SYNC = False | |
| PENDING_OFFLINE_QUEUES_SYNC = False | |
| def load_maintenance(): | |
| if os.path.exists(LOCAL_MAINTENANCE_PATH): | |
| with open(LOCAL_MAINTENANCE_PATH, "r", encoding="utf-8") as f: | |
| return json.load(f).get("maintenance", False) | |
| return False | |
| async def get_maintenance(): | |
| return {"maintenance": load_maintenance()} | |
| async def health_check(): | |
| return Response(content="OK", media_type="text/plain") | |
| def hash_password(plain_text): | |
| salt = bcrypt.gensalt() | |
| return bcrypt.hashpw(plain_text.encode('utf-8'), salt).decode('utf-8') | |
| def verify_password(plain_text, hashed_text): | |
| try: | |
| return bcrypt.checkpw(plain_text.encode('utf-8'), hashed_text.encode('utf-8')) | |
| except Exception: | |
| return False | |
| file_lock = asyncio.Lock() | |
| def conversation_key(user_a, user_b): | |
| return "|".join(sorted([user_a, user_b])) | |
| def fetch_hf_file(filename): | |
| if not hf_hub_url or not HF_TOKEN or not DATASET_REPO_ID: | |
| raise RuntimeError("Hugging Face storage is not configured") | |
| url = hf_hub_url(repo_id=DATASET_REPO_ID, filename=filename, repo_type="dataset") | |
| req = urllib.request.Request(url, headers={"Authorization": f"Bearer {HF_TOKEN}"}) | |
| with urllib.request.urlopen(req) as response: | |
| return response.read().decode("utf-8") | |
| def load_databases(): | |
| global USERS_DB, CONVERSATIONS_DB, SALTS_DB, RECENTS_DB, OFFLINE_QUEUES_DB, NEXT_USER_ID | |
| # Load offline queues safely | |
| if os.path.exists(LOCAL_OFFLINE_QUEUES_PATH): | |
| try: | |
| with open(LOCAL_OFFLINE_QUEUES_PATH, "r", encoding="utf-8") as f: | |
| OFFLINE_QUEUES_DB = json.load(f) | |
| except Exception: | |
| OFFLINE_QUEUES_DB = {} | |
| elif HF_TOKEN and DATASET_REPO_ID and hf_hub_download: | |
| try: | |
| downloaded_path = hf_hub_download(repo_id=DATASET_REPO_ID, filename=OFFLINE_QUEUES_FILENAME, repo_type="dataset", token=HF_TOKEN) | |
| with open(downloaded_path, "r", encoding="utf-8") as f: | |
| OFFLINE_QUEUES_DB = json.load(f) | |
| with open(LOCAL_OFFLINE_QUEUES_PATH, "w", encoding="utf-8") as f: | |
| json.dump(OFFLINE_QUEUES_DB, f, indent=4) | |
| except Exception: | |
| OFFLINE_QUEUES_DB = {} | |
| else: | |
| OFFLINE_QUEUES_DB = {} | |
| if not HF_TOKEN or not DATASET_REPO_ID: | |
| print("Running in local-only mode.") | |
| if os.path.exists(LOCAL_USERS_PATH): | |
| with open(LOCAL_USERS_PATH, "r", encoding="utf-8") as f: | |
| raw = json.load(f) | |
| USERS_DB = raw.get("users", raw) | |
| else: | |
| USERS_DB = {} | |
| if os.path.exists(LOCAL_CONVERSATIONS_PATH): | |
| with open(LOCAL_CONVERSATIONS_PATH, "r", encoding="utf-8") as f: | |
| CONVERSATIONS_DB = json.load(f) | |
| else: | |
| CONVERSATIONS_DB = {} | |
| if os.path.exists(LOCAL_SALTS_PATH): | |
| with open(LOCAL_SALTS_PATH, "r", encoding="utf-8") as f: | |
| SALTS_DB = json.load(f) | |
| else: | |
| SALTS_DB = {} | |
| if os.path.exists(LOCAL_RECENTS_PATH): | |
| with open(LOCAL_RECENTS_PATH, "r", encoding="utf-8") as f: | |
| RECENTS_DB = json.load(f) | |
| else: | |
| RECENTS_DB = {} | |
| existing_ids = [int(u["user_id"]) for u in USERS_DB.values() if str(u.get("user_id", "")).isdigit()] | |
| NEXT_USER_ID = max(existing_ids, default=0) + 1 | |
| return | |
| try: | |
| raw_users = fetch_hf_file(USERS_FILENAME) | |
| parsed = json.loads(raw_users) | |
| USERS_DB = parsed.get("users", parsed) | |
| with open(LOCAL_USERS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(USERS_DB, f, indent=4) | |
| print(f"Loaded {len(USERS_DB)} user(s) from HF.") | |
| except Exception as e: | |
| print(f"Failed to load users from HF: {e}") | |
| USERS_DB = {} | |
| try: | |
| raw_convos = fetch_hf_file(CONVERSATIONS_FILENAME) | |
| CONVERSATIONS_DB = json.loads(raw_convos) | |
| with open(LOCAL_CONVERSATIONS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(CONVERSATIONS_DB, f, indent=4) | |
| print(f"Loaded {len(CONVERSATIONS_DB)} conversation(s) from HF.") | |
| except Exception as e: | |
| print(f"No conversations file found or failed to load: {e}") | |
| CONVERSATIONS_DB = {} | |
| try: | |
| raw_salts = fetch_hf_file(SALTS_FILENAME) | |
| SALTS_DB = json.loads(raw_salts) | |
| with open(LOCAL_SALTS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(SALTS_DB, f, indent=4) | |
| print(f"Loaded {len(SALTS_DB)} salt(s) from HF.") | |
| except Exception as e: | |
| print(f"No salts file found or failed to load: {e}") | |
| SALTS_DB = {} | |
| try: | |
| raw_recents = fetch_hf_file(RECENTS_FILENAME) | |
| RECENTS_DB = json.loads(raw_recents) | |
| with open(LOCAL_RECENTS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(RECENTS_DB, f, indent=4) | |
| print(f"loaded {len(RECENTS_DB)} recent(s) from HF.") | |
| except Exception as e: | |
| print(f"No recents file or faile to load: {e}") | |
| RECENTS_DB = {} | |
| existing_ids = [int(u["user_id"]) for u in USERS_DB.values() if str(u.get("user_id", "")).isdigit()] | |
| NEXT_USER_ID = max(existing_ids, default=0) + 1 | |
| load_databases() | |
| MAX_RECENT_CONVERSATIONS_PER_USER = 100 | |
| def update_recent_chat_entry(user, partner, message_text, timestamp, display_name): | |
| if not user or not partner: | |
| return | |
| if user not in RECENTS_DB: | |
| RECENTS_DB[user] = [] | |
| entry = { | |
| "partner": partner, | |
| "last_message": message_text, | |
| "timestamp": timestamp, | |
| "display_name": display_name or partner, | |
| } | |
| RECENTS_DB[user] = [item for item in RECENTS_DB[user] if item.get("partner") != partner] | |
| RECENTS_DB[user].append(entry) | |
| RECENTS_DB[user].sort(key=lambda item: item.get("timestamp", 0), reverse=True) | |
| RECENTS_DB[user] = RECENTS_DB[user][:MAX_RECENT_CONVERSATIONS_PER_USER] | |
| def save_users_local(): | |
| global PENDING_USERS_SYNC | |
| with open(LOCAL_USERS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(USERS_DB, f, indent=4) | |
| PENDING_USERS_SYNC = True | |
| def save_conversations_local(): | |
| global PENDING_CONVERSATIONS_SYNC | |
| with open(LOCAL_CONVERSATIONS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(CONVERSATIONS_DB, f, indent=4) | |
| PENDING_CONVERSATIONS_SYNC = True | |
| def save_salts_local(): | |
| global PENDING_SALTS_SYNC | |
| with open(LOCAL_SALTS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(SALTS_DB, f, indent=4) | |
| PENDING_SALTS_SYNC = True | |
| def save_recents_local(): | |
| global PENDING_RECENTS_SYNC | |
| with open(LOCAL_RECENTS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(RECENTS_DB, f, indent=4) | |
| PENDING_RECENTS_SYNC = True | |
| def save_offline_queues_local(): | |
| global PENDING_OFFLINE_QUEUES_SYNC | |
| with open(LOCAL_OFFLINE_QUEUES_PATH, "w", encoding="utf-8") as f: | |
| json.dump(OFFLINE_QUEUES_DB, f, indent=4) | |
| PENDING_OFFLINE_QUEUES_SYNC = True | |
| async def background_cloud_sync(): | |
| global PENDING_USERS_SYNC, PENDING_CONVERSATIONS_SYNC, PENDING_SALTS_SYNC, PENDING_RECENTS_SYNC, PENDING_OFFLINE_QUEUES_SYNC | |
| while True: | |
| await asyncio.sleep(30) | |
| if hf_api and DATASET_REPO_ID: | |
| try: | |
| if PENDING_USERS_SYNC: | |
| await asyncio.to_thread(hf_api.upload_file, path_or_fileobj=LOCAL_USERS_PATH, path_in_repo=USERS_FILENAME, repo_id=DATASET_REPO_ID, repo_type="dataset") | |
| PENDING_USERS_SYNC = False | |
| if PENDING_CONVERSATIONS_SYNC: | |
| await asyncio.to_thread(hf_api.upload_file, path_or_fileobj=LOCAL_CONVERSATIONS_PATH, path_in_repo=CONVERSATIONS_FILENAME, repo_id=DATASET_REPO_ID, repo_type="dataset") | |
| PENDING_CONVERSATIONS_SYNC = False | |
| if PENDING_SALTS_SYNC: | |
| await asyncio.to_thread(hf_api.upload_file, path_or_fileobj=LOCAL_SALTS_PATH, path_in_repo=SALTS_FILENAME, repo_id=DATASET_REPO_ID, repo_type="dataset") | |
| PENDING_SALTS_SYNC = False | |
| if PENDING_RECENTS_SYNC: | |
| await asyncio.to_thread(hf_api.upload_file, path_or_fileobj=LOCAL_RECENTS_PATH, path_in_repo=RECENTS_FILENAME, repo_id=DATASET_REPO_ID, repo_type="dataset") | |
| PENDING_RECENTS_SYNC = False | |
| if PENDING_OFFLINE_QUEUES_SYNC: | |
| await asyncio.to_thread(hf_api.upload_file, path_or_fileobj=LOCAL_OFFLINE_QUEUES_PATH, path_in_repo=OFFLINE_QUEUES_FILENAME, repo_id=DATASET_REPO_ID, repo_type="dataset") | |
| PENDING_OFFLINE_QUEUES_SYNC = False | |
| except Exception as e: | |
| print(f"[-] Background cloud sync partial failure: {e}") | |
| async def startup_event(): | |
| asyncio.create_task(background_cloud_sync()) | |
| ONLINE_USERS = {} | |
| ACTIVE_TOKENS = {} | |
| async def chat_handler(websocket: WebSocket): | |
| global NEXT_USER_ID | |
| await websocket.accept() | |
| username = None | |
| is_authenticated = False | |
| try: | |
| while True: | |
| raw_data = await websocket.receive_text() | |
| try: | |
| auth_data = json.loads(raw_data) | |
| except json.JSONDecodeError: | |
| await websocket.send_text("[-] ERROR: Invalid JSON format.") | |
| continue | |
| action = auth_data.get("action") | |
| username_input_raw = auth_data.get("user").lower() if auth_data.get("user") else "" | |
| username_input_cleaned = username_input_raw.lower() | |
| password = auth_data.get("pass") | |
| display_name = auth_data.get("display", username_input_cleaned) | |
| if action == "signup": | |
| if not username_input_cleaned or not password: | |
| await websocket.send_text("[-] ERROR: Username or password cannot be blank.") | |
| continue | |
| if username_input_cleaned in USERS_DB: | |
| await websocket.send_text("[-] FAIL: Username already taken.") | |
| continue | |
| if len(password) >= 8: | |
| banned_keywords = ["admin", "websitecreator", "thewebsitecreator", "webs1tecreator", "thewebs1tecreator"] | |
| banned_chars = ["{", "}", "(", ")", "[", "]", "@", "#", ";", ":", "<", ">", "&", '"', "'", "\\"] | |
| cleaned_display_name = display_name.lower().replace(" ", "") | |
| cleaned_username = username_input_cleaned.lower().replace(" ", "") | |
| if len(display_name) > 30: | |
| await websocket.send_text("[-] ERROR: Display name is too long.") | |
| continue | |
| elif len(username_input_cleaned) > 30: | |
| await websocket.send_text("[-] ERROR: username is too long.") | |
| continue | |
| elif any(char in display_name for char in banned_chars): | |
| await websocket.send_text(r"""[-] ERROR: Display name contains banned character/s. ("{", "}", "(", ")", "[", "]", "@", "#", ";", ":", "<", ">", "&", '"', "'", "\\")""") | |
| continue | |
| elif any(char in username_input_cleaned for char in banned_chars): | |
| await websocket.send_text(r"""[-] ERROR: Username contains banned character/s. ("{", "}", "(", ")", "[", "]", "@", "#", ";", ":", "<", ">", "&", '"', "'", "\\")""") | |
| continue | |
| elif cleaned_display_name in ["zach(thewebsitecreator)", "zachthewebsitecreator"] or any(item in cleaned_username for item in banned_keywords) or any(item in cleaned_display_name for item in banned_keywords): | |
| await websocket.send_text("[-] ERROR: You cannot impersonate me.") | |
| continue | |
| hashed = await asyncio.to_thread(hash_password, password) | |
| async with file_lock: | |
| assigned_id = str(NEXT_USER_ID) | |
| NEXT_USER_ID += 1 | |
| USERS_DB[username_input_cleaned] = { | |
| "user_id": assigned_id, | |
| "password": hashed, | |
| "display_name": display_name, | |
| "friends": [], | |
| "pending_friend_requests": [], | |
| "sent_friend_requests": [], | |
| "session_token": "" | |
| } | |
| async with file_lock: | |
| SALTS_DB[USERS_DB[username_input_cleaned]["user_id"]] = secrets.token_hex(32) | |
| await asyncio.to_thread(save_users_local) | |
| await asyncio.to_thread(save_salts_local) | |
| await websocket.send_text("[+] SUCCESS: Account created! Please log in.") | |
| continue | |
| else: | |
| await websocket.send_text("[-] ERROR: Your password is too short. Please enter a longer password.") | |
| continue | |
| elif action == "token_login": | |
| token_input = auth_data.get("token") | |
| remember_token_input = auth_data.get("remember_token") | |
| matched_user = None | |
| used_remember_token = False | |
| if token_input: | |
| matched_user = ACTIVE_TOKENS.get(token_input) | |
| if not matched_user and remember_token_input: | |
| for u_key, u_info in USERS_DB.items(): | |
| u_id = str(u_info.get("user_id")) | |
| u_salt = SALTS_DB.get(u_id) | |
| stored_hash = u_info.get("remember_token_hash") | |
| if u_salt and stored_hash: | |
| check_hash = hashlib.sha256((remember_token_input + u_salt).encode()).hexdigest() | |
| if check_hash == stored_hash: | |
| matched_user = u_key | |
| used_remember_token = True | |
| break | |
| if matched_user: | |
| username = matched_user | |
| if username in ONLINE_USERS: | |
| try: | |
| await ONLINE_USERS[username].close() | |
| except Exception: | |
| pass | |
| ONLINE_USERS[username] = websocket | |
| is_authenticated = True | |
| new_token = str(uuid.uuid4()) | |
| if token_input in ACTIVE_TOKENS: | |
| del ACTIVE_TOKENS[token_input] | |
| ACTIVE_TOKENS[new_token] = username | |
| async with file_lock: | |
| USERS_DB[username]["session_token"] = new_token | |
| new_remember_token = None | |
| if used_remember_token: | |
| user_id = str(USERS_DB[username]["user_id"]) | |
| user_salt = SALTS_DB.get(user_id) | |
| if user_salt: | |
| new_remember_token = secrets.token_hex(32) | |
| new_combined = new_remember_token + user_salt | |
| new_salted_hash = hashlib.sha256(new_combined.encode()).hexdigest() | |
| async with file_lock: | |
| USERS_DB[username]["remember_token_hash"] = new_salted_hash | |
| await asyncio.to_thread(save_users_local) | |
| token_login_payload = { | |
| "action": "login_success", | |
| "token": new_token, | |
| "user": username, | |
| "display_name": USERS_DB[username]["display_name"], | |
| "pending_friend_requests": USERS_DB[username]["pending_friend_requests"], | |
| "friends": USERS_DB[username]["friends"], | |
| "sent_friend_requests": USERS_DB[username].get("sent_friend_requests", []), | |
| "recent_chats": RECENTS_DB.get(username, []) | |
| } | |
| if new_remember_token: | |
| token_login_payload["remember_token"] = new_remember_token | |
| await websocket.send_text(json.dumps(token_login_payload)) | |
| if username in OFFLINE_QUEUES_DB: | |
| for missed_msgs in OFFLINE_QUEUES_DB[username]: | |
| await websocket.send_text(missed_msgs) | |
| del OFFLINE_QUEUES_DB[username] | |
| await asyncio.to_thread(save_offline_queues_local) | |
| print(f"{username} re-authenticated via token.") | |
| else: | |
| await websocket.send_text("[-] FAIL: Invalid or expired session token.") | |
| continue | |
| elif action == "login": | |
| if not username_input_cleaned or not password: | |
| await websocket.send_text("[-] FAIL: Username and password required.") | |
| continue | |
| is_valid = False | |
| if username_input_cleaned in USERS_DB: | |
| is_valid = await asyncio.to_thread(verify_password, password, USERS_DB[username_input_cleaned]["password"]) | |
| if is_valid: | |
| username = username_input_cleaned | |
| if username in ONLINE_USERS: | |
| try: | |
| await ONLINE_USERS[username].close() | |
| except Exception: | |
| pass | |
| ONLINE_USERS[username] = websocket | |
| is_authenticated = True | |
| token = str(uuid.uuid4()) | |
| ACTIVE_TOKENS[token] = username | |
| user_id = str(USERS_DB[username]["user_id"]) | |
| remember_me = auth_data.get("remember_me", False) | |
| remember_token = None | |
| if remember_me: | |
| user_salt = SALTS_DB.get(user_id) | |
| if user_salt: | |
| remember_token = secrets.token_hex(32) | |
| combined_string = remember_token + user_salt | |
| salted_hash = hashlib.sha256(combined_string.encode()).hexdigest() | |
| async with file_lock: | |
| USERS_DB[username]["remember_token_hash"] = salted_hash | |
| await asyncio.to_thread(save_users_local) | |
| async with file_lock: | |
| USERS_DB[username]["session_token"] = token | |
| await asyncio.to_thread(save_users_local) | |
| response_payload = { | |
| "action": "login_success", | |
| "token": token, | |
| "user": username, | |
| "user_id": user_id, | |
| "display_name": USERS_DB[username]["display_name"], | |
| "pending_friend_requests": USERS_DB[username]["pending_friend_requests"], | |
| "friends": USERS_DB[username]["friends"], | |
| "sent_friend_requests": USERS_DB[username].get("sent_friend_requests", []), | |
| "recent_chats": RECENTS_DB.get(username, []) | |
| } | |
| if remember_token: | |
| response_payload["remember_token"] = remember_token | |
| await websocket.send_text(json.dumps(response_payload)) | |
| if username in OFFLINE_QUEUES_DB: | |
| for missed_msgs in OFFLINE_QUEUES_DB[username]: | |
| await websocket.send_text(missed_msgs) | |
| del OFFLINE_QUEUES_DB[username] | |
| await asyncio.to_thread(save_offline_queues_local) | |
| print(f"{username} logged in.") | |
| continue | |
| else: | |
| await websocket.send_text("[-] FAIL: Invalid username or password.") | |
| continue | |
| if not is_authenticated: | |
| await websocket.send_text("[-] ERROR: You must log in or sign up first.") | |
| continue | |
| if action == "logout": | |
| async with file_lock: | |
| old_token = USERS_DB[username].get("session_token") | |
| if old_token in ACTIVE_TOKENS: | |
| del ACTIVE_TOKENS[old_token] | |
| USERS_DB[username]["session_token"] = "" | |
| USERS_DB[username]["remember_token_hash"] = "" | |
| ONLINE_USERS.pop(username, None) | |
| await asyncio.to_thread(save_users_local) | |
| await websocket.send_text("[+] SUCCESS: Logged out.") | |
| await websocket.close() | |
| break | |
| # Master list for all the functions | |
| dispatch_table = { | |
| # I won't be adding comments for these because they're just self-explanatory | |
| "search_users": search_users, | |
| "send_friend_request": send_friend_request, | |
| "accept_friend_request": accept_friend_request, | |
| "decline_friend_request": decline_friend_request, | |
| "unfriend": unfriend, | |
| "send_chat_message": send_chat_message, | |
| "request_chat_history": request_chat_history, | |
| "mark_seen": mark_seen, | |
| "ping": ping | |
| } | |
| if action in dispatch_table: | |
| dispatch_table[action]() | |
| else: | |
| await websocket.send_text("[-] ERROR: Invalid action.") | |
| continue | |
| except WebSocketDisconnect: | |
| pass | |
| except Exception as e: | |
| print(f"Unhandled error in chat_handler for {username}: {e}") | |
| finally: | |
| if username and username in ONLINE_USERS and ONLINE_USERS[username] == websocket: | |
| del ONLINE_USERS[username] | |
| print(f"{username} disconnected.") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| print("[+] Starting the messenger backend on port 7860...") | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |
| the_unreachable_void("Zach", "pass123", "pass123", "Parabola", "No", "NoTokenForYouLol", "pass", "Yes", 1, "Goodbye") |