smart-bot-factory 0.1.4__py3-none-any.whl → 0.1.6__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 (38) 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 +147 -85
  4. smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +2 -0
  5. smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +95 -28
  6. smart_bot_factory/core/bot_utils.py +224 -88
  7. smart_bot_factory/core/conversation_manager.py +542 -535
  8. smart_bot_factory/core/decorators.py +927 -230
  9. smart_bot_factory/core/message_sender.py +2 -7
  10. smart_bot_factory/core/router.py +173 -0
  11. smart_bot_factory/core/router_manager.py +166 -0
  12. smart_bot_factory/creation/__init__.py +1 -2
  13. smart_bot_factory/creation/bot_builder.py +103 -13
  14. smart_bot_factory/creation/bot_testing.py +74 -13
  15. smart_bot_factory/event/__init__.py +12 -0
  16. smart_bot_factory/handlers/handlers.py +10 -2
  17. smart_bot_factory/integrations/supabase_client.py +272 -2
  18. smart_bot_factory/message/__init__.py +12 -0
  19. smart_bot_factory/router/__init__.py +9 -0
  20. smart_bot_factory/supabase/__init__.py +7 -0
  21. smart_bot_factory-0.1.6.dist-info/METADATA +466 -0
  22. {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.6.dist-info}/RECORD +26 -31
  23. smart_bot_factory/analytics/__init__.py +0 -7
  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/core/__init__.py +0 -22
  33. smart_bot_factory/integrations/__init__.py +0 -9
  34. smart_bot_factory-0.1.4.dist-info/METADATA +0 -126
  35. /smart_bot_factory/{configs/growthmed-helper/env_example.txt → supabase/example_usage.py} +0 -0
  36. {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.6.dist-info}/WHEEL +0 -0
  37. {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.6.dist-info}/entry_points.txt +0 -0
  38. {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.6.dist-info}/licenses/LICENSE +0 -0
@@ -2,65 +2,132 @@ scenarios:
2
2
  - name: "Быстрое решение по участию"
3
3
  steps:
4
4
  - user_input: "Привет, срочно нужна информация о конференции"
5
- expected_keywords: ["привет", "информация", "конференци"]
6
- forbidden_keywords: ["не спешите"]
5
+ expected_keywords:
6
+ - ["привет", "здравствуйте", "добро пожаловать", "добрый день"]
7
+ - ["информация", "данные", "подробности", "детали"]
8
+ - ["конференци", "мероприятие", "событие"]
9
+ forbidden_keywords: ["не спешите", "позже", "не торопитесь"]
7
10
 
8
11
  - user_input: "Завтра улетаю в командировку, успею зарегистрироваться?"
9
- expected_keywords: ["успеете", "регистрация", "быстро"]
10
- forbidden_keywords: ["поздно", "не успеете"]
12
+ expected_keywords:
13
+ - ["успеете", "успеете зарегистрироваться", "все получится"]
14
+ - ["регистрация", "запись", "участие"]
15
+ - ["быстро", "срочно", "экспресс", "ускоренно"]
16
+ forbidden_keywords: ["поздно", "не успеете", "время вышло"]
11
17
 
12
18
  - name: "Студент медвуза интересуется будущим"
13
19
  steps:
14
20
  - user_input: "Здравствуйте, я студент 5 курса медуниверситета"
15
- expected_keywords: ["здравствуйте", "студент", "медицин"]
16
- forbidden_keywords: ["рано для вас"]
21
+ expected_keywords:
22
+ - ["здравствуйте", "привет", "добро пожаловать"]
23
+ - ["студент", "обучающийся", "учащийся"]
24
+ - ["медицин", "медвуз", "университет", "медицинский"]
25
+ forbidden_keywords: ["рано для вас", "еще рано", "не подходит"]
17
26
 
18
27
  - user_input: "Стоит ли уже сейчас думать о бизнесе в медицине?"
