smodusermc commited on
Commit
e6051e5
·
verified ·
1 Parent(s): 26d48c6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +174 -50
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 (new connection each time)
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 {"token": token, "user": {"id": user['id'], "username": user['username'], "display_name": user['display_name'], "avatar_path": user['avatar_path'], "status": "online"}}
 
 
 
 
 
 
 
 
 
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(file: UploadFile = File(...), chunk_index: int = Query(...), total_chunks: int = Query(...),
276
- file_name: str = Query(...), file_type: str = Query(...), file_size: int = Query(...),
277
- upload_id: str = Query(...), token: str = Header(..., alias="X-Auth-Token")):
 
 
 
 
 
 
 
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] = {"chunks": {}, "filename": file_name, "file_type": file_type,
282
- "file_size": file_size, "total_chunks": total_chunks, "received": 0,
283
- "user_id": user['id'], "created_at": time.time()}
 
 
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, "username": username, "display_name": display_name,
378
- "avatar_path": avatar_path, "online_users": manager.get_online_users()
 
 
 
379
  })
 
 
380
  await manager.broadcast({
381
- "type": "user_status", "user_id": uid, "username": username,
382
- "display_name": display_name, "avatar_path": avatar_path, "status": "online"
 
 
 
 
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, "sender_id": uid, "sender_username": username, "sender_display_name": display_name,
422
- "sender_avatar_path": avatar_path, "content": content, "timestamp_ms": ts,
423
- "reply_to_id": reply_to_id, "file_path": file_path, "file_type": file_type,
424
- "file_name": file_name, "file_size": file_size, "is_edited": False, "is_deleted": False,
 
 
 
 
 
 
 
 
 
 
425
  "status": "sent"
426
  }
427
 
428
- await ws.send_json({"type": "new_message", "message": {**msg_obj, "status": "delivered"}, "client_id": client_id})
429
- await manager.broadcast({"type": "new_message", "message": msg_obj}, exclude_user_id=uid)
 
 
 
 
 
 
 
 
 
 
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 Exception:
455
  decrypted = "[Decryption Error]"
456
- read_cursor = await db.execute("SELECT COUNT(*) as cnt FROM read_receipts WHERE message_id = ?", (row['id'],))
 
 
 
 
 
457
  read_count = (await read_cursor.fetchone())['cnt']
 
458
  msgs.append({
459
- "id": row['id'], "sender_id": row['sender_id'], "sender_username": row['username'],
460
- "sender_display_name": row['display_name'], "sender_avatar_path": row['avatar_path'],
461
- "content": decrypted, "timestamp_ms": row['timestamp_ms'], "reply_to_id": row['reply_to_id'],
462
- "file_path": row['file_path'], "file_type": row['file_type'], "file_name": row['file_name'],
463
- "file_size": row['file_size'], "is_edited": bool(row['is_edited']), "is_deleted": False,
 
 
 
 
 
 
 
 
 
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({"type": "messages_loaded", "messages": msgs, "next_cursor": next_cursor, "has_more": len(msgs) == limit})
 
 
 
 
 
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: continue
 
475
  db = await get_db()
476
  try:
477
- async with db.execute("SELECT * FROM messages WHERE id = ? AND sender_id = ? AND is_deleted = 0", (mid, uid)) as c:
478
- if not await c.fetchone():
 
 
 
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("UPDATE messages SET encrypted_content = ?, is_edited = 1 WHERE id = ?", (new_enc, mid))
 
 
 
483
  await db.commit()
484
- await manager.broadcast({"type": "message_edited", "message_id": mid, "content": new_content, "editor_id": uid})
 
 
 
 
 
485
  finally:
486
  await db.close()
487
 
 
488
  elif mtype == "delete_message":
489
  mid = data.get("message_id")
490
- if not mid: continue
 
491
  db = await get_db()
492
  try:
493
- async with db.execute("SELECT * FROM messages WHERE id = ? AND sender_id = ?", (mid, uid)) as c:
494
- msg = await c.fetchone()
 
 
 
495
  if not msg:
496
- await ws.send_json({"type": "error", "code": "NOT_FOUND", "message": "Not found"})
497
  continue
498
  if msg['file_path']:
499
- try: delete_file(msg['file_path'])
500
- except: pass
 
 
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({"type": "message_deleted", "message_id": mid})
 
 
 
505
  finally:
506
  await db.close()
507
 
 
508
  elif mtype == "typing":
509
- await manager.broadcast({"type": "typing_indicator", "user_id": uid, "username": username, "display_name": display_name, "is_typing": bool(data.get("is_typing", False))}, exclude_user_id=uid)
510
-
 
 
 
 
 
 
 
 
511
  elif mtype == "mark_read":
512
  up_to = data.get("up_to_message_id")
513
- if not up_to: continue
 
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("INSERT OR IGNORE INTO read_receipts (user_id, message_id) VALUES (?,?)", (uid, r['id']))
 
 
 
525
  new_reads.append(r)
526
  await db.commit()
527
  for r in new_reads:
528
- await manager.send_to_user(r['sender_id'], {"type": "message_read", "message_id": r['id'], "reader_username": username})
 
 
 
 
529
  finally:
530
  await db.close()
531
 
 
532
  elif mtype == "get_online_users":
533
- await ws.send_json({"type": "online_users", "users": manager.get_online_users()})
 
 
 
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("UPDATE users SET status = 'offline', last_seen = ? WHERE id = ?", (int(time.time()), uid))
 
 
 
544
  await db.commit()
545
  finally:
546
  await db.close()
547
- await manager.broadcast({"type": "user_status", "user_id": uid, "username": username, "display_name": display_name, "status": "offline"})
 
 
 
 
 
 
 
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():