smart-bot-factory 0.3.10__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 (67) hide show
  1. {smart_bot_factory-0.3.10 → 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-0.3.10 → smart_bot_factory-1.0.1}/pyproject.toml +1 -1
  4. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_events.py +308 -117
  5. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/decorators.py +17 -13
  6. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/message_sender.py +57 -51
  7. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/event/__init__.py +1 -1
  8. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/handlers/handlers.py +21 -16
  9. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/uv.lock +1 -1
  10. smart_bot_factory-0.3.10/.github/ISSUE_TEMPLATE//342/234/250-/320/267/320/260/320/277/321/200/320/276/321/201-/321/204/321/203/320/275/320/272/321/206/320/270/320/270.md +0 -20
  11. smart_bot_factory-0.3.10/.github/ISSUE_TEMPLATE//360/237/220/233-/320/261/320/260/320/263-/321/200/320/265/320/277/320/276/321/200/321/202.md +0 -26
  12. smart_bot_factory-0.3.10/.github/workflows/ci.yml +0 -59
  13. smart_bot_factory-0.3.10/.github/workflows/publish-private.yml +0 -34
  14. smart_bot_factory-0.3.10/.github/workflows/publish.yml +0 -31
  15. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/.gitignore +0 -0
  16. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/.python-version +0 -0
  17. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/LICENSE +0 -0
  18. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/README.md +0 -0
  19. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/__init__.py +0 -0
  20. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/__init__.py +0 -0
  21. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_logic.py +0 -0
  22. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_manager.py +0 -0
  23. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_tester.py +0 -0
  24. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/timeout_checker.py +0 -0
  25. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/__init__.py +0 -0
  26. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/common.py +0 -0
  27. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/dialog_calendar.py +0 -0
  28. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/schemas.py +0 -0
  29. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/simple_calendar.py +0 -0
  30. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/analytics/analytics_manager.py +0 -0
  31. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/cli.py +0 -0
  32. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/config.py +0 -0
  33. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +0 -0
  34. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +0 -0
  35. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +0 -0
  36. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +0 -0
  37. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +0 -0
  38. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +0 -0
  39. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +0 -0
  40. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +0 -0
  41. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +0 -0
  42. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +0 -0
  43. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +0 -0
  44. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +0 -0
  45. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +0 -0
  46. {smart_bot_factory-0.3.10 → 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
  47. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/bot_utils.py +0 -0
  48. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/conversation_manager.py +0 -0
  49. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/router.py +0 -0
  50. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/router_manager.py +0 -0
  51. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/states.py +0 -0
  52. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/__init__.py +0 -0
  53. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/bot_builder.py +0 -0
  54. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/bot_testing.py +0 -0
  55. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/dashboard/__init__.py +0 -0
  56. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/integrations/openai_client.py +0 -0
  57. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/integrations/supabase_client.py +0 -0
  58. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/message/__init__.py +0 -0
  59. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/router/__init__.py +0 -0
  60. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/setup_checker.py +0 -0
  61. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/supabase/__init__.py +0 -0
  62. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/supabase/client.py +0 -0
  63. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/__init__.py +0 -0
  64. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/debug_routing.py +0 -0
  65. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/prompt_loader.py +0 -0
  66. {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/user_prompt_loader.py +0 -0
  67. {smart_bot_factory-0.3.10 → 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: 0.3.10
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 = "0.3.10"
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",
@@ -7,7 +7,7 @@ import logging
7
7
  import re
8
8
  from datetime import datetime, timedelta, timezone
9
9
  from functools import wraps
10
- from typing import Any, Callable, Dict, Union
10
+ from typing import Any, Callable, Dict, Optional, Union
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -2176,12 +2176,13 @@ async def check_event_already_processed(
2176
2176
  return False
2177
2177
 
2178
2178
 
2179
- async def process_admin_event(event: Dict):
2179
+ async def process_admin_event(event: Dict, single_user_id: Optional[int] = None):
2180
2180
  """
2181
2181
  Обрабатывает одно админское событие - скачивает файлы из Storage и отправляет пользователям
2182
2182
 
2183
2183
  Args:
2184
2184
  event: Событие из БД с данными для отправки
2185
+ single_user_id: ID пользователя для тестовой отправки. Если указан, сообщение будет отправлено только ему
2185
2186
  """
2186
2187
  import json
2187
2188
  import shutil
@@ -2265,17 +2266,20 @@ async def process_admin_event(event: Dict):
2265
2266
  raise
2266
2267
 
2267
2268
  # 2. Получаем пользователей
2268
- users = await supabase_client.get_users_by_segment(segment)
2269
-
2270
- if not users:
2271
- logger.warning(f"⚠️ Нет пользователей для сегмента '{segment}'")
2272
- return {
2273
- "success_count": 0,
2274
- "failed_count": 0,
2275
- "total_users": 0,
2276
- "segment": segment or "Все",
2277
- "warning": "Нет пользователей",
2278
- }
2269
+ if single_user_id:
2270
+ users = [{"telegram_id": single_user_id}]
2271
+ logger.info(f"🔍 Тестовая отправка для пользователя {single_user_id}")
2272
+ else:
2273
+ users = await supabase_client.get_users_by_segment(segment)
2274
+ if not users:
2275
+ logger.warning(f"⚠️ Нет пользователей для сегмента '{segment}'")
2276
+ return {
2277
+ "success_count": 0,
2278
+ "failed_count": 0,
2279
+ "total_users": 0,
2280
+ "segment": segment or "Все",
2281
+ "warning": "Нет пользователей",
2282
+ }
2279
2283
 
2280
2284
  success_count = 0
2281
2285
  failed_count = 0