diff --git a/app/max_client.py b/app/max_client.py
index c2e0d93..485ed9e 100644
--- a/app/max_client.py
+++ b/app/max_client.py
@@ -315,7 +315,11 @@ async def _handle(self, data: dict):
await self._send(
OpCode.AUTH_SNAPSHOT,
{
- "chatsCount": 10,
+ "chatsCount": 40,
+ "chatsSync": 0,
+ "contactsSync": 0,
+ "presenceSync": 0,
+ "draftsSync": 0,
"interactive": True,
"token": self.token,
},
@@ -338,13 +342,11 @@ async def _handle(self, data: dict):
elif op == OpCode.NOTIF_CHAT:
if self.mute_tracker:
- chat = payload.get("chat")
- if isinstance(chat, dict):
- self.mute_tracker.update_chat(chat)
+ self.mute_tracker.update_from_payload(payload)
elif op in (OpCode.NOTIF_CONFIG, OpCode.CONFIG):
if self.mute_tracker:
- self.mute_tracker.on_settings(payload)
+ self.mute_tracker.update_from_payload(payload)
elif op == OpCode.DISPATCH:
self._dispatch_counter += 1
diff --git a/app/max_listener.py b/app/max_listener.py
index 198520a..b4247ef 100644
--- a/app/max_listener.py
+++ b/app/max_listener.py
@@ -252,6 +252,13 @@ def _human_size(n: int) -> str:
return f"{n:.1f} ТБ"
+def _startup_message(chat_count: int, muted_count: int | None = None) -> str:
+ text = f"✅ Max: подключён | чатов: {chat_count}"
+ if muted_count is not None:
+ text += f" | 🔇 из них без звука: {muted_count}"
+ return text
+
+
def create_max_client(
max_token: str, max_device_id: str, sender: TelegramSender, max_chat_ids: str | None = None,
debug: bool = False, reply_enabled: bool = False,
@@ -288,8 +295,7 @@ async def handle_ready(snapshot: dict):
log.info("Unread-only mode: read marks loaded from snapshot")
if client.mute_tracker:
- client.mute_tracker.load_from_chats(snapshot.get("chats", []))
- log.info("Skip-muted mode: mute state loaded from snapshot")
+ client.mute_tracker.load_from_snapshot(snapshot)
if participant_ids:
log.info("Batch-resolving %d participants...", len(participant_ids))
@@ -311,7 +317,10 @@ async def handle_ready(snapshot: dict):
await sender.send("✅ Max: соединение восстановлено")
else:
chat_count = len(resolver.chats)
- await sender.send(f"✅ Max: подключён | чатов: {chat_count}")
+ muted_count = None
+ if client.skip_muted and client.mute_tracker:
+ muted_count = client.mute_tracker.muted_count()
+ await sender.send(_startup_message(chat_count, muted_count))
_first_connect = False
@client.on_disconnect
diff --git a/app/mute_tracker.py b/app/mute_tracker.py
index 4775b50..94da3fc 100644
--- a/app/mute_tracker.py
+++ b/app/mute_tracker.py
@@ -43,8 +43,37 @@ def load_from_chats(self, chats: list) -> None:
if isinstance(chat, dict) and chat.get("id") is not None:
self.update_chat(chat)
+ def load_from_snapshot(self, snapshot: dict) -> None:
+ """Load mute state from AUTH_SNAPSHOT (chats list + settings/config maps)."""
+ self.load_from_chats(snapshot.get("chats", []))
+ self.on_settings(snapshot)
+ config = snapshot.get("config")
+ if isinstance(config, dict):
+ self.on_settings(config)
+ log.info(
+ "Mute state loaded from snapshot: %d muted chat(s)",
+ len(self._permanent),
+ )
+
+ def muted_count(self) -> int:
+ return len(self._permanent)
+
+ def update_from_payload(self, payload: dict) -> None:
+ """Apply mute updates from NOTIF_CHAT / NOTIF_CONFIG payloads."""
+ if not isinstance(payload, dict):
+ return
+ chat = payload.get("chat")
+ if isinstance(chat, dict):
+ self.update_chat(chat)
+ return
+ if payload.get("id") is not None or payload.get("chatId") is not None:
+ self.update_chat(payload)
+ return
+ if payload.get("settings") is not None or payload.get("chats") is not None:
+ self.on_settings(payload)
+
def update_chat(self, chat: dict) -> None:
- chat_id = _normalize_chat_id(chat.get("id"))
+ chat_id = _normalize_chat_id(chat.get("id") or chat.get("chatId"))
if chat_id is None:
return
ddu = _extract_dont_disturb_until(chat)
diff --git a/tests/test_mute_tracker.py b/tests/test_mute_tracker.py
index 1b1afb3..96f455a 100644
--- a/tests/test_mute_tracker.py
+++ b/tests/test_mute_tracker.py
@@ -65,3 +65,49 @@ def test_notif_chat_update_unmutes(self):
mt.update_chat({"id": 5, "dontDisturbUntil": -1})
mt.update_chat({"id": 5, "dontDisturbUntil": 0})
assert mt.is_muted(5) is False
+
+ def test_load_from_snapshot_settings_chats(self):
+ mt = MuteTracker()
+ mt.load_from_snapshot({
+ "chats": [
+ {"id": 10, "title": "Visible"},
+ ],
+ "settings": {
+ "chats": {
+ "-68093732121255": {"dontDisturbUntil": -1},
+ "10": {"dontDisturbUntil": 0},
+ }
+ },
+ })
+ assert mt.is_muted(-68093732121255) is True
+ assert mt.is_muted(10) is False
+
+ def test_update_from_payload_nested_chat(self):
+ mt = MuteTracker()
+ mt.update_from_payload({
+ "chat": {"id": 99, "dontDisturbUntil": -1},
+ })
+ assert mt.is_muted(99) is True
+
+ def test_update_from_payload_settings(self):
+ mt = MuteTracker()
+ mt.update_from_payload({
+ "settings": {
+ "chats": {"42": {"dontDisturbUntil": -1}},
+ },
+ })
+ assert mt.is_muted(42) is True
+
+ def test_update_from_payload_chat_id_top_level(self):
+ mt = MuteTracker()
+ mt.update_from_payload({
+ "chatId": -123,
+ "dontDisturbUntil": -1,
+ })
+ assert mt.is_muted(-123) is True
+
+ def test_muted_count(self):
+ mt = MuteTracker()
+ assert mt.muted_count() == 0
+ mt.update_chat({"id": 1, "dontDisturbUntil": -1})
+ assert mt.muted_count() == 1
diff --git a/tests/test_skip_muted_listener.py b/tests/test_skip_muted_listener.py
new file mode 100644
index 0000000..325d8aa
--- /dev/null
+++ b/tests/test_skip_muted_listener.py
@@ -0,0 +1,115 @@
+"""Tests for skip-muted filtering in app/max_listener.py."""
+
+from unittest.mock import AsyncMock
+
+import pytest
+
+from app.max_client import MaxMessage
+from app.max_listener import _startup_message, create_max_client
+
+
+class TestStartupMessage:
+ def test_without_muted_count(self):
+ assert _startup_message(32) == "✅ Max: подключён | чатов: 32"
+
+ def test_with_muted_count(self):
+ text = _startup_message(32, 7)
+ assert "чатов: 32" in text
+ assert "🔇 из них без звука: 7" in text
+
+
+@pytest.fixture
+def muted_client():
+ sender = AsyncMock()
+ client = create_max_client(
+ max_token="tok",
+ max_device_id="dev",
+ sender=sender,
+ skip_muted=True,
+ )
+ return client, sender
+
+
+class TestSkipMutedListener:
+ async def test_muted_chat_message_not_forwarded(self, muted_client):
+ client, sender = muted_client
+ await client._on_ready_cb({
+ "profile": {"id": 1, "names": []},
+ "chats": [],
+ "settings": {
+ "chats": {
+ "100": {"dontDisturbUntil": -1},
+ }
+ },
+ })
+ sender.send.reset_mock()
+
+ await client._on_message_cb(
+ MaxMessage(chat_id=100, sender_id=2, text="hello")
+ )
+ sender.send.assert_not_called()
+
+ async def test_unmuted_chat_message_forwarded(self, muted_client):
+ client, sender = muted_client
+ await client._on_ready_cb({
+ "profile": {"id": 1, "names": []},
+ "chats": [{"id": 200, "type": "DIALOG", "participants": {"1": {}, "2": {}}}],
+ "settings": {"chats": {}},
+ })
+ sender.send.reset_mock()
+
+ await client._on_message_cb(
+ MaxMessage(chat_id=200, sender_id=2, text="hello")
+ )
+ sender.send.assert_called()
+
+ async def test_skip_muted_disabled_forwards_muted(self):
+ sender = AsyncMock()
+ client = create_max_client(
+ max_token="tok",
+ max_device_id="dev",
+ sender=sender,
+ skip_muted=False,
+ )
+ await client._on_ready_cb({
+ "profile": {"id": 1, "names": []},
+ "chats": [{"id": 100, "dontDisturbUntil": -1, "type": "DIALOG", "participants": {}}],
+ })
+ sender.send.reset_mock()
+
+ await client._on_message_cb(
+ MaxMessage(chat_id=100, sender_id=2, text="hello")
+ )
+ sender.send.assert_called()
+
+ async def test_startup_shows_muted_count(self, muted_client):
+ client, sender = muted_client
+ await client._on_ready_cb({
+ "profile": {"id": 1, "names": []},
+ "chats": [{"id": 100, "type": "GROUP", "title": "Test", "participants": {}}],
+ "settings": {
+ "chats": {
+ "100": {"dontDisturbUntil": -1},
+ "200": {"dontDisturbUntil": -1},
+ }
+ },
+ })
+ text = sender.send.call_args[0][0]
+ assert "чатов: 1" in text
+ assert "🔇 из них без звука: 2" in text
+
+ async def test_startup_shows_muted_count(self, muted_client):
+ client, sender = muted_client
+ await client._on_ready_cb({
+ "profile": {"id": 1, "names": []},
+ "chats": [{"id": 100, "type": "GROUP", "title": "Test", "participants": {}}],
+ "settings": {
+ "chats": {
+ "100": {"dontDisturbUntil": -1},
+ "200": {"dontDisturbUntil": -1},
+ }
+ },
+ })
+ text = sender.send.call_args[0][0]
+ assert "чатов: 1" in text
+ assert "🔇 из них без звука: 2" in text