Skip to content

Commit 0c0a1c7

Browse files
committed
feat(poke,connect): add poke stats, rename constant, fix message cleanup
- Rename CHAT_STYLES_DIALOGS_LIMIT to ACTIVE_CHATS_LIMIT in config.py, styles_handler.py, poke_handler.py - Remove poke_scanning message; add poke_result/poke_result_none with {checked}/{drafts} placeholders - Add checked/drafts counters to on_poke; send result message after scan loop - Move checked increment before draft/lock check so chats with existing drafts are still counted - Add keep_typing to on_connect_qr_callback for typing indicator during QR generation - Fix untracked connect_phone_invalid message in on_confirm_phone_callback PhoneNumberInvalid handler - Update tests: test_poke.py (side_effect for get_system_message, result assertions), test_connect_flow.py (expect error msg ID in sensitive_msg_ids)
1 parent 2643553 commit 0c0a1c7

File tree

7 files changed

+74
-31
lines changed

7 files changed

+74
-31
lines changed

config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def style_display_name(style: str) -> str:
154154
}
155155

156156
# Количество чатов в /styles для отображения
157-
CHAT_STYLES_DIALOGS_LIMIT = 16
157+
ACTIVE_CHATS_LIMIT = 16
158158

159159
# ====== ЧАСОВОЙ ПОЯС ======
160160
# 30 популярных UTC-смещений (часы); дробные: +3.5 Иран, +4.5 Афганистан,

