smart-bot-factory 0.1.4__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 (35) hide show
  1. smart_bot_factory/__init__.py +0 -30
  2. smart_bot_factory/admin/admin_logic.py +11 -11
  3. smart_bot_factory/cli.py +138 -71
  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 +251 -88
  9. smart_bot_factory/core/conversation_manager.py +542 -535
  10. smart_bot_factory/core/decorators.py +943 -230
  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-0.1.5.dist-info/METADATA +466 -0
  22. {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.5.dist-info}/RECORD +25 -30
  23. smart_bot_factory/configs/growthmed-helper/env_example.txt +0 -1
  24. smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +0 -9
  25. smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +0 -582
  26. smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +0 -66
  27. smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +0 -232
  28. smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +0 -28
  29. smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +0 -7
  30. smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +0 -16
  31. 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
  32. smart_bot_factory-0.1.4.dist-info/METADATA +0 -126
  33. {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.5.dist-info}/WHEEL +0 -0
  34. {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.5.dist-info}/entry_points.txt +0 -0
  35. {smart_bot_factory-0.1.4.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,28 +229,97 @@ 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" ✅ Событие сохранено в БД")
188
-
189
- # Вызываем зарегистрированный обработчик события или задачи
232
+ # Определяем категорию события и сохраняем в БД
233
+ event_id = None
234
+ should_execute_immediately = False
190
235
  should_notify = False
236
+
191
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
+
192
252
  # Сначала пробуем как обычное событие
193
- try:
194
- logger.info(f" 🎯 Вызываем обработчик события '{event_type}'")
195
- result = await execute_event_handler(event_type, user_id, event_info)
196
- logger.info(f" ✅ Обработчик события вернул: {result}")
197
-
198
- should_notify = result.get('notify', False)
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}")
199
275
 
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}")
205
-
206
- should_notify = result.get('notify', False)
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}")
207
323
 
208
324
  except ValueError as e:
209
325
  logger.warning(f" ⚠️ Обработчик/задача не найдены: {e}")
@@ -312,34 +428,30 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
312
428
  parse_mode = config.MESSAGE_PARSE_MODE if config.MESSAGE_PARSE_MODE != 'None' else None
313
429
  logger.info(f" 🔧 Parse mode: {parse_mode}")
314
430
 
315
- # В режиме отладки не скрываем JSON
316
- if config.DEBUG_MODE:
317
- final_text = text
318
- logger.info(f" 🐛 Отправляем полный текст (debug режим)")
319
- else:
320
- # Убираем JSON если он есть
321
- final_text, json_metadata = parse_ai_response(text)
322
- logger.info(f" ✂️ После очистки JSON: {len(final_text)} символов")
323
-
324
- # Добавляем информацию о файлах и каталогах в конец сообщения
325
- if json_metadata:
326
- logger.info(f" 📊 Найден JSON: {json_metadata}")
327
-
328
- files_list = json_metadata.get('файлы', [])
329
- directories_list = json_metadata.get('каталоги', [])
330
-
331
- files_info = []
332
- if files_list:
333
- files_str = "\n".join(f"• {file}" for file in files_list)
334
- files_info.append(f"\n\n📎 Доступные файлы:\n{files_str}")
335
-
336
- if directories_list:
337
- dirs_str = "\n".join(f" {directory}" for directory in directories_list)
338
- files_info.append(f"\n\n📂 Доступные каталоги:\n{dirs_str}")
339
-
340
- if files_info:
341
- final_text = final_text.strip() + "".join(files_info)
342
- 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}")
343
455
 
344
456
 
345
457
  # Проверяем, что есть что отправлять
@@ -349,30 +461,36 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
349
461
  final_text = "Ошибка формирования ответа. Попробуйте еще раз."
350
462
 
351
463
  logger.info(f"📱 Подготовка сообщения: {len(final_text)} символов")
464
+ logger.info(f" 📦 Файлов для обработки: {actual_files_list}")
465
+ logger.info(f" 📂 Каталогов для обработки: {actual_directories_list}")
352
466
 
353
467
  # Проверяем наличие файлов для отправки
354
- if files_list or directories_list:
468
+ if actual_files_list or actual_directories_list:
355
469
  # Функция определения типа медиа по расширению
356
470
  def get_media_type(file_path: str) -> str:
357
471
  ext = Path(file_path).suffix.lower()
358
472
  if ext in {'.jpg', '.jpeg', '.png'}:
359
473
  return 'photo'
360
- elif ext in {'.mp4'}:
474
+ elif ext in {'.mp4', '.mov'}:
361
475
  return 'video'
