From f03887aa00bcb49619d2685216847bd3d1bcbbb2 Mon Sep 17 00:00:00 2001 From: lizerk Date: Thu, 2 Apr 2026 10:05:20 +0300 Subject: [PATCH] Suppress chat notifications for active websocket session --- backend/realtime/consumers.py | 48 +++++++++++++- .../tests/realtime/test_websocket_events.py | 66 +++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/backend/realtime/consumers.py b/backend/realtime/consumers.py index b6802ad8..0f545cc4 100644 --- a/backend/realtime/consumers.py +++ b/backend/realtime/consumers.py @@ -186,6 +186,30 @@ class UserConsumer(ChatConsumerMixin, AsyncJsonWebsocketConsumer): elif action == "vote_poll": await self._handle_vote_poll(content) + @staticmethod + def _extract_notification_chat_id(notification): + """Достаёт chat_id из payload уведомления, если он есть.""" + if not isinstance(notification, dict): + return None + + data = notification.get("data") + if not isinstance(data, dict): + return None + + chat_id = data.get("chat_id") + try: + return int(chat_id) + except (TypeError, ValueError): + return None + + def _should_suppress_active_chat_notification(self, notification): + """Не показываем уведомление о сообщении в уже открытом чате.""" + if self.active_chat_id is None: + return False + + notification_chat_id = self._extract_notification_chat_id(notification) + return notification_chat_id == self.active_chat_id + # ==================== Обработчики уведомлений ==================== async def notification_message(self, event): @@ -193,16 +217,36 @@ class UserConsumer(ChatConsumerMixin, AsyncJsonWebsocketConsumer): Новое уведомление от WebSocketNotificationSender. Вызывается из notifications/senders/websocket.py """ + notification = event.get("message", {}) + + if self._should_suppress_active_chat_notification(notification): + logger.info( + "[UserWS] Suppressed notification for active chat %s in session %s", + self.active_chat_id, + self.channel_name, + ) + return + await self.send_json( - {"type": "notification", "notification": event.get("message", {})} + {"type": "notification", "notification": notification} ) async def notification_new(self, event): """Новое уведомление (вызывается из notifications/services.py)""" + notification = event.get("notification", {}) + + if self._should_suppress_active_chat_notification(notification): + logger.info( + "[UserWS] Suppressed notification_new for active chat %s in session %s", + self.active_chat_id, + self.channel_name, + ) + return + await self.send_json( { "type": "notification", - "notification": event.get("notification", {}), + "notification": notification, } ) diff --git a/backend/tests/realtime/test_websocket_events.py b/backend/tests/realtime/test_websocket_events.py index 088cb3d3..c4deb3f0 100644 --- a/backend/tests/realtime/test_websocket_events.py +++ b/backend/tests/realtime/test_websocket_events.py @@ -558,6 +558,72 @@ class TestChatMarkedReadEvent: class TestNotificationStateEvents: """Тесты синхронизации состояния уведомлений через user channel.""" + @pytest.mark.asyncio + async def test_active_chat_session_does_not_receive_own_chat_notification( + self, + ws_communicator, + user, + test_chat, + ): + """Уведомление о сообщении не должно приходить в сессию с открытым этим чатом.""" + import asyncio + + comm_active = await ws_communicator(user=user) + comm_background = await ws_communicator(user=user) + + connected_active, _ = await comm_active.connect() + connected_background, _ = await comm_background.connect() + assert connected_active and connected_background + + await comm_active.send_json_to( + { + "action": "open_chat", + "chat_id": test_chat.id, + "load_history": False, + } + ) + await comm_active.receive_json_from(timeout=5) + + channel_layer = get_channel_layer() + payload = { + "type": "notification", + "id": 777, + "verb": "chat_new_message", + "description": "New message in active chat", + "action_url": f"/messages/{test_chat.id}", + "timestamp": "2026-04-02T10:00:00Z", + "unread": True, + "data": { + "chat_id": test_chat.id, + "message_id": 123, + }, + } + + await channel_layer.group_send( + f"user_{user.id}", + { + "type": "notification_message", + "message": payload, + } + ) + + with pytest.raises(asyncio.TimeoutError): + await comm_active.receive_json_from(timeout=2) + + response = await comm_background.receive_json_from(timeout=5) + assert response["type"] == "notification" + assert response["notification"]["id"] == 777 + assert response["notification"]["data"]["chat_id"] == test_chat.id + + try: + await comm_background.disconnect() + except asyncio.CancelledError: + pass + try: + await comm_active.disconnect() + except asyncio.CancelledError: + pass + @pytest.mark.asyncio async def test_notification_read_sync_between_sessions(self, ws_communicator, user): comm1 = await ws_communicator(user=user)