19
- expected_keywords: ["стоит", "перспектив", "будущее"]
20
- forbidden_keywords: ["рано думать"]
28
+ expected_keywords:
29
+ - ["стоит", "правильно", "хорошая идея", "отличный подход"]
30
+ - ["перспектив", "возможности", "будущее", "карьера"]
31
+ - ["бизнес", "предпринимательство", "дело"]
32
+ forbidden_keywords: ["рано думать", "не стоит", "пока рано"]
21
33
 
22
34
  - user_input: "Есть ли льготы для студентов?"
23
- expected_keywords: ["льготы", "студент", "скидк"]
24
- forbidden_keywords: ["полная стоимость"]
35
+ expected_keywords:
36
+ - ["льготы", "скидки", "преференции", "льготные условия"]
37
+ - ["студент", "учащимся", "обучающимся"]
38
+ - ["специальные условия", "уменьшенная стоимость"]
39
+ forbidden_keywords: ["полная стоимость", "без скидок", "обычная цена"]
25
40
 
26
41
  - name: "Иностранный врач в России"
27
42
  steps:
28
43
  - user_input: "Hello, I am foreign doctor working in Moscow"
29
- expected_keywords: ["hello", "добро пожаловать", "moscow"]
30
- forbidden_keywords: ["только на русском"]
44
+ expected_keywords:
45
+ - ["hello", "hi", "добро пожаловать", "привет"]
46
+ - ["foreign", "иностранный", "зарубежный"]
47
+ - ["moscow", "москва", "россия"]
48
+ forbidden_keywords: ["только на русском", "only russian", "русский язык"]
31
49
 
32
50
  - user_input: "Is conference in English or Russian language?"
33
- expected_keywords: ["language", "english", "translation"]
34
- forbidden_keywords: ["только русский"]
51
+ expected_keywords:
52
+ - ["language", "язык", "english", "английский"]
53
+ - ["translation", "перевод", "переводим", "синхронный перевод"]
54
+ - ["both", "оба языка", "многоязычный"]
55
+ forbidden_keywords: ["только русский", "only russian", "без перевода"]
35
56
 
36
57
  - user_input: "I want to understand Russian medical business"
37
- expected_keywords: ["business", "russian", "understand"]
38
- forbidden_keywords: ["сложно будет"]
58
+ expected_keywords:
59
+ - ["business", "бизнес", "предпринимательство"]
60
+ - ["russian", "российский", "россии"]
61
+ - ["understand", "понять", "изучить", "разобраться"]
62
+ forbidden_keywords: ["сложно будет", "difficult", "не получится"]
39
63
 
40
64
  - name: "Пенсионер-врач ищет активность"
41
65
  steps:
42
66
  - user_input: "Добрый день, я врач на пенсии уже 3 года"
43
- expected_keywords: ["добрый день", "пенси", "врач"]
44
- forbidden_keywords: ["не актуально"]
67
+ expected_keywords:
68
+ - ["добрый день", "здравствуйте", "привет"]
69
+ - ["пенси", "пенсионер", "на пенсии", "отставной"]
70
+ - ["врач", "доктор", "медицинский работник"]
71
+ forbidden_keywords: ["не актуально", "не подходит", "слишком поздно"]
45
72
 
46
73
  - user_input: "Хочу быть в курсе современных тенденций"
47
- expected_keywords: ["современн", "тенденци", "актуальн"]
48
- forbidden_keywords: ["устарело"]
74
+ expected_keywords:
75
+ - ["современн", "актуальн", "новейш", "передов"]
76
+ - ["тенденци", "тренды", "направления", "развитие"]
77
+ - ["курс", "информирован", "знания", "обучение"]
78
+ forbidden_keywords: ["устарело", "не актуально", "пропустили время"]
49
79
 
50
80
  - user_input: "Подходит ли конференция для неработающих врачей?"
