smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/admin/__init__.py +7 -7
- smart_bot_factory/admin/admin_events.py +483 -383
- smart_bot_factory/admin/admin_logic.py +234 -158
- smart_bot_factory/admin/admin_manager.py +68 -53
- smart_bot_factory/admin/admin_tester.py +46 -40
- smart_bot_factory/admin/timeout_checker.py +201 -153
- smart_bot_factory/aiogram_calendar/__init__.py +11 -3
- smart_bot_factory/aiogram_calendar/common.py +12 -18
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
- smart_bot_factory/aiogram_calendar/schemas.py +49 -28
- smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
- smart_bot_factory/analytics/analytics_manager.py +414 -392
- smart_bot_factory/cli.py +204 -148
- smart_bot_factory/config.py +123 -102
- smart_bot_factory/core/bot_utils.py +474 -332
- smart_bot_factory/core/conversation_manager.py +287 -200
- smart_bot_factory/core/decorators.py +1200 -755
- smart_bot_factory/core/message_sender.py +287 -266
- smart_bot_factory/core/router.py +170 -100
- smart_bot_factory/core/router_manager.py +121 -83
- smart_bot_factory/core/states.py +4 -3
- smart_bot_factory/creation/__init__.py +1 -1
- smart_bot_factory/creation/bot_builder.py +320 -242
- smart_bot_factory/creation/bot_testing.py +440 -365
- smart_bot_factory/dashboard/__init__.py +1 -3
- smart_bot_factory/event/__init__.py +2 -7
- smart_bot_factory/handlers/handlers.py +676 -472
- smart_bot_factory/integrations/openai_client.py +218 -168
- smart_bot_factory/integrations/supabase_client.py +948 -637
- smart_bot_factory/message/__init__.py +18 -22
- smart_bot_factory/router/__init__.py +2 -2
- smart_bot_factory/setup_checker.py +162 -126
- smart_bot_factory/supabase/__init__.py +1 -1
- smart_bot_factory/supabase/client.py +631 -515
- smart_bot_factory/utils/__init__.py +2 -3
- smart_bot_factory/utils/debug_routing.py +38 -27
- smart_bot_factory/utils/prompt_loader.py +153 -120
- smart_bot_factory/utils/user_prompt_loader.py +55 -56
- smart_bot_factory/utm_link_generator.py +123 -116
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/METADATA +3 -1
- smart_bot_factory-0.3.9.dist-info/RECORD +59 -0
- smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,39 +5,39 @@
|
|
|
5
5
|
import logging
|
|
6
6
|
import time
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
9
10
|
import pytz
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
async def send_message_by_ai(
|
|
14
|
-
user_id: int,
|
|
15
|
-
message_text: str,
|
|
16
|
-
session_id: str = None
|
|
16
|
+
user_id: int, message_text: str, session_id: str = None
|
|
17
17
|
) -> Dict[str, Any]:
|
|
18
18
|
"""
|
|
19
19
|
Отправляет сообщение пользователю через ИИ (копирует логику process_user_message)
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
Args:
|
|
22
22
|
user_id: ID пользователя в Telegram
|
|
23
23
|
message_text: Текст сообщения для обработки ИИ
|
|
24
24
|
session_id: ID сессии чата (если не указан, будет использована активная сессия)
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
Returns:
|
|
27
27
|
Результат отправки
|
|
28
28
|
"""
|
|
29
29
|
try:
|
|
30
30
|
# Импортируем необходимые компоненты
|
|
31
|
-
from .bot_utils import parse_ai_response, process_events
|
|
32
|
-
|
|
33
31
|
# Получаем компоненты из глобального контекста
|
|
34
32
|
from ..handlers.handlers import get_global_var
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
from .bot_utils import parse_ai_response, process_events
|
|
34
|
+
|
|
35
|
+
bot = get_global_var("bot")
|
|
36
|
+
supabase_client = get_global_var("supabase_client")
|
|
37
|
+
openai_client = get_global_var("openai_client")
|
|
38
|
+
config = get_global_var("config")
|
|
39
|
+
prompt_loader = get_global_var("prompt_loader")
|
|
40
|
+
|
|
41
41
|
# Если session_id не указан, получаем активную сессию пользователя
|
|
42
42
|
if not session_id:
|
|
43
43
|
session_info = await supabase_client.get_active_session(user_id)
|
|
@@ -45,10 +45,10 @@ async def send_message_by_ai(
|
|
|
45
45
|
return {
|
|
46
46
|
"status": "error",
|
|
47
47
|
"error": "Активная сессия не найдена",
|
|
48
|
-
"user_id": user_id
|
|
48
|
+
"user_id": user_id,
|
|
49
49
|
}
|
|
50
|
-
session_id = session_info[
|
|
51
|
-
|
|
50
|
+
session_id = session_info["id"]
|
|
51
|
+
|
|
52
52
|
# Загружаем системный промпт
|
|
53
53
|
try:
|
|
54
54
|
system_prompt = await prompt_loader.load_system_prompt()
|
|
@@ -58,246 +58,237 @@ async def send_message_by_ai(
|
|
|
58
58
|
return {
|
|
59
59
|
"status": "error",
|
|
60
60
|
"error": "Не удалось загрузить системный промпт",
|
|
61
|
-
"user_id": user_id
|
|
61
|
+
"user_id": user_id,
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
# Сохраняем сообщение пользователя в БД
|
|
65
65
|
await supabase_client.add_message(
|
|
66
66
|
session_id=session_id,
|
|
67
|
-
role=
|
|
67
|
+
role="user",
|
|
68
68
|
content=message_text,
|
|
69
|
-
message_type=
|
|
69
|
+
message_type="text",
|
|
70
70
|
)
|
|
71
|
-
logger.info(
|
|
72
|
-
|
|
71
|
+
logger.info("✅ Сообщение пользователя сохранено в БД")
|
|
72
|
+
|
|
73
73
|
# Получаем историю сообщений
|
|
74
|
-
chat_history = await supabase_client.get_chat_history(
|
|
74
|
+
chat_history = await supabase_client.get_chat_history(
|
|
75
|
+
session_id, limit=config.MAX_CONTEXT_MESSAGES
|
|
76
|
+
)
|
|
75
77
|
logger.info(f"📚 Загружена история: {len(chat_history)} сообщений")
|
|
76
|
-
|
|
78
|
+
|
|
77
79
|
# Добавляем текущее время
|
|
78
|
-
moscow_tz = pytz.timezone(
|
|
80
|
+
moscow_tz = pytz.timezone("Europe/Moscow")
|
|
79
81
|
current_time = datetime.now(moscow_tz)
|
|
80
|
-
time_info = current_time.strftime(
|
|
81
|
-
|
|
82
|
+
time_info = current_time.strftime("%H:%M, %d.%m.%Y, %A")
|
|
83
|
+
|
|
82
84
|
# Модифицируем системный промпт, добавляя время
|
|
83
85
|
system_prompt_with_time = f"""
|
|
84
86
|
{system_prompt}
|
|
85
87
|
|
|
86
88
|
ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
|
|
87
89
|
"""
|
|
88
|
-
|
|
90
|
+
|
|
89
91
|
# Формируем контекст для OpenAI
|
|
90
92
|
messages = [{"role": "system", "content": system_prompt_with_time}]
|
|
91
|
-
|
|
92
|
-
for msg in chat_history[-config.MAX_CONTEXT_MESSAGES:]:
|
|
93
|
-
messages.append({
|
|
94
|
-
|
|
95
|
-
"content": msg['content']
|
|
96
|
-
})
|
|
97
|
-
|
|
93
|
+
|
|
94
|
+
for msg in chat_history[-config.MAX_CONTEXT_MESSAGES :]:
|
|
95
|
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
|
96
|
+
|
|
98
97
|
# Добавляем финальные инструкции
|
|
99
98
|
final_instructions = await prompt_loader.load_final_instructions()
|
|
100
99
|
if final_instructions:
|
|
101
100
|
messages.append({"role": "system", "content": final_instructions})
|
|
102
|
-
logger.info(
|
|
103
|
-
|
|
101
|
+
logger.info("🎯 Добавлены финальные инструкции")
|
|
102
|
+
|
|
104
103
|
logger.info(f"📝 Контекст сформирован: {len(messages)} сообщений")
|
|
105
|
-
|
|
104
|
+
|
|
106
105
|
# Отправляем действие "печатает"
|
|
107
106
|
await bot.send_chat_action(user_id, "typing")
|
|
108
|
-
|
|
107
|
+
|
|
109
108
|
# Получаем ответ от ИИ
|
|
110
109
|
start_time = time.time()
|
|
111
110
|
ai_response = await openai_client.get_completion(messages)
|
|
112
111
|
processing_time = int((time.time() - start_time) * 1000)
|
|
113
|
-
|
|
112
|
+
|
|
114
113
|
logger.info(f"🤖 OpenAI ответил за {processing_time}мс")
|
|
115
|
-
|
|
114
|
+
|
|
116
115
|
# Обрабатываем ответ
|
|
117
116
|
tokens_used = 0
|
|
118
117
|
ai_metadata = {}
|
|
119
118
|
response_text = ""
|
|
120
|
-
|
|
119
|
+
|
|
121
120
|
if not ai_response or not ai_response.strip():
|
|
122
|
-
logger.warning(
|
|
121
|
+
logger.warning("❌ OpenAI вернул пустой ответ!")
|
|
123
122
|
fallback_message = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос."
|
|
124
123
|
ai_response = fallback_message
|
|
125
124
|
response_text = fallback_message
|
|
126
125
|
else:
|
|
127
126
|
tokens_used = openai_client.estimate_tokens(ai_response)
|
|
128
127
|
response_text, ai_metadata = parse_ai_response(ai_response)
|
|
129
|
-
|
|
128
|
+
|
|
130
129
|
if not ai_metadata:
|
|
131
130
|
response_text = ai_response
|
|
132
131
|
ai_metadata = {}
|
|
133
132
|
elif not response_text.strip():
|
|
134
133
|
response_text = ai_response
|
|
135
|
-
|
|
134
|
+
|
|
136
135
|
# Обновляем этап сессии и качество лида
|
|
137
136
|
if ai_metadata:
|
|
138
|
-
stage = ai_metadata.get(
|
|
139
|
-
quality = ai_metadata.get(
|
|
140
|
-
|
|
137
|
+
stage = ai_metadata.get("этап")
|
|
138
|
+
quality = ai_metadata.get("качество")
|
|
139
|
+
|
|
141
140
|
if stage or quality is not None:
|
|
142
141
|
await supabase_client.update_session_stage(session_id, stage, quality)
|
|
143
|
-
logger.info(
|
|
144
|
-
|
|
142
|
+
logger.info("✅ Этап и качество обновлены в БД")
|
|
143
|
+
|
|
145
144
|
# Обрабатываем события
|
|
146
|
-
events = ai_metadata.get(
|
|
145
|
+
events = ai_metadata.get("събития", [])
|
|
147
146
|
if events:
|
|
148
147
|
logger.info(f"🔔 Обрабатываем {len(events)} событий")
|
|
149
148
|
should_send_response = await process_events(session_id, events, user_id)
|
|
150
|
-
|
|
149
|
+
|
|
151
150
|
# Сохраняем ответ ассистента
|
|
152
151
|
await supabase_client.add_message(
|
|
153
152
|
session_id=session_id,
|
|
154
|
-
role=
|
|
153
|
+
role="assistant",
|
|
155
154
|
content=response_text,
|
|
156
|
-
message_type=
|
|
155
|
+
message_type="text",
|
|
157
156
|
tokens_used=tokens_used,
|
|
158
157
|
processing_time_ms=processing_time,
|
|
159
|
-
ai_metadata=ai_metadata
|
|
158
|
+
ai_metadata=ai_metadata,
|
|
160
159
|
)
|
|
161
|
-
|
|
160
|
+
|
|
162
161
|
# Определяем финальный ответ
|
|
163
162
|
if config.DEBUG_MODE:
|
|
164
163
|
final_response = ai_response
|
|
165
164
|
else:
|
|
166
165
|
final_response = response_text
|
|
167
|
-
|
|
166
|
+
|
|
168
167
|
# Проверяем, нужно ли отправлять сообщение от ИИ
|
|
169
|
-
if
|
|
170
|
-
logger.info(
|
|
168
|
+
if "should_send_response" in locals() and not should_send_response:
|
|
169
|
+
logger.info(
|
|
170
|
+
"🔇 События запретили отправку сообщения от ИИ (message_sender), пропускаем отправку"
|
|
171
|
+
)
|
|
171
172
|
return {
|
|
172
173
|
"status": "skipped",
|
|
173
174
|
"reason": "send_ai_response=False",
|
|
174
|
-
"user_id": user_id
|
|
175
|
+
"user_id": user_id,
|
|
175
176
|
}
|
|
176
|
-
|
|
177
|
+
|
|
177
178
|
# Отправляем ответ пользователю напрямую через бота
|
|
178
|
-
await bot.send_message(
|
|
179
|
-
|
|
180
|
-
text=final_response
|
|
181
|
-
)
|
|
182
|
-
|
|
179
|
+
await bot.send_message(chat_id=user_id, text=final_response)
|
|
180
|
+
|
|
183
181
|
return {
|
|
184
182
|
"status": "success",
|
|
185
183
|
"user_id": user_id,
|
|
186
184
|
"response_text": response_text,
|
|
187
185
|
"tokens_used": tokens_used,
|
|
188
186
|
"processing_time_ms": processing_time,
|
|
189
|
-
"events_processed": len(events) if events else 0
|
|
187
|
+
"events_processed": len(events) if events else 0,
|
|
190
188
|
}
|
|
191
|
-
|
|
189
|
+
|
|
192
190
|
except Exception as e:
|
|
193
191
|
logger.error(f"❌ Ошибка в send_message_by_ai: {e}")
|
|
194
|
-
return {
|
|
195
|
-
|
|
196
|
-
"error": str(e),
|
|
197
|
-
"user_id": user_id
|
|
198
|
-
}
|
|
192
|
+
return {"status": "error", "error": str(e), "user_id": user_id}
|
|
193
|
+
|
|
199
194
|
|
|
200
195
|
async def send_message_by_human(
|
|
201
|
-
user_id: int,
|
|
202
|
-
message_text: str,
|
|
203
|
-
session_id: Optional[str] = None
|
|
196
|
+
user_id: int, message_text: str, session_id: Optional[str] = None
|
|
204
197
|
) -> Dict[str, Any]:
|
|
205
198
|
"""
|
|
206
199
|
Отправляет сообщение пользователю от имени человека (готовый текст)
|
|
207
|
-
|
|
200
|
+
|
|
208
201
|
Args:
|
|
209
202
|
user_id: ID пользователя в Telegram
|
|
210
203
|
message_text: Готовый текст сообщения
|
|
211
204
|
session_id: ID сессии (опционально, для сохранения в БД)
|
|
212
|
-
|
|
205
|
+
|
|
213
206
|
Returns:
|
|
214
207
|
Результат отправки
|
|
215
208
|
"""
|
|
216
209
|
try:
|
|
217
210
|
# Импортируем необходимые компоненты
|
|
218
211
|
from ..handlers.handlers import get_global_var
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
212
|
+
|
|
213
|
+
bot = get_global_var("bot")
|
|
214
|
+
supabase_client = get_global_var("supabase_client")
|
|
215
|
+
|
|
222
216
|
# Отправляем сообщение пользователю
|
|
223
|
-
message = await bot.send_message(
|
|
224
|
-
|
|
225
|
-
text=message_text
|
|
226
|
-
)
|
|
227
|
-
|
|
217
|
+
message = await bot.send_message(chat_id=user_id, text=message_text)
|
|
218
|
+
|
|
228
219
|
# Если указана сессия, сохраняем сообщение в БД
|
|
229
220
|
if session_id:
|
|
230
221
|
await supabase_client.add_message(
|
|
231
222
|
session_id=session_id,
|
|
232
|
-
role=
|
|
223
|
+
role="assistant",
|
|
233
224
|
content=message_text,
|
|
234
|
-
message_type=
|
|
235
|
-
metadata={
|
|
225
|
+
message_type="text",
|
|
226
|
+
metadata={"sent_by_human": True},
|
|
236
227
|
)
|
|
237
|
-
logger.info(
|
|
238
|
-
|
|
228
|
+
logger.info("💾 Сообщение от человека сохранено в БД")
|
|
229
|
+
|
|
239
230
|
return {
|
|
240
231
|
"status": "success",
|
|
241
232
|
"user_id": user_id,
|
|
242
233
|
"message_id": message.message_id,
|
|
243
234
|
"message_text": message_text,
|
|
244
|
-
"saved_to_db": bool(session_id)
|
|
235
|
+
"saved_to_db": bool(session_id),
|
|
245
236
|
}
|
|
246
|
-
|
|
237
|
+
|
|
247
238
|
except Exception as e:
|
|
248
239
|
logger.error(f"❌ Ошибка в send_message_by_human: {e}")
|
|
249
|
-
return {
|
|
250
|
-
|
|
251
|
-
"error": str(e),
|
|
252
|
-
"user_id": user_id
|
|
253
|
-
}
|
|
240
|
+
return {"status": "error", "error": str(e), "user_id": user_id}
|
|
241
|
+
|
|
254
242
|
|
|
255
243
|
async def send_message_to_users_by_stage(
|
|
256
|
-
stage: str,
|
|
257
|
-
message_text: str,
|
|
258
|
-
bot_id: str
|
|
244
|
+
stage: str, message_text: str, bot_id: str
|
|
259
245
|
) -> Dict[str, Any]:
|
|
260
246
|
"""
|
|
261
247
|
Отправляет сообщение всем пользователям, находящимся на определенной стадии
|
|
262
|
-
|
|
248
|
+
|
|
263
249
|
Args:
|
|
264
250
|
stage: Стадия диалога (например, 'introduction', 'qualification', 'closing')
|
|
265
251
|
message_text: Текст сообщения для отправки
|
|
266
252
|
bot_id: ID бота (если не указан, используется текущий бот)
|
|
267
|
-
|
|
253
|
+
|
|
268
254
|
Returns:
|
|
269
255
|
Результат отправки с количеством отправленных сообщений
|
|
270
256
|
"""
|
|
271
257
|
try:
|
|
272
258
|
# Импортируем необходимые компоненты
|
|
273
259
|
from ..handlers.handlers import get_global_var
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
260
|
+
|
|
261
|
+
bot = get_global_var("bot")
|
|
262
|
+
supabase_client = get_global_var("supabase_client")
|
|
263
|
+
current_bot_id = (
|
|
264
|
+
get_global_var("config").BOT_ID if get_global_var("config") else bot_id
|
|
265
|
+
)
|
|
266
|
+
|
|
278
267
|
if not current_bot_id:
|
|
279
|
-
return {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
268
|
+
return {"status": "error", "error": "Не удалось определить bot_id"}
|
|
269
|
+
|
|
270
|
+
logger.info(
|
|
271
|
+
f"🔍 Ищем пользователей на стадии '{stage}' для бота '{current_bot_id}'"
|
|
272
|
+
)
|
|
273
|
+
|
|
286
274
|
# Получаем последние сессии для каждого пользователя с нужной стадией
|
|
287
275
|
# Сначала получаем все активные сессии с нужной стадией
|
|
288
|
-
sessions_query =
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
276
|
+
sessions_query = (
|
|
277
|
+
supabase_client.client.table("sales_chat_sessions")
|
|
278
|
+
.select("user_id, id, current_stage, created_at")
|
|
279
|
+
.eq("status", "active")
|
|
280
|
+
.eq("current_stage", stage)
|
|
281
|
+
)
|
|
282
|
+
|
|
292
283
|
# Фильтруем по bot_id если указан
|
|
293
284
|
if current_bot_id:
|
|
294
|
-
sessions_query = sessions_query.eq(
|
|
295
|
-
|
|
285
|
+
sessions_query = sessions_query.eq("bot_id", current_bot_id)
|
|
286
|
+
|
|
296
287
|
# Сортируем по дате создания (последние сначала)
|
|
297
|
-
sessions_query = sessions_query.order(
|
|
298
|
-
|
|
288
|
+
sessions_query = sessions_query.order("created_at", desc=True)
|
|
289
|
+
|
|
299
290
|
sessions_data = sessions_query.execute()
|
|
300
|
-
|
|
291
|
+
|
|
301
292
|
if not sessions_data.data:
|
|
302
293
|
logger.info(f"📭 Пользователи на стадии '{stage}' не найдены")
|
|
303
294
|
return {
|
|
@@ -305,159 +296,156 @@ async def send_message_to_users_by_stage(
|
|
|
305
296
|
"stage": stage,
|
|
306
297
|
"users_found": 0,
|
|
307
298
|
"messages_sent": 0,
|
|
308
|
-
"errors": []
|
|
299
|
+
"errors": [],
|
|
309
300
|
}
|
|
310
|
-
|
|
301
|
+
|
|
311
302
|
# Выбираем уникальные user_id (берем только последнюю сессию для каждого пользователя)
|
|
312
303
|
unique_users = {}
|
|
313
304
|
for session in sessions_data.data:
|
|
314
|
-
user_id = session[
|
|
305
|
+
user_id = session["user_id"]
|
|
315
306
|
# Если пользователь еще не добавлен, добавляем его (так как сессии отсортированы по дате, первая будет самой последней)
|
|
316
307
|
if user_id not in unique_users:
|
|
317
308
|
unique_users[user_id] = {
|
|
318
|
-
|
|
319
|
-
|
|
309
|
+
"session_id": session["id"],
|
|
310
|
+
"current_stage": session["current_stage"],
|
|
320
311
|
}
|
|
321
|
-
|
|
322
|
-
logger.info(
|
|
323
|
-
|
|
312
|
+
|
|
313
|
+
logger.info(
|
|
314
|
+
f"👥 Найдено {len(unique_users)} уникальных пользователей на стадии '{stage}'"
|
|
315
|
+
)
|
|
316
|
+
|
|
324
317
|
# Отправляем сообщения
|
|
325
318
|
messages_sent = 0
|
|
326
319
|
errors = []
|
|
327
|
-
|
|
320
|
+
|
|
328
321
|
for user_id, user_data in unique_users.items():
|
|
329
|
-
session_id = user_data[
|
|
330
|
-
|
|
322
|
+
session_id = user_data["session_id"]
|
|
323
|
+
|
|
331
324
|
try:
|
|
332
325
|
# Отправляем сообщение пользователю
|
|
333
|
-
await bot.send_message(
|
|
334
|
-
|
|
335
|
-
text=message_text
|
|
336
|
-
)
|
|
337
|
-
|
|
326
|
+
await bot.send_message(chat_id=user_id, text=message_text)
|
|
327
|
+
|
|
338
328
|
# Сохраняем сообщение в БД
|
|
339
329
|
await supabase_client.add_message(
|
|
340
330
|
session_id=session_id,
|
|
341
|
-
role=
|
|
331
|
+
role="assistant",
|
|
342
332
|
content=message_text,
|
|
343
|
-
message_type=
|
|
333
|
+
message_type="text",
|
|
344
334
|
metadata={
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
335
|
+
"sent_by_stage_broadcast": True,
|
|
336
|
+
"target_stage": stage,
|
|
337
|
+
"broadcast_timestamp": datetime.now().isoformat(),
|
|
338
|
+
},
|
|
349
339
|
)
|
|
350
|
-
|
|
340
|
+
|
|
351
341
|
messages_sent += 1
|
|
352
|
-
logger.info(
|
|
353
|
-
|
|
342
|
+
logger.info(
|
|
343
|
+
f"✅ Сообщение отправлено пользователю {user_id} (стадия: {stage})"
|
|
344
|
+
)
|
|
345
|
+
|
|
354
346
|
except Exception as e:
|
|
355
347
|
error_msg = f"Ошибка отправки пользователю {user_id}: {str(e)}"
|
|
356
348
|
errors.append(error_msg)
|
|
357
349
|
logger.error(f"❌ {error_msg}")
|
|
358
|
-
|
|
350
|
+
|
|
359
351
|
result = {
|
|
360
352
|
"status": "success",
|
|
361
353
|
"stage": stage,
|
|
362
354
|
"users_found": len(unique_users),
|
|
363
355
|
"messages_sent": messages_sent,
|
|
364
|
-
"errors": errors
|
|
356
|
+
"errors": errors,
|
|
365
357
|
}
|
|
366
|
-
|
|
367
|
-
logger.info(
|
|
368
|
-
|
|
358
|
+
|
|
359
|
+
logger.info(
|
|
360
|
+
f"📊 Результат рассылки по стадии '{stage}': {messages_sent}/{len(unique_users)} сообщений отправлено"
|
|
361
|
+
)
|
|
362
|
+
|
|
369
363
|
return result
|
|
370
|
-
|
|
364
|
+
|
|
371
365
|
except Exception as e:
|
|
372
366
|
logger.error(f"❌ Ошибка в send_message_to_users_by_stage: {e}")
|
|
373
|
-
return {
|
|
374
|
-
"status": "error",
|
|
375
|
-
"error": str(e),
|
|
376
|
-
"stage": stage
|
|
377
|
-
}
|
|
367
|
+
return {"status": "error", "error": str(e), "stage": stage}
|
|
378
368
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
) -> Dict[str, Any]:
|
|
369
|
+
|
|
370
|
+
async def get_users_by_stage_stats(bot_id: Optional[str] = None) -> Dict[str, Any]:
|
|
382
371
|
"""
|
|
383
372
|
Получает статистику пользователей по стадиям
|
|
384
|
-
|
|
373
|
+
|
|
385
374
|
Args:
|
|
386
375
|
bot_id: ID бота (если не указан, используется текущий бот)
|
|
387
|
-
|
|
376
|
+
|
|
388
377
|
Returns:
|
|
389
378
|
Статистика по стадиям с количеством пользователей
|
|
390
379
|
"""
|
|
391
380
|
try:
|
|
392
381
|
# Импортируем необходимые компоненты
|
|
393
382
|
from ..handlers.handlers import get_global_var
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
383
|
+
|
|
384
|
+
supabase_client = get_global_var("supabase_client")
|
|
385
|
+
current_bot_id = (
|
|
386
|
+
get_global_var("config").BOT_ID if get_global_var("config") else bot_id
|
|
387
|
+
)
|
|
388
|
+
|
|
397
389
|
if not current_bot_id:
|
|
398
|
-
return {
|
|
399
|
-
|
|
400
|
-
"error": "Не удалось определить bot_id"
|
|
401
|
-
}
|
|
402
|
-
|
|
390
|
+
return {"status": "error", "error": "Не удалось определить bot_id"}
|
|
391
|
+
|
|
403
392
|
logger.info(f"📊 Получаем статистику по стадиям для бота '{current_bot_id}'")
|
|
404
|
-
|
|
393
|
+
|
|
405
394
|
# Получаем статистику по стадиям с user_id для подсчета уникальных пользователей
|
|
406
|
-
stats_query =
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
395
|
+
stats_query = (
|
|
396
|
+
supabase_client.client.table("sales_chat_sessions")
|
|
397
|
+
.select("user_id, current_stage, created_at")
|
|
398
|
+
.eq("status", "active")
|
|
399
|
+
)
|
|
400
|
+
|
|
410
401
|
# Фильтруем по bot_id если указан
|
|
411
402
|
if current_bot_id:
|
|
412
|
-
stats_query = stats_query.eq(
|
|
413
|
-
|
|
403
|
+
stats_query = stats_query.eq("bot_id", current_bot_id)
|
|
404
|
+
|
|
414
405
|
# Сортируем по дате создания (последние сначала)
|
|
415
|
-
stats_query = stats_query.order(
|
|
416
|
-
|
|
406
|
+
stats_query = stats_query.order("created_at", desc=True)
|
|
407
|
+
|
|
417
408
|
sessions_data = stats_query.execute()
|
|
418
|
-
|
|
409
|
+
|
|
419
410
|
# Подсчитываем уникальных пользователей по стадиям (берем последнюю сессию каждого пользователя)
|
|
420
411
|
user_stages = {} # {user_id: stage}
|
|
421
|
-
|
|
412
|
+
|
|
422
413
|
for session in sessions_data.data:
|
|
423
|
-
user_id = session[
|
|
424
|
-
stage = session[
|
|
425
|
-
|
|
414
|
+
user_id = session["user_id"]
|
|
415
|
+
stage = session["current_stage"] or "unknown"
|
|
416
|
+
|
|
426
417
|
# Если пользователь еще не добавлен, добавляем его стадию (первая встреченная - самая последняя)
|
|
427
418
|
if user_id not in user_stages:
|
|
428
419
|
user_stages[user_id] = stage
|
|
429
|
-
|
|
420
|
+
|
|
430
421
|
# Подсчитываем количество пользователей по стадиям
|
|
431
422
|
stage_stats = {}
|
|
432
423
|
for stage in user_stages.values():
|
|
433
424
|
stage_stats[stage] = stage_stats.get(stage, 0) + 1
|
|
434
|
-
|
|
425
|
+
|
|
435
426
|
total_users = len(user_stages)
|
|
436
|
-
|
|
427
|
+
|
|
437
428
|
# Сортируем по количеству пользователей (по убыванию)
|
|
438
429
|
sorted_stages = sorted(stage_stats.items(), key=lambda x: x[1], reverse=True)
|
|
439
|
-
|
|
430
|
+
|
|
440
431
|
result = {
|
|
441
432
|
"status": "success",
|
|
442
433
|
"bot_id": current_bot_id,
|
|
443
434
|
"total_active_users": total_users,
|
|
444
435
|
"stages": dict(sorted_stages),
|
|
445
|
-
"stages_list": sorted_stages
|
|
436
|
+
"stages_list": sorted_stages,
|
|
446
437
|
}
|
|
447
|
-
|
|
438
|
+
|
|
448
439
|
logger.info(f"📊 Статистика по стадиям: {total_users} активных пользователей")
|
|
449
440
|
for stage, count in sorted_stages:
|
|
450
441
|
logger.info(f" {stage}: {count} пользователей")
|
|
451
|
-
|
|
442
|
+
|
|
452
443
|
return result
|
|
453
|
-
|
|
444
|
+
|
|
454
445
|
except Exception as e:
|
|
455
446
|
logger.error(f"❌ Ошибка в get_users_by_stage_stats: {e}")
|
|
456
|
-
return {
|
|
457
|
-
|
|
458
|
-
"error": str(e),
|
|
459
|
-
"bot_id": bot_id
|
|
460
|
-
}
|
|
447
|
+
return {"status": "error", "error": str(e), "bot_id": bot_id}
|
|
448
|
+
|
|
461
449
|
|
|
462
450
|
async def send_message(
|
|
463
451
|
message,
|
|
@@ -466,11 +454,11 @@ async def send_message(
|
|
|
466
454
|
files_list: list = [],
|
|
467
455
|
directories_list: list = [],
|
|
468
456
|
parse_mode: str = "Markdown",
|
|
469
|
-
**kwargs
|
|
457
|
+
**kwargs,
|
|
470
458
|
):
|
|
471
459
|
"""
|
|
472
460
|
Пользовательская функция для отправки сообщений с файлами и кнопками
|
|
473
|
-
|
|
461
|
+
|
|
474
462
|
Args:
|
|
475
463
|
message: Message объект от aiogram
|
|
476
464
|
text: Текст сообщения
|
|
@@ -479,18 +467,18 @@ async def send_message(
|
|
|
479
467
|
directories_list: Список каталогов (отправятся все файлы)
|
|
480
468
|
parse_mode: Режим парсинга ('Markdown', 'HTML' или None)
|
|
481
469
|
**kwargs: Дополнительные параметры (reply_markup и т.д.)
|
|
482
|
-
|
|
470
|
+
|
|
483
471
|
Returns:
|
|
484
472
|
Message объект отправленного сообщения или None
|
|
485
|
-
|
|
473
|
+
|
|
486
474
|
Example:
|
|
487
475
|
from smart_bot_factory.message import send_message
|
|
488
476
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
|
489
|
-
|
|
477
|
+
|
|
490
478
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
491
479
|
[InlineKeyboardButton(text="Кнопка", callback_data="action")]
|
|
492
480
|
])
|
|
493
|
-
|
|
481
|
+
|
|
494
482
|
await send_message(
|
|
495
483
|
message=message,
|
|
496
484
|
text="Привет!",
|
|
@@ -501,91 +489,100 @@ async def send_message(
|
|
|
501
489
|
)
|
|
502
490
|
"""
|
|
503
491
|
from pathlib import Path
|
|
492
|
+
|
|
504
493
|
from aiogram.types import FSInputFile
|
|
505
494
|
from aiogram.utils.media_group import MediaGroupBuilder
|
|
506
|
-
|
|
507
|
-
logger.info(
|
|
495
|
+
|
|
496
|
+
logger.info("📤 send_message вызвана:")
|
|
508
497
|
logger.info(f" 👤 Пользователь: {message.from_user.id}")
|
|
509
498
|
logger.info(f" 📝 Длина текста: {len(text)} символов")
|
|
510
499
|
logger.info(f" 🔧 Parse mode: {parse_mode}")
|
|
511
|
-
|
|
500
|
+
|
|
512
501
|
try:
|
|
513
502
|
user_id = message.from_user.id
|
|
514
|
-
|
|
503
|
+
|
|
515
504
|
# Устанавливаем parse_mode (None если передана строка 'None')
|
|
516
|
-
actual_parse_mode = None if parse_mode ==
|
|
517
|
-
|
|
505
|
+
actual_parse_mode = None if parse_mode == "None" else parse_mode
|
|
506
|
+
|
|
518
507
|
# Текст уже готов, используем как есть
|
|
519
508
|
final_text = text
|
|
520
|
-
|
|
509
|
+
|
|
521
510
|
# Работаем с переданными файлами и каталогами
|
|
522
511
|
logger.info(f" 📦 Передано файлов: {files_list}")
|
|
523
512
|
logger.info(f" 📂 Передано каталогов: {directories_list}")
|
|
524
|
-
|
|
513
|
+
|
|
525
514
|
# Получаем список уже отправленных файлов и каталогов
|
|
526
515
|
sent_files = await supabase_client.get_sent_files(user_id)
|
|
527
516
|
sent_directories = await supabase_client.get_sent_directories(user_id)
|
|
528
|
-
|
|
517
|
+
|
|
529
518
|
logger.info(f" 📋 Уже отправлено файлов: {sent_files}")
|
|
530
519
|
logger.info(f" 📋 Уже отправлено каталогов: {sent_directories}")
|
|
531
|
-
|
|
520
|
+
|
|
532
521
|
# Фильтруем файлы и каталоги, которые уже отправлялись
|
|
533
522
|
actual_files_list = [f for f in files_list if f not in sent_files]
|
|
534
|
-
actual_directories_list = [
|
|
535
|
-
|
|
523
|
+
actual_directories_list = [
|
|
524
|
+
d for d in directories_list if str(d) not in sent_directories
|
|
525
|
+
]
|
|
526
|
+
|
|
536
527
|
logger.info(f" 🆕 После фильтрации файлов: {actual_files_list}")
|
|
537
528
|
logger.info(f" 🆕 После фильтрации каталогов: {actual_directories_list}")
|
|
538
|
-
|
|
529
|
+
|
|
539
530
|
# Проверяем, что есть что отправлять
|
|
540
531
|
if not final_text or not final_text.strip():
|
|
541
|
-
logger.error(
|
|
532
|
+
logger.error("❌ КРИТИЧЕСКАЯ ОШИБКА: final_text пуст после обработки!")
|
|
542
533
|
logger.error(f" Исходный text: '{text[:200]}...'")
|
|
543
534
|
final_text = "Ошибка формирования ответа. Попробуйте еще раз."
|
|
544
|
-
|
|
535
|
+
|
|
545
536
|
logger.info(f"📱 Подготовка сообщения: {len(final_text)} символов")
|
|
546
537
|
logger.info(f" 📦 Файлов для обработки: {actual_files_list}")
|
|
547
538
|
logger.info(f" 📂 Каталогов для обработки: {actual_directories_list}")
|
|
548
|
-
|
|
539
|
+
|
|
549
540
|
# Проверяем наличие файлов для отправки
|
|
550
541
|
if actual_files_list or actual_directories_list:
|
|
551
542
|
# Функция определения типа медиа по расширению
|
|
552
543
|
def get_media_type(file_path: str) -> str:
|
|
553
544
|
ext = Path(file_path).suffix.lower()
|
|
554
|
-
if ext in {
|
|
555
|
-
return
|
|
556
|
-
elif ext in {
|
|
557
|
-
return
|
|
545
|
+
if ext in {".jpg", ".jpeg", ".png"}:
|
|
546
|
+
return "photo"
|
|
547
|
+
elif ext in {".mp4", ".mov"}:
|
|
548
|
+
return "video"
|
|
558
549
|
else:
|
|
559
|
-
return
|
|
560
|
-
|
|
550
|
+
return "document"
|
|
551
|
+
|
|
561
552
|
# Создаем списки для разных типов файлов
|
|
562
553
|
video_files = []
|
|
563
554
|
photo_files = []
|
|
564
555
|
document_files = []
|
|
565
|
-
|
|
556
|
+
|
|
566
557
|
# Функция обработки файла
|
|
567
558
|
def process_file(file_path: Path, source: str = ""):
|
|
568
559
|
if file_path.is_file():
|
|
569
560
|
media_type = get_media_type(str(file_path))
|
|
570
|
-
if media_type ==
|
|
561
|
+
if media_type == "video":
|
|
571
562
|
video_files.append(file_path)
|
|
572
|
-
logger.info(
|
|
573
|
-
|
|
563
|
+
logger.info(
|
|
564
|
+
f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}"
|
|
565
|
+
)
|
|
566
|
+
elif media_type == "photo":
|
|
574
567
|
photo_files.append(file_path)
|
|
575
|
-
logger.info(
|
|
568
|
+
logger.info(
|
|
569
|
+
f" 📸 Добавлено фото{f' из {source}' if source else ''}: {file_path.name}"
|
|
570
|
+
)
|
|
576
571
|
else:
|
|
577
572
|
document_files.append(file_path)
|
|
578
|
-
logger.info(
|
|
573
|
+
logger.info(
|
|
574
|
+
f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}"
|
|
575
|
+
)
|
|
579
576
|
else:
|
|
580
577
|
logger.warning(f" ⚠️ Файл не найден: {file_path}")
|
|
581
|
-
|
|
578
|
+
|
|
582
579
|
# Обрабатываем прямые файлы
|
|
583
580
|
for file_name in actual_files_list:
|
|
584
581
|
try:
|
|
585
582
|
process_file(Path(f"files/{file_name}"))
|
|
586
583
|
except Exception as e:
|
|
587
584
|
logger.error(f" ❌ Ошибка обработки файла {file_name}: {e}")
|
|
588
|
-
|
|
585
|
+
|
|
589
586
|
# Обрабатываем файлы из каталогов
|
|
590
587
|
for dir_name in actual_directories_list:
|
|
591
588
|
dir_name = Path(dir_name)
|
|
@@ -595,82 +592,102 @@ async def send_message(
|
|
|
595
592
|
try:
|
|
596
593
|
process_file(file_path, dir_name)
|
|
597
594
|
except Exception as e:
|
|
598
|
-
logger.error(
|
|
595
|
+
logger.error(
|
|
596
|
+
f" ❌ Ошибка обработки файла {file_path}: {e}"
|
|
597
|
+
)
|
|
599
598
|
else:
|
|
600
599
|
logger.warning(f" ⚠️ Каталог не найден: {dir_name}")
|
|
601
600
|
except Exception as e:
|
|
602
601
|
logger.error(f" ❌ Ошибка обработки каталога {dir_name}: {e}")
|
|
603
|
-
|
|
602
|
+
|
|
604
603
|
# Списки для отслеживания реально отправленных файлов
|
|
605
604
|
sent_files_to_save = []
|
|
606
605
|
sent_dirs_to_save = []
|
|
607
|
-
|
|
606
|
+
|
|
608
607
|
# 1. Отправляем видео (если есть)
|
|
609
608
|
if video_files:
|
|
610
609
|
video_group = MediaGroupBuilder()
|
|
611
610
|
for file_path in video_files:
|
|
612
611
|
video_group.add_video(media=FSInputFile(str(file_path)))
|
|
613
|
-
|
|
612
|
+
|
|
614
613
|
videos = video_group.build()
|
|
615
614
|
if videos:
|
|
616
615
|
await message.answer_media_group(media=videos)
|
|
617
616
|
logger.info(f" ✅ Отправлено {len(videos)} видео")
|
|
618
|
-
|
|
617
|
+
|
|
619
618
|
# 2. Отправляем фото (если есть)
|
|
620
619
|
if photo_files:
|
|
621
620
|
photo_group = MediaGroupBuilder()
|
|
622
621
|
for file_path in photo_files:
|
|
623
622
|
photo_group.add_photo(media=FSInputFile(str(file_path)))
|
|
624
|
-
|
|
623
|
+
|
|
625
624
|
photos = photo_group.build()
|
|
626
625
|
if photos:
|
|
627
626
|
await message.answer_media_group(media=photos)
|
|
628
627
|
logger.info(f" ✅ Отправлено {len(photos)} фото")
|
|
629
|
-
|
|
628
|
+
|
|
630
629
|
# 3. Отправляем текст
|
|
631
|
-
result = await message.answer(
|
|
632
|
-
|
|
633
|
-
|
|
630
|
+
result = await message.answer(
|
|
631
|
+
final_text, parse_mode=actual_parse_mode, **kwargs
|
|
632
|
+
)
|
|
633
|
+
logger.info(" ✅ Отправлен текст сообщения")
|
|
634
|
+
|
|
634
635
|
# 4. Отправляем документы (если есть)
|
|
635
636
|
if document_files:
|
|
636
637
|
doc_group = MediaGroupBuilder()
|
|
637
638
|
for file_path in document_files:
|
|
638
639
|
doc_group.add_document(media=FSInputFile(str(file_path)))
|
|
639
|
-
|
|
640
|
+
|
|
640
641
|
docs = doc_group.build()
|
|
641
642
|
if docs:
|
|
642
643
|
await message.answer_media_group(media=docs)
|
|
643
644
|
logger.info(f" ✅ Отправлено {len(docs)} документов")
|
|
644
|
-
|
|
645
|
+
|
|
645
646
|
# 5. Собираем список реально отправленных файлов и каталогов
|
|
646
647
|
if video_files or photo_files or document_files:
|
|
647
648
|
sent_files_to_save.extend(actual_files_list)
|
|
648
|
-
logger.info(
|
|
649
|
+
logger.info(
|
|
650
|
+
f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}"
|
|
651
|
+
)
|
|
649
652
|
sent_dirs_to_save.extend([str(d) for d in actual_directories_list])
|
|
650
|
-
logger.info(
|
|
651
|
-
|
|
653
|
+
logger.info(
|
|
654
|
+
f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}"
|
|
655
|
+
)
|
|
656
|
+
|
|
652
657
|
# 6. Обновляем информацию в БД
|
|
653
658
|
if sent_files_to_save or sent_dirs_to_save:
|
|
654
659
|
try:
|
|
655
660
|
if sent_files_to_save:
|
|
656
661
|
logger.info(f" 💾 Сохраняем файлы в БД: {sent_files_to_save}")
|
|
657
|
-
await supabase_client.add_sent_files(
|
|
662
|
+
await supabase_client.add_sent_files(
|
|
663
|
+
user_id, sent_files_to_save
|
|
664
|
+
)
|
|
658
665
|
if sent_dirs_to_save:
|
|
659
|
-
logger.info(
|
|
660
|
-
|
|
661
|
-
|
|
666
|
+
logger.info(
|
|
667
|
+
f" 💾 Сохраняем каталоги в БД: {sent_dirs_to_save}"
|
|
668
|
+
)
|
|
669
|
+
await supabase_client.add_sent_directories(
|
|
670
|
+
user_id, sent_dirs_to_save
|
|
671
|
+
)
|
|
672
|
+
logger.info(
|
|
673
|
+
" ✅ Обновлена информация о отправленных файлах в БД"
|
|
674
|
+
)
|
|
662
675
|
except Exception as e:
|
|
663
|
-
logger.error(
|
|
676
|
+
logger.error(
|
|
677
|
+
f" ❌ Ошибка обновления информации о файлах в БД: {e}"
|
|
678
|
+
)
|
|
664
679
|
else:
|
|
665
|
-
logger.info(
|
|
666
|
-
|
|
680
|
+
logger.info(" ℹ️ Нет новых файлов для сохранения в БД")
|
|
681
|
+
|
|
667
682
|
return result
|
|
668
683
|
else:
|
|
669
684
|
# Если нет файлов, отправляем просто текст
|
|
670
685
|
logger.info(" ⚠️ Нет файлов для отправки, отправляем как текст")
|
|
671
|
-
result = await message.answer(
|
|
686
|
+
result = await message.answer(
|
|
687
|
+
final_text, parse_mode=actual_parse_mode, **kwargs
|
|
688
|
+
)
|
|
672
689
|
return result
|
|
673
|
-
|
|
690
|
+
|
|
674
691
|
except Exception as e:
|
|
675
692
|
# Проверяем, является ли ошибка блокировкой бота
|
|
676
693
|
if "Forbidden: bot was blocked by the user" in str(e):
|
|
@@ -679,23 +696,27 @@ async def send_message(
|
|
|
679
696
|
elif "TelegramForbiddenError" in str(type(e).__name__):
|
|
680
697
|
logger.warning(f"🚫 Бот заблокирован пользователем {user_id}")
|
|
681
698
|
return None
|
|
682
|
-
|
|
699
|
+
|
|
683
700
|
logger.error(f"❌ ОШИБКА в send_message: {e}")
|
|
684
701
|
logger.exception("Полный стек ошибки send_message:")
|
|
685
|
-
|
|
702
|
+
|
|
686
703
|
# Пытаемся отправить простое сообщение без форматирования
|
|
687
704
|
try:
|
|
688
705
|
fallback_text = "Произошла ошибка при отправке ответа. Попробуйте еще раз."
|
|
689
706
|
result = await message.answer(fallback_text)
|
|
690
|
-
logger.info(
|
|
707
|
+
logger.info("✅ Запасное сообщение отправлено")
|
|
691
708
|
return result
|
|
692
709
|
except Exception as e2:
|
|
693
710
|
if "Forbidden: bot was blocked by the user" in str(e2):
|
|
694
|
-
logger.warning(
|
|
711
|
+
logger.warning(
|
|
712
|
+
f"🚫 Бот заблокирован пользователем {user_id} (fallback)"
|
|
713
|
+
)
|
|
695
714
|
return None
|
|
696
715
|
elif "TelegramForbiddenError" in str(type(e2).__name__):
|
|
697
|
-
logger.warning(
|
|
716
|
+
logger.warning(
|
|
717
|
+
f"🚫 Бот заблокирован пользователем {user_id} (fallback)"
|
|
718
|
+
)
|
|
698
719
|
return None
|
|
699
|
-
|
|
720
|
+
|
|
700
721
|
logger.error(f"❌ Даже запасное сообщение не отправилось: {e2}")
|
|
701
722
|
raise
|