Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -105,7 +105,7 @@ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
|
|
| 105 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 106 |
|
| 107 |
# ------------------------------------------------------------------------
|
| 108 |
-
# Database helper
|
| 109 |
# ------------------------------------------------------------------------
|
| 110 |
async def get_db() -> aiosqlite.Connection:
|
| 111 |
conn = await aiosqlite.connect(DATABASE_URL)
|
|
@@ -115,7 +115,7 @@ async def get_db() -> aiosqlite.Connection:
|
|
| 115 |
return conn
|
| 116 |
|
| 117 |
# ------------------------------------------------------------------------
|
| 118 |
-
# Encryption helpers
|
| 119 |
# ------------------------------------------------------------------------
|
| 120 |
def encrypt_message(plaintext: str) -> str:
|
| 121 |
aesgcm = AESGCM(MESSAGE_KEY)
|
|
@@ -133,7 +133,7 @@ def decrypt_message(encrypted_b64: str) -> str:
|
|
| 133 |
return "[Decryption Error]"
|
| 134 |
|
| 135 |
# ------------------------------------------------------------------------
|
| 136 |
-
# Password hashing
|
| 137 |
# ------------------------------------------------------------------------
|
| 138 |
def hash_password(password: str, salt: Optional[str] = None) -> tuple[str, str]:
|
| 139 |
if salt is None:
|
|
@@ -175,7 +175,7 @@ async def health_check():
|
|
| 175 |
return {"status": "healthy", "timestamp": int(time.time()), "version": "1.0.0"}
|
| 176 |
|
| 177 |
@app.post("/api/auth/signup")
|
| 178 |
-
async def signup(username: str = Query(...), password: str = Query(...), display_name: str = Query(None)):
|
| 179 |
if not username.isalnum() and '_' not in username:
|
| 180 |
raise HTTPException(400, "Invalid username")
|
| 181 |
db = await get_db()
|
|
@@ -191,7 +191,7 @@ async def signup(username: str = Query(...), password: str = Query(...), display
|
|
| 191 |
(username.lower(), display, pwd_hash, salt, token)
|
| 192 |
)
|
| 193 |
await db.commit()
|
| 194 |
-
return {"token": token, "user": {"username": username, "display_name": display, "avatar_path": None, "status": "online"}}
|
| 195 |
finally:
|
| 196 |
await db.close()
|
| 197 |
|
|
@@ -206,7 +206,16 @@ async def login(username: str = Query(...), password: str = Query(...)):
|
|
| 206 |
token = generate_token()
|
| 207 |
await db.execute("UPDATE users SET token = ?, last_seen = ?, status = 'online' WHERE id = ?", (token, int(time.time()), user['id']))
|
| 208 |
await db.commit()
|
| 209 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
finally:
|
| 211 |
await db.close()
|
| 212 |
|
|
@@ -272,15 +281,24 @@ async def upload_avatar(file: UploadFile = File(...), token: str = Header(..., a
|
|
| 272 |
upload_sessions: Dict[str, dict] = {}
|
| 273 |
|
| 274 |
@app.post("/api/upload/chunk")
|
| 275 |
-
async def upload_chunk(
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
user = await authenticate_user(token)
|
| 279 |
if not user: raise HTTPException(401)
|
| 280 |
if upload_id not in upload_sessions:
|
| 281 |
-
upload_sessions[upload_id] = {
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
| 284 |
session = upload_sessions[upload_id]
|
| 285 |
if session['user_id'] != user['id']: raise HTTPException(403)
|
| 286 |
chunk_data = await file.read()
|
|
@@ -374,12 +392,21 @@ async def ws_endpoint(ws: WebSocket, token: str = Query(...)):
|
|
| 374 |
# Send initial data
|
| 375 |
await ws.send_json({
|
| 376 |
"type": "connection_established",
|
| 377 |
-
"user_id": uid,
|
| 378 |
-
"
|
|
|
|
|
|
|
|
|
|
| 379 |
})
|
|
|
|
|
|
|
| 380 |
await manager.broadcast({
|
| 381 |
-
"type": "user_status",
|
| 382 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
}, exclude_user_id=uid)
|
| 384 |
|
| 385 |
try:
|
|
@@ -387,6 +414,7 @@ async def ws_endpoint(ws: WebSocket, token: str = Query(...)):
|
|
| 387 |
data = await ws.receive_json()
|
| 388 |
mtype = data.get("type")
|
| 389 |
|
|
|
|
| 390 |
if mtype == "send_message":
|
| 391 |
content = data.get("content", "").strip()
|
| 392 |
reply_to_id = data.get("reply_to_id")
|
|
@@ -418,16 +446,37 @@ async def ws_endpoint(ws: WebSocket, token: str = Query(...)):
|
|
| 418 |
await db.close()
|
| 419 |
|
| 420 |
msg_obj = {
|
| 421 |
-
"id": mid,
|
| 422 |
-
"
|
| 423 |
-
"
|
| 424 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
"status": "sent"
|
| 426 |
}
|
| 427 |
|
| 428 |
-
|
| 429 |
-
await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
|
|
|
|
| 431 |
elif mtype == "load_messages":
|
| 432 |
cursor_id = data.get("cursor")
|
| 433 |
limit = min(data.get("limit", 50), 100)
|
|
@@ -451,66 +500,120 @@ async def ws_endpoint(ws: WebSocket, token: str = Query(...)):
|
|
| 451 |
for row in reversed(await rows.fetchall()):
|
| 452 |
try:
|
| 453 |
decrypted = decrypt_message(row['encrypted_content'])
|
| 454 |
-
except
|
| 455 |
decrypted = "[Decryption Error]"
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
read_count = (await read_cursor.fetchone())['cnt']
|
|
|
|
| 458 |
msgs.append({
|
| 459 |
-
"id": row['id'],
|
| 460 |
-
"
|
| 461 |
-
"
|
| 462 |
-
"
|
| 463 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
"status": "read" if read_count > 0 else "sent"
|
| 465 |
})
|
|
|
|
| 466 |
next_cursor = msgs[0]['id'] if msgs else None
|
| 467 |
-
await ws.send_json({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
finally:
|
| 469 |
await db.close()
|
| 470 |
|
|
|
|
| 471 |
elif mtype == "edit_message":
|
| 472 |
mid = data.get("message_id")
|
| 473 |
new_content = data.get("content", "").strip()
|
| 474 |
-
if not mid or not new_content:
|
|
|
|
| 475 |
db = await get_db()
|
| 476 |
try:
|
| 477 |
-
async with db.execute(
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
| 479 |
await ws.send_json({"type": "error", "code": "NOT_FOUND", "message": "Message not found"})
|
| 480 |
continue
|
| 481 |
new_enc = encrypt_message(new_content)
|
| 482 |
-
await db.execute(
|
|
|
|
|
|
|
|
|
|
| 483 |
await db.commit()
|
| 484 |
-
await manager.broadcast({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
finally:
|
| 486 |
await db.close()
|
| 487 |
|
|
|
|
| 488 |
elif mtype == "delete_message":
|
| 489 |
mid = data.get("message_id")
|
| 490 |
-
if not mid:
|
|
|
|
| 491 |
db = await get_db()
|
| 492 |
try:
|
| 493 |
-
async with db.execute(
|
| 494 |
-
|
|
|
|
|
|
|
|
|
|
| 495 |
if not msg:
|
| 496 |
-
await ws.send_json({"type": "error", "code": "NOT_FOUND", "message": "
|
| 497 |
continue
|
| 498 |
if msg['file_path']:
|
| 499 |
-
try:
|
| 500 |
-
|
|
|
|
|
|
|
| 501 |
await db.execute("DELETE FROM messages WHERE id = ?", (mid,))
|
| 502 |
await db.execute("DELETE FROM read_receipts WHERE message_id = ?", (mid,))
|
| 503 |
await db.commit()
|
| 504 |
-
await manager.broadcast({
|
|
|
|
|
|
|
|
|
|
| 505 |
finally:
|
| 506 |
await db.close()
|
| 507 |
|
|
|
|
| 508 |
elif mtype == "typing":
|
| 509 |
-
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
elif mtype == "mark_read":
|
| 512 |
up_to = data.get("up_to_message_id")
|
| 513 |
-
if not up_to:
|
|
|
|
| 514 |
db = await get_db()
|
| 515 |
try:
|
| 516 |
rows = await db.execute(
|
|
@@ -521,16 +624,27 @@ async def ws_endpoint(ws: WebSocket, token: str = Query(...)):
|
|
| 521 |
)
|
| 522 |
new_reads = []
|
| 523 |
async for r in rows:
|
| 524 |
-
await db.execute(
|
|
|
|
|
|
|
|
|
|
| 525 |
new_reads.append(r)
|
| 526 |
await db.commit()
|
| 527 |
for r in new_reads:
|
| 528 |
-
await manager.send_to_user(r['sender_id'], {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
finally:
|
| 530 |
await db.close()
|
| 531 |
|
|
|
|
| 532 |
elif mtype == "get_online_users":
|
| 533 |
-
await ws.send_json({
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
except WebSocketDisconnect:
|
| 536 |
pass
|
|
@@ -540,11 +654,21 @@ async def ws_endpoint(ws: WebSocket, token: str = Query(...)):
|
|
| 540 |
manager.disconnect(ws, uid)
|
| 541 |
db = await get_db()
|
| 542 |
try:
|
| 543 |
-
await db.execute(
|
|
|
|
|
|
|
|
|
|
| 544 |
await db.commit()
|
| 545 |
finally:
|
| 546 |
await db.close()
|
| 547 |
-
await manager.broadcast({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
|
| 549 |
@app.get("/")
|
| 550 |
async def root():
|
|
|
|
| 105 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 106 |
|
| 107 |
# ------------------------------------------------------------------------
|
| 108 |
+
# Database helper
|
| 109 |
# ------------------------------------------------------------------------
|
| 110 |
async def get_db() -> aiosqlite.Connection:
|
| 111 |
conn = await aiosqlite.connect(DATABASE_URL)
|
|
|
|
| 115 |
return conn
|
| 116 |
|
| 117 |
# ------------------------------------------------------------------------
|
| 118 |
+
# Encryption helpers (AES-256-GCM)
|
| 119 |
# ------------------------------------------------------------------------
|
| 120 |
def encrypt_message(plaintext: str) -> str:
|
| 121 |
aesgcm = AESGCM(MESSAGE_KEY)
|
|
|
|
| 133 |
return "[Decryption Error]"
|
| 134 |
|
| 135 |
# ------------------------------------------------------------------------
|
| 136 |
+
# Password hashing (PBKDF2)
|
| 137 |
# ------------------------------------------------------------------------
|
| 138 |
def hash_password(password: str, salt: Optional[str] = None) -> tuple[str, str]:
|
| 139 |
if salt is None:
|
|
|
|
| 175 |
return {"status": "healthy", "timestamp": int(time.time()), "version": "1.0.0"}
|
| 176 |
|
| 177 |
@app.post("/api/auth/signup")
|
| 178 |
+
async def signup(username: str = Query(..., min_length=3, max_length=30), password: str = Query(..., min_length=6), display_name: str = Query(None, max_length=50)):
|
| 179 |
if not username.isalnum() and '_' not in username:
|
| 180 |
raise HTTPException(400, "Invalid username")
|
| 181 |
db = await get_db()
|
|
|
|
| 191 |
(username.lower(), display, pwd_hash, salt, token)
|
| 192 |
)
|
| 193 |
await db.commit()
|
| 194 |
+
return {"token": token, "user": {"id": None, "username": username, "display_name": display, "avatar_path": None, "status": "online"}}
|
| 195 |
finally:
|
| 196 |
await db.close()
|
| 197 |
|
|
|
|
| 206 |
token = generate_token()
|
| 207 |
await db.execute("UPDATE users SET token = ?, last_seen = ?, status = 'online' WHERE id = ?", (token, int(time.time()), user['id']))
|
| 208 |
await db.commit()
|
| 209 |
+
return {
|
| 210 |
+
"token": token,
|
| 211 |
+
"user": {
|
| 212 |
+
"id": user['id'],
|
| 213 |
+
"username": user['username'],
|
| 214 |
+
"display_name": user['display_name'],
|
| 215 |
+
"avatar_path": user['avatar_path'],
|
| 216 |
+
"status": "online"
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
finally:
|
| 220 |
await db.close()
|
| 221 |
|
|
|
|
| 281 |
upload_sessions: Dict[str, dict] = {}
|
| 282 |
|
| 283 |
@app.post("/api/upload/chunk")
|
| 284 |
+
async def upload_chunk(
|
| 285 |
+
file: UploadFile = File(...),
|
| 286 |
+
chunk_index: int = Query(...),
|
| 287 |
+
total_chunks: int = Query(...),
|
| 288 |
+
file_name: str = Query(...),
|
| 289 |
+
file_type: str = Query(...),
|
| 290 |
+
file_size: int = Query(...),
|
| 291 |
+
upload_id: str = Query(...),
|
| 292 |
+
token: str = Header(..., alias="X-Auth-Token")
|
| 293 |
+
):
|
| 294 |
user = await authenticate_user(token)
|
| 295 |
if not user: raise HTTPException(401)
|
| 296 |
if upload_id not in upload_sessions:
|
| 297 |
+
upload_sessions[upload_id] = {
|
| 298 |
+
"chunks": {}, "filename": file_name, "file_type": file_type,
|
| 299 |
+
"file_size": file_size, "total_chunks": total_chunks, "received": 0,
|
| 300 |
+
"user_id": user['id'], "created_at": time.time()
|
| 301 |
+
}
|
| 302 |
session = upload_sessions[upload_id]
|
| 303 |
if session['user_id'] != user['id']: raise HTTPException(403)
|
| 304 |
chunk_data = await file.read()
|
|
|
|
| 392 |
# Send initial data
|
| 393 |
await ws.send_json({
|
| 394 |
"type": "connection_established",
|
| 395 |
+
"user_id": uid,
|
| 396 |
+
"username": username,
|
| 397 |
+
"display_name": display_name,
|
| 398 |
+
"avatar_path": avatar_path,
|
| 399 |
+
"online_users": manager.get_online_users()
|
| 400 |
})
|
| 401 |
+
|
| 402 |
+
# Broadcast new online status
|
| 403 |
await manager.broadcast({
|
| 404 |
+
"type": "user_status",
|
| 405 |
+
"user_id": uid,
|
| 406 |
+
"username": username,
|
| 407 |
+
"display_name": display_name,
|
| 408 |
+
"avatar_path": avatar_path,
|
| 409 |
+
"status": "online"
|
| 410 |
}, exclude_user_id=uid)
|
| 411 |
|
| 412 |
try:
|
|
|
|
| 414 |
data = await ws.receive_json()
|
| 415 |
mtype = data.get("type")
|
| 416 |
|
| 417 |
+
# -------------------- SEND MESSAGE --------------------
|
| 418 |
if mtype == "send_message":
|
| 419 |
content = data.get("content", "").strip()
|
| 420 |
reply_to_id = data.get("reply_to_id")
|
|
|
|
| 446 |
await db.close()
|
| 447 |
|
| 448 |
msg_obj = {
|
| 449 |
+
"id": mid,
|
| 450 |
+
"sender_id": uid,
|
| 451 |
+
"sender_username": username,
|
| 452 |
+
"sender_display_name": display_name,
|
| 453 |
+
"sender_avatar_path": avatar_path,
|
| 454 |
+
"content": content,
|
| 455 |
+
"timestamp_ms": ts,
|
| 456 |
+
"reply_to_id": reply_to_id,
|
| 457 |
+
"file_path": file_path,
|
| 458 |
+
"file_type": file_type,
|
| 459 |
+
"file_name": file_name,
|
| 460 |
+
"file_size": file_size,
|
| 461 |
+
"is_edited": False,
|
| 462 |
+
"is_deleted": False,
|
| 463 |
"status": "sent"
|
| 464 |
}
|
| 465 |
|
| 466 |
+
# Confirm to sender with delivered status
|
| 467 |
+
await ws.send_json({
|
| 468 |
+
"type": "new_message",
|
| 469 |
+
"message": {**msg_obj, "status": "delivered"},
|
| 470 |
+
"client_id": client_id
|
| 471 |
+
})
|
| 472 |
+
|
| 473 |
+
# Broadcast to others
|
| 474 |
+
await manager.broadcast({
|
| 475 |
+
"type": "new_message",
|
| 476 |
+
"message": msg_obj
|
| 477 |
+
}, exclude_user_id=uid)
|
| 478 |
|
| 479 |
+
# -------------------- LOAD MESSAGES --------------------
|
| 480 |
elif mtype == "load_messages":
|
| 481 |
cursor_id = data.get("cursor")
|
| 482 |
limit = min(data.get("limit", 50), 100)
|
|
|
|
| 500 |
for row in reversed(await rows.fetchall()):
|
| 501 |
try:
|
| 502 |
decrypted = decrypt_message(row['encrypted_content'])
|
| 503 |
+
except:
|
| 504 |
decrypted = "[Decryption Error]"
|
| 505 |
+
|
| 506 |
+
# Determine read status
|
| 507 |
+
read_cursor = await db.execute(
|
| 508 |
+
"SELECT COUNT(*) as cnt FROM read_receipts WHERE message_id = ?",
|
| 509 |
+
(row['id'],)
|
| 510 |
+
)
|
| 511 |
read_count = (await read_cursor.fetchone())['cnt']
|
| 512 |
+
|
| 513 |
msgs.append({
|
| 514 |
+
"id": row['id'],
|
| 515 |
+
"sender_id": row['sender_id'],
|
| 516 |
+
"sender_username": row['username'],
|
| 517 |
+
"sender_display_name": row['display_name'],
|
| 518 |
+
"sender_avatar_path": row['avatar_path'],
|
| 519 |
+
"content": decrypted,
|
| 520 |
+
"timestamp_ms": row['timestamp_ms'],
|
| 521 |
+
"reply_to_id": row['reply_to_id'],
|
| 522 |
+
"file_path": row['file_path'],
|
| 523 |
+
"file_type": row['file_type'],
|
| 524 |
+
"file_name": row['file_name'],
|
| 525 |
+
"file_size": row['file_size'],
|
| 526 |
+
"is_edited": bool(row['is_edited']),
|
| 527 |
+
"is_deleted": False,
|
| 528 |
"status": "read" if read_count > 0 else "sent"
|
| 529 |
})
|
| 530 |
+
|
| 531 |
next_cursor = msgs[0]['id'] if msgs else None
|
| 532 |
+
await ws.send_json({
|
| 533 |
+
"type": "messages_loaded",
|
| 534 |
+
"messages": msgs,
|
| 535 |
+
"next_cursor": next_cursor,
|
| 536 |
+
"has_more": len(msgs) == limit
|
| 537 |
+
})
|
| 538 |
finally:
|
| 539 |
await db.close()
|
| 540 |
|
| 541 |
+
# -------------------- EDIT MESSAGE --------------------
|
| 542 |
elif mtype == "edit_message":
|
| 543 |
mid = data.get("message_id")
|
| 544 |
new_content = data.get("content", "").strip()
|
| 545 |
+
if not mid or not new_content:
|
| 546 |
+
continue
|
| 547 |
db = await get_db()
|
| 548 |
try:
|
| 549 |
+
async with db.execute(
|
| 550 |
+
"SELECT * FROM messages WHERE id = ? AND sender_id = ? AND is_deleted = 0",
|
| 551 |
+
(mid, uid)
|
| 552 |
+
) as cursor:
|
| 553 |
+
if not await cursor.fetchone():
|
| 554 |
await ws.send_json({"type": "error", "code": "NOT_FOUND", "message": "Message not found"})
|
| 555 |
continue
|
| 556 |
new_enc = encrypt_message(new_content)
|
| 557 |
+
await db.execute(
|
| 558 |
+
"UPDATE messages SET encrypted_content = ?, is_edited = 1 WHERE id = ?",
|
| 559 |
+
(new_enc, mid)
|
| 560 |
+
)
|
| 561 |
await db.commit()
|
| 562 |
+
await manager.broadcast({
|
| 563 |
+
"type": "message_edited",
|
| 564 |
+
"message_id": mid,
|
| 565 |
+
"content": new_content,
|
| 566 |
+
"editor_id": uid
|
| 567 |
+
})
|
| 568 |
finally:
|
| 569 |
await db.close()
|
| 570 |
|
| 571 |
+
# -------------------- DELETE MESSAGE --------------------
|
| 572 |
elif mtype == "delete_message":
|
| 573 |
mid = data.get("message_id")
|
| 574 |
+
if not mid:
|
| 575 |
+
continue
|
| 576 |
db = await get_db()
|
| 577 |
try:
|
| 578 |
+
async with db.execute(
|
| 579 |
+
"SELECT * FROM messages WHERE id = ? AND sender_id = ?",
|
| 580 |
+
(mid, uid)
|
| 581 |
+
) as cursor:
|
| 582 |
+
msg = await cursor.fetchone()
|
| 583 |
if not msg:
|
| 584 |
+
await ws.send_json({"type": "error", "code": "NOT_FOUND", "message": "Message not found"})
|
| 585 |
continue
|
| 586 |
if msg['file_path']:
|
| 587 |
+
try:
|
| 588 |
+
delete_file(msg['file_path'])
|
| 589 |
+
except:
|
| 590 |
+
pass
|
| 591 |
await db.execute("DELETE FROM messages WHERE id = ?", (mid,))
|
| 592 |
await db.execute("DELETE FROM read_receipts WHERE message_id = ?", (mid,))
|
| 593 |
await db.commit()
|
| 594 |
+
await manager.broadcast({
|
| 595 |
+
"type": "message_deleted",
|
| 596 |
+
"message_id": mid
|
| 597 |
+
})
|
| 598 |
finally:
|
| 599 |
await db.close()
|
| 600 |
|
| 601 |
+
# -------------------- TYPING --------------------
|
| 602 |
elif mtype == "typing":
|
| 603 |
+
is_typing = bool(data.get("is_typing", False))
|
| 604 |
+
await manager.broadcast({
|
| 605 |
+
"type": "typing_indicator",
|
| 606 |
+
"user_id": uid,
|
| 607 |
+
"username": username,
|
| 608 |
+
"display_name": display_name,
|
| 609 |
+
"is_typing": is_typing
|
| 610 |
+
}, exclude_user_id=uid)
|
| 611 |
+
|
| 612 |
+
# -------------------- MARK READ --------------------
|
| 613 |
elif mtype == "mark_read":
|
| 614 |
up_to = data.get("up_to_message_id")
|
| 615 |
+
if not up_to:
|
| 616 |
+
continue
|
| 617 |
db = await get_db()
|
| 618 |
try:
|
| 619 |
rows = await db.execute(
|
|
|
|
| 624 |
)
|
| 625 |
new_reads = []
|
| 626 |
async for r in rows:
|
| 627 |
+
await db.execute(
|
| 628 |
+
"INSERT OR IGNORE INTO read_receipts (user_id, message_id) VALUES (?,?)",
|
| 629 |
+
(uid, r['id'])
|
| 630 |
+
)
|
| 631 |
new_reads.append(r)
|
| 632 |
await db.commit()
|
| 633 |
for r in new_reads:
|
| 634 |
+
await manager.send_to_user(r['sender_id'], {
|
| 635 |
+
"type": "message_read",
|
| 636 |
+
"message_id": r['id'],
|
| 637 |
+
"reader_username": username
|
| 638 |
+
})
|
| 639 |
finally:
|
| 640 |
await db.close()
|
| 641 |
|
| 642 |
+
# -------------------- GET ONLINE USERS --------------------
|
| 643 |
elif mtype == "get_online_users":
|
| 644 |
+
await ws.send_json({
|
| 645 |
+
"type": "online_users",
|
| 646 |
+
"users": manager.get_online_users()
|
| 647 |
+
})
|
| 648 |
|
| 649 |
except WebSocketDisconnect:
|
| 650 |
pass
|
|
|
|
| 654 |
manager.disconnect(ws, uid)
|
| 655 |
db = await get_db()
|
| 656 |
try:
|
| 657 |
+
await db.execute(
|
| 658 |
+
"UPDATE users SET status = 'offline', last_seen = ? WHERE id = ?",
|
| 659 |
+
(int(time.time()), uid)
|
| 660 |
+
)
|
| 661 |
await db.commit()
|
| 662 |
finally:
|
| 663 |
await db.close()
|
| 664 |
+
await manager.broadcast({
|
| 665 |
+
"type": "user_status",
|
| 666 |
+
"user_id": uid,
|
| 667 |
+
"username": username,
|
| 668 |
+
"display_name": display_name,
|
| 669 |
+
"avatar_path": avatar_path,
|
| 670 |
+
"status": "offline"
|
| 671 |
+
})
|
| 672 |
|
| 673 |
@app.get("/")
|
| 674 |
async def root():
|