51
- expected_keywords: ["подходит", "для всех", "врачей"]
52
- forbidden_keywords: ["только для практикующих"]
81
+ expected_keywords:
82
+ - ["подходит", "идеально", "отлично", "рекомендуем"]
83
+ - ["для всех", "всем", "любому", "каждому"]
84
+ - ["врачей", "специалистов", "медиков", "докторов"]
85
+ forbidden_keywords: ["только для практикующих", "не подходит", "не рекомендуется"]
53
86
 
54
87
  - name: "Технический директор медцентра"
55
88
  steps:
56
89
  - user_input: "Здравствуйте, я отвечаю за IT в медицинском центре"
57
- expected_keywords: ["здравствуйте", "it", "медицинск"]
58
- forbidden_keywords: ["не ваша тема"]
90
+ expected_keywords:
91
+ - ["здравствуйте", "привет", "добро пожаловать"]
92
+ - ["it", "информационные технологии", "технологии", "айти"]
93
+ - ["медицинск", "медицинский", "клиника", "медцентр"]
94
+ forbidden_keywords: ["не ваша тема", "не подходит", "не актуально"]
59
95
 
60
96
  - user_input: "Есть ли секции по цифровизации медицины?"
61
- expected_keywords: ["цифровизация", "технологи", "it"]
62
- forbidden_keywords: ["только бизнес"]
97
+ expected_keywords:
98
+ - ["цифровизация", "цифровые технологии", "информатизация"]
99
+ - ["технологи", "инновации", "современные решения"]
100
+ - ["it", "программирование", "автоматизация"]
101
+ forbidden_keywords: ["только бизнес", "без технологий", "не рассматриваем"]
63
102
 
64
103
  - user_input: "Интересуют телемедицина и электронные карты"
65
- expected_keywords: ["телемедицина", "электронн", "карты"]
66
- forbidden_keywords: ["не рассматриваем"]
104
+ expected_keywords:
105
+ - ["телемедицина", "удаленная медицина", "дистанционные консультации"]
106
+ - ["электронн", "цифровые", "компьютерные"]
107
+ - ["карты", "медицинские карты", "история болезни"]
108
+ forbidden_keywords: ["не рассматриваем", "не актуально", "не обсуждаем"]
109
+
110
+ - name: "Смешанный формат синонимов - демонстрация новых возможностей"
111
+ steps:
112
+ - user_input: "Здравствуйте, хочу узнать подробности о мероприятии"
113
+ expected_keywords:
114
+ - "здравствуйте" # Одиночное слово
115
+ - ["хочу", "интересуюсь", "планирую"] # Группа синонимов
116
+ - ["подробности", "детали", "информация"] # Еще одна группа синонимов
117
+ - "мероприятие" # Еще одно одиночное слово
118
+ forbidden_keywords: []
119
+
120
+ - user_input: "Когда будет конференция и сколько стоит участие?"
121
+ expected_keywords:
122
+ - ["когда", "дата", "время", "расписание"] # Группа синонимов
123
+ - "конференция" # Одиночное слово
124
+ - ["стоит", "цена", "стоимость", "сколько"] # Группа синонимов
125
+ - ["участие", "регистрация", "вход"] # Группа синонимов
126
+ forbidden_keywords: ["не знаю", "неизвестно", "не определились"]
127
+
128
+ - user_input: "Есть ли скидки для студентов и врачей?"
129
+ expected_keywords:
130
+ - ["скидки", "льготы", "преференции"] # Группа синонимов
131
+ - "студентов" # Одиночное слово
132
+ - ["врачей", "докторов", "медиков"] # Группа синонимов
133
+ forbidden_keywords: ["полная стоимость", "без скидок"]
@@ -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,7 @@ def get_global_var(var_name):
24
31
 
25
32
  logger = logging.getLogger(__name__)
26
33
 
34
+
27
35
  # Создаем роутер для общих команд
28
36
  utils_router = Router()
29
37
 
