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.
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/PKG-INFO +1 -1
- smart_bot_factory-1.0.1/hello.jpg +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/pyproject.toml +1 -1
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_events.py +308 -117
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/decorators.py +17 -13
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/message_sender.py +57 -51
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/event/__init__.py +1 -1
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/handlers/handlers.py +21 -16
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/uv.lock +1 -1
- 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
- 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
- smart_bot_factory-0.3.10/.github/workflows/ci.yml +0 -59
- smart_bot_factory-0.3.10/.github/workflows/publish-private.yml +0 -34
- smart_bot_factory-0.3.10/.github/workflows/publish.yml +0 -31
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/.gitignore +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/.python-version +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/LICENSE +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/README.md +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_logic.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_manager.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_tester.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/timeout_checker.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/common.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/dialog_calendar.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/schemas.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/aiogram_calendar/simple_calendar.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/analytics/analytics_manager.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/cli.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/config.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/bot_utils.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/conversation_manager.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/router.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/router_manager.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/core/states.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/bot_builder.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/creation/bot_testing.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/dashboard/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/integrations/openai_client.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/integrations/supabase_client.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/message/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/router/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/setup_checker.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/supabase/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/supabase/client.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/__init__.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/debug_routing.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/prompt_loader.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utils/user_prompt_loader.py +0 -0
- {smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/utm_link_generator.py +0 -0
|
Binary file
|
{smart_bot_factory-0.3.10 → smart_bot_factory-1.0.1}/smart_bot_factory/admin/admin_events.py
RENAMED
|
@@ -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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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"📅
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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"📅
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
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
|
-
|
|
794
|
-
utc_datetime = moscow_datetime.astimezone(pytz.UTC)
|
|
955
|
+
sent_count += 1
|
|
795
956
|
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
"
|
|
807
|
-
"
|
|
808
|
-
"
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
816
|
-
|
|
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
|
-
|
|
819
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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"📅
|
|
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
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
"
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
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
|