handlers/connect_handler.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,10 @@ async def on_connect_qr_callback(update: Update, context: ContextTypes.DEFAULT_T
357357
await query.edit_message_text(msg)
358358
return
359359

360-
await _start_qr_flow(
361-
u.id, u.language_code, context.bot, update.effective_chat.id,
362-
)
360+
async with keep_typing(context.bot, update.effective_chat.id):
361+
await _start_qr_flow(
362+
u.id, u.language_code, context.bot, update.effective_chat.id,
363+
)
363364

364365

365366
# ====== Phone flow text handlers ======
@@ -533,14 +534,15 @@ async def on_confirm_phone_callback(update: Update, context: ContextTypes.DEFAUL
533534

534535
if "PhoneNumberInvalid" in error_name:
535536
# Возвращаем в awaiting_phone — пользователь может ввести повторно
537+
msg = await get_system_message(language_code, "connect_phone_invalid")
538+
sent_err = await context.bot.send_message(chat_id=chat_id, text=msg)
539+
sensitive_msg_ids.append(sent_err.message_id)
536540
_put_pending_phone(u.id, {
537541
"state": "awaiting_phone",
538542
"language_code": language_code,
539543
"chat_id": chat_id,
540-
"sensitive_msg_ids": pending.get("sensitive_msg_ids") or [],
544+
"sensitive_msg_ids": sensitive_msg_ids,
541545
})
542-
msg = await get_system_message(language_code, "connect_phone_invalid")
543-
await context.bot.send_message(chat_id=chat_id, text=msg)
544546
if client is not None:
545547
await _safe_disconnect_temp_client(client, u.id)
546548
return

handlers/poke_handler.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from telegram.ext import ContextTypes
88

99
from config import (
10-
CHAT_STYLES_DIALOGS_LIMIT,
10+
ACTIVE_CHATS_LIMIT,
1111
DEBUG_PRINT,
1212
IGNORED_CHAT_IDS,
1313
POKE_FOLLOW_UP_TIMEOUT,
@@ -51,21 +51,19 @@ async def on_poke(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
5151
await update.message.reply_text(msg)
5252
return
5353

54-
# Одно сообщение — «проверяю»
55-
msg = await get_system_message(language_code, "poke_scanning")
56-
await update.message.reply_text(msg)
57-
5854
if DEBUG_PRINT:
5955
print(f"{get_timestamp()} [POKE] /poke from user {u.id}")
6056

6157
# Получаем список чатов
62-
chat_ids = await pyrogram_client.get_private_dialogs(u.id, limit=CHAT_STYLES_DIALOGS_LIMIT)
58+
chat_ids = await pyrogram_client.get_private_dialogs(u.id, limit=ACTIVE_CHATS_LIMIT)
6359

6460
user = await get_user(u.id)
6561
user_settings = (user or {}).get("settings") or {}
6662
lang = (user or {}).get("language_code")
6763

6864
now = datetime.now(tz=timezone.utc)
65+
checked = 0
66+
drafts = 0
6967

7068
for chat_id in chat_ids:
7169
# Global ignore
@@ -78,10 +76,13 @@ async def on_poke(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
7876

7977
key = (u.id, chat_id)
8078

79+
checked += 1
80+
8181
# Уже есть черновик или идёт генерация — пропускаем
8282
if _reply_locks.get(key) or _bot_drafts.get(key):
8383
continue
8484

85+
# Пустой чат или ошибка чтения — нечего анализировать
8586
last_msg = await pyrogram_client.get_last_message(u.id, chat_id)
8687
if not last_msg:
8788
continue
@@ -114,10 +115,16 @@ async def on_poke(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
114115
_bot_draft_echoes[key] = probe_text
115116
await pyrogram_client.set_draft(u.id, chat_id, probe_text)
116117

118+
drafts += 1
117119
asyncio.create_task(
118120
_generate_reply_for_chat(u.id, chat_id, user, user_settings, lang)
119121
)
120122

121123
if DEBUG_PRINT:
122124
direction = "follow-up" if last_msg.outgoing else "unanswered"
123125
print(f"{get_timestamp()} [POKE] Generating {direction} draft for user {u.id} in chat {chat_id}")
126+
127+
# Итоговое сообщение с конкретными цифрами
128+
result_key = "poke_result" if drafts else "poke_result_none"
129+
result_msg = await get_system_message(language_code, result_key)
130+
await update.message.reply_text(result_msg.format(checked=checked, drafts=drafts))

handlers/styles_handler.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from clients import pyrogram_client
99
from config import (
1010
AUTO_REPLY_OPTIONS,
11-
CHAT_STYLES_DIALOGS_LIMIT,
11+
ACTIVE_CHATS_LIMIT,
1212
DEBUG_PRINT,
1313
DEFAULT_STYLE,
1414
STYLE_OPTIONS,
@@ -112,7 +112,7 @@ def _get_relevant_dialogs(
112112
dialogs = [d for d in all_dialogs if d["chat_id"] in relevant_ids]
113113
# Чаты с per-chat auto-reply/ignore — сверху
114114
dialogs.sort(key=lambda d: d["chat_id"] not in ar_ids)
115-
return dialogs[:CHAT_STYLES_DIALOGS_LIMIT]
115+
return dialogs[:ACTIVE_CHATS_LIMIT]
116116

117117

118118
@serialize_user_updates
@@ -137,7 +137,7 @@ async def on_chats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
137137

138138
# Получаем широкий список диалогов для фильтрации
139139
all_dialogs = await pyrogram_client.get_dialog_info(
140-
u.id, limit=CHAT_STYLES_DIALOGS_LIMIT * 10,
140+
u.id, limit=ACTIVE_CHATS_LIMIT * 10,
141141
)
142142

143143
dialogs = _get_relevant_dialogs(all_dialogs, user_settings, u.id)
@@ -167,7 +167,7 @@ async def _refresh_keyboard(
167167
dialogs = context.user_data.get("chats_dialogs") or []
168168
if not dialogs:
169169
all_dialogs = await pyrogram_client.get_dialog_info(
170-
u.id, limit=CHAT_STYLES_DIALOGS_LIMIT * 10,
170+
u.id, limit=ACTIVE_CHATS_LIMIT * 10,
171171
)
172172
dialogs = _get_relevant_dialogs(all_dialogs, updated_settings, u.id)
173173
else:

system_messages.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@
133133
"prompt_clear": "🗑 Clear",
134134

135135
# — Poke (/poke) —
136-
"poke_scanning": "🔍 Started checking active chats…",
136+
"poke_result": "✅ Checked {checked} chats — generating {drafts} drafts.",
137+
"poke_result_none": "✅ Checked {checked} chats — no drafts needed right now.",
137138
"menu_poke": "Poke active chats",
138139
}
139140

tests/test_connect_flow.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,10 @@ class PhoneNumberInvalid(Exception):
266266
await on_confirm_phone_callback(update, context)
267267

268268
assert _pending_phone[user_id]["state"] == "awaiting_phone"
269-
assert _pending_phone[user_id]["sensitive_msg_ids"] == [42]
269+
# sensitive_msg_ids: оригинальный (42) + ошибка PhoneNumberInvalid
270+
ids = _pending_phone[user_id]["sensitive_msg_ids"]
271+
assert 42 in ids
272+
assert len(ids) == 2
270273

271274
@pytest.mark.asyncio
272275
async def test_floodwait_deletes_sensitive_messages(self):

tests/test_poke.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ def _make_message(outgoing: bool = False, age_seconds: int = 0, from_bot: bool =
3636
return msg
3737

3838

39+
def _sys_msg_side_effect(*args, **kwargs):
40+
"""Side-effect для get_system_message: возвращает разные строки по ключу."""
41+
key = args[1] if len(args) > 1 else kwargs.get("key", "")
42+
mapping = {
43+
"status_disconnected": "Connect first",
44+
"poke_result": "Checked {checked} chats — generating {drafts} drafts.",
45+
"poke_result_none": "Checked {checked} chats — no drafts needed.",
46+
"draft_typing": "{emoji} is typing...",
47+
}
48+
return mapping.get(key, key)
49+
50+
3951
class TestOnPoke:
4052
"""Тесты для on_poke()."""
4153

@@ -48,7 +60,7 @@ async def test_not_connected_shows_message(self):
4860
with patch("handlers.poke_handler.pyrogram_client") as mock_pc, \
4961
patch("handlers.poke_handler.ensure_effective_user", new_callable=AsyncMock), \
5062
patch("handlers.poke_handler.update_last_msg_at", new_callable=AsyncMock), \
51-
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, return_value="Connect first"):
63+
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, side_effect=_sys_msg_side_effect):
5264
mock_pc.is_active = MagicMock(return_value=False)
5365

5466
await on_poke(update, context)
@@ -57,7 +69,7 @@ async def test_not_connected_shows_message(self):
5769

5870
@pytest.mark.asyncio
5971
async def test_unanswered_incoming_generates_draft(self):
60-
"""Входящее сообщение без черновика → генерация."""
72+
"""Входящее сообщение без черновика → генерация + результат с drafts=1."""
6173
user_id = 123
6274
chat_id = 456
6375
update = _make_update(user_id=user_id)
@@ -68,7 +80,7 @@ async def test_unanswered_incoming_generates_draft(self):
6880
with patch("handlers.poke_handler.pyrogram_client") as mock_pc, \
6981
patch("handlers.poke_handler.ensure_effective_user", new_callable=AsyncMock), \
7082
patch("handlers.poke_handler.update_last_msg_at", new_callable=AsyncMock), \
71-
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, return_value="Scanning"), \
83+
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, side_effect=_sys_msg_side_effect), \
7284
patch("handlers.poke_handler.get_user", new_callable=AsyncMock, return_value={"settings": {}}), \
7385
patch("handlers.poke_handler._is_user_typing", new_callable=AsyncMock, return_value=False), \
7486
patch("handlers.poke_handler._generate_reply_for_chat", new_callable=AsyncMock) as mock_gen:
@@ -83,10 +95,15 @@ async def test_unanswered_incoming_generates_draft(self):
8395
await on_poke(update, context)
8496

8597
mock_gen.assert_called_once_with(user_id, chat_id, {"settings": {}}, {}, None)
98+
# result only
99+
update.message.reply_text.assert_called_once()
100+
result_call = update.message.reply_text.call_args_list[-1]
101+
assert "1 chats" in result_call.args[0]
102+
assert "1 drafts" in result_call.args[0]
86103

87104
@pytest.mark.asyncio
88105
async def test_incoming_with_existing_draft_skipped(self):
89-
"""Входящее с существующим черновиком → пропуск."""
106+
"""Входящее с существующим черновиком → пропуск, drafts=0."""
90107
user_id = 123
91108
chat_id = 456
92109
update = _make_update(user_id=user_id)
@@ -95,7 +112,7 @@ async def test_incoming_with_existing_draft_skipped(self):
95112
with patch("handlers.poke_handler.pyrogram_client") as mock_pc, \
96113
patch("handlers.poke_handler.ensure_effective_user", new_callable=AsyncMock), \
97114
patch("handlers.poke_handler.update_last_msg_at", new_callable=AsyncMock), \
98-
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, return_value="Scanning"), \
115+
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, side_effect=_sys_msg_side_effect), \
99116
patch("handlers.poke_handler.get_user", new_callable=AsyncMock, return_value={"settings": {}}), \
100117
patch("handlers.poke_handler._is_user_typing", new_callable=AsyncMock, return_value=False), \
101118
patch("handlers.poke_handler._generate_reply_for_chat", new_callable=AsyncMock) as mock_gen:
@@ -107,11 +124,15 @@ async def test_incoming_with_existing_draft_skipped(self):
107124
await on_poke(update, context)
108125

109126
mock_gen.assert_not_called()
127+
# result_none only
128+
update.message.reply_text.assert_called_once()
129+
result_call = update.message.reply_text.call_args_list[-1]
130+
assert "no drafts needed" in result_call.args[0]
110131
_bot_drafts.pop((user_id, chat_id), None)
111132

112133
@pytest.mark.asyncio
113134
async def test_outgoing_fresh_skipped(self):
114-
"""Исходящее свежее (< 12ч) → пропуск."""
135+
"""Исходящее свежее (< 12ч) → пропуск, drafts=0."""
115136
user_id = 123
116137
chat_id = 456
117138
update = _make_update(user_id=user_id)
@@ -122,7 +143,7 @@ async def test_outgoing_fresh_skipped(self):
122143
with patch("handlers.poke_handler.pyrogram_client") as mock_pc, \
123144
patch("handlers.poke_handler.ensure_effective_user", new_callable=AsyncMock), \
124145
patch("handlers.poke_handler.update_last_msg_at", new_callable=AsyncMock), \
125-
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, return_value="Scanning"), \
146+
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, side_effect=_sys_msg_side_effect), \
126147
patch("handlers.poke_handler.get_user", new_callable=AsyncMock, return_value={"settings": {}}), \
127148
patch("handlers.poke_handler._is_user_typing", new_callable=AsyncMock, return_value=False), \
128149
patch("handlers.poke_handler._generate_reply_for_chat", new_callable=AsyncMock) as mock_gen:
@@ -136,10 +157,15 @@ async def test_outgoing_fresh_skipped(self):
136157
await on_poke(update, context)
137158

138159
mock_gen.assert_not_called()
160+
# result_none only (checked=1, drafts=0)
161+
update.message.reply_text.assert_called_once()
162+
result_call = update.message.reply_text.call_args_list[-1]
163+
assert "1 chats" in result_call.args[0]
164+
assert "no drafts needed" in result_call.args[0]
139165

140166
@pytest.mark.asyncio
141167
async def test_outgoing_old_generates_followup(self):
142-
"""Исходящее старое (> 12ч) → follow-up генерация."""
168+
"""Исходящее старое (> 12ч) → follow-up генерация, drafts=1."""
143169
user_id = 123
144170
chat_id = 456
145171
update = _make_update(user_id=user_id)
@@ -150,7 +176,7 @@ async def test_outgoing_old_generates_followup(self):
150176
with patch("handlers.poke_handler.pyrogram_client") as mock_pc, \
151177
patch("handlers.poke_handler.ensure_effective_user", new_callable=AsyncMock), \
152178
patch("handlers.poke_handler.update_last_msg_at", new_callable=AsyncMock), \
153-
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, return_value="Scanning"), \
179+
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, side_effect=_sys_msg_side_effect), \
154180
patch("handlers.poke_handler.get_user", new_callable=AsyncMock, return_value={"settings": {}}), \
155181
patch("handlers.poke_handler._is_user_typing", new_callable=AsyncMock, return_value=False), \
156182
patch("handlers.poke_handler._generate_reply_for_chat", new_callable=AsyncMock) as mock_gen:
@@ -165,6 +191,10 @@ async def test_outgoing_old_generates_followup(self):
165191
await on_poke(update, context)
166192