@@ -182,28 +190,109 @@ async def process_events(session_id: str, events: list, user_id: int):
182
190
  logger.info(f" 📝 Тип: {event_type}")
183
191
  logger.info(f" 📄 Данные: {event_info}")
184
192
 
185
- # Сохраняем в БД
186
- await supabase_client.add_session_event(session_id, event_type, event_info)
187
- logger.info(f" ✅ Событие сохранено в БД")
188
-
189
- # Вызываем зарегистрированный обработчик события или задачи
193
+ # Определяем категорию события и сохраняем в БД
194
+ event_id = None
195
+ should_execute_immediately = False
190
196
  should_notify = False
197
+
191
198
  try:
199
+ # Проверяем зарегистрированные обработчики через роутер-менеджер
200
+ from ..core.decorators import get_router_manager, _event_handlers, _scheduled_tasks, _global_handlers
201
+
202
+ # Получаем обработчики из роутеров или fallback к старым декораторам
203
+ router_manager = get_router_manager()
204
+ if router_manager:
205
+ event_handlers = router_manager.get_event_handlers()
206
+ scheduled_tasks = router_manager.get_scheduled_tasks()
207
+ global_handlers = router_manager.get_global_handlers()
208
+ logger.debug(f"🔍 RouterManager найден: {len(global_handlers)} глобальных обработчиков")
209
+ else:
210
+ event_handlers = _event_handlers
211
+ scheduled_tasks = _scheduled_tasks
212
+ global_handlers = _global_handlers
213
+ logger.warning("⚠️ RouterManager не найден, используем старые декораторы")
214
+
192
215
  # Сначала пробуем как обычное событие
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)
216
+ if event_type in event_handlers:
217
+ try:
218
+ logger.info(f" 🎯 Сохраняем как user_event: '{event_type}'")
219
+ event_id = await save_immediate_event(event_type, user_id, event_info, session_id)
220
+ should_execute_immediately = True
221
+ logger.info(f" 💾 Событие сохранено в БД: {event_id}")
222
+ except ValueError as e:
223
+ if "once_only=True" in str(e):
224
+ logger.info(f" 🔄 Событие '{event_type}' уже обрабатывалось, пропускаем")
225
+ continue
226
+ else:
227
+ raise
228
+
229
+ # Если не user_event, пробуем как запланированную задачу
230
+ elif event_type in scheduled_tasks:
231
+ try:
232
+ # Извлекаем время из event_info (AI должен передавать число секунд)
233
+ try:
234
+ delay_seconds = int(event_info)
235
+ except ValueError:
236
+ # Если не число, используем значение по умолчанию
237
+ delay_seconds = 3600
238
+ logger.warning(f" ⚠️ Не удалось извлечь время из '{event_info}', используем 3600с")
199
239
 
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)
240
+ logger.info(f" ⏰ Сохраняем как scheduled_task: '{event_type}' через {delay_seconds}с")
241
+ result = await schedule_task_for_later_with_db(event_type, user_id, event_info, delay_seconds, session_id)
242
+ event_id = result['event_id']
243
+ should_notify = result.get('notify', False)
244
+ logger.info(f" 💾 Задача сохранена в БД: {event_id}")
245
+
246
+ except Exception as e:
247
+ if "once_only=True" in str(e):
248
+ logger.info(f" 🔄 Задача '{event_type}' уже запланирована, пропускаем")
249
+ continue
250
+ else:
251
+ logger.error(f" ❌ Ошибка планирования scheduled_task '{event_type}': {e}")
252
+ continue
253
+
254
+ # Если не scheduled_task, пробуем как глобальный обработчик
255
+ elif event_type in global_handlers:
256
+ try:
257
+ # Извлекаем время из event_info (AI должен передавать число секунд)
258
+ try:
259
+ delay_seconds = int(event_info)
260
+ except ValueError:
261
+ # Если не число, используем значение по умолчанию
262
+ delay_seconds = 3600
263
+ logger.warning(f" ⚠️ Не удалось извлечь время из '{event_info}', используем 3600с")
264
+
265
+ logger.info(f" 🌍 Сохраняем как global_handler: '{event_type}' через {delay_seconds}с")
266
+ result = await schedule_global_handler_for_later_with_db(event_type, delay_seconds, event_info)
267
+ event_id = result['event_id']
268
+ should_notify = result.get('notify', False)
269
+ logger.info(f" 💾 Глобальное событие сохранено в БД: {event_id}")
270
+
271
+ except Exception as e:
272
+ if "once_only=True" in str(e):
273
+ logger.info(f" 🔄 Глобальное событие '{event_type}' уже запланировано, пропускаем")
274
+ continue
275
+ else:
276
+ logger.error(f" ❌ Ошибка планирования global_handler '{event_type}': {e}")
277
+ continue
278
+
279
+ else:
280
+ logger.warning(f" ⚠️ Обработчик '{event_type}' не найден среди зарегистрированных")
281
+ logger.debug(f" 🔍 Доступные обработчики:")
282
+ logger.debug(f" - event_handlers: {list(event_handlers.keys())}")
283
+ logger.debug(f" - scheduled_tasks: {list(scheduled_tasks.keys())}")
284
+ logger.debug(f" - global_handlers: {list(global_handlers.keys())}")
285
+
286
+ # Выполняем немедленные события
287
+ if should_execute_immediately and event_id:
288
+ try:
289
+ result = await execute_event_handler(event_type, user_id, event_info)
290
+ should_notify = result.get('notify', False)
291
+ await update_event_result(event_id, 'completed', result)
292
+ logger.info(f" ✅ Событие выполнено: {result}")
293
+ except Exception as e:
294
+ await update_event_result(event_id, 'failed', None, str(e))
295
+ logger.error(f" ❌ Ошибка выполнения события: {e}")
207
296
 
