From bee650d9c015af5531c206698cb6eacd280b95c6 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Mon, 8 Dec 2025 17:44:27 -0500 Subject: [PATCH] in progress debugging checkpoint --- PROJECTS/encrypted-p2p-chat/.gitignore | 1 + .../backend/app/api/rooms.py | 153 +++++++++-- .../backend/app/core/surreal_manager.py | 240 +++++++++++++++++- .../backend/app/core/websocket_manager.py | 29 ++- .../backend/app/schemas/rooms.py | 1 + .../backend/app/schemas/surreal.py | 15 +- .../backend/app/schemas/websocket.py | 28 ++ .../backend/app/services/auth_service.py | 21 ++ .../backend/app/services/message_service.py | 11 +- .../backend/app/services/presence_service.py | 4 +- .../backend/app/services/websocket_service.py | 53 +++- .../encrypted-p2p-chat/docker-compose.dev.yml | 3 + .../frontend/src/components/Layout/Header.tsx | 2 + .../src/components/Layout/Sidebar.tsx | 4 + .../frontend/src/components/UI/Toast.tsx | 12 +- .../encrypted-p2p-chat/frontend/src/index.css | 2 +- .../encrypted-p2p-chat/frontend/src/index.tsx | 2 + .../frontend/src/pages/Chat.tsx | 93 +++++-- .../frontend/src/services/index.ts | 1 + .../frontend/src/services/room.service.ts | 83 ++++++ .../frontend/src/stores/auth.store.ts | 2 +- .../frontend/src/stores/ui.store.ts | 11 +- .../frontend/src/types/guards.ts | 40 ++- .../frontend/src/types/websocket.ts | 32 +++ .../src/websocket/message-handlers.ts | 47 +++- .../src/websocket/websocket-manager.ts | 26 +- 26 files changed, 824 insertions(+), 92 deletions(-) create mode 100644 PROJECTS/encrypted-p2p-chat/frontend/src/services/room.service.ts diff --git a/PROJECTS/encrypted-p2p-chat/.gitignore b/PROJECTS/encrypted-p2p-chat/.gitignore index a8849b3e..c2f5ab4c 100644 --- a/PROJECTS/encrypted-p2p-chat/.gitignore +++ b/PROJECTS/encrypted-p2p-chat/.gitignore @@ -26,6 +26,7 @@ wheels/ .installed.cfg *.egg venv/ +.venv/ ENV/ env/ diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/api/rooms.py b/PROJECTS/encrypted-p2p-chat/backend/app/api/rooms.py index 67e04eca..614f3780 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/api/rooms.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/api/rooms.py @@ -21,9 +21,11 @@ from app.schemas.rooms import ( RoomAPIResponse, RoomListResponse, ) +from app.schemas.websocket import RoomCreatedWS from app.core.enums import RoomType from app.models.Base import get_session from app.core.surreal_manager import surreal_db +from app.core.websocket_manager import connection_manager from app.services.auth_service import auth_service @@ -41,6 +43,17 @@ async def create_room( """ Create a new chat room """ + creator = await auth_service.get_user_by_id( + session, + UUID(request.creator_id), + ) + + if not creator: + raise HTTPException( + status_code = status.HTTP_404_NOT_FOUND, + detail = "Creator not found", + ) + participant = await auth_service.get_user_by_id( session, UUID(request.participant_id), @@ -55,9 +68,9 @@ async def create_room( now = datetime.now(UTC) room_data = { - "name": participant.display_name, + "name": None, "room_type": request.room_type.value, - "created_by": request.participant_id, + "created_by": request.creator_id, "created_at": now.isoformat(), "updated_at": now.isoformat(), "is_ephemeral": request.room_type == RoomType.EPHEMERAL, @@ -65,25 +78,53 @@ async def create_room( room = await surreal_db.create_room(room_data) + await surreal_db.add_room_participant(room.id, request.creator_id, "owner") + await surreal_db.add_room_participant(room.id, request.participant_id, "member") + logger.info( - "Created room %s with participant %s", + "Created room %s with creator %s and participant %s", room.id, + request.creator_id, request.participant_id, ) + participants_list = [ + ParticipantResponse( + user_id = str(creator.id), + username = creator.username, + display_name = creator.display_name, + role = "owner", + joined_at = now.isoformat(), + ), + ParticipantResponse( + user_id = str(participant.id), + username = participant.username, + display_name = participant.display_name, + role = "member", + joined_at = now.isoformat(), + ) + ] + + room_ws_notification = RoomCreatedWS( + room_id = room.id, + room_type = room.room_type.value, + name = creator.display_name, + participants = [p.model_dump() for p in participants_list], + is_encrypted = True, + created_at = room.created_at.isoformat(), + updated_at = room.updated_at.isoformat(), + ) + + await connection_manager.send_message( + UUID(request.participant_id), + room_ws_notification.model_dump(mode = "json"), + ) + return RoomAPIResponse( id = room.id, type = RoomType(room.room_type), name = participant.display_name, - participants = [ - ParticipantResponse( - user_id = str(participant.id), - username = participant.username, - display_name = participant.display_name, - role = "member", - joined_at = now.isoformat(), - ) - ], + participants = participants_list, unread_count = 0, is_encrypted = True, created_at = room.created_at.isoformat(), @@ -92,11 +133,75 @@ async def create_room( @router.get("", status_code = status.HTTP_200_OK) -async def list_rooms() -> RoomListResponse: +async def list_rooms( + user_id: str, + session: AsyncSession = Depends(get_session), +) -> RoomListResponse: """ - List all rooms for the current user + List all rooms for the specified user """ - return RoomListResponse(rooms = []) + logger.info("list_rooms called for user_id: %s", user_id) + + user = await auth_service.get_user_by_id(session, UUID(user_id)) + + if not user: + raise HTTPException( + status_code = status.HTTP_404_NOT_FOUND, + detail = "User not found", + ) + + room_data_list = await surreal_db.get_rooms_for_user(user_id) + logger.info("SurrealDB returned %d rooms for user %s", len(room_data_list), user_id) + logger.info("Room data: %s", room_data_list) + + rooms: list[RoomAPIResponse] = [] + + for room_data in room_data_list: + if not room_data: + continue + + room_id = str(room_data.get("id", "")) + participants_data = await surreal_db.get_room_participants(room_id) + + participants: list[ParticipantResponse] = [] + + for p_data in participants_data: + p_user_id = p_data.get("user_id") + if not p_user_id: + continue + + p_user = await auth_service.get_user_by_id(session, UUID(p_user_id)) + if p_user: + participants.append( + ParticipantResponse( + user_id = str(p_user.id), + username = p_user.username, + display_name = p_user.display_name, + role = p_data.get("role", "member"), + joined_at = str(p_data.get("joined_at", "")), + ) + ) + + other_participant = next( + (p for p in participants if p.user_id != user_id), + None + ) + room_name = other_participant.display_name if other_participant else None + + rooms.append( + RoomAPIResponse( + id = room_id, + type = RoomType(room_data.get("room_type", "direct")), + name = room_name, + participants = participants, + unread_count = 0, + is_encrypted = True, + created_at = str(room_data.get("created_at", "")), + updated_at = str(room_data.get("updated_at", "")), + ) + ) + + return RoomListResponse(rooms = rooms) @router.get("/{room_id}", status_code = status.HTTP_200_OK) @@ -110,6 +215,22 @@ async def get_room(room_id: str) -> RoomAPIResponse: ) +@router.get("/{room_id}/messages", status_code = status.HTTP_200_OK) +async def get_room_messages( + room_id: str, + limit: int = 50, + offset: int = 0, +) -> dict: + """ + Get messages for a specific room + """ + messages = await surreal_db.get_room_messages(room_id, limit, offset) + return { + "messages": [msg.model_dump(mode = "json") for msg in messages], + "has_more": len(messages) == limit + } + + @router.delete("/{room_id}", status_code = status.HTTP_204_NO_CONTENT) async def delete_room(room_id: str) -> None: """ @@ -119,3 +240,5 @@ async def delete_room(room_id: str) -> None: status_code = status.HTTP_404_NOT_FOUND, detail = "Room not found", ) + + diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/core/surreal_manager.py b/PROJECTS/encrypted-p2p-chat/backend/app/core/surreal_manager.py index 109dd80f..97d28203 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/core/surreal_manager.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/core/surreal_manager.py @@ -43,6 +43,13 @@ class SurrealDBManager: if self._connected: return + logger.warning( + "SurrealDB connecting: URL=%s, NS=%s, DB=%s", + settings.SURREAL_URL, + settings.SURREAL_NAMESPACE, + settings.SURREAL_DATABASE, + ) + self.db = AsyncSurreal(settings.SURREAL_URL) await self.db.connect() @@ -59,7 +66,11 @@ class SurrealDBManager: ) self._connected = True - logger.info("Connected to SurrealDB at %s", settings.SURREAL_URL) + logger.warning( + "SurrealDB connected successfully to %s/%s", + settings.SURREAL_NAMESPACE, + settings.SURREAL_DATABASE, + ) async def disconnect(self) -> None: """ @@ -75,7 +86,62 @@ class SurrealDBManager: Ensure connection is established """ if not self._connected: + logger.warning("ensure_connected: Connection was lost, reconnecting...") await self.connect() + else: + logger.warning( + "ensure_connected: Already connected (id=%s)", + id(self.db) + ) + + async def _debug_connection_state(self) -> dict[str, Any]: + """ + Debug helper to check current connection state + """ + try: + info_result = await self.db.query("INFO FOR DB") + rooms_count = await self.db.query("SELECT count() FROM rooms GROUP ALL") + participants_count = await self.db.query( + "SELECT count() FROM room_participants GROUP ALL" + ) + return { + "db_info": info_result, + "rooms_count": rooms_count, + "participants_count": participants_count, + } + except Exception as e: + return {"error": str(e)} + + def _extract_query_result(self, result: Any) -> list[dict[str, Any]]: + """ + Extract query results from SurrealDB v2 response format + + SDK v2 can return: + - Direct list of dicts: [{'field': 'value'}, ...] + - Wrapped result: [{'result': [...]}] + - Empty list: [] + """ + if not result: + return [] + + if not isinstance(result, list): + return [] + + if len(result) == 0: + return [] + + first_item = result[0] + + if isinstance(first_item, dict): + if "result" in first_item: + return first_item["result"] or [] + return result + + if isinstance(first_item, str): + logger.warning("Query returned string instead of data: %s", first_item) + return [] + + return result async def create_message( self, @@ -86,7 +152,9 @@ class SurrealDBManager: Create a new message in SurrealDB """ await self.ensure_connected() + logger.warning("create_message - data: %s", message_data) result = await self.db.create("messages", message_data) + logger.warning("create_message - result: %s", result) result["id"] = str(result["id"]) return MessageResponse(**result) @@ -100,6 +168,11 @@ class SurrealDBManager: Get messages for a specific room with pagination """ await self.ensure_connected() + logger.warning("get_room_messages - room_id: %s", room_id) + + all_messages = await self.db.query("SELECT * FROM messages") + logger.warning("get_room_messages - ALL messages in DB: %s", all_messages) + query = """ SELECT * FROM messages WHERE room_id = $room_id @@ -115,7 +188,9 @@ class SurrealDBManager: "offset": offset, } ) - messages = result[0]["result"] if result else [] + logger.warning("get_room_messages - RAW result: %s", result) + messages = self._extract_query_result(result) + logger.warning("get_room_messages - extracted messages: %s", messages) return [MessageResponse(**msg) for msg in messages] async def create_room(self, room_data: dict[str, Any]) -> RoomResponse: @@ -123,9 +198,130 @@ class SurrealDBManager: Create a new chat room """ await self.ensure_connected() - result = await self.db.create("rooms", room_data) - result["id"] = str(result["id"]) - return RoomResponse(**result) + query = "CREATE rooms CONTENT $data" + logger.warning("create_room - executing query with data: %s", room_data) + + result = await self.db.query(query, {"data": room_data}) + logger.warning("create_room - RAW result type: %s", type(result)) + logger.warning("create_room - RAW result: %s", result) + + if isinstance(result, list) and len(result) > 0: + first = result[0] + logger.info("create_room - first element type: %s, value: %s", type(first), first) + + if isinstance(first, dict): + if "result" in first and first["result"]: + room = first["result"][0] if isinstance(first["result"], list) else first["result"] + elif "id" in first: + room = first + else: + logger.error("create_room - unexpected dict structure: %s", first) + raise ValueError(f"Unexpected result structure: {first}") + elif isinstance(first, list) and len(first) > 0: + room = first[0] + else: + logger.error("create_room - unexpected first element: %s", first) + raise ValueError(f"Unexpected result: {first}") + + room["id"] = str(room["id"]) + logger.info("create_room - final room: %s", room) + return RoomResponse(**room) + + logger.error("create_room - no results returned: %s", result) + raise ValueError(f"Failed to create room, result: {result}") + + async def add_room_participant( + self, + room_id: str, + user_id: str, + role: str = "member", + ) -> None: + """ + Add a participant to a room + """ + await self.ensure_connected() + from datetime import UTC, datetime + + query = """ + CREATE room_participants CONTENT { + room_id: $room_id, + user_id: $user_id, + role: $role, + joined_at: $joined_at + } RETURN AFTER + """ + params = { + "room_id": room_id, + "user_id": user_id, + "role": role, + "joined_at": datetime.now(UTC).isoformat(), + } + logger.warning("add_room_participant - params: %s", params) + + result = await self.db.query(query, params) + logger.warning("add_room_participant result: %s", result) + + verify = await self.db.query("SELECT * FROM room_participants") + logger.warning("VERIFY ALL room_participants: %s", verify) + + debug_state = await self._debug_connection_state() + logger.warning("add_room_participant - DB STATE after insert: %s", debug_state) + + async def get_rooms_for_user(self, user_id: str) -> list[dict[str, Any]]: + """ + Get all rooms a user is a participant of + """ + await self.ensure_connected() + + debug_state = await self._debug_connection_state() + logger.warning("get_rooms_for_user - DB STATE: %s", debug_state) + + participants_query = """ + SELECT room_id FROM room_participants WHERE user_id = $user_id + """ + logger.warning("get_rooms_for_user - fetching participants for user_id: %s", user_id) + result = await self.db.query(participants_query, {"user_id": user_id}) + logger.warning("get_rooms_for_user - RAW result: %s", result) + participant_data = self._extract_query_result(result) + logger.warning("Participant data after extract: %s", participant_data) + + if not participant_data: + logger.warning("No room_participants found for user %s", user_id) + return [] + + room_ids = [p["room_id"] for p in participant_data if p.get("room_id")] + logger.info("Room IDs to fetch: %s", room_ids) + + if not room_ids: + logger.warning("No valid room_ids for user %s", user_id) + return [] + + rooms: list[dict[str, Any]] = [] + for room_id in room_ids: + try: + room = await self.db.select(room_id) + if room: + if isinstance(room, list): + rooms.extend(room) + else: + rooms.append(room) + except Exception as e: + logger.warning("Failed to fetch room %s: %s", room_id, e) + + logger.info("Fetched %d rooms", len(rooms)) + return rooms + + async def get_room_participants(self, room_id: str) -> list[dict[str, Any]]: + """ + Get all participants of a room + """ + await self.ensure_connected() + query = """ + SELECT * FROM room_participants + WHERE room_id = $room_id OR room_id = type::string($room_id) + """ + result = await self.db.query(query, {"room_id": room_id}) + return self._extract_query_result(result) async def get_user_rooms(self, user_id: str) -> list[RoomResponse]: """ @@ -136,9 +332,11 @@ class SurrealDBManager: SELECT ->member_of->rooms.* AS rooms FROM $user_id """ - result = await self.db.query(query, {"user_id": f"users:{user_id}"}) - rooms = result[0]["result"][0]["rooms"] if result else [] - return [RoomResponse(**room) for room in rooms] + result = await self.db.query(query, {"user_id": f"users:`{user_id}`"}) + data = self._extract_query_result(result) + if data and isinstance(data[0], dict) and data[0].get("rooms"): + return [RoomResponse(**room) for room in data[0]["rooms"]] + return [] async def update_presence( self, @@ -151,7 +349,7 @@ class SurrealDBManager: """ await self.ensure_connected() await self.db.merge( - f"presence:{user_id}", + f"presence:`{user_id}`", { "user_id": user_id, "status": status, @@ -170,8 +368,8 @@ class SurrealDBManager: FROM $room_id WHERE status = '{PresenceStatus.ONLINE.value}' """ - result = await self.db.query(query, {"room_id": f"rooms:{room_id}"}) - presence_list = result[0]["result"] if result else [] + result = await self.db.query(query, {"room_id": f"rooms:`{room_id}`"}) + presence_list = self._extract_query_result(result) return [PresenceResponse(**p) for p in presence_list] async def live_messages( @@ -194,6 +392,26 @@ class SurrealDBManager: self.live_queries[room_id] = live_id return live_id + async def live_messages_for_user( + self, + user_id: str, + callback: Callable[[LiveMessageUpdate], + None], + ) -> str: + """ + Subscribe to live message updates where user is the recipient + """ + await self.ensure_connected() + query = f"LIVE SELECT * FROM messages WHERE recipient_id = '{user_id}'" + + def wrapper(data: dict[str, Any]) -> None: + update = LiveMessageUpdate(**data) + callback(update) + + live_id = await self.db.live(query, wrapper) + self.live_queries[f"user_{user_id}"] = live_id + return live_id + async def live_presence( self, room_id: str, diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/core/websocket_manager.py b/PROJECTS/encrypted-p2p-chat/backend/app/core/websocket_manager.py index 6d14a1e5..68de96a6 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/core/websocket_manager.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/core/websocket_manager.py @@ -70,7 +70,20 @@ class ConnectionManager: len(self.active_connections[user_id]) ) - await presence_service.set_user_online(user_id) + try: + await presence_service.set_user_online(user_id) + except Exception as e: + logger.error("Failed to set user %s online: %s", user_id, e) + await self._send_error( + websocket, + "database_error", + "Failed to initialize connection" + ) + self.active_connections[user_id].remove(websocket) + if not self.active_connections[user_id]: + del self.active_connections[user_id] + await websocket.close() + return False self.heartbeat_tasks[user_id] = asyncio.create_task( self._heartbeat_loop(websocket, @@ -199,8 +212,8 @@ class ConnectionManager: """ asyncio.create_task(self._handle_live_message(user_id, update)) - live_id = await surreal_db.live_messages( - room_id = str(user_id), + live_id = await surreal_db.live_messages_for_user( + user_id = str(user_id), callback = message_callback ) @@ -226,10 +239,12 @@ class ConnectionManager: message_id = message_data.id, sender_id = message_data.sender_id, recipient_id = str(user_id), - ciphertext = message_data.encrypted_content, - nonce = "", - header = message_data.encrypted_header, - sender_username = "", + room_id = message_data.room_id or "", + content = "[Encrypted message - requires decryption]", + ciphertext = message_data.ciphertext, + nonce = message_data.nonce, + header = message_data.header, + sender_username = message_data.sender_username, timestamp = message_data.created_at ) diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/schemas/rooms.py b/PROJECTS/encrypted-p2p-chat/backend/app/schemas/rooms.py index abe5c3b5..f9d8a545 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/schemas/rooms.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/schemas/rooms.py @@ -12,6 +12,7 @@ class CreateRoomRequest(BaseModel): """ Request to create a new room """ + creator_id: str participant_id: str room_type: RoomType = RoomType.DIRECT diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/schemas/surreal.py b/PROJECTS/encrypted-p2p-chat/backend/app/schemas/surreal.py index 9e63b43a..a8ea16a3 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/schemas/surreal.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/schemas/surreal.py @@ -15,12 +15,15 @@ class MessageResponse(BaseModel): Message response from SurrealDB """ id: str - room_id: str + room_id: str | None = None sender_id: str - encrypted_content: str - encrypted_header: str - created_at: datetime - updated_at: datetime + recipient_id: str + ciphertext: str + nonce: str + header: str + sender_username: str + created_at: datetime | None = None + updated_at: datetime | None = None class RoomResponse(BaseModel): @@ -28,7 +31,7 @@ class RoomResponse(BaseModel): Room response from SurrealDB """ id: str - name: str + name: str | None = None room_type: RoomType created_by: str created_at: datetime diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/schemas/websocket.py b/PROJECTS/encrypted-p2p-chat/backend/app/schemas/websocket.py index c5d37301..206782a9 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/schemas/websocket.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/schemas/websocket.py @@ -31,6 +31,8 @@ class EncryptedMessageWS(BaseWSMessage): message_id: str = Field(max_length = MESSAGE_ID_MAX_LENGTH) sender_id: str recipient_id: str + room_id: str + content: str = "" ciphertext: str = Field(max_length = ENCRYPTED_CONTENT_MAX_LENGTH) nonce: str header: str @@ -91,3 +93,29 @@ class WSHeartbeat(BaseModel): """ type: str = "heartbeat" timestamp: datetime + + +class RoomCreatedWS(BaseModel): + """ + Room created notification sent over WebSocket + """ + type: str = "room_created" + room_id: str + room_type: str + name: str | None + participants: list[dict[str, Any]] + is_encrypted: bool + created_at: str + updated_at: str + + +class MessageSentWS(BaseWSMessage): + """ + Confirmation sent back to sender after message is stored + """ + type: str = "message_sent" + temp_id: str + message_id: str + room_id: str + status: str = "sent" + created_at: datetime diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/services/auth_service.py b/PROJECTS/encrypted-p2p-chat/backend/app/services/auth_service.py index 08290539..1f9cc0ce 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/services/auth_service.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/services/auth_service.py @@ -28,7 +28,9 @@ from app.core.exceptions import ( from app.core.passkey.passkey_manager import passkey_manager from app.core.redis_manager import redis_manager from app.models.Credential import Credential +from app.models.IdentityKey import IdentityKey from app.models.User import User +from app.services.prekey_service import prekey_service from app.schemas.auth import ( AuthenticationBeginRequest, AuthenticationCompleteRequest, @@ -418,6 +420,11 @@ class AuthService: device_name = request.device_name, ) + await prekey_service.initialize_user_keys( + session = session, + user_id = user.id, + ) + logger.info("Registration completed for user: %s", username) return UserResponse( @@ -565,6 +572,20 @@ class AuthService: backup_eligible = verified.backup_eligible, ) + ik_statement = select(IdentityKey).where(IdentityKey.user_id == user.id) + ik_result = await session.execute(ik_statement) + existing_ik = ik_result.scalar_one_or_none() + + if not existing_ik: + logger.info( + "Initializing encryption keys for existing user: %s", + user.username + ) + await prekey_service.initialize_user_keys( + session = session, + user_id = user.id, + ) + logger.info("Authentication successful for user: %s", user.username) return UserResponse( diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/services/message_service.py b/PROJECTS/encrypted-p2p-chat/backend/app/services/message_service.py index b49b9062..20cf8fa8 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/services/message_service.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/services/message_service.py @@ -271,7 +271,8 @@ class MessageService: session: AsyncSession, sender_id: UUID, recipient_id: UUID, - plaintext: str + plaintext: str, + room_id: str | None = None, ) -> Any: """ Encrypts message with Double Ratchet and stores in SurrealDB @@ -324,13 +325,19 @@ class MessageService: "previous_chain_length": encrypted_msg.previous_chain_length } + from datetime import UTC, datetime + + now = datetime.now(UTC) surreal_message = { "sender_id": str(sender_id), "recipient_id": str(recipient_id), + "room_id": room_id, "ciphertext": bytes_to_base64url(encrypted_msg.ciphertext), "nonce": bytes_to_base64url(encrypted_msg.nonce), "header": json.dumps(message_header), - "sender_username": sender_user.username + "sender_username": sender_user.username, + "created_at": now.isoformat(), + "updated_at": now.isoformat(), } try: diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/services/presence_service.py b/PROJECTS/encrypted-p2p-chat/backend/app/services/presence_service.py index ccd47ea0..bece4765 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/services/presence_service.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/services/presence_service.py @@ -71,7 +71,7 @@ class PresenceService: try: last_seen = datetime.now(UTC).isoformat() await surreal_db.db.merge( - f"presence:{user_id}", + f"presence:`{user_id}`", { "last_seen": last_seen, "updated_at": "time::now()" @@ -87,7 +87,7 @@ class PresenceService: """ try: await surreal_db.ensure_connected() - result = await surreal_db.db.select(f"presence:{user_id}") + result = await surreal_db.db.select(f"presence:`{user_id}`") if not result: return { diff --git a/PROJECTS/encrypted-p2p-chat/backend/app/services/websocket_service.py b/PROJECTS/encrypted-p2p-chat/backend/app/services/websocket_service.py index 6af1dc54..477cb963 100644 --- a/PROJECTS/encrypted-p2p-chat/backend/app/services/websocket_service.py +++ b/PROJECTS/encrypted-p2p-chat/backend/app/services/websocket_service.py @@ -20,6 +20,7 @@ from app.core.enums import PresenceStatus from app.core.websocket_manager import connection_manager from app.schemas.websocket import ( EncryptedMessageWS, + MessageSentWS, ReadReceiptWS, TypingIndicatorWS, ) @@ -94,44 +95,70 @@ class WebSocketService: """ try: recipient_id = UUID(message.get("recipient_id")) + room_id = message.get("room_id") plaintext = message.get("plaintext") + temp_id = message.get("temp_id", "") if not plaintext: logger.error("Missing plaintext in message from %s", user_id) return + if not room_id: + logger.error("Missing room_id in message from %s", user_id) + return + async with async_session_maker() as session: result = await message_service.send_encrypted_message( session, user_id, recipient_id, - plaintext + plaintext, + room_id, ) ws_message = EncryptedMessageWS( - message_id = result.id if hasattr(result, - 'id') else "unknown", + message_id = result.id if hasattr(result, 'id') else "unknown", sender_id = str(user_id), recipient_id = str(recipient_id), - ciphertext = message.get("ciphertext", - ""), - nonce = message.get("nonce", - ""), - header = message.get("header", - ""), - sender_username = message.get("sender_username", - "") + room_id = room_id, + content = plaintext, + ciphertext = result.ciphertext if hasattr(result, 'ciphertext') else "", + nonce = result.nonce if hasattr(result, 'nonce') else "", + header = result.header if hasattr(result, 'header') else "", + sender_username = result.sender_username if hasattr(result, 'sender_username') else "" + ) + + is_recipient_connected = connection_manager.is_user_connected(recipient_id) + logger.warning( + "Sending to recipient %s - connected: %s", + recipient_id, + is_recipient_connected ) await connection_manager.send_message( recipient_id, ws_message.model_dump(mode = "json") ) + logger.warning("Message sent to recipient %s", recipient_id) + + confirmation = MessageSentWS( + temp_id = temp_id, + message_id = result.id if hasattr(result, 'id') else "unknown", + room_id = room_id, + status = "sent", + created_at = result.created_at if hasattr(result, 'created_at') else datetime.now(UTC) + ) + + await connection_manager.send_message( + user_id, + confirmation.model_dump(mode = "json") + ) logger.info( - "Encrypted message forwarded: %s -> %s", + "Encrypted message forwarded: %s -> %s in room %s", user_id, - recipient_id + recipient_id, + room_id ) except ValueError as e: diff --git a/PROJECTS/encrypted-p2p-chat/docker-compose.dev.yml b/PROJECTS/encrypted-p2p-chat/docker-compose.dev.yml index a4cfc7db..fbf66017 100644 --- a/PROJECTS/encrypted-p2p-chat/docker-compose.dev.yml +++ b/PROJECTS/encrypted-p2p-chat/docker-compose.dev.yml @@ -75,7 +75,10 @@ services: SECRET_KEY: ${SECRET_KEY:?SECRET_KEY required} DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-chat_user}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-chat_auth} SURREAL_URL: ${SURREAL_URL:-ws://surrealdb:8000} + SURREAL_USER: ${SURREAL_USER:-root} SURREAL_PASSWORD: ${SURREAL_PASSWORD} + SURREAL_NAMESPACE: ${SURREAL_NAMESPACE:-chat} + SURREAL_DATABASE: ${SURREAL_DATABASE:-production} REDIS_URL: ${REDIS_URL:-redis://redis:6379} RP_ID: ${RP_ID:-localhost} RP_NAME: ${RP_NAME:-Encrypted P2P Chat} diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/components/Layout/Header.tsx b/PROJECTS/encrypted-p2p-chat/frontend/src/components/Layout/Header.tsx index 19c8f40e..cc7997e6 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/components/Layout/Header.tsx +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/components/Layout/Header.tsx @@ -10,6 +10,7 @@ import { $isAuthenticated, $sidebarOpen, toggleSidebar, + openModal, } from "../../stores" import { IconButton } from "../UI/IconButton" import { Badge } from "../UI/Badge" @@ -70,6 +71,7 @@ export function Header(): JSX.Element { icon={} ariaLabel="Search" size="sm" + onClick={() => openModal("new-conversation")} /> diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/components/Layout/Sidebar.tsx b/PROJECTS/encrypted-p2p-chat/frontend/src/components/Layout/Sidebar.tsx index 1f65ff71..aa92c5d5 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/components/Layout/Sidebar.tsx +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/components/Layout/Sidebar.tsx @@ -13,6 +13,7 @@ import { $activeRoomId, $totalUnreadCount, setActiveRoom, + openModal, } from "../../stores" import { Avatar } from "../UI/Avatar" import { Badge } from "../UI/Badge" @@ -27,7 +28,9 @@ export function Sidebar(): JSX.Element { const roomList = (): Room[] => { const roomsObj = rooms() + console.log("[Sidebar] rooms store:", roomsObj) const roomArray: Room[] = Object.values(roomsObj) + console.log("[Sidebar] roomArray length:", roomArray.length) return roomArray.sort((a, b) => { const aTime = a.last_message?.created_at ?? a.updated_at const bTime = b.last_message?.created_at ?? b.updated_at @@ -59,6 +62,7 @@ export function Sidebar(): JSX.Element { variant="subtle" size="sm" class="w-full justify-start gap-2 px-3" + onClick={() => openModal("new-conversation")} /> diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/components/UI/Toast.tsx b/PROJECTS/encrypted-p2p-chat/frontend/src/components/UI/Toast.tsx index 6ef237c1..c71199a4 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/components/UI/Toast.tsx +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/components/UI/Toast.tsx @@ -18,11 +18,11 @@ const VARIANT_CLASSES: Record = { error: "border-error text-error", } -const VARIANT_ICONS: Record = { - info: , - success: , - warning: , - error: , +const VARIANT_ICONS: Record JSX.Element> = { + info: InfoIcon, + success: CheckIcon, + warning: WarningIcon, + error: ErrorIcon, } export function Toast(props: ToastProps): JSX.Element { @@ -42,7 +42,7 @@ export function Toast(props: ToastProps): JSX.Element { role="alert" > - {VARIANT_ICONS[props.variant]} + {VARIANT_ICONS[props.variant]()}
diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/index.css b/PROJECTS/encrypted-p2p-chat/frontend/src/index.css index fa634558..eb7d2ad2 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/index.css +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/index.css @@ -1,5 +1,5 @@ -@import "tailwindcss"; @import url('https://fonts.googleapis.com/css2?family=Jersey+10&display=swap'); +@import "tailwindcss"; @theme { --color-black: #000000; diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/index.tsx b/PROJECTS/encrypted-p2p-chat/frontend/src/index.tsx index 6ba03a47..25c3bfb6 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/index.tsx +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/index.tsx @@ -2,6 +2,7 @@ import { render } from "solid-js/web"; import { Router } from "@solidjs/router"; import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"; import App from "./App"; +import { ToastContainer } from "./components/UI/Toast"; import "./index.css"; const queryClient = new QueryClient({ @@ -26,6 +27,7 @@ render( + ), root diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/pages/Chat.tsx b/PROJECTS/encrypted-p2p-chat/frontend/src/pages/Chat.tsx index ce41d187..ea277eb5 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/pages/Chat.tsx +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/pages/Chat.tsx @@ -2,7 +2,7 @@ // © AngelaMos | 2025 // Chat.tsx // =================== -import { createSignal, Show, onMount, onCleanup } from "solid-js" +import { Show, onMount, onCleanup, createEffect } from "solid-js" import type { JSX } from "solid-js" import type { Participant } from "../types" import { useStore } from "@nanostores/solid" @@ -16,27 +16,45 @@ import { import { $activeRoom, $activeRoomId, + $activeModal, $userId, + $currentUser, showToast, - addRoom, setActiveRoom, + openModal, + closeModal, + addMessage, } from "../stores" -import { api } from "../lib/api-client" import { connectWebSocket, disconnectWebSocket, wsManager, } from "../websocket" +import { roomService } from "../services" export default function Chat(): JSX.Element { const activeRoom = useStore($activeRoom) const activeRoomId = useStore($activeRoomId) const userId = useStore($userId) - const [showNewChat, setShowNewChat] = createSignal(false) + const activeModal = useStore($activeModal) - onMount(() => { - if (userId()) { + createEffect(() => { + const roomId = activeRoomId() + if (roomId) { + roomService.loadMessages(roomId) + } + }) + + onMount(async () => { + const currentUserId = userId() + console.log("[Chat] onMount - userId:", currentUserId) + if (currentUserId) { + console.log("[Chat] Connecting WebSocket and loading rooms...") connectWebSocket() + await roomService.loadRooms(currentUserId) + console.log("[Chat] Rooms loaded") + } else { + console.log("[Chat] No userId, skipping room load") } }) @@ -47,40 +65,73 @@ export default function Chat(): JSX.Element { const handleSendMessage = (content: string): void => { const roomId = activeRoomId() const room = activeRoom() + const currentUserId = userId() + const user = $currentUser.get() - if (roomId === null || room === null) return + if (roomId === null || room === null) { + showToast("error", "SEND FAILED", "NO ACTIVE ROOM") + return + } - const recipientId = room.participants.find((p: Participant) => p.user_id !== userId())?.user_id + if (currentUserId === null) { + showToast("error", "SEND FAILED", "NOT AUTHENTICATED") + return + } + + const recipientId = room.participants.find((p: Participant) => p.user_id !== currentUserId)?.user_id if (recipientId === undefined) { showToast("error", "SEND FAILED", "NO RECIPIENT FOUND") return } - wsManager.sendEncryptedMessage( + const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2)}` + const now = new Date().toISOString() + + addMessage(roomId, { + id: tempId, + room_id: roomId, + sender_id: currentUserId, + sender_username: user?.username ?? "me", + content, + status: "sending", + is_encrypted: false, + created_at: now, + updated_at: now, + }) + + const sent = wsManager.sendEncryptedMessage( recipientId, roomId, - content + content, + tempId ) + + if (!sent) { + showToast("error", "SEND FAILED", "NOT CONNECTED") + } } const handleCreateRoom = async (targetUserId: string): Promise => { - try { - const room = await api.rooms.create({ - participant_id: targetUserId, - room_type: "direct", - }) + const currentUserId = userId() - addRoom(room) + if (currentUserId === null) { + showToast("error", "FAILED", "NOT AUTHENTICATED") + return + } + + const room = await roomService.createRoom(currentUserId, targetUserId) + + if (room) { setActiveRoom(room.id) - setShowNewChat(false) - } catch { + closeModal() + } else { showToast("error", "FAILED", "COULD NOT CREATE CONVERSATION") } } const handleNewChat = (): void => { - setShowNewChat(true) + openModal("new-conversation") } return ( @@ -111,8 +162,8 @@ export default function Chat(): JSX.Element {
setShowNewChat(false)} + isOpen={activeModal() === "new-conversation"} + onClose={closeModal} onCreateRoom={handleCreateRoom} /> diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/services/index.ts b/PROJECTS/encrypted-p2p-chat/frontend/src/services/index.ts index c6cccf3e..271927aa 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/services/index.ts +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/services/index.ts @@ -3,3 +3,4 @@ // index.ts // =================== export * from "./auth.service" +export * from "./room.service" diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/services/room.service.ts b/PROJECTS/encrypted-p2p-chat/frontend/src/services/room.service.ts new file mode 100644 index 00000000..cad643f3 --- /dev/null +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/services/room.service.ts @@ -0,0 +1,83 @@ +/** + * ©AngelaMos | 2025 + * room.service.ts + */ + +import { api } from "../lib/api-client" +import type { Message, Room } from "../types" +import { + setRooms, + addRoom, + setRoomMessages, + setHasMore, +} from "../stores" + +export async function loadRooms(userId: string): Promise { + console.log("[RoomService] loadRooms called with userId:", userId) + try { + const response = await api.rooms.list(userId) + console.log("[RoomService] API response:", response) + console.log("[RoomService] rooms count:", response.rooms.length) + setRooms(response.rooms) + return response.rooms + } catch (err) { + console.error("[RoomService] Failed to load rooms:", err) + return [] + } +} + +export async function createRoom( + creatorId: string, + participantId: string, + roomType: "direct" | "group" | "ephemeral" = "direct" +): Promise { + try { + const room = await api.rooms.create({ + creator_id: creatorId, + participant_id: participantId, + room_type: roomType, + }) + addRoom(room) + return room + } catch (err) { + console.error("[RoomService] Failed to create room:", err) + return null + } +} + +export async function loadMessages( + roomId: string, + limit: number = 50, + offset: number = 0 +): Promise { + try { + const response = await api.rooms.getMessages(roomId, limit, offset) + const messages: Message[] = response.messages.map((msg) => ({ + id: msg.id, + room_id: msg.room_id, + sender_id: msg.sender_id, + sender_username: msg.sender_username, + content: "[Encrypted message]", + status: "delivered" as const, + is_encrypted: true, + encrypted_content: msg.ciphertext, + nonce: msg.nonce, + header: msg.header, + created_at: msg.created_at, + updated_at: msg.created_at, + })) + const reversedMessages = messages.reverse() + setRoomMessages(roomId, reversedMessages) + setHasMore(roomId, response.has_more) + return reversedMessages + } catch (err) { + console.error("[RoomService] Failed to load messages:", err) + return [] + } +} + +export const roomService = { + loadRooms, + createRoom, + loadMessages, +} diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/stores/auth.store.ts b/PROJECTS/encrypted-p2p-chat/frontend/src/stores/auth.store.ts index 84077c0a..497b8fca 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/stores/auth.store.ts +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/stores/auth.store.ts @@ -16,7 +16,7 @@ const $userIdRaw = persistentAtom("chat:user_id", "", { export const $userId = computed($userIdRaw, (id) => id !== "" ? id : null) -export const $isAuthenticated = computed($currentUser, (user) => user !== null) +export const $isAuthenticated = computed($userId, (id) => id !== null) export const $session = atom(null) diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/stores/ui.store.ts b/PROJECTS/encrypted-p2p-chat/frontend/src/stores/ui.store.ts index 8e9e52e6..5c6e6335 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/stores/ui.store.ts +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/stores/ui.store.ts @@ -79,6 +79,15 @@ export function showToast( description?: string, duration?: number ): string { + const existingToasts = $toasts.get() + const duplicate = existingToasts.find( + (t) => t.title === title && t.description === description && t.variant === variant + ) + + if (duplicate !== undefined) { + return duplicate.id + } + const id = `toast-${++toastIdCounter}` const toast: ToastNotification = { id, @@ -88,7 +97,7 @@ export function showToast( duration: duration ?? 5000, } - $toasts.set([...$toasts.get(), toast]) + $toasts.set([...existingToasts, toast]) const toastDuration = toast.duration ?? 5000 if (toastDuration > 0) { diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/types/guards.ts b/PROJECTS/encrypted-p2p-chat/frontend/src/types/guards.ts index b5070a52..7e871d9c 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/types/guards.ts +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/types/guards.ts @@ -19,6 +19,8 @@ import type { ReadReceiptWS, HeartbeatWS, ErrorMessageWS, + RoomCreatedWS, + MessageSentWS, } from "./index" export function isString(value: unknown): value is string { @@ -141,9 +143,10 @@ export function isEncryptedMessageWS(value: unknown): value is EncryptedMessageW isNonEmptyString(value.sender_id) && isNonEmptyString(value.recipient_id) && isNonEmptyString(value.room_id) && - isNonEmptyString(value.ciphertext) && - isNonEmptyString(value.nonce) && - isNonEmptyString(value.header) && + isString(value.content) && + isString(value.ciphertext) && + isString(value.nonce) && + isString(value.header) && isNonEmptyString(value.sender_username) ) } @@ -199,6 +202,33 @@ export function isErrorMessageWS(value: unknown): value is ErrorMessageWS { ) } +export function isRoomCreatedWS(value: unknown): value is RoomCreatedWS { + if (!isObject(value)) return false + + return ( + value.type === "room_created" && + isNonEmptyString(value.room_id) && + isNonEmptyString(value.room_type) && + isArray(value.participants) && + isBoolean(value.is_encrypted) && + isNonEmptyString(value.created_at) && + isNonEmptyString(value.updated_at) + ) +} + +export function isMessageSentWS(value: unknown): value is MessageSentWS { + if (!isObject(value)) return false + + return ( + value.type === "message_sent" && + isNonEmptyString(value.temp_id) && + isNonEmptyString(value.message_id) && + isNonEmptyString(value.room_id) && + isNonEmptyString(value.status) && + isNonEmptyString(value.created_at) + ) +} + export function isWSMessage(value: unknown): value is WSMessage { return ( isEncryptedMessageWS(value) || @@ -206,7 +236,9 @@ export function isWSMessage(value: unknown): value is WSMessage { isPresenceUpdateWS(value) || isReadReceiptWS(value) || isHeartbeatWS(value) || - isErrorMessageWS(value) + isErrorMessageWS(value) || + isRoomCreatedWS(value) || + isMessageSentWS(value) ) } diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/types/websocket.ts b/PROJECTS/encrypted-p2p-chat/frontend/src/types/websocket.ts index 7d44aad4..ad726621 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/types/websocket.ts +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/types/websocket.ts @@ -11,6 +11,8 @@ export type WSMessageType = | "receipt" | "heartbeat" | "error" + | "room_created" + | "message_sent" export interface BaseWSMessage { type: WSMessageType @@ -23,6 +25,7 @@ export interface EncryptedMessageWS extends BaseWSMessage { sender_id: string recipient_id: string room_id: string + content: string ciphertext: string nonce: string header: string @@ -63,6 +66,32 @@ export interface ErrorMessageWS extends BaseWSMessage { details?: Record } +export interface RoomCreatedWS extends BaseWSMessage { + type: "room_created" + room_id: string + room_type: string + name: string | null + participants: Array<{ + user_id: string + username: string + display_name: string + role: string + joined_at: string + }> + is_encrypted: boolean + created_at: string + updated_at: string +} + +export interface MessageSentWS extends BaseWSMessage { + type: "message_sent" + temp_id: string + message_id: string + room_id: string + status: string + created_at: string +} + export type WSMessage = | EncryptedMessageWS | TypingIndicatorWS @@ -70,12 +99,15 @@ export type WSMessage = | ReadReceiptWS | HeartbeatWS | ErrorMessageWS + | RoomCreatedWS + | MessageSentWS export interface WSOutgoingEncryptedMessage { type: "encrypted_message" recipient_id: string room_id: string plaintext: string + temp_id: string } export interface WSOutgoingTyping { diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/websocket/message-handlers.ts b/PROJECTS/encrypted-p2p-chat/frontend/src/websocket/message-handlers.ts index 5071b6bc..175b44d1 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/websocket/message-handlers.ts +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/websocket/message-handlers.ts @@ -9,7 +9,10 @@ import type { PresenceUpdateWS, ReadReceiptWS, ErrorMessageWS, + RoomCreatedWS, + MessageSentWS, Message, + Room, } from "../types" import { isEncryptedMessageWS, @@ -17,11 +20,17 @@ import { isPresenceUpdateWS, isReadReceiptWS, isErrorMessageWS, + isRoomCreatedWS, + isMessageSentWS, } from "../types/guards" import { addMessage, updateMessageStatus, + confirmPendingMessage, } from "../stores/messages.store" +import { + addRoom, +} from "../stores/rooms.store" import { setUserPresence, } from "../stores/presence.store" @@ -38,7 +47,7 @@ const encryptedMessageHandler: MessageHandler = (message) => room_id: message.room_id, sender_id: message.sender_id, sender_username: message.sender_username, - content: message.ciphertext, + content: message.content, status: "delivered", is_encrypted: true, encrypted_content: message.ciphertext, @@ -76,6 +85,38 @@ const errorMessageHandler: MessageHandler = (message) => { showToast("error", "CONNECTION ERROR", message.error_message.toUpperCase()) } +const roomCreatedHandler: MessageHandler = (message) => { + const room: Room = { + id: message.room_id, + type: message.room_type as "direct" | "group" | "ephemeral", + name: message.name, + participants: message.participants, + unread_count: 0, + is_encrypted: message.is_encrypted, + created_at: message.created_at, + updated_at: message.updated_at, + } + + addRoom(room) + showToast("info", "NEW CHAT", `NEW CONVERSATION STARTED`) +} + +const messageSentHandler: MessageHandler = (message) => { + const confirmedMessage: Message = { + id: message.message_id, + room_id: message.room_id, + sender_id: "", + sender_username: "", + content: "", + status: "sent", + is_encrypted: true, + created_at: message.created_at, + updated_at: message.created_at, + } + + confirmPendingMessage(message.room_id, message.temp_id, confirmedMessage) +} + export function handleWSMessage(message: WSMessage): void { if (isEncryptedMessageWS(message)) { encryptedMessageHandler(message) @@ -87,6 +128,10 @@ export function handleWSMessage(message: WSMessage): void { readReceiptHandler(message) } else if (isErrorMessageWS(message)) { errorMessageHandler(message) + } else if (isRoomCreatedWS(message)) { + roomCreatedHandler(message) + } else if (isMessageSentWS(message)) { + messageSentHandler(message) } } diff --git a/PROJECTS/encrypted-p2p-chat/frontend/src/websocket/websocket-manager.ts b/PROJECTS/encrypted-p2p-chat/frontend/src/websocket/websocket-manager.ts index 4e734822..650ca17d 100644 --- a/PROJECTS/encrypted-p2p-chat/frontend/src/websocket/websocket-manager.ts +++ b/PROJECTS/encrypted-p2p-chat/frontend/src/websocket/websocket-manager.ts @@ -27,6 +27,8 @@ import { isReadReceiptWS, isHeartbeatWS, isErrorMessageWS, + isRoomCreatedWS, + isMessageSentWS, } from "../types/guards" export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "reconnecting" @@ -42,6 +44,7 @@ export const $isConnected = computed( const MAX_RECONNECT_ATTEMPTS = 10 const MAX_RECONNECT_DELAY = 30000 +const FATAL_ERROR_CODES = ["max_connections", "unauthorized", "invalid_user", "database_error"] class WebSocketManager { private ws: WebSocket | null = null @@ -49,6 +52,7 @@ class WebSocketManager { private reconnectTimeout: ReturnType | null = null private messageQueue: WSOutgoingMessage[] = [] private intentionalClose = false + private fatalError = false connect(): void { const userId = $userId.get() @@ -61,6 +65,10 @@ class WebSocketManager { return } + if (this.fatalError) { + return + } + this.intentionalClose = false $connectionStatus.set("connecting") $lastError.set(null) @@ -76,6 +84,7 @@ class WebSocketManager { disconnect(): void { this.intentionalClose = true + this.fatalError = false this.cleanup() $connectionStatus.set("disconnected") $reconnectAttempts.set(0) @@ -99,13 +108,15 @@ class WebSocketManager { sendEncryptedMessage( recipientId: string, roomId: string, - plaintext: string + plaintext: string, + tempId: string ): boolean { const message: WSOutgoingEncryptedMessage = { type: "encrypted_message", recipient_id: recipientId, room_id: roomId, plaintext, + temp_id: tempId, } return this.send(message) } @@ -176,6 +187,14 @@ class WebSocketManager { } else if (isHeartbeatWS(message)) { return } else if (isErrorMessageWS(message)) { + if (FATAL_ERROR_CODES.includes(message.error_code)) { + this.fatalError = true + $lastError.set(message.error_message) + } + handleWSMessage(message) + } else if (isRoomCreatedWS(message)) { + handleWSMessage(message) + } else if (isMessageSentWS(message)) { handleWSMessage(message) } } @@ -188,6 +207,11 @@ class WebSocketManager { return } + if (this.fatalError) { + $connectionStatus.set("disconnected") + return + } + const attempts = $reconnectAttempts.get() if (attempts >= MAX_RECONNECT_ATTEMPTS) {