chat_manager.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import json
  2. import datetime
  3. import db
  4. import auth_utils
  5. from typing import Dict, List, Any
  6. from fastapi import WebSocket
  7. class ChatConnectionManager:
  8. def __init__(self):
  9. # order_id -> list of active websockets
  10. # order_id -> list of dicts: {"ws": websocket, "role": str}
  11. self.active_connections: Dict[int, List[Dict[str, Any]]] = {}
  12. async def connect(self, websocket: WebSocket, order_id: int, role: str):
  13. await websocket.accept()
  14. if order_id not in self.active_connections:
  15. self.active_connections[order_id] = []
  16. self.active_connections[order_id].append({"ws": websocket, "role": role})
  17. print(f"DEBUG: Chat WS connected for Order #{order_id} (Role: {role}). Total connections: {len(self.active_connections[order_id])}")
  18. await self.broadcast_presence(order_id)
  19. def disconnect(self, websocket: WebSocket, order_id: int):
  20. if order_id in self.active_connections:
  21. self.active_connections[order_id] = [c for c in self.active_connections[order_id] if c["ws"] != websocket]
  22. print(f"DEBUG: Chat WS disconnected for Order #{order_id}. Remaining: {len(self.active_connections[order_id])}")
  23. if not self.active_connections[order_id]:
  24. del self.active_connections[order_id]
  25. else:
  26. import asyncio
  27. try:
  28. asyncio.create_task(self.broadcast_presence(order_id))
  29. except Exception:
  30. pass
  31. async def broadcast_presence(self, order_id: int):
  32. if order_id in self.active_connections:
  33. roles = [c["role"] for c in self.active_connections[order_id]]
  34. payload = json.dumps({"type": "presence", "online_roles": list(set(roles))})
  35. for connection in self.active_connections[order_id]:
  36. try:
  37. await connection["ws"].send_text(payload)
  38. except:
  39. pass
  40. async def broadcast_to_order(self, order_id: int, message: Any):
  41. if order_id in self.active_connections:
  42. # We must serialize datetime to JSON string
  43. if isinstance(message, dict) and 'created_at' in message:
  44. if isinstance(message['created_at'], datetime.datetime):
  45. message['created_at'] = message['created_at'].isoformat()
  46. if "type" not in message:
  47. message["type"] = "message"
  48. payload = json.dumps(message)
  49. print(f"DEBUG: Broadcasting to Order #{order_id} to {len(self.active_connections[order_id])} receivers")
  50. for connection in self.active_connections[order_id]:
  51. try:
  52. await connection["ws"].send_text(payload)
  53. except Exception as e:
  54. print(f"DEBUG: Failed to send WS message to a connection: {e}")
  55. else:
  56. print(f"DEBUG: Attempted broadcast to Order #{order_id} but NO active connections found")
  57. async def handle_message(self, order_id: int, user_id: int, role: str, text: str):
  58. is_admin = (role == 'admin')
  59. text = text.strip()
  60. if not text:
  61. return
  62. # Save to DB
  63. json_text = json.dumps({"text": text})
  64. query = "INSERT INTO order_messages (order_id, user_id, is_from_admin, message) VALUES (%s, %s, %s, %s)"
  65. msg_id = db.execute_commit(query, (order_id, user_id, is_admin, json_text))
  66. now = datetime.datetime.utcnow().isoformat()
  67. user_info = db.execute_query("SELECT first_name, email FROM users WHERE id = %s", (user_id,))
  68. first_name = user_info[0]['first_name'] if user_info else None
  69. email = user_info[0]['email'] if user_info else None
  70. message_payload = {
  71. "id": msg_id,
  72. "is_from_admin": is_admin,
  73. "message": json_text,
  74. "created_at": now,
  75. "type": "message",
  76. "first_name": first_name,
  77. "email": email
  78. }
  79. # Broadcast to all in this chat
  80. await self.broadcast_to_order(order_id, message_payload)
  81. # Global notifications (toasts, unread counts)
  82. from services.global_manager import global_manager
  83. if is_admin:
  84. # Notify the client who owns the order
  85. order = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
  86. if order:
  87. await global_manager.notify_user(order[0]['user_id'])
  88. else:
  89. # Notify admins
  90. await global_manager.notify_admins()
  91. await global_manager.notify_admins_new_message(order_id, text)
  92. # External hooks (email, etc.)
  93. try:
  94. from services import event_hooks
  95. event_hooks.on_message_received(order_id, user_id, text)
  96. except Exception as e:
  97. print(f"ERROR in chat event hooks: {e}")
  98. return msg_id
  99. manager = ChatConnectionManager()