208
297
  except ValueError as e:
209
298
  logger.warning(f" ⚠️ Обработчик/задача не найдены: {e}")
@@ -312,34 +401,30 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
312
401
  parse_mode = config.MESSAGE_PARSE_MODE if config.MESSAGE_PARSE_MODE != 'None' else None
313
402
  logger.info(f" 🔧 Parse mode: {parse_mode}")
314
403
 
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)} каталогах")
404
+ # Получаем user_id и импортируем supabase_client
405
+ user_id = message.from_user.id
406
+ supabase_client = get_global_var('supabase_client')
407
+
408
+ # Текст уже готов, используем как есть
409
+ final_text = text
410
+
411
+ # Работаем с переданными файлами и каталогами
412
+ logger.info(f" 📦 Передано файлов: {files_list}")
413
+ logger.info(f" 📂 Передано каталогов: {directories_list}")
414
+
415
+ # Получаем список уже отправленных файлов и каталогов
416
+ sent_files = await supabase_client.get_sent_files(user_id)
417
+ sent_directories = await supabase_client.get_sent_directories(user_id)
418
+
419
+ logger.info(f" 📋 Уже отправлено файлов: {sent_files}")
420
+ logger.info(f" 📋 Уже отправлено каталогов: {sent_directories}")
421
+
422
+ # Фильтруем файлы и каталоги, которые уже отправлялись
423
+ actual_files_list = [f for f in files_list if f not in sent_files]
424
+ actual_directories_list = [d for d in directories_list if str(d) not in sent_directories]
425
+
426
+ logger.info(f" 🆕 После фильтрации файлов: {actual_files_list}")
427
+ logger.info(f" 🆕 После фильтрации каталогов: {actual_directories_list}")
343
428
 
344
429
 
345
430
  # Проверяем, что есть что отправлять
@@ -349,30 +434,36 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
349
434
  final_text = "Ошибка формирования ответа. Попробуйте еще раз."
350
435
 
351
436
  logger.info(f"📱 Подготовка сообщения: {len(final_text)} символов")
437
+ logger.info(f" 📦 Файлов для обработки: {actual_files_list}")
438
+ logger.info(f" 📂 Каталогов для обработки: {actual_directories_list}")
352
439
 
353
440
  # Проверяем наличие файлов для отправки
354
- if files_list or directories_list:
441
+ if actual_files_list or actual_directories_list:
355
442
  # Функция определения типа медиа по расширению
356
443
  def get_media_type(file_path: str) -> str:
357
444
  ext = Path(file_path).suffix.lower()
358
445
  if ext in {'.jpg', '.jpeg', '.png'}:
359
446
  return 'photo'
360
- elif ext in {'.mp4'}:
447
+ elif ext in {'.mp4', '.mov'}:
361
448
  return 'video'
362
449
  else:
363
450
  return 'document'
364
451
 
365
452
  # Создаем списки для разных типов файлов
366
- media_files = [] # для фото и видео
453
+ video_files = [] # для видео
454
+ photo_files = [] # для фото
367
455
  document_files = [] # для документов
368
456
 
369
457
  # Функция обработки файла
370
458
  def process_file(file_path: Path, source: str = ""):
371
459
  if file_path.is_file():
372
460
  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}")
461
+ if media_type == 'video':
462
+ video_files.append(file_path)
463
+ logger.info(f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}")
464
+ elif media_type == 'photo':
465
+ photo_files.append(file_path)
466
+ logger.info(f" 📸 Добавлено фото{f' из {source}' if source else ''}: {file_path.name}")
376
467
  else:
377
468
  document_files.append(file_path)
378
469
  logger.info(f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}")
@@ -380,54 +471,58 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
380
471
  logger.warning(f" ⚠️ Файл не найден: {file_path}")
381
472
 
382
473
  # Обрабатываем прямые файлы
383
- for file_name in files_list:
474
+ for file_name in actual_files_list:
384
475
  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)
476
+ process_file(Path(f"files/{file_name}"))
390
477
  except Exception as e:
391
478
  logger.error(f" ❌ Ошибка обработки файла {file_name}: {e}")
392
479
 
393
480
  # Обрабатываем файлы из каталогов
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}")
481
+ for dir_name in actual_directories_list:
482
+ dir_name = Path(dir_name)
399
483
  try:
400
- if dir_path.is_dir():
401
- for file_path in dir_path.iterdir():
484
+ if dir_name.is_dir():
485
+ for file_path in dir_name.iterdir():
402
486
  try:
403
- process_file(file_path, dir_path)
487
+ process_file(file_path, dir_name)
404
488
  except Exception as e:
405
489
  logger.error(f" ❌ Ошибка обработки файла {file_path}: {e}")
406
490
  else:
407
- logger.warning(f" ⚠️ Каталог не найден: {dir_path}")
491
+ logger.warning(f" ⚠️ Каталог не найден: {dir_name}")
408
492
  except Exception as e:
409
- logger.error(f" ❌ Ошибка обработки каталога {dir_path}: {e}")
493
+ logger.error(f" ❌ Ошибка обработки каталога {dir_name}: {e}")
494
+
495
+ # Списки для отслеживания реально отправленных файлов
496
+ sent_files_to_save = []
497
+ sent_dirs_to_save = []
410
498
 
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)))
499
+ # 1. Отправляем видео (если есть)
500
+ if video_files:
501
+ video_group = MediaGroupBuilder()
502
+ for file_path in video_files:
503
+ video_group.add_video(media=FSInputFile(str(file_path)))
420
504
 
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" ✅ Отправлен текст сообщения")
505
+ videos = video_group.build()
506
+ if videos:
507
+ await message.answer_media_group(media=videos)
508
+ logger.info(f" ✅ Отправлено {len(videos)} видео")
509
+
510
+ # 2. Отправляем фото (если есть)
511
+ if photo_files:
512
+ photo_group = MediaGroupBuilder()
513
+ for file_path in photo_files:
514
+ photo_group.add_photo(media=FSInputFile(str(file_path)))
515
+
516
+ photos = photo_group.build()
517
+ if photos:
518
+ await message.answer_media_group(media=photos)
519
+ logger.info(f" ✅ Отправлено {len(photos)} фото")
520
+
521
+ # 3. Отправляем текст
522
+ result = await message.answer(final_text, parse_mode=parse_mode)
523
+ logger.info(f" ✅ Отправлен текст сообщения")
429
524
 
430
- # Отправляем документы отдельно (если есть)
525
+ # 4. Отправляем документы (если есть)
431
526
  if document_files:
432
527
  doc_group = MediaGroupBuilder()
433
528
  for file_path in document_files:
@@ -436,7 +531,32 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
436
531
  docs = doc_group.build()
437
532
  if docs:
438
533
  await message.answer_media_group(media=docs)
439
- logger.info(f" ✅ Отправлена группа документов: {len(docs)} файлов")
534
+ logger.info(f" ✅ Отправлено {len(docs)} документов")
535
+
536
+ # 5. Собираем список реально отправленных файлов и каталогов
537
+ # Если были отправлены файлы из actual_files_list - сохраняем их
538
+ if video_files or photo_files or document_files:
539
+ # Сохраняем прямые файлы из actual_files_list (если отправлены)
540
+ sent_files_to_save.extend(actual_files_list)
541
+ logger.info(f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}")
542
+ # Сохраняем каталоги из actual_directories_list (если отправлены файлы из них)
543
+ sent_dirs_to_save.extend([str(d) for d in actual_directories_list])
544
+ logger.info(f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}")
545
+
546
+ # 6. Обновляем информацию в БД
547
+ if sent_files_to_save or sent_dirs_to_save:
548
+ try:
549
+ if sent_files_to_save:
550
+ logger.info(f" 💾 Сохраняем файлы в БД: {sent_files_to_save}")
551
+ await supabase_client.add_sent_files(user_id, sent_files_to_save)
552
+ if sent_dirs_to_save:
553
+ logger.info(f" 💾 Сохраняем каталоги в БД: {sent_dirs_to_save}")
554
+ await supabase_client.add_sent_directories(user_id, sent_dirs_to_save)
555
+ logger.info(f" ✅ Обновлена информация о отправленных файлах в БД")
556
+ except Exception as e:
557
+ logger.error(f" ❌ Ошибка обновления информации о файлах в БД: {e}")
558
+ else:
559
+ logger.info(f" ℹ️ Нет новых файлов для сохранения в БД")
440
560
 
441
561
  return result
442
562
  else:
@@ -446,6 +566,14 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
446
566
  return result
447
567
 
448
568
  except Exception as e:
569
+ # Проверяем, является ли ошибка блокировкой бота
570
+ if "Forbidden: bot was blocked by the user" in str(e):
571
+ logger.warning(f"🚫 Бот заблокирован пользователем {user_id}")
572
+ return None
573
+ elif "TelegramForbiddenError" in str(type(e).__name__):
574
+ logger.warning(f"🚫 Бот заблокирован пользователем {user_id}")
575
+ return None
576
+
449
577
  logger.error(f"❌ ОШИБКА в send_message: {e}")
450
578
  logger.exception("Полный стек ошибки send_message:")
451
579
 
@@ -456,6 +584,14 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
456
584
  logger.info(f"✅ Запасное сообщение отправлено")
457
585
  return result
458
586
  except Exception as e2:
587
+ # Проверяем и здесь блокировку бота
588
+ if "Forbidden: bot was blocked by the user" in str(e2):
589
+ logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
590
+ return None
591
+ elif "TelegramForbiddenError" in str(type(e2).__name__):
592
+ logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
593
+ return None
594
+
459
595
  logger.error(f"❌ Даже запасное сообщение не отправилось: {e2}")
460
596
  raise
461
597