smart-bot-factory 0.1.3__py3-none-any.whl → 0.1.5__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/__init__.py +0 -48
- smart_bot_factory/admin/admin_logic.py +11 -11
- smart_bot_factory/cli.py +299 -106
- smart_bot_factory/clients/__init__.py +33 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +2 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +95 -28
- smart_bot_factory/core/__init__.py +43 -22
- smart_bot_factory/core/bot_utils.py +268 -95
- smart_bot_factory/core/conversation_manager.py +542 -535
- smart_bot_factory/core/decorators.py +943 -229
- smart_bot_factory/core/globals.py +68 -0
- smart_bot_factory/core/message_sender.py +6 -6
- smart_bot_factory/core/router.py +172 -0
- smart_bot_factory/core/router_manager.py +165 -0
- smart_bot_factory/creation/__init__.py +1 -2
- smart_bot_factory/creation/bot_builder.py +116 -8
- smart_bot_factory/creation/bot_testing.py +74 -13
- smart_bot_factory/handlers/handlers.py +10 -2
- smart_bot_factory/integrations/__init__.py +1 -0
- smart_bot_factory/integrations/supabase_client.py +272 -2
- smart_bot_factory/utm_link_generator.py +106 -0
- smart_bot_factory-0.1.5.dist-info/METADATA +466 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/RECORD +26 -31
- smart_bot_factory/configs/growthmed-helper/env_example.txt +0 -1
- smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +0 -9
- smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +0 -582
- smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +0 -66
- smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +0 -232
- smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +0 -28
- smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +0 -7
- smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +0 -16
- smart_bot_factory/configs/growthmed-helper/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
- smart_bot_factory/uv.lock +0 -2004
- smart_bot_factory-0.1.3.dist-info/METADATA +0 -126
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
+
import re
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from aiogram import Router
|
|
6
7
|
from aiogram.filters import Command
|
|
@@ -13,7 +14,13 @@ from aiogram.types import (
|
|
|
13
14
|
from aiogram.utils.media_group import MediaGroupBuilder
|
|
14
15
|
|
|
15
16
|
from pathlib import Path
|
|
16
|
-
from ..core.decorators import
|
|
17
|
+
from ..core.decorators import (
|
|
18
|
+
execute_event_handler,
|
|
19
|
+
save_immediate_event,
|
|
20
|
+
schedule_task_for_later_with_db,
|
|
21
|
+
schedule_global_handler_for_later_with_db,
|
|
22
|
+
update_event_result
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
# Функция для получения глобальных переменных
|
|
19
26
|
def get_global_var(var_name):
|
|
@@ -24,6 +31,46 @@ def get_global_var(var_name):
|
|
|
24
31
|
|
|
25
32
|
logger = logging.getLogger(__name__)
|
|
26
33
|
|
|
34
|
+
def parse_time_expression(time_text: str) -> int:
|
|
35
|
+
"""
|
|
36
|
+
Парсит временные выражения и возвращает количество секунд
|
|
37
|
+
|
|
38
|
+
Поддерживаемые форматы:
|
|
39
|
+
- "30" -> 30 секунд (основной формат)
|
|
40
|
+
- "Запуск через 30 секунд" -> 30 секунд
|
|
41
|
+
- "через 5 минут" -> 300 секунд
|
|
42
|
+
- "через 1 час" -> 3600 секунд
|
|
43
|
+
- "через 2 часа" -> 7200 секунд
|
|
44
|
+
"""
|
|
45
|
+
# Сначала пробуем простое число (основной формат)
|
|
46
|
+
try:
|
|
47
|
+
return int(time_text)
|
|
48
|
+
except ValueError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# Убираем лишние символы и приводим к нижнему регистру
|
|
52
|
+
text = re.sub(r'[^\w\s\d]', '', time_text.lower())
|
|
53
|
+
|
|
54
|
+
# Ищем числа в тексте
|
|
55
|
+
numbers = re.findall(r'\d+', text)
|
|
56
|
+
if not numbers:
|
|
57
|
+
raise ValueError(f"Не найдено число в выражении: {time_text}")
|
|
58
|
+
|
|
59
|
+
number = int(numbers[0])
|
|
60
|
+
|
|
61
|
+
# Определяем единицу времени
|
|
62
|
+
if 'секунд' in text:
|
|
63
|
+
return number
|
|
64
|
+
elif 'минут' in text:
|
|
65
|
+
return number * 60
|
|
66
|
+
elif 'час' in text:
|
|
67
|
+
return number * 3600
|
|
68
|
+
elif 'день' in text or 'дней' in text:
|
|
69
|
+
return number * 86400
|
|
70
|
+
else:
|
|
71
|
+
# По умолчанию считаем секундами
|
|
72
|
+
return number
|
|
73
|
+
|
|
27
74
|
# Создаем роутер для общих команд
|
|
28
75
|
utils_router = Router()
|
|
29
76
|
|
|
@@ -182,32 +229,111 @@ async def process_events(session_id: str, events: list, user_id: int):
|
|
|
182
229
|
logger.info(f" 📝 Тип: {event_type}")
|
|
183
230
|
logger.info(f" 📄 Данные: {event_info}")
|
|
184
231
|
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
232
|
+
# Определяем категорию события и сохраняем в БД
|
|
233
|
+
event_id = None
|
|
234
|
+
should_execute_immediately = False
|
|
235
|
+
should_notify = False
|
|
188
236
|
|
|
189
|
-
# Уведомляем админов
|
|
190
|
-
await notify_admins_about_event(user_id, event)
|
|
191
|
-
logger.info(f" ✅ Админы уведомлены")
|
|
192
|
-
|
|
193
|
-
# Вызываем зарегистрированный обработчик события или задачи
|
|
194
237
|
try:
|
|
238
|
+
# Проверяем зарегистрированные обработчики через роутер-менеджер
|
|
239
|
+
from ..core.decorators import get_router_manager, _event_handlers, _scheduled_tasks, _global_handlers
|
|
240
|
+
|
|
241
|
+
# Получаем обработчики из роутеров или fallback к старым декораторам
|
|
242
|
+
router_manager = get_router_manager()
|
|
243
|
+
if router_manager:
|
|
244
|
+
event_handlers = router_manager.get_event_handlers()
|
|
245
|
+
scheduled_tasks = router_manager.get_scheduled_tasks()
|
|
246
|
+
global_handlers = router_manager.get_global_handlers()
|
|
247
|
+
else:
|
|
248
|
+
event_handlers = _event_handlers
|
|
249
|
+
scheduled_tasks = _scheduled_tasks
|
|
250
|
+
global_handlers = _global_handlers
|
|
251
|
+
|
|
195
252
|
# Сначала пробуем как обычное событие
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
253
|
+
if event_type in event_handlers:
|
|
254
|
+
try:
|
|
255
|
+
logger.info(f" 🎯 Сохраняем как user_event: '{event_type}'")
|
|
256
|
+
event_id = await save_immediate_event(event_type, user_id, event_info, session_id)
|
|
257
|
+
should_execute_immediately = True
|
|
258
|
+
logger.info(f" 💾 Событие сохранено в БД: {event_id}")
|
|
259
|
+
except ValueError as e:
|
|
260
|
+
if "once_only=True" in str(e):
|
|
261
|
+
logger.info(f" 🔄 Событие '{event_type}' уже обрабатывалось, пропускаем")
|
|
262
|
+
continue
|
|
263
|
+
else:
|
|
264
|
+
raise
|
|
265
|
+
|
|
266
|
+
# Если не user_event, пробуем как запланированную задачу
|
|
267
|
+
elif event_type in scheduled_tasks:
|
|
268
|
+
try:
|
|
269
|
+
delay_seconds = parse_time_expression(event_info)
|
|
270
|
+
logger.info(f" ⏰ Сохраняем как scheduled_task: '{event_type}' через {delay_seconds}с")
|
|
271
|
+
result = await schedule_task_for_later_with_db(event_type, user_id, event_info, delay_seconds, session_id)
|
|
272
|
+
event_id = result['event_id']
|
|
273
|
+
should_notify = result.get('notify', False)
|
|
274
|
+
logger.info(f" 💾 Задача сохранена в БД: {event_id}")
|
|
275
|
+
|
|
276
|
+
except ValueError as e:
|
|
277
|
+
if "once_only=True" in str(e):
|
|
278
|
+
logger.info(f" 🔄 Задача '{event_type}' уже запланирована, пропускаем")
|
|
279
|
+
continue
|
|
280
|
+
else:
|
|
281
|
+
logger.error(f" ❌ Ошибка парсинга времени для scheduled_task '{event_type}': {e}")
|
|
282
|
+
# Fallback - планируем через 1 час
|
|
283
|
+
result = await schedule_task_for_later_with_db(event_type, user_id, event_info, 3600, session_id)
|
|
284
|
+
event_id = result['event_id']
|
|
285
|
+
should_notify = result.get('notify', False)
|
|
286
|
+
logger.info(f" ⏰ Задача сохранена с fallback временем (1 час): {event_id}")
|
|
287
|
+
|
|
288
|
+
# Если не scheduled_task, пробуем как глобальный обработчик
|
|
289
|
+
elif event_type in global_handlers:
|
|
290
|
+
try:
|
|
291
|
+
delay_seconds = parse_time_expression(event_info)
|
|
292
|
+
logger.info(f" 🌍 Сохраняем как global_handler: '{event_type}' через {delay_seconds}с")
|
|
293
|
+
result = await schedule_global_handler_for_later_with_db(event_type, delay_seconds, event_info)
|
|
294
|
+
event_id = result['event_id']
|
|
295
|
+
should_notify = result.get('notify', False)
|
|
296
|
+
logger.info(f" 💾 Глобальное событие сохранено в БД: {event_id}")
|
|
297
|
+
|
|
298
|
+
except ValueError as e:
|
|
299
|
+
if "once_only=True" in str(e):
|
|
300
|
+
logger.info(f" 🔄 Глобальное событие '{event_type}' уже запланировано, пропускаем")
|
|
301
|
+
continue
|
|
302
|
+
else:
|
|
303
|
+
logger.error(f" ❌ Ошибка парсинга времени для global_handler '{event_type}': {e}")
|
|
304
|
+
# Fallback - планируем через 1 час
|
|
305
|
+
result = await schedule_global_handler_for_later_with_db(event_type, 3600, event_info)
|
|
306
|
+
event_id = result['event_id']
|
|
307
|
+
should_notify = result.get('notify', False)
|
|
308
|
+
logger.info(f" 🌍 Глобальное событие сохранено с fallback временем (1 час): {event_id}")
|
|
309
|
+
|
|
310
|
+
else:
|
|
311
|
+
logger.warning(f" ⚠️ Обработчик '{event_type}' не найден среди зарегистрированных")
|
|
312
|
+
|
|
313
|
+
# Выполняем немедленные события
|
|
314
|
+
if should_execute_immediately and event_id:
|
|
315
|
+
try:
|
|
316
|
+
result = await execute_event_handler(event_type, user_id, event_info)
|
|
317
|
+
should_notify = result.get('notify', False)
|
|
318
|
+
await update_event_result(event_id, 'completed', result)
|
|
319
|
+
logger.info(f" ✅ Событие выполнено: {result}")
|
|
320
|
+
except Exception as e:
|
|
321
|
+
await update_event_result(event_id, 'failed', None, str(e))
|
|
322
|
+
logger.error(f" ❌ Ошибка выполнения события: {e}")
|
|
323
|
+
|
|
205
324
|
except ValueError as e:
|
|
206
325
|
logger.warning(f" ⚠️ Обработчик/задача не найдены: {e}")
|
|
207
326
|
except Exception as e:
|
|
208
327
|
logger.error(f" ❌ Ошибка в обработчике/задаче: {e}")
|
|
209
328
|
logger.exception(" Стек ошибки:")
|
|
210
329
|
|
|
330
|
+
# Уведомляем админов только если result.notify = True
|
|
331
|
+
if should_notify:
|
|
332
|
+
await notify_admins_about_event(user_id, event)
|
|
333
|
+
logger.info(f" ✅ Админы уведомлены")
|
|
334
|
+
else:
|
|
335
|
+
logger.info(f" 🔕 Уведомления админам отключены для '{event_type}'")
|
|
336
|
+
|
|
211
337
|
except Exception as e:
|
|
212
338
|
logger.error(f"❌ Ошибка обработки события {event}: {e}")
|
|
213
339
|
logger.exception("Стек ошибки:")
|
|
@@ -302,34 +428,30 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
|
|
|
302
428
|
parse_mode = config.MESSAGE_PARSE_MODE if config.MESSAGE_PARSE_MODE != 'None' else None
|
|
303
429
|
logger.info(f" 🔧 Parse mode: {parse_mode}")
|
|
304
430
|
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if files_info:
|
|
331
|
-
final_text = final_text.strip() + "".join(files_info)
|
|
332
|
-
logger.info(f" ✨ Добавлена информация о {len(files_list)} файлах и {len(directories_list)} каталогах")
|
|
431
|
+
# Получаем user_id и импортируем supabase_client
|
|
432
|
+
user_id = message.from_user.id
|
|
433
|
+
supabase_client = get_global_var('supabase_client')
|
|
434
|
+
|
|
435
|
+
# Текст уже готов, используем как есть
|
|
436
|
+
final_text = text
|
|
437
|
+
|
|
438
|
+
# Работаем с переданными файлами и каталогами
|
|
439
|
+
logger.info(f" 📦 Передано файлов: {files_list}")
|
|
440
|
+
logger.info(f" 📂 Передано каталогов: {directories_list}")
|
|
441
|
+
|
|
442
|
+
# Получаем список уже отправленных файлов и каталогов
|
|
443
|
+
sent_files = await supabase_client.get_sent_files(user_id)
|
|
444
|
+
sent_directories = await supabase_client.get_sent_directories(user_id)
|
|
445
|
+
|
|
446
|
+
logger.info(f" 📋 Уже отправлено файлов: {sent_files}")
|
|
447
|
+
logger.info(f" 📋 Уже отправлено каталогов: {sent_directories}")
|
|
448
|
+
|
|
449
|
+
# Фильтруем файлы и каталоги, которые уже отправлялись
|
|
450
|
+
actual_files_list = [f for f in files_list if f not in sent_files]
|
|
451
|
+
actual_directories_list = [d for d in directories_list if str(d) not in sent_directories]
|
|
452
|
+
|
|
453
|
+
logger.info(f" 🆕 После фильтрации файлов: {actual_files_list}")
|
|
454
|
+
logger.info(f" 🆕 После фильтрации каталогов: {actual_directories_list}")
|
|
333
455
|
|
|
334
456
|
|
|
335
457
|
# Проверяем, что есть что отправлять
|
|
@@ -339,30 +461,36 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
|
|
|
339
461
|
final_text = "Ошибка формирования ответа. Попробуйте еще раз."
|
|
340
462
|
|
|
341
463
|
logger.info(f"📱 Подготовка сообщения: {len(final_text)} символов")
|
|
464
|
+
logger.info(f" 📦 Файлов для обработки: {actual_files_list}")
|
|
465
|
+
logger.info(f" 📂 Каталогов для обработки: {actual_directories_list}")
|
|
342
466
|
|
|
343
467
|
# Проверяем наличие файлов для отправки
|
|
344
|
-
if
|
|
468
|
+
if actual_files_list or actual_directories_list:
|
|
345
469
|
# Функция определения типа медиа по расширению
|
|
346
470
|
def get_media_type(file_path: str) -> str:
|
|
347
471
|
ext = Path(file_path).suffix.lower()
|
|
348
472
|
if ext in {'.jpg', '.jpeg', '.png'}:
|
|
349
473
|
return 'photo'
|
|
350
|
-
elif ext in {'.mp4'}:
|
|
474
|
+
elif ext in {'.mp4', '.mov'}:
|
|
351
475
|
return 'video'
|
|
352
476
|
else:
|
|
353
477
|
return 'document'
|
|
354
478
|
|
|
355
479
|
# Создаем списки для разных типов файлов
|
|
356
|
-
|
|
480
|
+
video_files = [] # для видео
|
|
481
|
+
photo_files = [] # для фото
|
|
357
482
|
document_files = [] # для документов
|
|
358
483
|
|
|
359
484
|
# Функция обработки файла
|
|
360
485
|
def process_file(file_path: Path, source: str = ""):
|
|
361
486
|
if file_path.is_file():
|
|
362
487
|
media_type = get_media_type(str(file_path))
|
|
363
|
-
if media_type
|
|
364
|
-
|
|
365
|
-
logger.info(f"
|
|
488
|
+
if media_type == 'video':
|
|
489
|
+
video_files.append(file_path)
|
|
490
|
+
logger.info(f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}")
|
|
491
|
+
elif media_type == 'photo':
|
|
492
|
+
photo_files.append(file_path)
|
|
493
|
+
logger.info(f" 📸 Добавлено фото{f' из {source}' if source else ''}: {file_path.name}")
|
|
366
494
|
else:
|
|
367
495
|
document_files.append(file_path)
|
|
368
496
|
logger.info(f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}")
|
|
@@ -370,54 +498,58 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
|
|
|
370
498
|
logger.warning(f" ⚠️ Файл не найден: {file_path}")
|
|
371
499
|
|
|
372
500
|
# Обрабатываем прямые файлы
|
|
373
|
-
for file_name in
|
|
501
|
+
for file_name in actual_files_list:
|
|
374
502
|
try:
|
|
375
|
-
|
|
376
|
-
config = get_global_var('config')
|
|
377
|
-
bot_id = config.BOT_ID if config else "unknown"
|
|
378
|
-
file_path = Path(f"bots/{bot_id}/files/{file_name}")
|
|
379
|
-
process_file(file_path)
|
|
503
|
+
process_file(Path(f"files/{file_name}"))
|
|
380
504
|
except Exception as e:
|
|
381
505
|
logger.error(f" ❌ Ошибка обработки файла {file_name}: {e}")
|
|
382
506
|
|
|
383
507
|
# Обрабатываем файлы из каталогов
|
|
384
|
-
for dir_name in
|
|
385
|
-
|
|
386
|
-
config = get_global_var('config')
|
|
387
|
-
bot_id = config.BOT_ID if config else "unknown"
|
|
388
|
-
dir_path = Path(f"bots/{bot_id}/{dir_name}")
|
|
508
|
+
for dir_name in actual_directories_list:
|
|
509
|
+
dir_name = Path(dir_name)
|
|
389
510
|
try:
|
|
390
|
-
if
|
|
391
|
-
for file_path in
|
|
511
|
+
if dir_name.is_dir():
|
|
512
|
+
for file_path in dir_name.iterdir():
|
|
392
513
|
try:
|
|
393
|
-
process_file(file_path,
|
|
514
|
+
process_file(file_path, dir_name)
|
|
394
515
|
except Exception as e:
|
|
395
516
|
logger.error(f" ❌ Ошибка обработки файла {file_path}: {e}")
|
|
396
517
|
else:
|
|
397
|
-
logger.warning(f" ⚠️ Каталог не найден: {
|
|
518
|
+
logger.warning(f" ⚠️ Каталог не найден: {dir_name}")
|
|
398
519
|
except Exception as e:
|
|
399
|
-
logger.error(f" ❌ Ошибка обработки каталога {
|
|
520
|
+
logger.error(f" ❌ Ошибка обработки каталога {dir_name}: {e}")
|
|
400
521
|
|
|
401
|
-
#
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
522
|
+
# Списки для отслеживания реально отправленных файлов
|
|
523
|
+
sent_files_to_save = []
|
|
524
|
+
sent_dirs_to_save = []
|
|
525
|
+
|
|
526
|
+
# 1. Отправляем видео (если есть)
|
|
527
|
+
if video_files:
|
|
528
|
+
video_group = MediaGroupBuilder()
|
|
529
|
+
for file_path in video_files:
|
|
530
|
+
video_group.add_video(media=FSInputFile(str(file_path)))
|
|
410
531
|
|
|
411
|
-
|
|
412
|
-
if
|
|
413
|
-
|
|
414
|
-
logger.info(f" ✅ Отправлено
|
|
415
|
-
else:
|
|
416
|
-
# Если нет медиа, отправляем просто текст
|
|
417
|
-
result = await message.answer(final_text, parse_mode=parse_mode, **kwargs)
|
|
418
|
-
logger.info(f" ✅ Отправлен текст сообщения")
|
|
532
|
+
videos = video_group.build()
|
|
533
|
+
if videos:
|
|
534
|
+
await message.answer_media_group(media=videos)
|
|
535
|
+
logger.info(f" ✅ Отправлено {len(videos)} видео")
|
|
419
536
|
|
|
420
|
-
# Отправляем
|
|
537
|
+
# 2. Отправляем фото (если есть)
|
|
538
|
+
if photo_files:
|
|
539
|
+
photo_group = MediaGroupBuilder()
|
|
540
|
+
for file_path in photo_files:
|
|
541
|
+
photo_group.add_photo(media=FSInputFile(str(file_path)))
|
|
542
|
+
|
|
543
|
+
photos = photo_group.build()
|
|
544
|
+
if photos:
|
|
545
|
+
await message.answer_media_group(media=photos)
|
|
546
|
+
logger.info(f" ✅ Отправлено {len(photos)} фото")
|
|
547
|
+
|
|
548
|
+
# 3. Отправляем текст
|
|
549
|
+
result = await message.answer(final_text, parse_mode=parse_mode)
|
|
550
|
+
logger.info(f" ✅ Отправлен текст сообщения")
|
|
551
|
+
|
|
552
|
+
# 4. Отправляем документы (если есть)
|
|
421
553
|
if document_files:
|
|
422
554
|
doc_group = MediaGroupBuilder()
|
|
423
555
|
for file_path in document_files:
|
|
@@ -426,7 +558,32 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
|
|
|
426
558
|
docs = doc_group.build()
|
|
427
559
|
if docs:
|
|
428
560
|
await message.answer_media_group(media=docs)
|
|
429
|
-
logger.info(f" ✅
|
|
561
|
+
logger.info(f" ✅ Отправлено {len(docs)} документов")
|
|
562
|
+
|
|
563
|
+
# 5. Собираем список реально отправленных файлов и каталогов
|
|
564
|
+
# Если были отправлены файлы из actual_files_list - сохраняем их
|
|
565
|
+
if video_files or photo_files or document_files:
|
|
566
|
+
# Сохраняем прямые файлы из actual_files_list (если отправлены)
|
|
567
|
+
sent_files_to_save.extend(actual_files_list)
|
|
568
|
+
logger.info(f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}")
|
|
569
|
+
# Сохраняем каталоги из actual_directories_list (если отправлены файлы из них)
|
|
570
|
+
sent_dirs_to_save.extend([str(d) for d in actual_directories_list])
|
|
571
|
+
logger.info(f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}")
|
|
572
|
+
|
|
573
|
+
# 6. Обновляем информацию в БД
|
|
574
|
+
if sent_files_to_save or sent_dirs_to_save:
|
|
575
|
+
try:
|
|
576
|
+
if sent_files_to_save:
|
|
577
|
+
logger.info(f" 💾 Сохраняем файлы в БД: {sent_files_to_save}")
|
|
578
|
+
await supabase_client.add_sent_files(user_id, sent_files_to_save)
|
|
579
|
+
if sent_dirs_to_save:
|
|
580
|
+
logger.info(f" 💾 Сохраняем каталоги в БД: {sent_dirs_to_save}")
|
|
581
|
+
await supabase_client.add_sent_directories(user_id, sent_dirs_to_save)
|
|
582
|
+
logger.info(f" ✅ Обновлена информация о отправленных файлах в БД")
|
|
583
|
+
except Exception as e:
|
|
584
|
+
logger.error(f" ❌ Ошибка обновления информации о файлах в БД: {e}")
|
|
585
|
+
else:
|
|
586
|
+
logger.info(f" ℹ️ Нет новых файлов для сохранения в БД")
|
|
430
587
|
|
|
431
588
|
return result
|
|
432
589
|
else:
|
|
@@ -436,6 +593,14 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
|
|
|
436
593
|
return result
|
|
437
594
|
|
|
438
595
|
except Exception as e:
|
|
596
|
+
# Проверяем, является ли ошибка блокировкой бота
|
|
597
|
+
if "Forbidden: bot was blocked by the user" in str(e):
|
|
598
|
+
logger.warning(f"🚫 Бот заблокирован пользователем {user_id}")
|
|
599
|
+
return None
|
|
600
|
+
elif "TelegramForbiddenError" in str(type(e).__name__):
|
|
601
|
+
logger.warning(f"🚫 Бот заблокирован пользователем {user_id}")
|
|
602
|
+
return None
|
|
603
|
+
|
|
439
604
|
logger.error(f"❌ ОШИБКА в send_message: {e}")
|
|
440
605
|
logger.exception("Полный стек ошибки send_message:")
|
|
441
606
|
|
|
@@ -446,6 +611,14 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
|
|
|
446
611
|
logger.info(f"✅ Запасное сообщение отправлено")
|
|
447
612
|
return result
|
|
448
613
|
except Exception as e2:
|
|
614
|
+
# Проверяем и здесь блокировку бота
|
|
615
|
+
if "Forbidden: bot was blocked by the user" in str(e2):
|
|
616
|
+
logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
|
|
617
|
+
return None
|
|
618
|
+
elif "TelegramForbiddenError" in str(type(e2).__name__):
|
|
619
|
+
logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
|
|
620
|
+
return None
|
|
621
|
+
|
|
449
622
|
logger.error(f"❌ Даже запасное сообщение не отправилось: {e2}")
|
|
450
623
|
raise
|
|
451
624
|
|
|
@@ -647,19 +820,19 @@ OpenAI API: {'✅' if openai_status else '❌'}
|
|
|
647
820
|
|
|
648
821
|
|
|
649
822
|
def parse_utm_from_start_param(start_param: str) -> dict:
|
|
650
|
-
"""Парсит UTM-метки из start параметра в формате
|
|
823
|
+
"""Парсит UTM-метки из start параметра в формате source-vk_campaign-summer2025
|
|
651
824
|
|
|
652
825
|
Args:
|
|
653
|
-
start_param: строка вида '
|
|
826
|
+
start_param: строка вида 'source-vk_campaign-summer2025' или полная ссылка
|
|
654
827
|
|
|
655
828
|
Returns:
|
|
656
829
|
dict: {'utm_source': 'vk', 'utm_campaign': 'summer2025'}
|
|
657
830
|
|
|
658
831
|
Examples:
|
|
659
|
-
>>> parse_utm_from_start_param('
|
|
832
|
+
>>> parse_utm_from_start_param('source-vk_campaign-summer2025')
|
|
660
833
|
{'utm_source': 'vk', 'utm_campaign': 'summer2025'}
|
|
661
834
|
|
|
662
|
-
>>> parse_utm_from_start_param('https://t.me/bot?start=
|
|
835
|
+
>>> parse_utm_from_start_param('https://t.me/bot?start=source-vk_campaign-summer2025')
|
|
663
836
|
{'utm_source': 'vk', 'utm_campaign': 'summer2025'}
|
|
664
837
|
"""
|
|
665
838
|
import re
|
|
@@ -676,15 +849,15 @@ def parse_utm_from_start_param(start_param: str) -> dict:
|
|
|
676
849
|
else:
|
|
677
850
|
return {}
|
|
678
851
|
|
|
679
|
-
# Парсим формат:
|
|
852
|
+
# Парсим новый формат: source-vk_campaign-summer2025
|
|
680
853
|
if '_' in start_param and '-' in start_param:
|
|
681
854
|
parts = start_param.split('_')
|
|
682
855
|
for part in parts:
|
|
683
856
|
if '-' in part:
|
|
684
857
|
key, value = part.split('-', 1)
|
|
685
|
-
# Преобразуем
|
|
686
|
-
if key
|
|
687
|
-
key = 'utm_' + key
|
|
858
|
+
# Преобразуем source в utm_source
|
|
859
|
+
if key in ['source', 'medium', 'campaign', 'content', 'term']:
|
|
860
|
+
key = 'utm_' + key
|
|
688
861
|
utm_data[key] = value
|
|
689
862
|
|
|
690
863
|
except Exception as e:
|