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.

Files changed (37) hide show
  1. smart_bot_factory/__init__.py +0 -48
  2. smart_bot_factory/admin/admin_logic.py +11 -11
  3. smart_bot_factory/cli.py +299 -106
  4. smart_bot_factory/clients/__init__.py +33 -0
  5. smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +2 -0
  6. smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +95 -28
  7. smart_bot_factory/core/__init__.py +43 -22
  8. smart_bot_factory/core/bot_utils.py +268 -95
  9. smart_bot_factory/core/conversation_manager.py +542 -535
  10. smart_bot_factory/core/decorators.py +943 -229
  11. smart_bot_factory/core/globals.py +68 -0
  12. smart_bot_factory/core/message_sender.py +6 -6
  13. smart_bot_factory/core/router.py +172 -0
  14. smart_bot_factory/core/router_manager.py +165 -0
  15. smart_bot_factory/creation/__init__.py +1 -2
  16. smart_bot_factory/creation/bot_builder.py +116 -8
  17. smart_bot_factory/creation/bot_testing.py +74 -13
  18. smart_bot_factory/handlers/handlers.py +10 -2
  19. smart_bot_factory/integrations/__init__.py +1 -0
  20. smart_bot_factory/integrations/supabase_client.py +272 -2
  21. smart_bot_factory/utm_link_generator.py +106 -0
  22. smart_bot_factory-0.1.5.dist-info/METADATA +466 -0
  23. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/RECORD +26 -31
  24. smart_bot_factory/configs/growthmed-helper/env_example.txt +0 -1
  25. smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +0 -9
  26. smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +0 -582
  27. smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +0 -66
  28. smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +0 -232
  29. smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +0 -28
  30. smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +0 -7
  31. smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +0 -16
  32. 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
  33. smart_bot_factory/uv.lock +0 -2004
  34. smart_bot_factory-0.1.3.dist-info/METADATA +0 -126
  35. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/WHEEL +0 -0
  36. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/entry_points.txt +0 -0
  37. {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 execute_event_handler, execute_scheduled_task
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
- await supabase_client.add_session_event(session_id, event_type, event_info)
187
- logger.info(f" ✅ Событие сохранено в БД")
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
- try:
197
- logger.info(f" 🎯 Вызываем обработчик события '{event_type}'")
198
- result = await execute_event_handler(event_type, user_id, event_info)
199
- logger.info(f" ✅ Обработчик события вернул: {result}")
200
- except ValueError:
201
- # Если обработчик события не найден, пробуем как запланированную задачу
202
- logger.info(f" ⏰ Пробуем как запланированную задачу '{event_type}'")
203
- result = await execute_scheduled_task(event_type, user_id, event_info)
204
- logger.info(f" Задача выполнена: {result}")
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
- # В режиме отладки не скрываем JSON
306
- if config.DEBUG_MODE:
307
- final_text = text
308
- logger.info(f" 🐛 Отправляем полный текст (debug режим)")
309
- else:
310
- # Убираем JSON если он есть
311
- final_text, json_metadata = parse_ai_response(text)
312
- logger.info(f" ✂️ После очистки JSON: {len(final_text)} символов")
313
-
314
- # Добавляем информацию о файлах и каталогах в конец сообщения
315
- if json_metadata:
316
- logger.info(f" 📊 Найден JSON: {json_metadata}")
317
-
318
- files_list = json_metadata.get('файлы', [])
319
- directories_list = json_metadata.get('каталоги', [])
320
-
321
- files_info = []
322
- if files_list:
323
- files_str = "\n".join(f"• {file}" for file in files_list)
324
- files_info.append(f"\n\n📎 Доступные файлы:\n{files_str}")
325
-
326
- if directories_list:
327
- dirs_str = "\n".join(f" {directory}" for directory in directories_list)
328
- files_info.append(f"\n\n📂 Доступные каталоги:\n{dirs_str}")
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 files_list or directories_list:
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
- media_files = [] # для фото и видео
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 in ('photo', 'video'):
364
- media_files.append((file_path, media_type))
365
- logger.info(f" 📸 Добавлен медиафайл{f' из {source}' if source else ''}: {file_path.name}")
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 files_list:
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 directories_list:
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 dir_path.is_dir():
391
- for file_path in dir_path.iterdir():
511
+ if dir_name.is_dir():
512
+ for file_path in dir_name.iterdir():
392
513
  try:
393
- process_file(file_path, dir_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" ⚠️ Каталог не найден: {dir_path}")
518
+ logger.warning(f" ⚠️ Каталог не найден: {dir_name}")
398
519
  except Exception as e:
399
- logger.error(f" ❌ Ошибка обработки каталога {dir_path}: {e}")
520
+ logger.error(f" ❌ Ошибка обработки каталога {dir_name}: {e}")
400
521
 
401
- # Отправляем сообщение с медиа (если есть)
402
- if media_files:
403
- # Создаем медиа-группу с фото/видео и текстом
404
- media_group = MediaGroupBuilder(caption=final_text)
405
- for file_path, media_type in media_files:
406
- if media_type == 'photo':
407
- media_group.add_photo(media=FSInputFile(str(file_path)))
408
- else: # video
409
- media_group.add_video(media=FSInputFile(str(file_path)))
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
- media = media_group.build()
412
- if media:
413
- result = await message.answer_media_group(media=media)
414
- logger.info(f" ✅ Отправлено сообщение с {len(media)} медиафайлами")
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" ✅ Отправлена группа документов: {len(docs)} файлов")
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 параметра в формате utmSource-vk_utmCampaign-summer2025
823
+ """Парсит UTM-метки из start параметра в формате source-vk_campaign-summer2025
651
824
 
652
825
  Args:
653
- start_param: строка вида 'utmSource-vk_utmCampaign-summer2025' или полная ссылка
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('utmSource-vk_utmCampaign-summer2025')
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=utmSource-vk_utmCampaign-summer2025')
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
- # Парсим формат: utmSource-vk_utmCampaign-summer2025
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
- # Преобразуем utmSource в utm_source
686
- if key.startswith('utm'):
687
- key = 'utm_' + key[3:].lower()
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: