smart-bot-factory 1.0.0__tar.gz → 1.0.1__tar.gz

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 (62) hide show
  1. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/PKG-INFO +1 -1
  2. smart_bot_factory-1.0.1/hello.jpg +0 -0
  3. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/pyproject.toml +1 -1
  4. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_events.py +308 -117
  5. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/core/message_sender.py +57 -51
  6. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/uv.lock +1 -1
  7. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/.gitignore +0 -0
  8. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/.python-version +0 -0
  9. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/LICENSE +0 -0
  10. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/README.md +0 -0
  11. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/__init__.py +0 -0
  12. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/__init__.py +0 -0
  13. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_logic.py +0 -0
  14. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_manager.py +0 -0
  15. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_tester.py +0 -0
  16. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/timeout_checker.py +0 -0
  17. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/__init__.py +0 -0
  18. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/common.py +0 -0
  19. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/dialog_calendar.py +0 -0
  20. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/schemas.py +0 -0
  21. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/simple_calendar.py +0 -0
  22. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/analytics/analytics_manager.py +0 -0
  23. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/cli.py +0 -0
  24. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/config.py +0 -0
  25. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +0 -0
  26. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +0 -0
  27. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +0 -0
  28. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +0 -0
  29. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +0 -0
  30. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +0 -0
  31. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +0 -0
  32. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +0 -0
  33. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +0 -0
  34. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +0 -0
  35. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +0 -0
  36. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +0 -0
  37. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +0 -0
  38. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/welcome_file//320/247/320/265/320/272 /320/273/320/270/321/201/321/202 /320/277/320/276 152/320/244/320/227 /320/270 323/320/244/320/227 /320/264/320/273/321/217 /320/274/320/265/320/264/320/270/321/206/320/270/320/275/321/213.pdf" +0 -0
  39. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/core/bot_utils.py +0 -0
  40. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/core/conversation_manager.py +0 -0
  41. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/core/decorators.py +0 -0
  42. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/core/router.py +0 -0
  43. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/core/router_manager.py +0 -0
  44. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/core/states.py +0 -0
  45. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/__init__.py +0 -0
  46. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/bot_builder.py +0 -0
  47. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/bot_testing.py +0 -0
  48. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/dashboard/__init__.py +0 -0
  49. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/event/__init__.py +0 -0
  50. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/handlers/handlers.py +0 -0
  51. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/integrations/openai_client.py +0 -0
  52. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/integrations/supabase_client.py +0 -0
  53. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/message/__init__.py +0 -0
  54. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/router/__init__.py +0 -0
  55. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/setup_checker.py +0 -0
  56. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/supabase/__init__.py +0 -0
  57. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/supabase/client.py +0 -0
  58. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/__init__.py +0 -0
  59. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/debug_routing.py +0 -0
  60. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/prompt_loader.py +0 -0
  61. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/user_prompt_loader.py +0 -0
  62. {smart_bot_factory-1.0.0 → smart_bot_factory-1.0.1}/smart_bot_factory/utm_link_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smart-bot-factory
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Библиотека для создания умных чат-ботов
5
5
  Author-email: Kopatych <eserov73@gmail.com>
6
6
  License: MIT
Binary file
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "smart-bot-factory"
3
- version = "1.0.0"
3
+ version = "1.0.1"
4
4
  description = "Библиотека для создания умных чат-ботов"
5
5
  authors = [
6
6
  {name = "Kopatych", email = "eserov73@gmail.com"}
@@ -116,24 +116,83 @@ async def process_event_name(message: Message, state: FSMContext):
116
116
 
117
117
  # Сохраняем название
118
118
  await state.update_data(event_name=event_name)
119
- await state.set_state(AdminStates.create_event_date)
120
119
 
121
- # Показываем календарь для выбора даты
122
- calendar = SimpleCalendar(locale="ru", today_btn="Сегодня", cancel_btn="Отмена")
123
- # Ограничиваем выбор датами от вчера до +12 месяцев (чтобы сегодня был доступен)
124
- calendar.set_dates_range(
125
- datetime.now() + relativedelta(days=-1),
126
- datetime.now() + relativedelta(months=+12),
120
+ # Создаем клавиатуру с выбором времени
121
+ keyboard = InlineKeyboardMarkup(
122
+ inline_keyboard=[
123
+ [
124
+ InlineKeyboardButton(text="🚀 Запустить сразу", callback_data="timing:immediate"),
125
+ InlineKeyboardButton(text="📅 Выбрать время", callback_data="timing:scheduled")
126
+ ]
127
+ ]
127
128
  )
128
- calendar_markup = await calendar.start_calendar()
129
129
 
130
130
  await message.answer(
131
- f"✅ Название события: **{event_name}**\n\n" "📅 Выберите дату отправки:",
132
- reply_markup=calendar_markup,
133
- parse_mode="Markdown",
131
+ f"✅ Название события: **{event_name}**\n\n"
132
+ "🕒 Когда запустить событие?",
133
+ reply_markup=keyboard,
134
+ parse_mode="Markdown"
134
135
  )
135
136
 
136
137
 
138
+ @admin_events_router.callback_query(F.data.startswith("timing:"))
139
+ async def process_event_timing(callback_query: CallbackQuery, state: FSMContext):
140
+ """Обработка выбора времени запуска события"""
141
+ action = callback_query.data.split(":", 1)[1]
142
+
143
+ if action == "immediate":
144
+ # Устанавливаем текущее время
145
+ now = datetime.now(MOSCOW_TZ)
146
+ await state.update_data(
147
+ event_date=now.strftime("%Y-%m-%d"),
148
+ event_time=now.strftime("%H:%M"),
149
+ is_immediate=True
150
+ )
151
+ # Переходим к выбору сегмента
152
+ await state.set_state(AdminStates.create_event_segment)
153
+
154
+ # Получаем все доступные сегменты
155
+ from ..handlers.handlers import get_global_var
156
+ supabase_client = get_global_var("supabase_client")
157
+ segments = await supabase_client.get_all_segments()
158
+
159
+ # Создаем клавиатуру с сегментами
160
+ keyboard = []
161
+ keyboard.append([InlineKeyboardButton(text="📢 Отправить всем", callback_data="segment:all")])
162
+ if segments:
163
+ for i in range(0, len(segments), 2):
164
+ row = [InlineKeyboardButton(text=f"👥 {segments[i]}", callback_data=f"segment:{segments[i]}")]
165
+ if i + 1 < len(segments):
166
+ row.append(InlineKeyboardButton(text=f"👥 {segments[i+1]}", callback_data=f"segment:{segments[i+1]}"))
167
+ keyboard.append(row)
168
+
169
+ markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
170
+ await callback_query.message.edit_text(
171
+ f"✅ Время: **Сейчас**\n\n"
172
+ f"👥 Выберите сегмент пользователей для отправки:\n"
173
+ f"_(Найдено сегментов: {len(segments)})_",
174
+ reply_markup=markup,
175
+ parse_mode="Markdown"
176
+ )
177
+
178
+ else: # scheduled
179
+ await state.set_state(AdminStates.create_event_date)
180
+ # Показываем календарь для выбора даты
181
+ calendar = SimpleCalendar(locale="ru", today_btn="Сегодня", cancel_btn="Отмена")
182
+ # Ограничиваем выбор датами от вчера до +12 месяцев (чтобы сегодня был доступен)
183
+ calendar.set_dates_range(
184
+ datetime.now() + relativedelta(days=-1),
185
+ datetime.now() + relativedelta(months=+12),
186
+ )
187
+ calendar_markup = await calendar.start_calendar()
188
+
189
+ await callback_query.message.edit_text(
190
+ "📅 Выберите дату отправки:",
191
+ reply_markup=calendar_markup,
192
+ parse_mode="Markdown"
193
+ )
194
+
195
+
137
196
  @admin_events_router.callback_query(
138
197
  SimpleCalendarCallback.filter(), AdminStates.create_event_date
139
198
  )
@@ -584,17 +643,23 @@ async def process_files_action(callback_query: CallbackQuery, state: FSMContext)
584
643
  # Переход к подтверждению
585
644
  await state.set_state(AdminStates.create_event_confirm)
586
645
 
587
- # Формируем дату и время для отображения (московское время)
588
- event_date = data.get("event_date")
589
- event_time = data.get("event_time")
590
- naive_datetime = datetime.strptime(f"{event_date} {event_time}", "%Y-%m-%d %H:%M")
591
- moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
646
+ # Формируем дату и время для отображения
647
+ is_immediate = data.get("is_immediate", False)
648
+
649
+ if is_immediate:
650
+ time_display = "Прямо сейчас 🔥"
651
+ else:
652
+ event_date = data.get("event_date")
653
+ event_time = data.get("event_time")
654
+ naive_datetime = datetime.strptime(f"{event_date} {event_time}", "%Y-%m-%d %H:%M")
655
+ moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
656
+ time_display = f"{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)"
592
657
 
593
658
  # Отправляем сообщение с подтверждением
594
659
  summary = (
595
660
  f"📋 **Подтверждение создания события**\n\n"
596
661
  f"📝 Название: **{data.get('event_name')}**\n"
597
- f"📅 Дата и время: **{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)**\n"
662
+ f"📅 Время запуска: **{time_display}**\n"
598
663
  f"👥 Сегмент: **{data.get('segment_display')}**\n"
599
664
  f"📎 Файлов: **{len(files)}**\n\n"
600
665
  "Подтвердите создание события:"
@@ -727,15 +792,21 @@ async def show_event_preview(callback_query: CallbackQuery, state: FSMContext):
727
792
  )
728
793
 
729
794
  # 3. Отправляем сообщение с подтверждением (такое же как было)
730
- event_date = data.get("event_date")
731
- event_time = data.get("event_time")
732
- naive_datetime = datetime.strptime(f"{event_date} {event_time}", "%Y-%m-%d %H:%M")
733
- moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
795
+ is_immediate = data.get("is_immediate", False)
796
+
797
+ if is_immediate:
798
+ time_display = "Прямо сейчас 🔥"
799
+ else:
800
+ event_date = data.get("event_date")
801
+ event_time = data.get("event_time")
802
+ naive_datetime = datetime.strptime(f"{event_date} {event_time}", "%Y-%m-%d %H:%M")
803
+ moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
804
+ time_display = f"{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)"
734
805
 
735
806
  summary = (
736
807
  f"📋 **Подтверждение создания события**\n\n"
737
808
  f"📝 Название: **{data.get('event_name')}**\n"
738
- f"📅 Дата и время: **{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)**\n"
809
+ f"📅 Время запуска: **{time_display}**\n"
739
810
  f"👥 Сегмент: **{data.get('segment_display')}**\n"
740
811
  f"📎 Файлов: **{len(files)}**\n\n"
741
812
  "Подтвердите создание события:"
@@ -773,127 +844,247 @@ async def process_event_confirmation(callback_query: CallbackQuery, state: FSMCo
773
844
  )
774
845
  return
775
846
 
776
- # Создаем событие
847
+ # Получаем данные события
777
848
  data = await state.get_data()
849
+ is_immediate = data.get("is_immediate", False)
850
+ files = data.get("files", [])
778
851
 
779
852
  from ..handlers.handlers import get_global_var
780
-
853
+ from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaVideo
854
+ bot = get_global_var("bot")
781
855
  supabase_client = get_global_var("supabase_client")
782
856
 
783
- # Формируем datetime для планирования
784
- event_date = data.get("event_date")
785
- event_time = data.get("event_time")
786
-
787
- # Создаем naive datetime из введенного московского времени
788
- naive_datetime = datetime.strptime(f"{event_date} {event_time}", "%Y-%m-%d %H:%M")
857
+ if is_immediate:
858
+ # Для немедленной отправки - сразу рассылаем сообщения
859
+ try:
860
+ # Показываем сообщение о начале рассылки
861
+ await callback_query.message.edit_text(
862
+ "📤 **Выполняется рассылка...**",
863
+ parse_mode="Markdown"
864
+ )
789
865
 
790
- # Привязываем к московской временной зоне
791
- moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
866
+ # Получаем список пользователей для рассылки
867
+ segment = data.get("segment")
868
+ users = await supabase_client.get_users_by_segment(segment)
869
+ total_users = len(users)
870
+ sent_count = 0
871
+ failed_count = 0
872
+
873
+ # Преобразуем результат в нужный формат
874
+ user_ids = [user["telegram_id"] for user in users] # используем telegram_id вместо user_id
875
+
876
+ # Группируем файлы по стадиям
877
+ files_with_msg = [f for f in files if f.get("stage") == "with_message"]
878
+ files_after = [f for f in files if f.get("stage") == "after_message"]
879
+
880
+ # Отправляем сообщения каждому пользователю
881
+ for user in users:
882
+ user_id = user["telegram_id"]
883
+ try:
884
+ # 1. Отправляем основное сообщение с медиа (если есть)
885
+ if files_with_msg:
886
+ sorted_files = sorted(files_with_msg, key=lambda x: x.get("order", 0))
887
+
888
+ # Если только один файл - отправляем как обычный файл с caption
889
+ if len(sorted_files) == 1:
890
+ file_info = sorted_files[0]
891
+ if file_info["type"] == "photo":
892
+ await bot.send_photo(
893
+ chat_id=user_id,
894
+ photo=FSInputFile(file_info["file_path"]),
895
+ caption=data.get("event_message"),
896
+ parse_mode="Markdown"
897
+ )
898
+ elif file_info["type"] == "video":
899
+ await bot.send_video(
900
+ chat_id=user_id,
901
+ video=FSInputFile(file_info["file_path"]),
902
+ caption=data.get("event_message"),
903
+ parse_mode="Markdown"
904
+ )
905
+ else:
906
+ # Если несколько файлов - используем media_group
907
+ media_group = []
908
+ first_file = True
909
+
910
+ for file_info in sorted_files:
911
+ if file_info["type"] == "photo":
912
+ media = InputMediaPhoto(
913
+ media=FSInputFile(file_info["file_path"]),
914
+ caption=data.get("event_message") if first_file else None,
915
+ parse_mode="Markdown" if first_file else None,
916
+ )
917
+ media_group.append(media)
918
+ elif file_info["type"] == "video":
919
+ media = InputMediaVideo(
920
+ media=FSInputFile(file_info["file_path"]),
921
+ caption=data.get("event_message") if first_file else None,
922
+ parse_mode="Markdown" if first_file else None,
923
+ )
924
+ media_group.append(media)
925
+ first_file = False
926
+
927
+ if media_group:
928
+ await bot.send_media_group(chat_id=user_id, media=media_group)
929
+ else:
930
+ # Только текст
931
+ await bot.send_message(
932
+ chat_id=user_id,
933
+ text=data.get("event_message"),
934
+ parse_mode="Markdown"
935
+ )
936
+
937
+ # 2. Отправляем дополнительные файлы
938
+ for file_info in files_after:
939
+ if file_info["type"] == "document":
940
+ await bot.send_document(
941
+ chat_id=user_id,
942
+ document=FSInputFile(file_info["file_path"])
943
+ )
944
+ elif file_info["type"] == "photo":
945
+ await bot.send_photo(
946
+ chat_id=user_id,
947
+ photo=FSInputFile(file_info["file_path"])
948
+ )
949
+ elif file_info["type"] == "video":
950
+ await bot.send_video(
951
+ chat_id=user_id,
952
+ video=FSInputFile(file_info["file_path"])
953
+ )
792
954
 
793
- # Конвертируем в UTC для сохранения в БД
794
- utc_datetime = moscow_datetime.astimezone(pytz.UTC)
955
+ sent_count += 1
795
956
 
796
- logger.info(
797
- f" Время события: Москва={moscow_datetime.strftime('%d.%m.%Y %H:%M %Z')}, UTC={utc_datetime.strftime('%d.%m.%Y %H:%M %Z')}"
798
- )
957
+ except Exception as e:
958
+ logger.error(f" Ошибка отправки пользователю {user_id}: {e}")
959
+ failed_count += 1
960
+
961
+ # Сохраняем событие в БД
962
+ event_status = "success" if failed_count == 0 else "partial_success"
963
+ await supabase_client.save_admin_event(
964
+ event_name=data.get("event_name"),
965
+ event_data={
966
+ "segment": segment,
967
+ "total_users": total_users,
968
+ "sent_success": sent_count,
969
+ "failed_count": failed_count,
970
+ "type": "immediate_event",
971
+ "admin_id": callback_query.from_user.id,
972
+ "execution_status": event_status,
973
+ "completed_at": datetime.now(pytz.UTC).isoformat()
974
+ },
975
+ scheduled_datetime=datetime.now(pytz.UTC)
976
+ )
799
977
 
800
- # Сначала сохраняем событие в БД чтобы получить ID
801
- try:
802
- # Создаем событие без файлов
803
- event = await supabase_client.save_admin_event(
804
- event_name=data.get("event_name"),
805
- event_data={
806
- "segment": data.get("segment"),
807
- "message": data.get("event_message"),
808
- "files": [], # Пока пустой список
809
- },
810
- scheduled_datetime=utc_datetime,
811
- )
812
- event_id = event["id"]
813
- logger.info(f"✅ Создано событие с ID {event_id}")
978
+ # Показываем итоговое сообщение
979
+ status = "✅" if failed_count == 0 else "⚠️"
980
+
981
+ await callback_query.message.edit_text(
982
+ f"{status} **Админское событие выполнено**\n\n"
983
+ f"📝 Название: **{data.get('event_name')}**\n"
984
+ f"👥 Сегмент: **{data.get('segment_display')}**\n\n"
985
+ f"📊 Результат:\n"
986
+ f" Доставлено: **{sent_count}**\n"
987
+ f"• Не доставлено: **{failed_count}**",
988
+ parse_mode="Markdown"
989
+ )
814
990
 
815
- # Теперь загружаем файлы в Storage
816
- import os
991
+ except Exception as e:
992
+ logger.error(f"❌ Ошибка массовой рассылки: {e}")
993
+
994
+ # Сохраняем ошибку события в БД
995
+ await supabase_client.save_admin_event(
996
+ event_name=data.get("event_name"),
997
+ event_data={
998
+ "segment": segment,
999
+ "error": str(e),
1000
+ "type": "immediate_event",
1001
+ "admin_id": callback_query.from_user.id,
1002
+ "execution_status": "error",
1003
+ "completed_at": datetime.now(pytz.UTC).isoformat()
1004
+ },
1005
+ scheduled_datetime=datetime.now(pytz.UTC)
1006
+ )
817
1007
 
818
- files = data.get("files", [])
819
- uploaded_files = []
1008
+ await callback_query.message.edit_text(
1009
+ f"❌ **Ошибка выполнения админского события**\n\n"
1010
+ f"📝 Название: **{data.get('event_name')}**\n"
1011
+ f"👥 Сегмент: **{data.get('segment_display')}**\n\n"
1012
+ f"Произошла техническая ошибка. Попробуйте позже.",
1013
+ parse_mode="Markdown"
1014
+ )
820
1015
 
821
- for file_info in files:
822
- try:
823
- # Читаем локальный файл
824
- with open(file_info["file_path"], "rb") as f:
825
- file_bytes = f.read()
826
-
827
- # Генерируем уникальный ID для файла
828
- file_id = generate_file_id()
829
-
830
- # Загружаем в Storage
831
- storage_info = await supabase_client.upload_event_file(
832
- event_id=event_id,
833
- file_data=file_bytes,
834
- original_name=file_info["name"],
835
- file_id=file_id,
836
- )
1016
+ else:
1017
+ # Для отложенного события - стандартная логика
1018
+ try:
1019
+ # Формируем datetime для планирования
1020
+ event_date = data.get("event_date")
1021
+ event_time = data.get("event_time")
1022
+ naive_datetime = datetime.strptime(f"{event_date} {event_time}", "%Y-%m-%d %H:%M")
1023
+ moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
1024
+ utc_datetime = moscow_datetime.astimezone(pytz.UTC)
1025
+
1026
+ # Создаем событие
1027
+ event = await supabase_client.save_admin_event(
1028
+ event_name=data.get("event_name"),
1029
+ event_data={
1030
+ "segment": data.get("segment"),
1031
+ "message": data.get("event_message"),
1032
+ "files": [],
1033
+ },
1034
+ scheduled_datetime=utc_datetime,
1035
+ )
1036
+ event_id = event["id"]
837
1037
 
838
- # Сохраняем метаданные файла
839
- uploaded_files.append(
840
- {
1038
+ # Загружаем файлы в Storage
1039
+ uploaded_files = []
1040
+ for file_info in files:
1041
+ try:
1042
+ with open(file_info["file_path"], "rb") as f:
1043
+ file_bytes = f.read()
1044
+ file_id = generate_file_id()
1045
+ storage_info = await supabase_client.upload_event_file(
1046
+ event_id=event_id,
1047
+ file_data=file_bytes,
1048
+ original_name=file_info["name"],
1049
+ file_id=file_id,
1050
+ )
1051
+ uploaded_files.append({
841
1052
  "type": file_info["type"],
842
1053
  "storage_path": storage_info["storage_path"],
843
- "original_name": file_info[
844
- "name"
845
- ], # Используем оригинальное имя из file_info
1054
+ "original_name": file_info["name"],
846
1055
  "stage": file_info["stage"],
847
1056
  "has_caption": file_info.get("has_caption", False),
848
1057
  "order": file_info.get("order", 0),
849
- }
850
- )
851
-
852
- # Удаляем временный локальный файл
853
- try:
854
- os.remove(file_info["file_path"])
855
- logger.info(f"🗑️ Удален временный файл: {file_info['file_path']}")
1058
+ })
856
1059
  except Exception as e:
857
- logger.warning(f"⚠️ Не удалось удалить временный файл: {e}")
858
-
859
- except Exception as e:
860
- logger.error(f"❌ Ошибка загрузки файла {file_info['name']}: {e}")
861
- # Если ошибка - удаляем все файлы события и само событие
862
- try:
1060
+ logger.error(f" Ошибка загрузки файла {file_info['name']}: {e}")
863
1061
  await supabase_client.delete_event_files(event_id)
864
- # TODO: добавить метод удаления события
865
- except Exception:
866
- logger.error("Ошибка при удалении файлов события")
867
- raise
868
-
869
- logger.info(
870
- f"✅ Загружено {len(uploaded_files)} файлов в Storage для события {event_id}"
871
- )
1062
+ raise
872
1063
 
873
- # Обновляем событие с информацией о файлах
874
- event_data = {
875
- "segment": data.get("segment"),
876
- "message": data.get("event_message"),
877
- "files": uploaded_files,
878
- }
879
-
880
- # TODO: добавить метод обновления события
881
- (
882
- supabase_client.client.table("scheduled_events")
883
- .update({"event_data": json.dumps(event_data, ensure_ascii=False)})
884
- .eq("id", event_id)
885
- .execute()
886
- )
1064
+ # Обновляем событие с информацией о файлах
1065
+ event_data = {
1066
+ "segment": data.get("segment"),
1067
+ "message": data.get("event_message"),
1068
+ "files": uploaded_files,
1069
+ }
1070
+ supabase_client.client.table("scheduled_events").update(
1071
+ {"event_data": json.dumps(event_data, ensure_ascii=False)}
1072
+ ).eq("id", event_id).execute()
887
1073
 
888
- except Exception as e:
889
- logger.error(f"❌ Ошибка создания события: {e}")
890
- raise
1074
+ except Exception as e:
1075
+ logger.error(f"❌ Ошибка создания отложенного события: {e}")
1076
+ raise
1077
+ is_immediate = data.get("is_immediate", False)
1078
+
1079
+ if is_immediate:
1080
+ time_display = "🔥 Прямо сейчас"
1081
+ else:
1082
+ time_display = f"{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)"
891
1083
 
892
- # Показываем сообщение об успехе
893
1084
  await callback_query.message.edit_text(
894
1085
  f"✅ **Событие успешно создано!**\n\n"
895
1086
  f"📝 Название: `{data.get('event_name')}`\n"
896
- f"📅 Запланировано на: **{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)**\n"
1087
+ f"📅 Время запуска: **{time_display}**\n"
897
1088
  f"👥 Сегмент: **{data.get('segment_display')}**\n\n"
898
1089
  f"💡 _Нажмите на название для копирования_",
899
1090
  parse_mode="Markdown",
@@ -5,11 +5,15 @@
5
5
  import logging
6
6
  import time
7
7
  from datetime import datetime
8
- from typing import Any, Dict, Optional
8
+ from typing import Any, Dict, Optional, Union
9
9
 
10
- from aiogram.types import InlineKeyboardMarkup
10
+ from aiogram.types import InlineKeyboardMarkup, FSInputFile
11
11
  import pytz
12
12
 
13
+ from project_root_finder import root
14
+
15
+ PROJECT_ROOT = root
16
+
13
17
  logger = logging.getLogger(__name__)
14
18
 
15
19
 
@@ -194,16 +198,18 @@ async def send_message_by_ai(
194
198
 
195
199
 
196
200
  async def send_message_by_human(
197
- user_id: int, message_text: str, session_id: Optional[str] = None, parse_mode: str = "Markdown", reply_markup: Optional[InlineKeyboardMarkup] = None
201
+ user_id: int, message_text: str, session_id: Optional[str] = None, parse_mode: str = "Markdown", reply_markup: Optional[InlineKeyboardMarkup] = None, photo: Optional[str] = None
198
202
  ) -> Dict[str, Any]:
199
203
  """
200
- Отправляет сообщение пользователю от имени человека (готовый текст)
204
+ Отправляет сообщение пользователю от имени человека (готовый текст или фото с подписью).
201
205
 
202
206
  Args:
203
207
  user_id: ID пользователя в Telegram
204
- message_text: Готовый текст сообщения
208
+ message_text: Готовый текст сообщения или подпись к фото
205
209
  session_id: ID сессии (опционально, для сохранения в БД)
206
-
210
+ parse_mode: Тип форматирования текста
211
+ reply_markup: Клавиатура/markup (опционально)
212
+ photo: (str) путь к локальному файлу относительно корня проекта
207
213
  Returns:
208
214
  Результат отправки
209
215
  """
@@ -214,8 +220,29 @@ async def send_message_by_human(
214
220
  bot = get_global_var("bot")
215
221
  supabase_client = get_global_var("supabase_client")
216
222
 
217
- # Отправляем сообщение пользователю
218
- message = await bot.send_message(chat_id=user_id, text=message_text, parse_mode=parse_mode, reply_markup=reply_markup)
223
+ msg_type = "text"
224
+ message = None
225
+
226
+ if photo:
227
+ from pathlib import Path
228
+ photo_path = PROJECT_ROOT / photo
229
+ if not photo_path.exists():
230
+ raise FileNotFoundError(f"Файл с фото не найден: {photo}")
231
+ message = await bot.send_photo(
232
+ chat_id=user_id,
233
+ photo=FSInputFile(str(photo_path)),
234
+ caption=message_text,
235
+ parse_mode=parse_mode,
236
+ reply_markup=reply_markup
237
+ )
238
+ msg_type = "photo"
239
+ else:
240
+ message = await bot.send_message(
241
+ chat_id=user_id,
242
+ text=message_text,
243
+ parse_mode=parse_mode,
244
+ reply_markup=reply_markup,
245
+ )
219
246
 
220
247
  # Если указана сессия, сохраняем сообщение в БД
221
248
  if session_id:
@@ -223,10 +250,10 @@ async def send_message_by_human(
223
250
  session_id=session_id,
224
251
  role="assistant",
225
252
  content=message_text,
226
- message_type="text",
227
- metadata={"sent_by_human": True},
253
+ message_type=msg_type,
254
+ metadata={"sent_by_human": True, "has_photo": bool(photo)},
228
255
  )
229
- logger.info("💾 Сообщение от человека сохранено в БД")
256
+ logger.info(f"💾 Сообщение от человека сохранено в БД (photo={bool(photo)})")
230
257
 
231
258
  return {
232
259
  "status": "success",
@@ -234,6 +261,7 @@ async def send_message_by_human(
234
261
  "message_id": message.message_id,
235
262
  "message_text": message_text,
236
263
  "saved_to_db": bool(session_id),
264
+ "has_photo": bool(photo)
237
265
  }
238
266
 
239
267
  except Exception as e:
@@ -242,54 +270,43 @@ async def send_message_by_human(
242
270
 
243
271
 
244
272
  async def send_message_to_users_by_stage(
245
- stage: str, message_text: str, bot_id: str
273
+ stage: str, message_text: str, bot_id: str, photo: Optional[str] = None
246
274
  ) -> Dict[str, Any]:
247
275
  """
248
- Отправляет сообщение всем пользователям, находящимся на определенной стадии
276
+ Отправляет сообщение (или фото с подписью) всем пользователям, находящимся на определенной стадии
249
277
 
250
278
  Args:
251
279
  stage: Стадия диалога (например, 'introduction', 'qualification', 'closing')
252
- message_text: Текст сообщения для отправки
280
+ message_text: Текст сообщения для отправки / подпись к фото
253
281
  bot_id: ID бота (если не указан, используется текущий бот)
254
-
282
+ photo: путь к файлу с фото (относительно корня проекта, опционально)
255
283
  Returns:
256
284
  Результат отправки с количеством отправленных сообщений
257
285
  """
258
286
  try:
259
- # Импортируем необходимые компоненты
260
287
  from ..handlers.handlers import get_global_var
288
+ from pathlib import Path
261
289
 
262
290
  bot = get_global_var("bot")
263
291
  supabase_client = get_global_var("supabase_client")
264
292
  current_bot_id = (
265
293
  get_global_var("config").BOT_ID if get_global_var("config") else bot_id
266
294
  )
267
-
268
295
  if not current_bot_id:
269
296
  return {"status": "error", "error": "Не удалось определить bot_id"}
270
-
271
297
  logger.info(
272
298
  f"🔍 Ищем пользователей на стадии '{stage}' для бота '{current_bot_id}'"
273
299
  )
274
-
275
- # Получаем последние сессии для каждого пользователя с нужной стадией
276
- # Сначала получаем все активные сессии с нужной стадией
277
300
  sessions_query = (
278
301
  supabase_client.client.table("sales_chat_sessions")
279
302
  .select("user_id, id, current_stage, created_at")
280
303
  .eq("status", "active")
281
304
  .eq("current_stage", stage)
282
305
  )
283
-
284
- # Фильтруем по bot_id если указан
285
306
  if current_bot_id:
286
307
  sessions_query = sessions_query.eq("bot_id", current_bot_id)
287
-
288
- # Сортируем по дате создания (последние сначала)
289
308
  sessions_query = sessions_query.order("created_at", desc=True)
290
-
291
309
  sessions_data = sessions_query.execute()
292
-
293
310
  if not sessions_data.data:
294
311
  logger.info(f"📭 Пользователи на стадии '{stage}' не найдены")
295
312
  return {
@@ -299,56 +316,48 @@ async def send_message_to_users_by_stage(
299
316
  "messages_sent": 0,
300
317
  "errors": [],
301
318
  }
302
-
303
- # Выбираем уникальные user_id (берем только последнюю сессию для каждого пользователя)
304
319
  unique_users = {}
305
320
  for session in sessions_data.data:
306
321
  user_id = session["user_id"]
307
- # Если пользователь еще не добавлен, добавляем его (так как сессии отсортированы по дате, первая будет самой последней)
308
322
  if user_id not in unique_users:
309
323
  unique_users[user_id] = {
310
324
  "session_id": session["id"],
311
325
  "current_stage": session["current_stage"],
312
326
  }
313
-
314
327
  logger.info(
315
328
  f"👥 Найдено {len(unique_users)} уникальных пользователей на стадии '{stage}'"
316
329
  )
317
-
318
- # Отправляем сообщения
319
330
  messages_sent = 0
320
331
  errors = []
321
-
332
+ photo_path = None
333
+ if photo:
334
+ photo_path = PROJECT_ROOT / photo
335
+ if not photo_path.exists():
336
+ raise FileNotFoundError(f"Файл с фото не найден: {photo}")
322
337
  for user_id, user_data in unique_users.items():
323
338
  session_id = user_data["session_id"]
324
-
325
339
  try:
326
- # Отправляем сообщение пользователю
327
- await bot.send_message(chat_id=user_id, text=message_text)
328
-
329
- # Сохраняем сообщение в БД
340
+ if photo_path:
341
+ msg = await bot.send_photo(chat_id=user_id, photo=FSInputFile(str(photo_path)), caption=message_text)
342
+ msg_type = "photo"
343
+ else:
344
+ msg = await bot.send_message(chat_id=user_id, text=message_text)
345
+ msg_type = "text"
330
346
  await supabase_client.add_message(
331
347
  session_id=session_id,
332
348
  role="assistant",
333
349
  content=message_text,
334
- message_type="text",
335
- metadata={
336
- "sent_by_stage_broadcast": True,
337
- "target_stage": stage,
338
- "broadcast_timestamp": datetime.now().isoformat(),
339
- },
350
+ message_type=msg_type,
351
+ metadata={"sent_by_stage_broadcast": True, "target_stage": stage, "broadcast_timestamp": datetime.now().isoformat(), "has_photo": bool(photo)},
340
352
  )
341
-
342
353
  messages_sent += 1
343
354
  logger.info(
344
355
  f"✅ Сообщение отправлено пользователю {user_id} (стадия: {stage})"
345
356
  )
346
-
347
357
  except Exception as e:
348
358
  error_msg = f"Ошибка отправки пользователю {user_id}: {str(e)}"
349
359
  errors.append(error_msg)
350
360
  logger.error(f"❌ {error_msg}")
351
-
352
361
  result = {
353
362
  "status": "success",
354
363
  "stage": stage,
@@ -356,13 +365,10 @@ async def send_message_to_users_by_stage(
356
365
  "messages_sent": messages_sent,
357
366
  "errors": errors,
358
367
  }
359
-
360
368
  logger.info(
361
369
  f"📊 Результат рассылки по стадии '{stage}': {messages_sent}/{len(unique_users)} сообщений отправлено"
362
370
  )
363
-
364
371
  return result
365
-
366
372
  except Exception as e:
367
373
  logger.error(f"❌ Ошибка в send_message_to_users_by_stage: {e}")
368
374
  return {"status": "error", "error": str(e), "stage": stage}
@@ -1670,7 +1670,7 @@ wheels = [
1670
1670
 
1671
1671
  [[package]]
1672
1672
  name = "smart-bot-factory"
1673
- version = "0.3.11.dev2"
1673
+ version = "1.0.0"
1674
1674
  source = { editable = "." }
1675
1675
  dependencies = [
1676
1676
  { name = "aiofiles" },