167193
mock_gen.assert_called_once()
194+
# result only
195+
update.message.reply_text.assert_called_once()
196+
result_call = update.message.reply_text.call_args_list[-1]
197+
assert "1 drafts" in result_call.args[0]
168198

169199
@pytest.mark.asyncio
170200
async def test_ignored_chat_skipped(self):
@@ -179,7 +209,7 @@ async def test_ignored_chat_skipped(self):
179209
with patch("handlers.poke_handler.pyrogram_client") as mock_pc, \
180210
patch("handlers.poke_handler.ensure_effective_user", new_callable=AsyncMock), \
181211
patch("handlers.poke_handler.update_last_msg_at", new_callable=AsyncMock), \
182-
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, return_value="Scanning"), \
212+
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, side_effect=_sys_msg_side_effect), \
183213
patch("handlers.poke_handler.get_user", new_callable=AsyncMock, return_value={"settings": settings}), \
184214
patch("handlers.poke_handler._is_user_typing", new_callable=AsyncMock, return_value=False), \
185215
patch("handlers.poke_handler._generate_reply_for_chat", new_callable=AsyncMock) as mock_gen:
@@ -203,7 +233,7 @@ async def test_bot_message_skipped(self):
203233
with patch("handlers.poke_handler.pyrogram_client") as mock_pc, \
204234
patch("handlers.poke_handler.ensure_effective_user", new_callable=AsyncMock), \
205235
patch("handlers.poke_handler.update_last_msg_at", new_callable=AsyncMock), \
206-
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, return_value="Scanning"), \
236+
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, side_effect=_sys_msg_side_effect), \
207237
patch("handlers.poke_handler.get_user", new_callable=AsyncMock, return_value={"settings": {}}), \
208238
patch("handlers.poke_handler._is_user_typing", new_callable=AsyncMock, return_value=False), \
209239
patch("handlers.poke_handler._generate_reply_for_chat", new_callable=AsyncMock) as mock_gen:
@@ -231,7 +261,7 @@ async def test_user_typing_skipped(self):
231261
with patch("handlers.poke_handler.pyrogram_client") as mock_pc, \
232262
patch("handlers.poke_handler.ensure_effective_user", new_callable=AsyncMock), \
233263
patch("handlers.poke_handler.update_last_msg_at", new_callable=AsyncMock), \
234-
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, return_value="Scanning"), \
264+
patch("handlers.poke_handler.get_system_message", new_callable=AsyncMock, side_effect=_sys_msg_side_effect), \
235265
patch("handlers.poke_handler.get_user", new_callable=AsyncMock, return_value={"settings": {}}), \
236266
patch("handlers.poke_handler._is_user_typing", new_callable=AsyncMock, return_value=True) as mock_typing, \
237267
patch("handlers.poke_handler._generate_reply_for_chat", new_callable=AsyncMock) as mock_gen:

0 commit comments

Comments
 (0)