362
476
  else:
363
477
  return 'document'
364
478
 
365
479
  # Создаем списки для разных типов файлов
366
- media_files = [] # для фото и видео
480
+ video_files = [] # для видео
481
+ photo_files = [] # для фото
367
482
  document_files = [] # для документов
368
483
 
369
484
  # Функция обработки файла
370
485
  def process_file(file_path: Path, source: str = ""):
371
486
  if file_path.is_file():
372
487
  media_type = get_media_type(str(file_path))
373
- if media_type in ('photo', 'video'):
374
- media_files.append((file_path, media_type))
375
- 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}")
376
494
  else:
377
495
  document_files.append(file_path)
378
496
  logger.info(f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}")
@@ -380,54 +498,58 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
380
498
  logger.warning(f" ⚠️ Файл не найден: {file_path}")
381
499
 
382
500
  # Обрабатываем прямые файлы
383
- for file_name in files_list:
501
+ for file_name in actual_files_list:
384
502
  try:
385
- # Получаем путь к папке бота
386
- config = get_global_var('config')
387
- bot_id = config.BOT_ID if config else "unknown"
388
- file_path = Path(f"bots/{bot_id}/files/{file_name}")
389
- process_file(file_path)
503
+ process_file(Path(f"files/{file_name}"))
390
504
  except Exception as e:
391
505
  logger.error(f" ❌ Ошибка обработки файла {file_name}: {e}")
392
506
 
393
507
  # Обрабатываем файлы из каталогов
394
- for dir_name in directories_list:
395
- # Получаем путь к каталогу относительно папки бота
396
- config = get_global_var('config')
397
- bot_id = config.BOT_ID if config else "unknown"
398
- dir_path = Path(f"bots/{bot_id}/{dir_name}")
508
+ for dir_name in actual_directories_list:
509
+ dir_name = Path(dir_name)
399
510
  try:
400
- if dir_path.is_dir():
401
- for file_path in dir_path.iterdir():
511
+ if dir_name.is_dir():
512
+ for file_path in dir_name.iterdir():
402
513
  try:
403
- process_file(file_path, dir_path)
514
+ process_file(file_path, dir_name)
404
515
  except Exception as e:
405
516
  logger.error(f" ❌ Ошибка обработки файла {file_path}: {e}")
406
517
  else:
407
- logger.warning(f" ⚠️ Каталог не найден: {dir_path}")
518
+ logger.warning(f" ⚠️ Каталог не найден: {dir_name}")
408
519
  except Exception as e:
409
- logger.error(f" ❌ Ошибка обработки каталога {dir_path}: {e}")
520
+ logger.error(f" ❌ Ошибка обработки каталога {dir_name}: {e}")
410
521
 
411
- # Отправляем сообщение с медиа (если есть)
412
- if media_files:
413
- # Создаем медиа-группу с фото/видео и текстом
414
- media_group = MediaGroupBuilder(caption=final_text)
415
- for file_path, media_type in media_files:
416
- if media_type == 'photo':
417
- media_group.add_photo(media=FSInputFile(str(file_path)))
418
- else: # video
419
- 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)))
420
531
 
421
- media = media_group.build()
422
- if media:
423
- result = await message.answer_media_group(media=media)
424
- logger.info(f" ✅ Отправлено сообщение с {len(media)} медиафайлами")
425
- else:
426
- # Если нет медиа, отправляем просто текст
427
- result = await message.answer(final_text, parse_mode=parse_mode, **kwargs)
428
- 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)} видео")
429
536
 
430
- # Отправляем документы отдельно (если есть)
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. Отправляем документы (если есть)
431
553
  if document_files:
432
554
  doc_group = MediaGroupBuilder()
433
555
  for file_path in document_files:
@@ -436,7 +558,32 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
436
558
  docs = doc_group.build()
437
559
  if docs:
438
560
  await message.answer_media_group(media=docs)
439
- 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" ℹ️ Нет новых файлов для сохранения в БД")
440
587
 
441
588
  return result
442
589
  else:
@@ -446,6 +593,14 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
446
593
  return result
447
594
 
448
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
+
449
604
  logger.error(f"❌ ОШИБКА в send_message: {e}")
450
605
  logger.exception("Полный стек ошибки send_message:")
451
606
 
@@ -456,6 +611,14 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
456
611
  logger.info(f"✅ Запасное сообщение отправлено")
457
612
  return result
458
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
+
459
622
  logger.error(f"❌ Даже запасное сообщение не отправилось: {e2}")
460
623
  raise
461
624