smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/admin/__init__.py +7 -7
- smart_bot_factory/admin/admin_events.py +483 -383
- smart_bot_factory/admin/admin_logic.py +234 -158
- smart_bot_factory/admin/admin_manager.py +68 -53
- smart_bot_factory/admin/admin_tester.py +46 -40
- smart_bot_factory/admin/timeout_checker.py +201 -153
- smart_bot_factory/aiogram_calendar/__init__.py +11 -3
- smart_bot_factory/aiogram_calendar/common.py +12 -18
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
- smart_bot_factory/aiogram_calendar/schemas.py +49 -28
- smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
- smart_bot_factory/analytics/analytics_manager.py +414 -392
- smart_bot_factory/cli.py +204 -148
- smart_bot_factory/config.py +123 -102
- smart_bot_factory/core/bot_utils.py +474 -332
- smart_bot_factory/core/conversation_manager.py +287 -200
- smart_bot_factory/core/decorators.py +1129 -749
- smart_bot_factory/core/message_sender.py +287 -266
- smart_bot_factory/core/router.py +170 -100
- smart_bot_factory/core/router_manager.py +121 -83
- smart_bot_factory/core/states.py +4 -3
- smart_bot_factory/creation/__init__.py +1 -1
- smart_bot_factory/creation/bot_builder.py +320 -242
- smart_bot_factory/creation/bot_testing.py +440 -365
- smart_bot_factory/dashboard/__init__.py +1 -3
- smart_bot_factory/event/__init__.py +2 -7
- smart_bot_factory/handlers/handlers.py +676 -472
- smart_bot_factory/integrations/openai_client.py +218 -168
- smart_bot_factory/integrations/supabase_client.py +928 -637
- smart_bot_factory/message/__init__.py +18 -22
- smart_bot_factory/router/__init__.py +2 -2
- smart_bot_factory/setup_checker.py +162 -126
- smart_bot_factory/supabase/__init__.py +1 -1
- smart_bot_factory/supabase/client.py +631 -515
- smart_bot_factory/utils/__init__.py +2 -3
- smart_bot_factory/utils/debug_routing.py +38 -27
- smart_bot_factory/utils/prompt_loader.py +153 -120
- smart_bot_factory/utils/user_prompt_loader.py +55 -56
- smart_bot_factory/utm_link_generator.py +123 -116
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
- smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
- smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
# Обработчики для создания админских событий
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
7
|
+
import uuid
|
|
6
8
|
from datetime import datetime
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
import pytz
|
|
11
|
+
from aiogram import F, Router
|
|
9
12
|
from aiogram.filters import Command
|
|
10
13
|
from aiogram.fsm.context import FSMContext
|
|
11
|
-
from aiogram.types import
|
|
14
|
+
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
|
|
15
|
+
InlineKeyboardMarkup, Message)
|
|
12
16
|
from aiogram_media_group import media_group_handler
|
|
17
|
+
from dateutil.relativedelta import relativedelta
|
|
18
|
+
|
|
13
19
|
from ..aiogram_calendar import SimpleCalendar, SimpleCalendarCallback
|
|
14
|
-
import
|
|
15
|
-
import uuid
|
|
16
|
-
import json
|
|
20
|
+
from ..core.states import AdminStates
|
|
17
21
|
|
|
18
22
|
TEMP_DIR = "temp_event_files"
|
|
19
23
|
|
|
24
|
+
|
|
20
25
|
def generate_file_id() -> str:
|
|
21
26
|
"""Генерирует уникальный ID для файла"""
|
|
22
27
|
return f"file_{uuid.uuid4().hex}"
|
|
23
28
|
|
|
29
|
+
|
|
24
30
|
def ensure_temp_dir():
|
|
25
31
|
"""Создает временную папку если её нет"""
|
|
26
32
|
if not os.path.exists(TEMP_DIR):
|
|
27
33
|
os.makedirs(TEMP_DIR)
|
|
28
34
|
logger.info(f"📁 Создана временная папка {TEMP_DIR}")
|
|
29
35
|
|
|
36
|
+
|
|
30
37
|
async def cleanup_temp_files(state: FSMContext = None):
|
|
31
38
|
"""Очистка временных файлов события"""
|
|
32
39
|
# Удаляем все файлы из временной папки
|
|
@@ -36,238 +43,261 @@ async def cleanup_temp_files(state: FSMContext = None):
|
|
|
36
43
|
logger.info(f"🗑️ Удалена папка {TEMP_DIR}")
|
|
37
44
|
except Exception as e:
|
|
38
45
|
logger.error(f"❌ Ошибка при удалении {TEMP_DIR}: {e}")
|
|
39
|
-
|
|
46
|
+
|
|
40
47
|
# Очищаем информацию о файлах в состоянии
|
|
41
48
|
if state:
|
|
42
49
|
try:
|
|
43
50
|
data = await state.get_data()
|
|
44
|
-
if
|
|
45
|
-
data[
|
|
51
|
+
if "files" in data:
|
|
52
|
+
data["files"] = []
|
|
46
53
|
await state.set_data(data)
|
|
47
54
|
except Exception as e:
|
|
48
55
|
logger.error(f"❌ Ошибка при очистке состояния: {e}")
|
|
49
56
|
|
|
57
|
+
|
|
50
58
|
logger = logging.getLogger(__name__)
|
|
51
59
|
|
|
52
60
|
# Московская временная зона
|
|
53
|
-
MOSCOW_TZ = pytz.timezone(
|
|
54
|
-
|
|
55
|
-
# Импортируем состояния
|
|
56
|
-
from ..core.states import AdminStates
|
|
61
|
+
MOSCOW_TZ = pytz.timezone("Europe/Moscow")
|
|
57
62
|
|
|
58
63
|
# Создаем роутер для админских событий
|
|
59
64
|
admin_events_router = Router()
|
|
60
65
|
|
|
66
|
+
|
|
61
67
|
def setup_admin_events_handlers(dp):
|
|
62
68
|
"""Настройка обработчиков админских событий"""
|
|
63
69
|
dp.include_router(admin_events_router)
|
|
64
70
|
|
|
71
|
+
|
|
65
72
|
@admin_events_router.message(Command(commands=["создать_событие", "create_event"]))
|
|
66
73
|
async def create_event_start(message: Message, state: FSMContext):
|
|
67
74
|
"""Начало создания события"""
|
|
68
75
|
from ..handlers.handlers import get_global_var
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
|
|
77
|
+
admin_manager = get_global_var("admin_manager")
|
|
78
|
+
|
|
71
79
|
if not admin_manager.is_admin(message.from_user.id):
|
|
72
80
|
return
|
|
73
|
-
|
|
81
|
+
|
|
74
82
|
await state.set_state(AdminStates.create_event_name)
|
|
75
|
-
|
|
83
|
+
|
|
76
84
|
await message.answer(
|
|
77
85
|
"📝 **Введите название события**\n\n"
|
|
78
86
|
"💡 _По этому названию вы сможете:\n"
|
|
79
87
|
"• Найти событие в списке\n"
|
|
80
88
|
"• Отменить его при необходимости_",
|
|
81
|
-
parse_mode=
|
|
89
|
+
parse_mode="Markdown",
|
|
82
90
|
)
|
|
83
91
|
|
|
92
|
+
|
|
84
93
|
@admin_events_router.message(AdminStates.create_event_name)
|
|
85
94
|
async def process_event_name(message: Message, state: FSMContext):
|
|
86
95
|
"""Обработка названия события"""
|
|
87
96
|
from ..handlers.handlers import get_global_var
|
|
88
|
-
|
|
97
|
+
|
|
89
98
|
event_name = message.text.strip()
|
|
90
|
-
|
|
99
|
+
|
|
91
100
|
if not event_name:
|
|
92
101
|
await message.answer("❌ Название не может быть пустым. Попробуйте еще раз:")
|
|
93
102
|
return
|
|
94
|
-
|
|
103
|
+
|
|
95
104
|
# Проверяем уникальность названия (только среди активных событий)
|
|
96
|
-
supabase_client = get_global_var(
|
|
105
|
+
supabase_client = get_global_var("supabase_client")
|
|
97
106
|
name_exists = await supabase_client.check_event_name_exists(event_name)
|
|
98
|
-
|
|
107
|
+
|
|
99
108
|
if name_exists:
|
|
100
109
|
await message.answer(
|
|
101
110
|
f"⚠️ **Событие с названием «{event_name}» уже существует и находится в статусе ожидания!**\n\n"
|
|
102
111
|
f"Пожалуйста, выберите другое название или дождитесь выполнения/отмены существующего события.\n\n"
|
|
103
112
|
f"💡 _Вы можете использовать это же название после завершения или отмены текущего события._",
|
|
104
|
-
parse_mode=
|
|
113
|
+
parse_mode="Markdown",
|
|
105
114
|
)
|
|
106
115
|
return
|
|
107
|
-
|
|
116
|
+
|
|
108
117
|
# Сохраняем название
|
|
109
118
|
await state.update_data(event_name=event_name)
|
|
110
119
|
await state.set_state(AdminStates.create_event_date)
|
|
111
|
-
|
|
120
|
+
|
|
112
121
|
# Показываем календарь для выбора даты
|
|
113
|
-
calendar = SimpleCalendar(locale=
|
|
122
|
+
calendar = SimpleCalendar(locale="ru", today_btn="Сегодня", cancel_btn="Отмена")
|
|
114
123
|
# Ограничиваем выбор датами от вчера до +12 месяцев (чтобы сегодня был доступен)
|
|
115
|
-
calendar.set_dates_range(
|
|
124
|
+
calendar.set_dates_range(
|
|
125
|
+
datetime.now() + relativedelta(days=-1),
|
|
126
|
+
datetime.now() + relativedelta(months=+12),
|
|
127
|
+
)
|
|
116
128
|
calendar_markup = await calendar.start_calendar()
|
|
117
|
-
|
|
129
|
+
|
|
118
130
|
await message.answer(
|
|
119
|
-
f"✅ Название события: **{event_name}**\n\n"
|
|
120
|
-
"📅 Выберите дату отправки:",
|
|
131
|
+
f"✅ Название события: **{event_name}**\n\n" "📅 Выберите дату отправки:",
|
|
121
132
|
reply_markup=calendar_markup,
|
|
122
|
-
parse_mode=
|
|
133
|
+
parse_mode="Markdown",
|
|
123
134
|
)
|
|
124
135
|
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
|
|
137
|
+
@admin_events_router.callback_query(
|
|
138
|
+
SimpleCalendarCallback.filter(), AdminStates.create_event_date
|
|
139
|
+
)
|
|
140
|
+
async def process_event_date(
|
|
141
|
+
callback_query: CallbackQuery, callback_data: dict, state: FSMContext
|
|
142
|
+
):
|
|
127
143
|
"""Обработка выбора даты"""
|
|
128
|
-
calendar = SimpleCalendar(locale=
|
|
129
|
-
|
|
144
|
+
calendar = SimpleCalendar(locale="ru", cancel_btn="Отмена", today_btn="Сегодня")
|
|
145
|
+
|
|
130
146
|
# Ограничиваем выбор датами от вчера до +12 месяцев (чтобы сегодня был доступен)
|
|
131
|
-
calendar.set_dates_range(
|
|
147
|
+
calendar.set_dates_range(
|
|
148
|
+
datetime.now() + relativedelta(days=-1),
|
|
149
|
+
datetime.now() + relativedelta(months=+12),
|
|
150
|
+
)
|
|
132
151
|
selected, date = await calendar.process_selection(callback_query, callback_data)
|
|
133
|
-
|
|
134
|
-
if selected ==
|
|
152
|
+
|
|
153
|
+
if selected == "cancel":
|
|
135
154
|
# Нажата кнопка "Отмена"
|
|
136
155
|
await state.clear()
|
|
137
156
|
await callback_query.message.edit_text(
|
|
138
|
-
"❌ Создание события отменено",
|
|
139
|
-
parse_mode='Markdown'
|
|
157
|
+
"❌ Создание события отменено", parse_mode="Markdown"
|
|
140
158
|
)
|
|
141
159
|
elif selected:
|
|
142
160
|
# Дата выбрана успешно (True или обычный выбор)
|
|
143
|
-
await state.update_data(event_date=date.strftime(
|
|
161
|
+
await state.update_data(event_date=date.strftime("%Y-%m-%d"))
|
|
144
162
|
await state.set_state(AdminStates.create_event_time)
|
|
145
|
-
|
|
163
|
+
|
|
146
164
|
await callback_query.message.edit_text(
|
|
147
165
|
f"✅ Дата: **{date.strftime('%d.%m.%Y')}**\n\n"
|
|
148
166
|
"⏰ Введите время отправки в формате ЧЧ:ММ\n"
|
|
149
167
|
"_(Например: 14:30)_",
|
|
150
|
-
parse_mode=
|
|
168
|
+
parse_mode="Markdown",
|
|
151
169
|
)
|
|
152
170
|
# Если selected is False/None - это навигация по календарю, ничего не делаем
|
|
153
171
|
# Календарь сам обновится при навигации
|
|
154
172
|
|
|
173
|
+
|
|
155
174
|
@admin_events_router.message(AdminStates.create_event_time)
|
|
156
175
|
async def process_event_time(message: Message, state: FSMContext):
|
|
157
176
|
"""Обработка времени события"""
|
|
158
177
|
time_str = message.text.strip()
|
|
159
|
-
|
|
178
|
+
|
|
160
179
|
# Валидация формата времени
|
|
161
180
|
try:
|
|
162
|
-
|
|
181
|
+
datetime.strptime(time_str, "%H:%M").time()
|
|
163
182
|
except ValueError:
|
|
164
183
|
await message.answer(
|
|
165
184
|
"❌ Неверный формат времени. Используйте формат HH:MM\n"
|
|
166
185
|
"_(Например: 14:30)_",
|
|
167
|
-
parse_mode=
|
|
186
|
+
parse_mode="Markdown",
|
|
168
187
|
)
|
|
169
188
|
return
|
|
170
|
-
|
|
189
|
+
|
|
171
190
|
# Сохраняем время
|
|
172
191
|
await state.update_data(event_time=time_str)
|
|
173
192
|
await state.set_state(AdminStates.create_event_segment)
|
|
174
|
-
|
|
193
|
+
|
|
175
194
|
# Получаем все доступные сегменты
|
|
176
195
|
from ..handlers.handlers import get_global_var
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
|
|
197
|
+
supabase_client = get_global_var("supabase_client")
|
|
198
|
+
|
|
179
199
|
segments = await supabase_client.get_all_segments()
|
|
180
|
-
|
|
200
|
+
|
|
181
201
|
# Создаем клавиатуру с сегментами
|
|
182
202
|
keyboard = []
|
|
183
|
-
|
|
203
|
+
|
|
184
204
|
# Большая кнопка "Отправить всем" на два столбца
|
|
185
|
-
keyboard.append(
|
|
186
|
-
InlineKeyboardButton(text="📢 Отправить всем", callback_data="segment:all")
|
|
187
|
-
|
|
188
|
-
|
|
205
|
+
keyboard.append(
|
|
206
|
+
[InlineKeyboardButton(text="📢 Отправить всем", callback_data="segment:all")]
|
|
207
|
+
)
|
|
208
|
+
|
|
189
209
|
# Кнопки сегментов (по 2 в ряд)
|
|
190
210
|
if segments:
|
|
191
211
|
for i in range(0, len(segments), 2):
|
|
192
212
|
row = []
|
|
193
|
-
row.append(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
213
|
+
row.append(
|
|
214
|
+
InlineKeyboardButton(
|
|
215
|
+
text=f"👥 {segments[i]}", callback_data=f"segment:{segments[i]}"
|
|
216
|
+
)
|
|
217
|
+
)
|
|
197
218
|
if i + 1 < len(segments):
|
|
198
|
-
row.append(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
row.append(
|
|
220
|
+
InlineKeyboardButton(
|
|
221
|
+
text=f"👥 {segments[i+1]}",
|
|
222
|
+
callback_data=f"segment:{segments[i+1]}",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
202
225
|
keyboard.append(row)
|
|
203
|
-
|
|
226
|
+
|
|
204
227
|
markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
|
|
205
|
-
|
|
206
|
-
data = await state.get_data()
|
|
228
|
+
|
|
207
229
|
await message.answer(
|
|
208
230
|
f"✅ Время: **{time_str}**\n\n"
|
|
209
231
|
f"👥 Выберите сегмент пользователей для отправки:\n"
|
|
210
232
|
f"_(Найдено сегментов: {len(segments)})_",
|
|
211
233
|
reply_markup=markup,
|
|
212
|
-
parse_mode=
|
|
234
|
+
parse_mode="Markdown",
|
|
213
235
|
)
|
|
214
236
|
|
|
215
|
-
|
|
237
|
+
|
|
238
|
+
@admin_events_router.callback_query(
|
|
239
|
+
F.data.startswith("segment:"), AdminStates.create_event_segment
|
|
240
|
+
)
|
|
216
241
|
async def process_event_segment(callback_query: CallbackQuery, state: FSMContext):
|
|
217
242
|
"""Обработка выбора сегмента"""
|
|
218
243
|
segment_data = callback_query.data.split(":", 1)[1]
|
|
219
|
-
|
|
244
|
+
|
|
220
245
|
# segment_data = "all" или название сегмента
|
|
221
246
|
segment_name = None if segment_data == "all" else segment_data
|
|
222
247
|
segment_display = "Все пользователи" if segment_data == "all" else segment_data
|
|
223
|
-
|
|
248
|
+
|
|
224
249
|
# Сохраняем сегмент
|
|
225
250
|
await state.update_data(segment=segment_name, segment_display=segment_display)
|
|
226
251
|
await state.set_state(AdminStates.create_event_message)
|
|
227
|
-
|
|
252
|
+
|
|
228
253
|
await callback_query.message.edit_text(
|
|
229
254
|
f"✅ Сегмент: **{segment_display}**\n\n"
|
|
230
255
|
"💬 **Введите сообщение для пользователей**\n\n"
|
|
231
256
|
"📸 _Вы можете прикрепить к сообщению **фото или видео** — они будут отправлены пользователям в том же порядке_\n\n"
|
|
232
257
|
"📄 _Если нужно добавить **PDF или другие документы**, вы сможете это сделать на следующем шаге_",
|
|
233
|
-
parse_mode=
|
|
258
|
+
parse_mode="Markdown",
|
|
234
259
|
)
|
|
235
|
-
|
|
236
|
-
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@admin_events_router.message(
|
|
263
|
+
AdminStates.create_event_message,
|
|
264
|
+
F.media_group_id,
|
|
265
|
+
F.content_type.in_({"photo", "video"}),
|
|
266
|
+
)
|
|
237
267
|
@media_group_handler
|
|
238
268
|
async def handle_album(messages: list[Message], state: FSMContext):
|
|
239
269
|
"""Обработка альбома фотографий/видео"""
|
|
240
270
|
if not messages:
|
|
241
271
|
return
|
|
242
|
-
|
|
272
|
+
|
|
243
273
|
# Берем текст из первого сообщения с подписью
|
|
244
274
|
event_message = next((msg.caption for msg in messages if msg.caption), None)
|
|
245
275
|
if not event_message:
|
|
246
276
|
await messages[0].answer(
|
|
247
277
|
"❌ **Добавьте подпись к альбому**\n\n"
|
|
248
278
|
"💡 _Отправьте альбом заново с текстом сообщения в подписи к любой фотографии_",
|
|
249
|
-
parse_mode=
|
|
279
|
+
parse_mode="Markdown",
|
|
250
280
|
)
|
|
251
281
|
return
|
|
252
|
-
|
|
282
|
+
|
|
253
283
|
# Сохраняем сообщение
|
|
254
284
|
await state.update_data(event_message=event_message)
|
|
255
|
-
|
|
285
|
+
|
|
256
286
|
# Показываем сообщение о начале загрузки
|
|
257
287
|
await messages[0].answer(
|
|
258
|
-
"📸 **Загружаю файлы...**\n\n"
|
|
259
|
-
"
|
|
260
|
-
parse_mode='Markdown'
|
|
288
|
+
"📸 **Загружаю файлы...**\n\n" "💡 _Дождитесь загрузки всех файлов из альбома_",
|
|
289
|
+
parse_mode="Markdown",
|
|
261
290
|
)
|
|
262
|
-
|
|
291
|
+
|
|
263
292
|
# Сохраняем все файлы
|
|
264
293
|
from ..handlers.handlers import get_global_var
|
|
265
|
-
|
|
294
|
+
|
|
295
|
+
bot = get_global_var("bot")
|
|
266
296
|
ensure_temp_dir()
|
|
267
|
-
|
|
297
|
+
|
|
268
298
|
data = await state.get_data()
|
|
269
|
-
files = data.get(
|
|
270
|
-
|
|
299
|
+
files = data.get("files", [])
|
|
300
|
+
|
|
271
301
|
for i, message in enumerate(messages, 1):
|
|
272
302
|
try:
|
|
273
303
|
if message.photo:
|
|
@@ -276,52 +306,61 @@ async def handle_album(messages: list[Message], state: FSMContext):
|
|
|
276
306
|
file_name = f"photo_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{i}.jpg"
|
|
277
307
|
file_path = os.path.join(TEMP_DIR, file_name)
|
|
278
308
|
await bot.download_file(file.file_path, file_path)
|
|
279
|
-
|
|
280
|
-
files.append(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
309
|
+
|
|
310
|
+
files.append(
|
|
311
|
+
{
|
|
312
|
+
"type": "photo",
|
|
313
|
+
"file_path": file_path,
|
|
314
|
+
"name": file_name,
|
|
315
|
+
"stage": "with_message",
|
|
316
|
+
"has_caption": bool(message.caption),
|
|
317
|
+
"order": i, # Сохраняем порядок в альбоме
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
|
|
289
321
|
elif message.video:
|
|
290
322
|
file = await bot.get_file(message.video.file_id)
|
|
291
323
|
file_name = message.video.file_name or f"{message.video.file_id}.mp4"
|
|
292
324
|
file_path = os.path.join(TEMP_DIR, file_name)
|
|
293
325
|
await bot.download_file(file.file_path, file_path)
|
|
294
|
-
|
|
295
|
-
files.append(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
326
|
+
|
|
327
|
+
files.append(
|
|
328
|
+
{
|
|
329
|
+
"type": "video",
|
|
330
|
+
"file_path": file_path,
|
|
331
|
+
"name": file_name,
|
|
332
|
+
"stage": "with_message",
|
|
333
|
+
"has_caption": bool(message.caption),
|
|
334
|
+
"order": i, # Сохраняем порядок в альбоме
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
304
338
|
# Показываем прогресс каждые 5 файлов
|
|
305
339
|
if i % 5 == 0:
|
|
306
340
|
await messages[0].answer(
|
|
307
|
-
f"📸 Загружено файлов: {i}/{len(messages)}",
|
|
308
|
-
parse_mode='Markdown'
|
|
341
|
+
f"📸 Загружено файлов: {i}/{len(messages)}", parse_mode="Markdown"
|
|
309
342
|
)
|
|
310
|
-
|
|
343
|
+
|
|
311
344
|
except Exception as e:
|
|
312
345
|
logger.error(f"❌ Ошибка загрузки файла {i}: {e}")
|
|
313
346
|
continue
|
|
314
|
-
|
|
347
|
+
|
|
315
348
|
# Сохраняем файлы
|
|
316
349
|
await state.update_data(files=files)
|
|
317
|
-
|
|
350
|
+
|
|
318
351
|
# Переходим к следующему этапу
|
|
319
352
|
await state.set_state(AdminStates.create_event_files)
|
|
320
|
-
|
|
321
|
-
keyboard = InlineKeyboardMarkup(
|
|
322
|
-
[
|
|
323
|
-
|
|
324
|
-
|
|
353
|
+
|
|
354
|
+
keyboard = InlineKeyboardMarkup(
|
|
355
|
+
inline_keyboard=[
|
|
356
|
+
[
|
|
357
|
+
InlineKeyboardButton(
|
|
358
|
+
text="➡️ Продолжить без файлов", callback_data="files:skip"
|
|
359
|
+
)
|
|
360
|
+
]
|
|
361
|
+
]
|
|
362
|
+
)
|
|
363
|
+
|
|
325
364
|
await messages[0].answer(
|
|
326
365
|
f"✅ **Сообщение и {len(files)} файлов сохранены!**\n\n"
|
|
327
366
|
"📎 **Дополнительные файлы**\n\n"
|
|
@@ -333,38 +372,43 @@ async def handle_album(messages: list[Message], state: FSMContext):
|
|
|
333
372
|
"💡 _Можно отправить несколько файлов по очереди_\n\n"
|
|
334
373
|
"Или нажмите кнопку, если дополнительных файлов нет:",
|
|
335
374
|
reply_markup=keyboard,
|
|
336
|
-
parse_mode=
|
|
375
|
+
parse_mode="Markdown",
|
|
337
376
|
)
|
|
338
377
|
|
|
339
|
-
|
|
378
|
+
|
|
379
|
+
@admin_events_router.message(
|
|
380
|
+
AdminStates.create_event_message, F.text | F.photo | F.video
|
|
381
|
+
)
|
|
340
382
|
async def process_event_message(message: Message, state: FSMContext):
|
|
341
383
|
"""Обработка одиночного сообщения с текстом/фото/видео"""
|
|
342
384
|
# Если это часть альбома - пропускаем, его обработает другой handler
|
|
343
385
|
if message.media_group_id:
|
|
344
386
|
return
|
|
345
|
-
|
|
387
|
+
|
|
346
388
|
event_message = message.text or message.caption or ""
|
|
347
|
-
|
|
389
|
+
|
|
348
390
|
# Проверяем текст
|
|
349
391
|
if not event_message.strip():
|
|
350
392
|
await message.answer("❌ Сообщение не может быть пустым. Попробуйте еще раз:")
|
|
351
393
|
return
|
|
352
|
-
|
|
394
|
+
|
|
353
395
|
# Сохраняем сообщение
|
|
354
396
|
await state.update_data(event_message=event_message)
|
|
355
|
-
|
|
397
|
+
|
|
356
398
|
# Если есть медиа, сохраняем его
|
|
357
399
|
data = await state.get_data()
|
|
358
|
-
files = data.get(
|
|
359
|
-
|
|
400
|
+
files = data.get("files", [])
|
|
401
|
+
|
|
360
402
|
if message.photo or message.video:
|
|
361
403
|
import os
|
|
404
|
+
|
|
362
405
|
from ..handlers.handlers import get_global_var
|
|
363
|
-
|
|
364
|
-
|
|
406
|
+
|
|
407
|
+
bot = get_global_var("bot")
|
|
408
|
+
|
|
365
409
|
# Создаем временную папку
|
|
366
410
|
ensure_temp_dir()
|
|
367
|
-
|
|
411
|
+
|
|
368
412
|
if message.photo:
|
|
369
413
|
# Скачиваем фото
|
|
370
414
|
photo = message.photo[-1] # Берем самое большое фото
|
|
@@ -372,41 +416,51 @@ async def process_event_message(message: Message, state: FSMContext):
|
|
|
372
416
|
file_name = f"photo_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
|
|
373
417
|
file_path = os.path.join(TEMP_DIR, file_name)
|
|
374
418
|
await bot.download_file(file.file_path, file_path)
|
|
375
|
-
|
|
376
|
-
files.append(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
419
|
+
|
|
420
|
+
files.append(
|
|
421
|
+
{
|
|
422
|
+
"type": "photo",
|
|
423
|
+
"file_path": file_path,
|
|
424
|
+
"name": file_name,
|
|
425
|
+
"stage": "with_message",
|
|
426
|
+
"has_caption": bool(message.caption),
|
|
427
|
+
}
|
|
428
|
+
)
|
|
383
429
|
logger.info(f"Фото сохранено: {file_path} (with_message)")
|
|
384
|
-
|
|
430
|
+
|
|
385
431
|
elif message.video:
|
|
386
432
|
# Скачиваем видео
|
|
387
433
|
file = await bot.get_file(message.video.file_id)
|
|
388
434
|
file_name = message.video.file_name or f"{message.video.file_id}.mp4"
|
|
389
435
|
file_path = os.path.join(TEMP_DIR, file_name)
|
|
390
436
|
await bot.download_file(file.file_path, file_path)
|
|
391
|
-
|
|
392
|
-
files.append(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
437
|
+
|
|
438
|
+
files.append(
|
|
439
|
+
{
|
|
440
|
+
"type": "video",
|
|
441
|
+
"file_path": file_path,
|
|
442
|
+
"name": file_name,
|
|
443
|
+
"stage": "with_message",
|
|
444
|
+
"has_caption": bool(message.caption),
|
|
445
|
+
}
|
|
446
|
+
)
|
|
399
447
|
logger.info(f"Видео сохранено: {file_path} (with_message)")
|
|
400
|
-
|
|
448
|
+
|
|
401
449
|
await state.update_data(files=files)
|
|
402
|
-
|
|
450
|
+
|
|
403
451
|
# Переходим к добавлению файлов
|
|
404
452
|
await state.set_state(AdminStates.create_event_files)
|
|
405
|
-
|
|
406
|
-
keyboard = InlineKeyboardMarkup(
|
|
407
|
-
[
|
|
408
|
-
|
|
409
|
-
|
|
453
|
+
|
|
454
|
+
keyboard = InlineKeyboardMarkup(
|
|
455
|
+
inline_keyboard=[
|
|
456
|
+
[
|
|
457
|
+
InlineKeyboardButton(
|
|
458
|
+
text="➡️ Продолжить без файлов", callback_data="files:skip"
|
|
459
|
+
)
|
|
460
|
+
]
|
|
461
|
+
]
|
|
462
|
+
)
|
|
463
|
+
|
|
410
464
|
await message.answer(
|
|
411
465
|
"✅ **Сообщение сохранено!**\n\n"
|
|
412
466
|
"📎 **Дополнительные файлы**\n\n"
|
|
@@ -418,87 +472,107 @@ async def process_event_message(message: Message, state: FSMContext):
|
|
|
418
472
|
"💡 _Можно отправить несколько файлов по очереди_\n\n"
|
|
419
473
|
"Или нажмите кнопку, если дополнительных файлов нет:",
|
|
420
474
|
reply_markup=keyboard,
|
|
421
|
-
parse_mode=
|
|
475
|
+
parse_mode="Markdown",
|
|
422
476
|
)
|
|
423
477
|
|
|
424
478
|
|
|
425
|
-
@admin_events_router.message(
|
|
479
|
+
@admin_events_router.message(
|
|
480
|
+
AdminStates.create_event_files, F.document | F.photo | F.video
|
|
481
|
+
)
|
|
426
482
|
async def process_event_files(message: Message, state: FSMContext):
|
|
427
483
|
"""Обработка файлов для события"""
|
|
428
484
|
import os
|
|
485
|
+
|
|
429
486
|
from ..handlers.handlers import get_global_var
|
|
430
|
-
|
|
487
|
+
|
|
431
488
|
data = await state.get_data()
|
|
432
|
-
files = data.get(
|
|
433
|
-
bot = get_global_var(
|
|
434
|
-
|
|
489
|
+
files = data.get("files", [])
|
|
490
|
+
bot = get_global_var("bot")
|
|
491
|
+
|
|
435
492
|
# Создаем временную папку
|
|
436
493
|
ensure_temp_dir()
|
|
437
|
-
|
|
494
|
+
|
|
438
495
|
# Скачиваем и добавляем файл в список
|
|
439
496
|
if message.document:
|
|
440
497
|
file = await bot.get_file(message.document.file_id)
|
|
441
498
|
file_path = os.path.join(TEMP_DIR, message.document.file_name)
|
|
442
499
|
await bot.download_file(file.file_path, file_path)
|
|
443
|
-
|
|
444
|
-
files.append(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
500
|
+
|
|
501
|
+
files.append(
|
|
502
|
+
{
|
|
503
|
+
"type": "document",
|
|
504
|
+
"file_path": file_path,
|
|
505
|
+
"name": message.document.file_name,
|
|
506
|
+
"stage": "after_message",
|
|
507
|
+
}
|
|
508
|
+
)
|
|
450
509
|
logger.info(f"Документ сохранен: {file_path} (after_message)")
|
|
451
|
-
|
|
510
|
+
|
|
452
511
|
elif message.photo:
|
|
453
512
|
photo = message.photo[-1]
|
|
454
513
|
file = await bot.get_file(photo.file_id)
|
|
455
514
|
file_name = f"photo_{datetime.now().strftime('%H%M%S')}.jpg"
|
|
456
515
|
file_path = os.path.join(TEMP_DIR, file_name)
|
|
457
516
|
await bot.download_file(file.file_path, file_path)
|
|
458
|
-
|
|
459
|
-
files.append(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
517
|
+
|
|
518
|
+
files.append(
|
|
519
|
+
{
|
|
520
|
+
"type": "photo",
|
|
521
|
+
"file_path": file_path,
|
|
522
|
+
"name": file_name,
|
|
523
|
+
"stage": "after_message",
|
|
524
|
+
}
|
|
525
|
+
)
|
|
465
526
|
logger.info(f"Фото сохранено: {file_path} (after_message)")
|
|
466
|
-
|
|
527
|
+
|
|
467
528
|
elif message.video:
|
|
468
529
|
file = await bot.get_file(message.video.file_id)
|
|
469
|
-
file_name =
|
|
530
|
+
file_name = (
|
|
531
|
+
message.video.file_name or f"{message.video.file_id}.mp4"
|
|
532
|
+
) # Используем оригинальное имя или file_id
|
|
470
533
|
file_path = os.path.join(TEMP_DIR, file_name)
|
|
471
534
|
await bot.download_file(file.file_path, file_path)
|
|
472
|
-
|
|
473
|
-
files.append(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
535
|
+
|
|
536
|
+
files.append(
|
|
537
|
+
{
|
|
538
|
+
"type": "video",
|
|
539
|
+
"file_path": file_path,
|
|
540
|
+
"name": file_name,
|
|
541
|
+
"stage": "after_message",
|
|
542
|
+
}
|
|
543
|
+
)
|
|
479
544
|
logger.info(f"Видео сохранено: {file_path} (after_message)")
|
|
480
|
-
|
|
545
|
+
|
|
481
546
|
await state.update_data(files=files)
|
|
482
|
-
|
|
547
|
+
|
|
483
548
|
# Кнопка для завершения добавления файлов
|
|
484
|
-
keyboard = InlineKeyboardMarkup(
|
|
485
|
-
[
|
|
486
|
-
|
|
487
|
-
|
|
549
|
+
keyboard = InlineKeyboardMarkup(
|
|
550
|
+
inline_keyboard=[
|
|
551
|
+
[
|
|
552
|
+
InlineKeyboardButton(
|
|
553
|
+
text="✅ Завершить добавление файлов", callback_data="files:done"
|
|
554
|
+
)
|
|
555
|
+
]
|
|
556
|
+
]
|
|
557
|
+
)
|
|
558
|
+
|
|
488
559
|
await message.answer(
|
|
489
560
|
f"✅ Файл добавлен (всего: {len(files)})\n\n"
|
|
490
561
|
"Отправьте еще файлы или нажмите кнопку для завершения:",
|
|
491
|
-
reply_markup=keyboard
|
|
562
|
+
reply_markup=keyboard,
|
|
492
563
|
)
|
|
493
564
|
|
|
494
|
-
|
|
565
|
+
|
|
566
|
+
@admin_events_router.callback_query(
|
|
567
|
+
F.data.startswith("files:"), AdminStates.create_event_files
|
|
568
|
+
)
|
|
495
569
|
async def process_files_action(callback_query: CallbackQuery, state: FSMContext):
|
|
496
570
|
"""Обработка действий с файлами"""
|
|
497
571
|
action = callback_query.data.split(":", 1)[1]
|
|
498
|
-
|
|
572
|
+
|
|
499
573
|
data = await state.get_data()
|
|
500
|
-
files = data.get(
|
|
501
|
-
|
|
574
|
+
files = data.get("files", [])
|
|
575
|
+
|
|
502
576
|
if action == "skip" and not files:
|
|
503
577
|
# Если файлов нет и нажали "Продолжить без файлов" - очищаем
|
|
504
578
|
files = []
|
|
@@ -506,16 +580,16 @@ async def process_files_action(callback_query: CallbackQuery, state: FSMContext)
|
|
|
506
580
|
elif action == "skip":
|
|
507
581
|
# Если файлы уже есть - оставляем их
|
|
508
582
|
logger.info(f"Продолжаем с {len(files)} существующими файлами")
|
|
509
|
-
|
|
583
|
+
|
|
510
584
|
# Переход к подтверждению
|
|
511
585
|
await state.set_state(AdminStates.create_event_confirm)
|
|
512
|
-
|
|
586
|
+
|
|
513
587
|
# Формируем дату и время для отображения (московское время)
|
|
514
|
-
event_date = data.get(
|
|
515
|
-
event_time = data.get(
|
|
516
|
-
naive_datetime = datetime.strptime(f"{event_date} {event_time}",
|
|
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")
|
|
517
591
|
moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
|
|
518
|
-
|
|
592
|
+
|
|
519
593
|
# Отправляем сообщение с подтверждением
|
|
520
594
|
summary = (
|
|
521
595
|
f"📋 **Подтверждение создания события**\n\n"
|
|
@@ -525,78 +599,86 @@ async def process_files_action(callback_query: CallbackQuery, state: FSMContext)
|
|
|
525
599
|
f"📎 Файлов: **{len(files)}**\n\n"
|
|
526
600
|
"Подтвердите создание события:"
|
|
527
601
|
)
|
|
528
|
-
|
|
529
|
-
keyboard = InlineKeyboardMarkup(
|
|
530
|
-
[
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
InlineKeyboardButton(text="👁 Предпросмотр", callback_data="preview:show")
|
|
602
|
+
|
|
603
|
+
keyboard = InlineKeyboardMarkup(
|
|
604
|
+
inline_keyboard=[
|
|
605
|
+
[
|
|
606
|
+
InlineKeyboardButton(text="✅ Создать", callback_data="confirm:yes"),
|
|
607
|
+
InlineKeyboardButton(text="❌ Отменить", callback_data="confirm:no"),
|
|
608
|
+
],
|
|
609
|
+
[InlineKeyboardButton(text="👁 Предпросмотр", callback_data="preview:show")],
|
|
536
610
|
]
|
|
537
|
-
|
|
538
|
-
|
|
611
|
+
)
|
|
612
|
+
|
|
539
613
|
await callback_query.message.edit_text(
|
|
540
|
-
summary,
|
|
541
|
-
reply_markup=keyboard,
|
|
542
|
-
parse_mode='Markdown'
|
|
614
|
+
summary, reply_markup=keyboard, parse_mode="Markdown"
|
|
543
615
|
)
|
|
544
616
|
|
|
545
|
-
|
|
617
|
+
|
|
618
|
+
@admin_events_router.callback_query(
|
|
619
|
+
F.data == "preview:show", AdminStates.create_event_confirm
|
|
620
|
+
)
|
|
546
621
|
async def show_event_preview(callback_query: CallbackQuery, state: FSMContext):
|
|
547
622
|
"""Показываем предпросмотр сообщения"""
|
|
548
623
|
from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaVideo
|
|
549
|
-
|
|
624
|
+
|
|
550
625
|
# Получаем данные
|
|
551
626
|
data = await state.get_data()
|
|
552
|
-
files = data.get(
|
|
627
|
+
files = data.get("files", [])
|
|
553
628
|
logger.info(f"Предпросмотр: получено {len(files)} файлов из состояния")
|
|
554
|
-
|
|
629
|
+
|
|
555
630
|
# Удаляем сообщение с кнопкой предпросмотра
|
|
556
631
|
await callback_query.message.delete()
|
|
557
|
-
|
|
632
|
+
|
|
558
633
|
# Анализируем файлы
|
|
559
|
-
files_with_msg = [f for f in files if f.get(
|
|
560
|
-
files_after = [f for f in files if f.get(
|
|
561
|
-
logger.info(
|
|
562
|
-
|
|
634
|
+
files_with_msg = [f for f in files if f.get("stage") == "with_message"]
|
|
635
|
+
files_after = [f for f in files if f.get("stage") == "after_message"]
|
|
636
|
+
logger.info(
|
|
637
|
+
f"Предпросмотр: {len(files_with_msg)} файлов с сообщением, {len(files_after)} дополнительных файлов"
|
|
638
|
+
)
|
|
639
|
+
|
|
563
640
|
# 1. Отправляем сообщение с прикрепленными файлами
|
|
564
641
|
if files_with_msg:
|
|
565
642
|
media_group = []
|
|
566
643
|
first_file = True
|
|
567
|
-
|
|
644
|
+
|
|
568
645
|
# Сортируем файлы по порядку
|
|
569
|
-
sorted_files = sorted(files_with_msg, key=lambda x: x.get(
|
|
570
|
-
|
|
646
|
+
sorted_files = sorted(files_with_msg, key=lambda x: x.get("order", 0))
|
|
647
|
+
|
|
571
648
|
# Добавляем файлы в том порядке, в котором они были загружены
|
|
572
649
|
for file_info in sorted_files:
|
|
573
|
-
logger.info(
|
|
650
|
+
logger.info(
|
|
651
|
+
f"Добавляем в медиа-группу: {file_info['type']} файл {file_info['file_path']}"
|
|
652
|
+
)
|
|
574
653
|
try:
|
|
575
|
-
if file_info[
|
|
654
|
+
if file_info["type"] == "photo":
|
|
576
655
|
media = InputMediaPhoto(
|
|
577
|
-
media=FSInputFile(file_info[
|
|
578
|
-
caption=data.get(
|
|
579
|
-
parse_mode=
|
|
656
|
+
media=FSInputFile(file_info["file_path"]),
|
|
657
|
+
caption=data.get("event_message") if first_file else None,
|
|
658
|
+
parse_mode="Markdown" if first_file else None,
|
|
580
659
|
)
|
|
581
660
|
media_group.append(media)
|
|
582
661
|
first_file = False
|
|
583
|
-
elif file_info[
|
|
662
|
+
elif file_info["type"] == "video":
|
|
584
663
|
media = InputMediaVideo(
|
|
585
|
-
media=FSInputFile(file_info[
|
|
586
|
-
caption=data.get(
|
|
587
|
-
parse_mode=
|
|
664
|
+
media=FSInputFile(file_info["file_path"]),
|
|
665
|
+
caption=data.get("event_message") if first_file else None,
|
|
666
|
+
parse_mode="Markdown" if first_file else None,
|
|
588
667
|
)
|
|
589
668
|
media_group.append(media)
|
|
590
669
|
first_file = False
|
|
591
670
|
logger.info("✅ Файл успешно добавлен в медиа-группу")
|
|
592
671
|
except Exception as e:
|
|
593
672
|
logger.error(f"❌ Ошибка добавления файла в медиа-группу: {e}")
|
|
594
|
-
|
|
673
|
+
|
|
595
674
|
if media_group:
|
|
596
675
|
try:
|
|
597
676
|
from ..handlers.handlers import get_global_var
|
|
598
|
-
|
|
599
|
-
|
|
677
|
+
|
|
678
|
+
bot = get_global_var("bot")
|
|
679
|
+
await bot.send_media_group(
|
|
680
|
+
chat_id=callback_query.message.chat.id, media=media_group
|
|
681
|
+
)
|
|
600
682
|
logger.info(f"✅ Отправлена медиа-группа из {len(media_group)} файлов")
|
|
601
683
|
except Exception as e:
|
|
602
684
|
logger.error(f"❌ Ошибка отправки медиа-группы: {e}")
|
|
@@ -607,14 +689,18 @@ async def show_event_preview(callback_query: CallbackQuery, state: FSMContext):
|
|
|
607
689
|
if isinstance(media, InputMediaPhoto):
|
|
608
690
|
await callback_query.message.answer_photo(
|
|
609
691
|
photo=media.media,
|
|
610
|
-
caption=
|
|
611
|
-
|
|
692
|
+
caption=(
|
|
693
|
+
data.get("event_message") if first_file else None
|
|
694
|
+
),
|
|
695
|
+
parse_mode="Markdown" if first_file else None,
|
|
612
696
|
)
|
|
613
697
|
elif isinstance(media, InputMediaVideo):
|
|
614
698
|
await callback_query.message.answer_video(
|
|
615
699
|
video=media.media,
|
|
616
|
-
caption=
|
|
617
|
-
|
|
700
|
+
caption=(
|
|
701
|
+
data.get("event_message") if first_file else None
|
|
702
|
+
),
|
|
703
|
+
parse_mode="Markdown" if first_file else None,
|
|
618
704
|
)
|
|
619
705
|
first_file = False
|
|
620
706
|
except Exception as e2:
|
|
@@ -622,31 +708,30 @@ async def show_event_preview(callback_query: CallbackQuery, state: FSMContext):
|
|
|
622
708
|
else:
|
|
623
709
|
# Только текст
|
|
624
710
|
await callback_query.message.answer(
|
|
625
|
-
data.get(
|
|
626
|
-
parse_mode='Markdown'
|
|
711
|
+
data.get("event_message"), parse_mode="Markdown"
|
|
627
712
|
)
|
|
628
|
-
|
|
713
|
+
|
|
629
714
|
# 2. Отправляем дополнительные файлы
|
|
630
715
|
for file_info in files_after:
|
|
631
|
-
if file_info[
|
|
716
|
+
if file_info["type"] == "document":
|
|
632
717
|
await callback_query.message.answer_document(
|
|
633
|
-
FSInputFile(file_info[
|
|
718
|
+
FSInputFile(file_info["file_path"])
|
|
634
719
|
)
|
|
635
|
-
elif file_info[
|
|
720
|
+
elif file_info["type"] == "photo":
|
|
636
721
|
await callback_query.message.answer_photo(
|
|
637
|
-
FSInputFile(file_info[
|
|
722
|
+
FSInputFile(file_info["file_path"])
|
|
638
723
|
)
|
|
639
|
-
elif file_info[
|
|
724
|
+
elif file_info["type"] == "video":
|
|
640
725
|
await callback_query.message.answer_video(
|
|
641
|
-
FSInputFile(file_info[
|
|
726
|
+
FSInputFile(file_info["file_path"])
|
|
642
727
|
)
|
|
643
|
-
|
|
728
|
+
|
|
644
729
|
# 3. Отправляем сообщение с подтверждением (такое же как было)
|
|
645
|
-
event_date = data.get(
|
|
646
|
-
event_time = data.get(
|
|
647
|
-
naive_datetime = datetime.strptime(f"{event_date} {event_time}",
|
|
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")
|
|
648
733
|
moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
|
|
649
|
-
|
|
734
|
+
|
|
650
735
|
summary = (
|
|
651
736
|
f"📋 **Подтверждение создания события**\n\n"
|
|
652
737
|
f"📝 Название: **{data.get('event_name')}**\n"
|
|
@@ -655,142 +740,155 @@ async def show_event_preview(callback_query: CallbackQuery, state: FSMContext):
|
|
|
655
740
|
f"📎 Файлов: **{len(files)}**\n\n"
|
|
656
741
|
"Подтвердите создание события:"
|
|
657
742
|
)
|
|
658
|
-
|
|
659
|
-
keyboard = InlineKeyboardMarkup(
|
|
660
|
-
[
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
InlineKeyboardButton(text="👁 Предпросмотр", callback_data="preview:show")
|
|
743
|
+
|
|
744
|
+
keyboard = InlineKeyboardMarkup(
|
|
745
|
+
inline_keyboard=[
|
|
746
|
+
[
|
|
747
|
+
InlineKeyboardButton(text="✅ Создать", callback_data="confirm:yes"),
|
|
748
|
+
InlineKeyboardButton(text="❌ Отменить", callback_data="confirm:no"),
|
|
749
|
+
],
|
|
750
|
+
[InlineKeyboardButton(text="👁 Предпросмотр", callback_data="preview:show")],
|
|
666
751
|
]
|
|
667
|
-
|
|
668
|
-
|
|
752
|
+
)
|
|
753
|
+
|
|
669
754
|
await callback_query.message.answer(
|
|
670
|
-
summary,
|
|
671
|
-
reply_markup=keyboard,
|
|
672
|
-
parse_mode='Markdown'
|
|
755
|
+
summary, reply_markup=keyboard, parse_mode="Markdown"
|
|
673
756
|
)
|
|
674
757
|
|
|
675
|
-
|
|
758
|
+
|
|
759
|
+
@admin_events_router.callback_query(
|
|
760
|
+
F.data.startswith("confirm:"), AdminStates.create_event_confirm
|
|
761
|
+
)
|
|
676
762
|
async def process_event_confirmation(callback_query: CallbackQuery, state: FSMContext):
|
|
677
763
|
"""Обработка подтверждения создания события"""
|
|
678
764
|
action = callback_query.data.split(":", 1)[1]
|
|
679
|
-
|
|
765
|
+
|
|
680
766
|
if action == "no":
|
|
681
767
|
# Очищаем временные файлы
|
|
682
768
|
await cleanup_temp_files(state)
|
|
683
769
|
# Очищаем состояние
|
|
684
770
|
await state.clear()
|
|
685
771
|
await callback_query.message.edit_text(
|
|
686
|
-
"❌ Создание события отменено",
|
|
687
|
-
parse_mode='Markdown'
|
|
772
|
+
"❌ Создание события отменено", parse_mode="Markdown"
|
|
688
773
|
)
|
|
689
774
|
return
|
|
690
|
-
|
|
775
|
+
|
|
691
776
|
# Создаем событие
|
|
692
777
|
data = await state.get_data()
|
|
693
|
-
|
|
778
|
+
|
|
694
779
|
from ..handlers.handlers import get_global_var
|
|
695
|
-
|
|
696
|
-
|
|
780
|
+
|
|
781
|
+
supabase_client = get_global_var("supabase_client")
|
|
782
|
+
|
|
697
783
|
# Формируем datetime для планирования
|
|
698
|
-
event_date = data.get(
|
|
699
|
-
event_time = data.get(
|
|
700
|
-
|
|
784
|
+
event_date = data.get("event_date")
|
|
785
|
+
event_time = data.get("event_time")
|
|
786
|
+
|
|
701
787
|
# Создаем naive datetime из введенного московского времени
|
|
702
|
-
naive_datetime = datetime.strptime(f"{event_date} {event_time}",
|
|
703
|
-
|
|
788
|
+
naive_datetime = datetime.strptime(f"{event_date} {event_time}", "%Y-%m-%d %H:%M")
|
|
789
|
+
|
|
704
790
|
# Привязываем к московской временной зоне
|
|
705
791
|
moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
|
|
706
|
-
|
|
792
|
+
|
|
707
793
|
# Конвертируем в UTC для сохранения в БД
|
|
708
794
|
utc_datetime = moscow_datetime.astimezone(pytz.UTC)
|
|
709
|
-
|
|
710
|
-
logger.info(
|
|
711
|
-
|
|
795
|
+
|
|
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
|
+
)
|
|
799
|
+
|
|
712
800
|
# Сначала сохраняем событие в БД чтобы получить ID
|
|
713
801
|
try:
|
|
714
802
|
# Создаем событие без файлов
|
|
715
803
|
event = await supabase_client.save_admin_event(
|
|
716
|
-
event_name=data.get(
|
|
804
|
+
event_name=data.get("event_name"),
|
|
717
805
|
event_data={
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
806
|
+
"segment": data.get("segment"),
|
|
807
|
+
"message": data.get("event_message"),
|
|
808
|
+
"files": [], # Пока пустой список
|
|
721
809
|
},
|
|
722
|
-
scheduled_datetime=utc_datetime
|
|
810
|
+
scheduled_datetime=utc_datetime,
|
|
723
811
|
)
|
|
724
|
-
event_id = event[
|
|
812
|
+
event_id = event["id"]
|
|
725
813
|
logger.info(f"✅ Создано событие с ID {event_id}")
|
|
726
|
-
|
|
814
|
+
|
|
727
815
|
# Теперь загружаем файлы в Storage
|
|
728
816
|
import os
|
|
729
|
-
|
|
817
|
+
|
|
818
|
+
files = data.get("files", [])
|
|
730
819
|
uploaded_files = []
|
|
731
|
-
|
|
820
|
+
|
|
732
821
|
for file_info in files:
|
|
733
822
|
try:
|
|
734
823
|
# Читаем локальный файл
|
|
735
|
-
with open(file_info[
|
|
824
|
+
with open(file_info["file_path"], "rb") as f:
|
|
736
825
|
file_bytes = f.read()
|
|
737
|
-
|
|
826
|
+
|
|
738
827
|
# Генерируем уникальный ID для файла
|
|
739
828
|
file_id = generate_file_id()
|
|
740
|
-
|
|
829
|
+
|
|
741
830
|
# Загружаем в Storage
|
|
742
831
|
storage_info = await supabase_client.upload_event_file(
|
|
743
832
|
event_id=event_id,
|
|
744
833
|
file_data=file_bytes,
|
|
745
|
-
original_name=file_info[
|
|
746
|
-
file_id=file_id
|
|
834
|
+
original_name=file_info["name"],
|
|
835
|
+
file_id=file_id,
|
|
747
836
|
)
|
|
748
|
-
|
|
837
|
+
|
|
749
838
|
# Сохраняем метаданные файла
|
|
750
|
-
uploaded_files.append(
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
839
|
+
uploaded_files.append(
|
|
840
|
+
{
|
|
841
|
+
"type": file_info["type"],
|
|
842
|
+
"storage_path": storage_info["storage_path"],
|
|
843
|
+
"original_name": file_info[
|
|
844
|
+
"name"
|
|
845
|
+
], # Используем оригинальное имя из file_info
|
|
846
|
+
"stage": file_info["stage"],
|
|
847
|
+
"has_caption": file_info.get("has_caption", False),
|
|
848
|
+
"order": file_info.get("order", 0),
|
|
849
|
+
}
|
|
850
|
+
)
|
|
851
|
+
|
|
759
852
|
# Удаляем временный локальный файл
|
|
760
853
|
try:
|
|
761
|
-
os.remove(file_info[
|
|
854
|
+
os.remove(file_info["file_path"])
|
|
762
855
|
logger.info(f"🗑️ Удален временный файл: {file_info['file_path']}")
|
|
763
856
|
except Exception as e:
|
|
764
857
|
logger.warning(f"⚠️ Не удалось удалить временный файл: {e}")
|
|
765
|
-
|
|
858
|
+
|
|
766
859
|
except Exception as e:
|
|
767
860
|
logger.error(f"❌ Ошибка загрузки файла {file_info['name']}: {e}")
|
|
768
861
|
# Если ошибка - удаляем все файлы события и само событие
|
|
769
862
|
try:
|
|
770
863
|
await supabase_client.delete_event_files(event_id)
|
|
771
864
|
# TODO: добавить метод удаления события
|
|
772
|
-
except:
|
|
773
|
-
|
|
865
|
+
except Exception:
|
|
866
|
+
logger.error("Ошибка при удалении файлов события")
|
|
774
867
|
raise
|
|
775
|
-
|
|
776
|
-
logger.info(
|
|
777
|
-
|
|
868
|
+
|
|
869
|
+
logger.info(
|
|
870
|
+
f"✅ Загружено {len(uploaded_files)} файлов в Storage для события {event_id}"
|
|
871
|
+
)
|
|
872
|
+
|
|
778
873
|
# Обновляем событие с информацией о файлах
|
|
779
874
|
event_data = {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
875
|
+
"segment": data.get("segment"),
|
|
876
|
+
"message": data.get("event_message"),
|
|
877
|
+
"files": uploaded_files,
|
|
783
878
|
}
|
|
784
|
-
|
|
879
|
+
|
|
785
880
|
# TODO: добавить метод обновления события
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
+
)
|
|
887
|
+
|
|
790
888
|
except Exception as e:
|
|
791
889
|
logger.error(f"❌ Ошибка создания события: {e}")
|
|
792
890
|
raise
|
|
793
|
-
|
|
891
|
+
|
|
794
892
|
# Показываем сообщение об успехе
|
|
795
893
|
await callback_query.message.edit_text(
|
|
796
894
|
f"✅ **Событие успешно создано!**\n\n"
|
|
@@ -798,69 +896,67 @@ async def process_event_confirmation(callback_query: CallbackQuery, state: FSMCo
|
|
|
798
896
|
f"📅 Запланировано на: **{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)**\n"
|
|
799
897
|
f"👥 Сегмент: **{data.get('segment_display')}**\n\n"
|
|
800
898
|
f"💡 _Нажмите на название для копирования_",
|
|
801
|
-
parse_mode=
|
|
899
|
+
parse_mode="Markdown",
|
|
802
900
|
)
|
|
803
|
-
|
|
901
|
+
|
|
804
902
|
# Очищаем временные файлы и состояние
|
|
805
903
|
await cleanup_temp_files(state)
|
|
806
904
|
await state.set_state(AdminStates.admin_mode)
|
|
807
905
|
|
|
906
|
+
|
|
808
907
|
@admin_events_router.message(Command(commands=["список_событий", "list_events"]))
|
|
809
908
|
async def list_events_command(message: Message, state: FSMContext):
|
|
810
909
|
"""Просмотр всех запланированных событий"""
|
|
811
910
|
from ..handlers.handlers import get_global_var
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
911
|
+
|
|
912
|
+
admin_manager = get_global_var("admin_manager")
|
|
913
|
+
supabase_client = get_global_var("supabase_client")
|
|
914
|
+
|
|
815
915
|
if not admin_manager.is_admin(message.from_user.id):
|
|
816
916
|
return
|
|
817
|
-
|
|
917
|
+
|
|
818
918
|
try:
|
|
819
919
|
# Получаем все pending события (незавершенные и неотмененные)
|
|
820
|
-
events = await supabase_client.get_admin_events(status=
|
|
821
|
-
|
|
920
|
+
events = await supabase_client.get_admin_events(status="pending")
|
|
921
|
+
|
|
822
922
|
if not events:
|
|
823
923
|
await message.answer(
|
|
824
924
|
"📋 **Нет активных событий**\n\n"
|
|
825
925
|
"Используйте `/create_event` для создания нового события",
|
|
826
|
-
parse_mode=
|
|
926
|
+
parse_mode="Markdown",
|
|
827
927
|
)
|
|
828
928
|
return
|
|
829
|
-
|
|
929
|
+
|
|
830
930
|
# Формируем список событий в красивом формате
|
|
831
|
-
text_parts = [
|
|
832
|
-
|
|
833
|
-
]
|
|
834
|
-
|
|
931
|
+
text_parts = [f"📋 **Активные события** ({len(events)})\n"]
|
|
932
|
+
|
|
835
933
|
for idx, event in enumerate(events, 1):
|
|
836
|
-
event_name = event[
|
|
837
|
-
|
|
934
|
+
event_name = event["event_type"]
|
|
935
|
+
|
|
838
936
|
# Конвертируем UTC в московское время для отображения
|
|
839
|
-
utc_time = datetime.fromisoformat(
|
|
937
|
+
utc_time = datetime.fromisoformat(
|
|
938
|
+
event["scheduled_at"].replace("Z", "+00:00")
|
|
939
|
+
)
|
|
840
940
|
moscow_time = utc_time.astimezone(MOSCOW_TZ)
|
|
841
|
-
|
|
941
|
+
|
|
842
942
|
# Красивый формат с эмодзи и структурой
|
|
843
943
|
text_parts.append(
|
|
844
944
|
f"📌 **{idx}.** `{event_name}`\n"
|
|
845
945
|
f" 🕐 {moscow_time.strftime('%d.%m.%Y в %H:%M')} МСК\n"
|
|
846
946
|
)
|
|
847
|
-
|
|
947
|
+
|
|
848
948
|
text_parts.append(
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
)
|
|
853
|
-
|
|
854
|
-
await message.answer(
|
|
855
|
-
"\n".join(text_parts),
|
|
856
|
-
parse_mode='Markdown'
|
|
949
|
+
"━━━━━━━━━━━━━━━━━━━━\n"
|
|
950
|
+
"💡 _Нажмите на название для копирования_\n"
|
|
951
|
+
"🗑️ Удалить: `/delete_event название`"
|
|
857
952
|
)
|
|
858
|
-
|
|
953
|
+
|
|
954
|
+
await message.answer("\n".join(text_parts), parse_mode="Markdown")
|
|
955
|
+
|
|
859
956
|
except Exception as e:
|
|
860
957
|
logger.error(f"Ошибка получения событий: {e}")
|
|
861
958
|
await message.answer(
|
|
862
|
-
f"❌ Ошибка получения событий:\n`{str(e)}`",
|
|
863
|
-
parse_mode='Markdown'
|
|
959
|
+
f"❌ Ошибка получения событий:\n`{str(e)}`", parse_mode="Markdown"
|
|
864
960
|
)
|
|
865
961
|
|
|
866
962
|
|
|
@@ -868,12 +964,13 @@ async def list_events_command(message: Message, state: FSMContext):
|
|
|
868
964
|
async def delete_event_command(message: Message, state: FSMContext):
|
|
869
965
|
"""Удаление события по названию"""
|
|
870
966
|
from ..handlers.handlers import get_global_var
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
967
|
+
|
|
968
|
+
admin_manager = get_global_var("admin_manager")
|
|
969
|
+
supabase_client = get_global_var("supabase_client")
|
|
970
|
+
|
|
874
971
|
if not admin_manager.is_admin(message.from_user.id):
|
|
875
972
|
return
|
|
876
|
-
|
|
973
|
+
|
|
877
974
|
# Парсим название из команды
|
|
878
975
|
parts = message.text.split(maxsplit=1)
|
|
879
976
|
if len(parts) < 2:
|
|
@@ -881,52 +978,55 @@ async def delete_event_command(message: Message, state: FSMContext):
|
|
|
881
978
|
"❌ Укажите название события:\n"
|
|
882
979
|
"`/delete_event название`\n\n"
|
|
883
980
|
"Используйте /list_events для просмотра списка событий",
|
|
884
|
-
parse_mode=
|
|
981
|
+
parse_mode="Markdown",
|
|
885
982
|
)
|
|
886
983
|
return
|
|
887
|
-
|
|
984
|
+
|
|
888
985
|
event_name = parts[1].strip()
|
|
889
|
-
|
|
986
|
+
|
|
890
987
|
try:
|
|
891
988
|
# Сначала получаем событие чтобы узнать его ID
|
|
892
|
-
response =
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
989
|
+
response = (
|
|
990
|
+
supabase_client.client.table("scheduled_events")
|
|
991
|
+
.select("id")
|
|
992
|
+
.eq("event_type", event_name)
|
|
993
|
+
.eq("event_category", "admin_event")
|
|
994
|
+
.eq("status", "pending")
|
|
995
|
+
.execute()
|
|
996
|
+
)
|
|
997
|
+
|
|
898
998
|
if response.data:
|
|
899
|
-
event_id = response.data[0][
|
|
900
|
-
|
|
999
|
+
event_id = response.data[0]["id"]
|
|
1000
|
+
|
|
901
1001
|
# Удаляем файлы из Storage
|
|
902
1002
|
try:
|
|
903
1003
|
await supabase_client.delete_event_files(event_id)
|
|
904
|
-
logger.info(
|
|
1004
|
+
logger.info(
|
|
1005
|
+
f"🗑️ Удалены файлы события '{event_name}' (ID: {event_id}) из Storage"
|
|
1006
|
+
)
|
|
905
1007
|
except Exception as e:
|
|
906
1008
|
logger.error(f"❌ Ошибка удаления файлов события из Storage: {e}")
|
|
907
|
-
|
|
1009
|
+
|
|
908
1010
|
# Отмечаем событие как отмененное
|
|
909
|
-
supabase_client.client.table(
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1011
|
+
supabase_client.client.table("scheduled_events").update(
|
|
1012
|
+
{"status": "cancelled"}
|
|
1013
|
+
).eq("id", event_id).execute()
|
|
1014
|
+
|
|
913
1015
|
await message.answer(
|
|
914
1016
|
f"✅ Событие `{event_name}` успешно отменено\n"
|
|
915
1017
|
f"_(файлы удалены из Storage)_",
|
|
916
|
-
parse_mode=
|
|
1018
|
+
parse_mode="Markdown",
|
|
917
1019
|
)
|
|
918
1020
|
logger.info(f"Отменено событие '{event_name}' (ID: {event_id})")
|
|
919
1021
|
else:
|
|
920
1022
|
await message.answer(
|
|
921
1023
|
f"❌ Активное событие с названием `{event_name}` не найдено\n\n"
|
|
922
1024
|
f"Используйте /list_events для просмотра списка активных событий",
|
|
923
|
-
parse_mode=
|
|
1025
|
+
parse_mode="Markdown",
|
|
924
1026
|
)
|
|
925
|
-
|
|
1027
|
+
|
|
926
1028
|
except Exception as e:
|
|
927
1029
|
logger.error(f"Ошибка удаления события: {e}")
|
|
928
1030
|
await message.answer(
|
|
929
|
-
f"❌ Ошибка удаления события:\n`{str(e)}`",
|
|
930
|
-
parse_mode='Markdown'
|
|
1031
|
+
f"❌ Ошибка удаления события:\n`{str(e)}`", parse_mode="Markdown"
|
|
931
1032
|
)
|
|
932
|
-
|