smart-bot-factory 0.3.3__py3-none-any.whl → 0.3.5__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/admin_events.py +420 -245
- smart_bot_factory/admin/admin_logic.py +5 -0
- smart_bot_factory/core/bot_utils.py +34 -9
- smart_bot_factory/core/decorators.py +41 -26
- smart_bot_factory/core/message_sender.py +11 -2
- smart_bot_factory/core/router.py +59 -8
- smart_bot_factory/handlers/handlers.py +25 -6
- smart_bot_factory/integrations/supabase_client.py +32 -23
- smart_bot_factory/utils/prompt_loader.py +12 -8
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.5.dist-info}/METADATA +1 -2
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.5.dist-info}/RECORD +14 -14
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.5.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.5.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
# Обработчики для создания админских событий
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from datetime import datetime
|
|
5
7
|
from dateutil.relativedelta import relativedelta
|
|
6
8
|
from aiogram import Router, F
|
|
7
9
|
from aiogram.filters import Command
|
|
8
10
|
from aiogram.fsm.context import FSMContext
|
|
9
11
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
|
12
|
+
from aiogram_media_group import media_group_handler
|
|
10
13
|
from ..aiogram_calendar import SimpleCalendar, SimpleCalendarCallback
|
|
11
14
|
import pytz
|
|
15
|
+
import uuid
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
TEMP_DIR = "temp_event_files"
|
|
19
|
+
|
|
20
|
+
def generate_file_id() -> str:
|
|
21
|
+
"""Генерирует уникальный ID для файла"""
|
|
22
|
+
return f"file_{uuid.uuid4().hex}"
|
|
23
|
+
|
|
24
|
+
def ensure_temp_dir():
|
|
25
|
+
"""Создает временную папку если её нет"""
|
|
26
|
+
if not os.path.exists(TEMP_DIR):
|
|
27
|
+
os.makedirs(TEMP_DIR)
|
|
28
|
+
logger.info(f"📁 Создана временная папка {TEMP_DIR}")
|
|
29
|
+
|
|
30
|
+
async def cleanup_temp_files(state: FSMContext = None):
|
|
31
|
+
"""Очистка временных файлов события"""
|
|
32
|
+
# Удаляем все файлы из временной папки
|
|
33
|
+
if os.path.exists(TEMP_DIR):
|
|
34
|
+
try:
|
|
35
|
+
shutil.rmtree(TEMP_DIR)
|
|
36
|
+
logger.info(f"🗑️ Удалена папка {TEMP_DIR}")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.error(f"❌ Ошибка при удалении {TEMP_DIR}: {e}")
|
|
39
|
+
|
|
40
|
+
# Очищаем информацию о файлах в состоянии
|
|
41
|
+
if state:
|
|
42
|
+
try:
|
|
43
|
+
data = await state.get_data()
|
|
44
|
+
if 'files' in data:
|
|
45
|
+
data['files'] = []
|
|
46
|
+
await state.set_data(data)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"❌ Ошибка при очистке состояния: {e}")
|
|
12
49
|
|
|
13
50
|
logger = logging.getLogger(__name__)
|
|
14
51
|
|
|
@@ -37,9 +74,10 @@ async def create_event_start(message: Message, state: FSMContext):
|
|
|
37
74
|
await state.set_state(AdminStates.create_event_name)
|
|
38
75
|
|
|
39
76
|
await message.answer(
|
|
40
|
-
"📝
|
|
41
|
-
"
|
|
42
|
-
"
|
|
77
|
+
"📝 **Введите название события**\n\n"
|
|
78
|
+
"💡 _По этому названию вы сможете:\n"
|
|
79
|
+
"• Найти событие в списке\n"
|
|
80
|
+
"• Отменить его при необходимости_",
|
|
43
81
|
parse_mode='Markdown'
|
|
44
82
|
)
|
|
45
83
|
|
|
@@ -194,174 +232,196 @@ async def process_event_segment(callback_query: CallbackQuery, state: FSMContext
|
|
|
194
232
|
"📄 _Если нужно добавить **PDF или другие документы**, вы сможете это сделать на следующем шаге_",
|
|
195
233
|
parse_mode='Markdown'
|
|
196
234
|
)
|
|
197
|
-
|
|
198
|
-
@admin_events_router.message(AdminStates.create_event_message)
|
|
199
|
-
async def process_event_message(message: Message, state: FSMContext):
|
|
200
|
-
"""Обработка сообщения для пользователей"""
|
|
201
|
-
event_message = message.text or message.caption or ""
|
|
202
235
|
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
@admin_events_router.message(AdminStates.create_event_message, F.media_group_id, F.content_type.in_({'photo', 'video'}))
|
|
237
|
+
@media_group_handler
|
|
238
|
+
async def handle_album(messages: list[Message], state: FSMContext):
|
|
239
|
+
"""Обработка альбома фотографий/видео"""
|
|
240
|
+
if not messages:
|
|
205
241
|
return
|
|
206
242
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
243
|
+
# Берем текст из первого сообщения с подписью
|
|
244
|
+
event_message = next((msg.caption for msg in messages if msg.caption), None)
|
|
245
|
+
if not event_message:
|
|
246
|
+
await messages[0].answer(
|
|
247
|
+
"❌ **Добавьте подпись к альбому**\n\n"
|
|
248
|
+
"💡 _Отправьте альбом заново с текстом сообщения в подписи к любой фотографии_",
|
|
249
|
+
parse_mode='Markdown'
|
|
250
|
+
)
|
|
251
|
+
return
|
|
215
252
|
|
|
216
|
-
|
|
253
|
+
# Сохраняем сообщение
|
|
254
|
+
await state.update_data(event_message=event_message)
|
|
217
255
|
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
# Создаем временную папку если её нет
|
|
226
|
-
temp_dir = "temp_event_files"
|
|
227
|
-
os.makedirs(temp_dir, exist_ok=True)
|
|
228
|
-
|
|
229
|
-
# Скачиваем фото
|
|
230
|
-
photo = message.photo[-1] # Берем самое большое фото
|
|
231
|
-
file = await bot.get_file(photo.file_id)
|
|
232
|
-
file_path = os.path.join(temp_dir, f"{photo.file_id}.jpg")
|
|
233
|
-
await bot.download_file(file.file_path, file_path)
|
|
234
|
-
|
|
235
|
-
files.append({
|
|
236
|
-
'type': 'photo',
|
|
237
|
-
'file_path': file_path,
|
|
238
|
-
'name': f"{photo.file_id}.jpg",
|
|
239
|
-
'stage': 'with_message',
|
|
240
|
-
'has_caption': bool(message.caption) # Первое фото с текстом
|
|
241
|
-
})
|
|
242
|
-
logger.info(f"Фото сохранено: {file_path} (with_message)")
|
|
243
|
-
|
|
244
|
-
await state.update_data(files=files)
|
|
256
|
+
# Показываем сообщение о начале загрузки
|
|
257
|
+
await messages[0].answer(
|
|
258
|
+
"📸 **Загружаю файлы...**\n\n"
|
|
259
|
+
"💡 _Дождитесь загрузки всех файлов из альбома_",
|
|
260
|
+
parse_mode='Markdown'
|
|
261
|
+
)
|
|
245
262
|
|
|
246
|
-
#
|
|
247
|
-
if not message.media_group_id:
|
|
248
|
-
await state.set_state(AdminStates.create_event_files)
|
|
249
|
-
|
|
250
|
-
# Кнопки для добавления файлов или пропуска
|
|
251
|
-
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
252
|
-
[InlineKeyboardButton(text="➡️ Продолжить без файлов", callback_data="files:skip")]
|
|
253
|
-
])
|
|
254
|
-
|
|
255
|
-
await message.answer(
|
|
256
|
-
"✅ **Сообщение сохранено!**\n\n"
|
|
257
|
-
"📎 **Дополнительные файлы**\n\n"
|
|
258
|
-
"Теперь вы можете отправить:\n"
|
|
259
|
-
"📄 PDF документы\n"
|
|
260
|
-
"📁 Файлы любых форматов\n"
|
|
261
|
-
"🎥 Дополнительные видео\n"
|
|
262
|
-
"🖼 Дополнительные фото\n\n"
|
|
263
|
-
"💡 _Можно отправить несколько файлов по очереди_\n\n"
|
|
264
|
-
"Или нажмите кнопку, если дополнительных файлов нет:",
|
|
265
|
-
reply_markup=keyboard,
|
|
266
|
-
parse_mode='Markdown'
|
|
267
|
-
)
|
|
268
|
-
# Если альбом - ничего не делаем, остальные фото обработаются отдельным handler
|
|
269
|
-
|
|
270
|
-
@admin_events_router.message(AdminStates.create_event_message, F.photo)
|
|
271
|
-
async def process_album_photos(message: Message, state: FSMContext):
|
|
272
|
-
"""Обработка остальных фото из альбома"""
|
|
273
|
-
import os
|
|
263
|
+
# Сохраняем все файлы
|
|
274
264
|
from ..handlers.handlers import get_global_var
|
|
265
|
+
bot = get_global_var('bot')
|
|
266
|
+
ensure_temp_dir()
|
|
275
267
|
|
|
276
268
|
data = await state.get_data()
|
|
269
|
+
files = data.get('files', [])
|
|
277
270
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
271
|
+
for i, message in enumerate(messages, 1):
|
|
272
|
+
try:
|
|
273
|
+
if message.photo:
|
|
274
|
+
photo = message.photo[-1]
|
|
275
|
+
file = await bot.get_file(photo.file_id)
|
|
276
|
+
file_name = f"photo_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{i}.jpg"
|
|
277
|
+
file_path = os.path.join(TEMP_DIR, file_name)
|
|
278
|
+
await bot.download_file(file.file_path, file_path)
|
|
279
|
+
|
|
280
|
+
files.append({
|
|
281
|
+
'type': 'photo',
|
|
282
|
+
'file_path': file_path,
|
|
283
|
+
'name': file_name,
|
|
284
|
+
'stage': 'with_message',
|
|
285
|
+
'has_caption': bool(message.caption),
|
|
286
|
+
'order': i # Сохраняем порядок в альбоме
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
elif message.video:
|
|
290
|
+
file = await bot.get_file(message.video.file_id)
|
|
291
|
+
file_name = message.video.file_name or f"{message.video.file_id}.mp4"
|
|
292
|
+
file_path = os.path.join(TEMP_DIR, file_name)
|
|
293
|
+
await bot.download_file(file.file_path, file_path)
|
|
294
|
+
|
|
295
|
+
files.append({
|
|
296
|
+
'type': 'video',
|
|
297
|
+
'file_path': file_path,
|
|
298
|
+
'name': file_name,
|
|
299
|
+
'stage': 'with_message',
|
|
300
|
+
'has_caption': bool(message.caption),
|
|
301
|
+
'order': i # Сохраняем порядок в альбоме
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
# Показываем прогресс каждые 5 файлов
|
|
305
|
+
if i % 5 == 0:
|
|
306
|
+
await messages[0].answer(
|
|
307
|
+
f"📸 Загружено файлов: {i}/{len(messages)}",
|
|
308
|
+
parse_mode='Markdown'
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"❌ Ошибка загрузки файла {i}: {e}")
|
|
313
|
+
continue
|
|
281
314
|
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
return
|
|
315
|
+
# Сохраняем файлы
|
|
316
|
+
await state.update_data(files=files)
|
|
285
317
|
|
|
286
|
-
|
|
287
|
-
|
|
318
|
+
# Переходим к следующему этапу
|
|
319
|
+
await state.set_state(AdminStates.create_event_files)
|
|
288
320
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
321
|
+
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
322
|
+
[InlineKeyboardButton(text="➡️ Продолжить без файлов", callback_data="files:skip")]
|
|
323
|
+
])
|
|
292
324
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
325
|
+
await messages[0].answer(
|
|
326
|
+
f"✅ **Сообщение и {len(files)} файлов сохранены!**\n\n"
|
|
327
|
+
"📎 **Дополнительные файлы**\n\n"
|
|
328
|
+
"Теперь вы можете отправить:\n"
|
|
329
|
+
"📄 PDF документы\n"
|
|
330
|
+
"📁 Файлы любых форматов\n"
|
|
331
|
+
"🎥 Дополнительные видео\n"
|
|
332
|
+
"🖼 Дополнительные фото\n\n"
|
|
333
|
+
"💡 _Можно отправить несколько файлов по очереди_\n\n"
|
|
334
|
+
"Или нажмите кнопку, если дополнительных файлов нет:",
|
|
335
|
+
reply_markup=keyboard,
|
|
336
|
+
parse_mode='Markdown'
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
@admin_events_router.message(AdminStates.create_event_message, F.text | F.photo | F.video)
|
|
340
|
+
async def process_event_message(message: Message, state: FSMContext):
|
|
341
|
+
"""Обработка одиночного сообщения с текстом/фото/видео"""
|
|
342
|
+
# Если это часть альбома - пропускаем, его обработает другой handler
|
|
343
|
+
if message.media_group_id:
|
|
344
|
+
return
|
|
298
345
|
|
|
299
|
-
|
|
300
|
-
'type': 'photo',
|
|
301
|
-
'file_path': file_path,
|
|
302
|
-
'name': f"{photo.file_id}.jpg",
|
|
303
|
-
'stage': 'with_message',
|
|
304
|
-
'has_caption': False # Остальные фото без текста
|
|
305
|
-
})
|
|
346
|
+
event_message = message.text or message.caption or ""
|
|
306
347
|
|
|
307
|
-
|
|
308
|
-
|
|
348
|
+
# Проверяем текст
|
|
349
|
+
if not event_message.strip():
|
|
350
|
+
await message.answer("❌ Сообщение не может быть пустым. Попробуйте еще раз:")
|
|
351
|
+
return
|
|
309
352
|
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
await asyncio.sleep(2)
|
|
353
|
+
# Сохраняем сообщение
|
|
354
|
+
await state.update_data(event_message=event_message)
|
|
313
355
|
|
|
314
|
-
#
|
|
356
|
+
# Если есть медиа, сохраняем его
|
|
315
357
|
data = await state.get_data()
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
358
|
+
files = data.get('files', [])
|
|
359
|
+
|
|
360
|
+
if message.photo or message.video:
|
|
361
|
+
import os
|
|
362
|
+
from ..handlers.handlers import get_global_var
|
|
363
|
+
bot = get_global_var('bot')
|
|
320
364
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
[InlineKeyboardButton(text="➡️ Продолжить", callback_data="files:skip")]
|
|
324
|
-
])
|
|
365
|
+
# Создаем временную папку
|
|
366
|
+
ensure_temp_dir()
|
|
325
367
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
368
|
+
if message.photo:
|
|
369
|
+
# Скачиваем фото
|
|
370
|
+
photo = message.photo[-1] # Берем самое большое фото
|
|
371
|
+
file = await bot.get_file(photo.file_id)
|
|
372
|
+
file_name = f"photo_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
|
|
373
|
+
file_path = os.path.join(TEMP_DIR, file_name)
|
|
374
|
+
await bot.download_file(file.file_path, file_path)
|
|
375
|
+
|
|
376
|
+
files.append({
|
|
377
|
+
'type': 'photo',
|
|
378
|
+
'file_path': file_path,
|
|
379
|
+
'name': file_name,
|
|
380
|
+
'stage': 'with_message',
|
|
381
|
+
'has_caption': bool(message.caption)
|
|
382
|
+
})
|
|
383
|
+
logger.info(f"Фото сохранено: {file_path} (with_message)")
|
|
384
|
+
|
|
385
|
+
elif message.video:
|
|
386
|
+
# Скачиваем видео
|
|
387
|
+
file = await bot.get_file(message.video.file_id)
|
|
388
|
+
file_name = message.video.file_name or f"{message.video.file_id}.mp4"
|
|
389
|
+
file_path = os.path.join(TEMP_DIR, file_name)
|
|
390
|
+
await bot.download_file(file.file_path, file_path)
|
|
391
|
+
|
|
392
|
+
files.append({
|
|
393
|
+
'type': 'video',
|
|
394
|
+
'file_path': file_path,
|
|
395
|
+
'name': file_name,
|
|
396
|
+
'stage': 'with_message',
|
|
397
|
+
'has_caption': bool(message.caption)
|
|
398
|
+
})
|
|
399
|
+
logger.info(f"Видео сохранено: {file_path} (with_message)")
|
|
344
400
|
|
|
345
|
-
|
|
346
|
-
|
|
401
|
+
await state.update_data(files=files)
|
|
402
|
+
|
|
403
|
+
# Переходим к добавлению файлов
|
|
404
|
+
await state.set_state(AdminStates.create_event_files)
|
|
347
405
|
|
|
348
406
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
349
|
-
[InlineKeyboardButton(text="➡️ Продолжить", callback_data="files:skip")]
|
|
407
|
+
[InlineKeyboardButton(text="➡️ Продолжить без файлов", callback_data="files:skip")]
|
|
350
408
|
])
|
|
351
409
|
|
|
352
|
-
await
|
|
353
|
-
|
|
410
|
+
await message.answer(
|
|
411
|
+
"✅ **Сообщение сохранено!**\n\n"
|
|
354
412
|
"📎 **Дополнительные файлы**\n\n"
|
|
355
413
|
"Теперь вы можете отправить:\n"
|
|
356
414
|
"📄 PDF документы\n"
|
|
357
415
|
"📁 Файлы любых форматов\n"
|
|
358
|
-
"🎥 Дополнительные видео\n
|
|
359
|
-
"
|
|
416
|
+
"🎥 Дополнительные видео\n"
|
|
417
|
+
"🖼 Дополнительные фото\n\n"
|
|
418
|
+
"💡 _Можно отправить несколько файлов по очереди_\n\n"
|
|
360
419
|
"Или нажмите кнопку, если дополнительных файлов нет:",
|
|
361
420
|
reply_markup=keyboard,
|
|
362
421
|
parse_mode='Markdown'
|
|
363
422
|
)
|
|
364
423
|
|
|
424
|
+
|
|
365
425
|
@admin_events_router.message(AdminStates.create_event_files, F.document | F.photo | F.video)
|
|
366
426
|
async def process_event_files(message: Message, state: FSMContext):
|
|
367
427
|
"""Обработка файлов для события"""
|
|
@@ -372,14 +432,13 @@ async def process_event_files(message: Message, state: FSMContext):
|
|
|
372
432
|
files = data.get('files', [])
|
|
373
433
|
bot = get_global_var('bot')
|
|
374
434
|
|
|
375
|
-
# Создаем временную папку
|
|
376
|
-
|
|
377
|
-
os.makedirs(temp_dir, exist_ok=True)
|
|
435
|
+
# Создаем временную папку
|
|
436
|
+
ensure_temp_dir()
|
|
378
437
|
|
|
379
438
|
# Скачиваем и добавляем файл в список
|
|
380
439
|
if message.document:
|
|
381
440
|
file = await bot.get_file(message.document.file_id)
|
|
382
|
-
file_path = os.path.join(
|
|
441
|
+
file_path = os.path.join(TEMP_DIR, message.document.file_name)
|
|
383
442
|
await bot.download_file(file.file_path, file_path)
|
|
384
443
|
|
|
385
444
|
files.append({
|
|
@@ -393,26 +452,28 @@ async def process_event_files(message: Message, state: FSMContext):
|
|
|
393
452
|
elif message.photo:
|
|
394
453
|
photo = message.photo[-1]
|
|
395
454
|
file = await bot.get_file(photo.file_id)
|
|
396
|
-
|
|
455
|
+
file_name = f"photo_{datetime.now().strftime('%H%M%S')}.jpg"
|
|
456
|
+
file_path = os.path.join(TEMP_DIR, file_name)
|
|
397
457
|
await bot.download_file(file.file_path, file_path)
|
|
398
458
|
|
|
399
459
|
files.append({
|
|
400
460
|
'type': 'photo',
|
|
401
461
|
'file_path': file_path,
|
|
402
|
-
'name':
|
|
462
|
+
'name': file_name,
|
|
403
463
|
'stage': 'after_message'
|
|
404
464
|
})
|
|
405
465
|
logger.info(f"Фото сохранено: {file_path} (after_message)")
|
|
406
466
|
|
|
407
467
|
elif message.video:
|
|
408
468
|
file = await bot.get_file(message.video.file_id)
|
|
409
|
-
|
|
469
|
+
file_name = message.video.file_name or f"{message.video.file_id}.mp4" # Используем оригинальное имя или file_id
|
|
470
|
+
file_path = os.path.join(TEMP_DIR, file_name)
|
|
410
471
|
await bot.download_file(file.file_path, file_path)
|
|
411
472
|
|
|
412
473
|
files.append({
|
|
413
474
|
'type': 'video',
|
|
414
475
|
'file_path': file_path,
|
|
415
|
-
'name':
|
|
476
|
+
'name': file_name,
|
|
416
477
|
'stage': 'after_message'
|
|
417
478
|
})
|
|
418
479
|
logger.info(f"Видео сохранено: {file_path} (after_message)")
|
|
@@ -438,9 +499,13 @@ async def process_files_action(callback_query: CallbackQuery, state: FSMContext)
|
|
|
438
499
|
data = await state.get_data()
|
|
439
500
|
files = data.get('files', [])
|
|
440
501
|
|
|
441
|
-
if action == "skip":
|
|
502
|
+
if action == "skip" and not files:
|
|
503
|
+
# Если файлов нет и нажали "Продолжить без файлов" - очищаем
|
|
442
504
|
files = []
|
|
443
505
|
await state.update_data(files=files)
|
|
506
|
+
elif action == "skip":
|
|
507
|
+
# Если файлы уже есть - оставляем их
|
|
508
|
+
logger.info(f"Продолжаем с {len(files)} существующими файлами")
|
|
444
509
|
|
|
445
510
|
# Переход к подтверждению
|
|
446
511
|
await state.set_state(AdminStates.create_event_confirm)
|
|
@@ -451,37 +516,109 @@ async def process_files_action(callback_query: CallbackQuery, state: FSMContext)
|
|
|
451
516
|
naive_datetime = datetime.strptime(f"{event_date} {event_time}", '%Y-%m-%d %H:%M')
|
|
452
517
|
moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
|
|
453
518
|
|
|
454
|
-
#
|
|
519
|
+
# Отправляем сообщение с подтверждением
|
|
520
|
+
summary = (
|
|
521
|
+
f"📋 **Подтверждение создания события**\n\n"
|
|
522
|
+
f"📝 Название: **{data.get('event_name')}**\n"
|
|
523
|
+
f"📅 Дата и время: **{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)**\n"
|
|
524
|
+
f"👥 Сегмент: **{data.get('segment_display')}**\n"
|
|
525
|
+
f"📎 Файлов: **{len(files)}**\n\n"
|
|
526
|
+
"Подтвердите создание события:"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
530
|
+
[
|
|
531
|
+
InlineKeyboardButton(text="✅ Создать", callback_data="confirm:yes"),
|
|
532
|
+
InlineKeyboardButton(text="❌ Отменить", callback_data="confirm:no")
|
|
533
|
+
],
|
|
534
|
+
[
|
|
535
|
+
InlineKeyboardButton(text="👁 Предпросмотр", callback_data="preview:show")
|
|
536
|
+
]
|
|
537
|
+
])
|
|
538
|
+
|
|
539
|
+
await callback_query.message.edit_text(
|
|
540
|
+
summary,
|
|
541
|
+
reply_markup=keyboard,
|
|
542
|
+
parse_mode='Markdown'
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
@admin_events_router.callback_query(F.data == "preview:show", AdminStates.create_event_confirm)
|
|
546
|
+
async def show_event_preview(callback_query: CallbackQuery, state: FSMContext):
|
|
547
|
+
"""Показываем предпросмотр сообщения"""
|
|
455
548
|
from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaVideo
|
|
456
549
|
|
|
550
|
+
# Получаем данные
|
|
551
|
+
data = await state.get_data()
|
|
552
|
+
files = data.get('files', [])
|
|
553
|
+
logger.info(f"Предпросмотр: получено {len(files)} файлов из состояния")
|
|
554
|
+
|
|
555
|
+
# Удаляем сообщение с кнопкой предпросмотра
|
|
556
|
+
await callback_query.message.delete()
|
|
557
|
+
|
|
457
558
|
# Анализируем файлы
|
|
458
559
|
files_with_msg = [f for f in files if f.get('stage') == 'with_message']
|
|
459
560
|
files_after = [f for f in files if f.get('stage') == 'after_message']
|
|
561
|
+
logger.info(f"Предпросмотр: {len(files_with_msg)} файлов с сообщением, {len(files_after)} дополнительных файлов")
|
|
460
562
|
|
|
461
563
|
# 1. Отправляем сообщение с прикрепленными файлами
|
|
462
564
|
if files_with_msg:
|
|
463
565
|
media_group = []
|
|
464
566
|
first_file = True
|
|
465
567
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
568
|
+
# Сортируем файлы по порядку
|
|
569
|
+
sorted_files = sorted(files_with_msg, key=lambda x: x.get('order', 0))
|
|
570
|
+
|
|
571
|
+
# Добавляем файлы в том порядке, в котором они были загружены
|
|
572
|
+
for file_info in sorted_files:
|
|
573
|
+
logger.info(f"Добавляем в медиа-группу: {file_info['type']} файл {file_info['file_path']}")
|
|
574
|
+
try:
|
|
575
|
+
if file_info['type'] == 'photo':
|
|
576
|
+
media = InputMediaPhoto(
|
|
577
|
+
media=FSInputFile(file_info['file_path']),
|
|
578
|
+
caption=data.get('event_message') if first_file else None,
|
|
579
|
+
parse_mode='Markdown' if first_file else None
|
|
580
|
+
)
|
|
581
|
+
media_group.append(media)
|
|
582
|
+
first_file = False
|
|
583
|
+
elif file_info['type'] == 'video':
|
|
584
|
+
media = InputMediaVideo(
|
|
585
|
+
media=FSInputFile(file_info['file_path']),
|
|
586
|
+
caption=data.get('event_message') if first_file else None,
|
|
587
|
+
parse_mode='Markdown' if first_file else None
|
|
588
|
+
)
|
|
589
|
+
media_group.append(media)
|
|
590
|
+
first_file = False
|
|
591
|
+
logger.info("✅ Файл успешно добавлен в медиа-группу")
|
|
592
|
+
except Exception as e:
|
|
593
|
+
logger.error(f"❌ Ошибка добавления файла в медиа-группу: {e}")
|
|
482
594
|
|
|
483
595
|
if media_group:
|
|
484
|
-
|
|
596
|
+
try:
|
|
597
|
+
from ..handlers.handlers import get_global_var
|
|
598
|
+
bot = get_global_var('bot')
|
|
599
|
+
await bot.send_media_group(chat_id=callback_query.message.chat.id, media=media_group)
|
|
600
|
+
logger.info(f"✅ Отправлена медиа-группа из {len(media_group)} файлов")
|
|
601
|
+
except Exception as e:
|
|
602
|
+
logger.error(f"❌ Ошибка отправки медиа-группы: {e}")
|
|
603
|
+
# Если не удалось отправить группой, отправляем по одному
|
|
604
|
+
first_file = True
|
|
605
|
+
for media in media_group:
|
|
606
|
+
try:
|
|
607
|
+
if isinstance(media, InputMediaPhoto):
|
|
608
|
+
await callback_query.message.answer_photo(
|
|
609
|
+
photo=media.media,
|
|
610
|
+
caption=data.get('event_message') if first_file else None,
|
|
611
|
+
parse_mode='Markdown' if first_file else None
|
|
612
|
+
)
|
|
613
|
+
elif isinstance(media, InputMediaVideo):
|
|
614
|
+
await callback_query.message.answer_video(
|
|
615
|
+
video=media.media,
|
|
616
|
+
caption=data.get('event_message') if first_file else None,
|
|
617
|
+
parse_mode='Markdown' if first_file else None
|
|
618
|
+
)
|
|
619
|
+
first_file = False
|
|
620
|
+
except Exception as e2:
|
|
621
|
+
logger.error(f"❌ Ошибка отправки отдельного файла: {e2}")
|
|
485
622
|
else:
|
|
486
623
|
# Только текст
|
|
487
624
|
await callback_query.message.answer(
|
|
@@ -504,14 +641,18 @@ async def process_files_action(callback_query: CallbackQuery, state: FSMContext)
|
|
|
504
641
|
FSInputFile(file_info['file_path'])
|
|
505
642
|
)
|
|
506
643
|
|
|
507
|
-
# 3. Отправляем сообщение с подтверждением
|
|
644
|
+
# 3. Отправляем сообщение с подтверждением (такое же как было)
|
|
645
|
+
event_date = data.get('event_date')
|
|
646
|
+
event_time = data.get('event_time')
|
|
647
|
+
naive_datetime = datetime.strptime(f"{event_date} {event_time}", '%Y-%m-%d %H:%M')
|
|
648
|
+
moscow_datetime = MOSCOW_TZ.localize(naive_datetime)
|
|
649
|
+
|
|
508
650
|
summary = (
|
|
509
651
|
f"📋 **Подтверждение создания события**\n\n"
|
|
510
652
|
f"📝 Название: **{data.get('event_name')}**\n"
|
|
511
653
|
f"📅 Дата и время: **{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)**\n"
|
|
512
654
|
f"👥 Сегмент: **{data.get('segment_display')}**\n"
|
|
513
655
|
f"📎 Файлов: **{len(files)}**\n\n"
|
|
514
|
-
f"⬆️ _Сообщение выше будет отправлено {data.get('segment_display', 'всем пользователям')}_\n\n"
|
|
515
656
|
"Подтвердите создание события:"
|
|
516
657
|
)
|
|
517
658
|
|
|
@@ -519,10 +660,13 @@ async def process_files_action(callback_query: CallbackQuery, state: FSMContext)
|
|
|
519
660
|
[
|
|
520
661
|
InlineKeyboardButton(text="✅ Создать", callback_data="confirm:yes"),
|
|
521
662
|
InlineKeyboardButton(text="❌ Отменить", callback_data="confirm:no")
|
|
663
|
+
],
|
|
664
|
+
[
|
|
665
|
+
InlineKeyboardButton(text="👁 Предпросмотр", callback_data="preview:show")
|
|
522
666
|
]
|
|
523
667
|
])
|
|
524
668
|
|
|
525
|
-
await callback_query.message.
|
|
669
|
+
await callback_query.message.answer(
|
|
526
670
|
summary,
|
|
527
671
|
reply_markup=keyboard,
|
|
528
672
|
parse_mode='Markdown'
|
|
@@ -534,6 +678,9 @@ async def process_event_confirmation(callback_query: CallbackQuery, state: FSMCo
|
|
|
534
678
|
action = callback_query.data.split(":", 1)[1]
|
|
535
679
|
|
|
536
680
|
if action == "no":
|
|
681
|
+
# Очищаем временные файлы
|
|
682
|
+
await cleanup_temp_files(state)
|
|
683
|
+
# Очищаем состояние
|
|
537
684
|
await state.clear()
|
|
538
685
|
await callback_query.message.edit_text(
|
|
539
686
|
"❌ Создание события отменено",
|
|
@@ -562,88 +709,101 @@ async def process_event_confirmation(callback_query: CallbackQuery, state: FSMCo
|
|
|
562
709
|
|
|
563
710
|
logger.info(f"⏰ Время события: Москва={moscow_datetime.strftime('%d.%m.%Y %H:%M %Z')}, UTC={utc_datetime.strftime('%d.%m.%Y %H:%M %Z')}")
|
|
564
711
|
|
|
565
|
-
#
|
|
566
|
-
import os
|
|
567
|
-
event_name = data.get('event_name')
|
|
568
|
-
files = data.get('files', [])
|
|
569
|
-
uploaded_files = []
|
|
570
|
-
|
|
712
|
+
# Сначала сохраняем событие в БД чтобы получить ID
|
|
571
713
|
try:
|
|
714
|
+
# Создаем событие без файлов
|
|
715
|
+
event = await supabase_client.save_admin_event(
|
|
716
|
+
event_name=data.get('event_name'),
|
|
717
|
+
event_data={
|
|
718
|
+
'segment': data.get('segment'),
|
|
719
|
+
'message': data.get('event_message'),
|
|
720
|
+
'files': [] # Пока пустой список
|
|
721
|
+
},
|
|
722
|
+
scheduled_datetime=utc_datetime
|
|
723
|
+
)
|
|
724
|
+
event_id = event['id']
|
|
725
|
+
logger.info(f"✅ Создано событие с ID {event_id}")
|
|
726
|
+
|
|
727
|
+
# Теперь загружаем файлы в Storage
|
|
728
|
+
import os
|
|
729
|
+
files = data.get('files', [])
|
|
730
|
+
uploaded_files = []
|
|
731
|
+
|
|
572
732
|
for file_info in files:
|
|
573
|
-
# Читаем локальный файл
|
|
574
|
-
with open(file_info['file_path'], 'rb') as f:
|
|
575
|
-
file_bytes = f.read()
|
|
576
|
-
|
|
577
|
-
# Загружаем в Storage
|
|
578
|
-
storage_info = await supabase_client.upload_event_file(
|
|
579
|
-
event_name=event_name,
|
|
580
|
-
file_data=file_bytes,
|
|
581
|
-
file_name=file_info['name']
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
# Сохраняем только метаданные (БЕЗ file_id и локального пути)
|
|
585
|
-
uploaded_files.append({
|
|
586
|
-
'type': file_info['type'],
|
|
587
|
-
'name': file_info['name'],
|
|
588
|
-
'storage_path': storage_info['storage_path'],
|
|
589
|
-
'stage': file_info['stage'],
|
|
590
|
-
'has_caption': file_info.get('has_caption', False)
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
# Удаляем временный локальный файл
|
|
594
733
|
try:
|
|
595
|
-
|
|
596
|
-
|
|
734
|
+
# Читаем локальный файл
|
|
735
|
+
with open(file_info['file_path'], 'rb') as f:
|
|
736
|
+
file_bytes = f.read()
|
|
737
|
+
|
|
738
|
+
# Генерируем уникальный ID для файла
|
|
739
|
+
file_id = generate_file_id()
|
|
740
|
+
|
|
741
|
+
# Загружаем в Storage
|
|
742
|
+
storage_info = await supabase_client.upload_event_file(
|
|
743
|
+
event_id=event_id,
|
|
744
|
+
file_data=file_bytes,
|
|
745
|
+
original_name=file_info['name'],
|
|
746
|
+
file_id=file_id
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Сохраняем метаданные файла
|
|
750
|
+
uploaded_files.append({
|
|
751
|
+
'type': file_info['type'],
|
|
752
|
+
'storage_path': storage_info['storage_path'],
|
|
753
|
+
'original_name': file_info['name'], # Используем оригинальное имя из file_info
|
|
754
|
+
'stage': file_info['stage'],
|
|
755
|
+
'has_caption': file_info.get('has_caption', False),
|
|
756
|
+
'order': file_info.get('order', 0)
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
# Удаляем временный локальный файл
|
|
760
|
+
try:
|
|
761
|
+
os.remove(file_info['file_path'])
|
|
762
|
+
logger.info(f"🗑️ Удален временный файл: {file_info['file_path']}")
|
|
763
|
+
except Exception as e:
|
|
764
|
+
logger.warning(f"⚠️ Не удалось удалить временный файл: {e}")
|
|
765
|
+
|
|
597
766
|
except Exception as e:
|
|
598
|
-
logger.
|
|
767
|
+
logger.error(f"❌ Ошибка загрузки файла {file_info['name']}: {e}")
|
|
768
|
+
# Если ошибка - удаляем все файлы события и само событие
|
|
769
|
+
try:
|
|
770
|
+
await supabase_client.delete_event_files(event_id)
|
|
771
|
+
# TODO: добавить метод удаления события
|
|
772
|
+
except:
|
|
773
|
+
pass
|
|
774
|
+
raise
|
|
775
|
+
|
|
776
|
+
logger.info(f"✅ Загружено {len(uploaded_files)} файлов в Storage для события {event_id}")
|
|
599
777
|
|
|
600
|
-
|
|
778
|
+
# Обновляем событие с информацией о файлах
|
|
779
|
+
event_data = {
|
|
780
|
+
'segment': data.get('segment'),
|
|
781
|
+
'message': data.get('event_message'),
|
|
782
|
+
'files': uploaded_files
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
# TODO: добавить метод обновления события
|
|
786
|
+
response = supabase_client.client.table('scheduled_events').update({
|
|
787
|
+
'event_data': json.dumps(event_data, ensure_ascii=False)
|
|
788
|
+
}).eq('id', event_id).execute()
|
|
601
789
|
|
|
602
790
|
except Exception as e:
|
|
603
|
-
logger.error(f"❌ Ошибка
|
|
604
|
-
# Если ошибка - пытаемся удалить уже загруженные файлы
|
|
605
|
-
try:
|
|
606
|
-
await supabase_client.delete_event_files(event_name)
|
|
607
|
-
except:
|
|
608
|
-
pass
|
|
791
|
+
logger.error(f"❌ Ошибка создания события: {e}")
|
|
609
792
|
raise
|
|
610
793
|
|
|
611
|
-
#
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
794
|
+
# Показываем сообщение об успехе
|
|
795
|
+
await callback_query.message.edit_text(
|
|
796
|
+
f"✅ **Событие успешно создано!**\n\n"
|
|
797
|
+
f"📝 Название: `{data.get('event_name')}`\n"
|
|
798
|
+
f"📅 Запланировано на: **{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)**\n"
|
|
799
|
+
f"👥 Сегмент: **{data.get('segment_display')}**\n\n"
|
|
800
|
+
f"💡 _Нажмите на название для копирования_",
|
|
801
|
+
parse_mode='Markdown'
|
|
802
|
+
)
|
|
617
803
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
event_name=event_name,
|
|
622
|
-
event_data=event_data,
|
|
623
|
-
scheduled_datetime=utc_datetime
|
|
624
|
-
)
|
|
625
|
-
|
|
626
|
-
await callback_query.message.edit_text(
|
|
627
|
-
f"✅ **Событие успешно создано!**\n\n"
|
|
628
|
-
f"📝 Название: `{data.get('event_name')}`\n"
|
|
629
|
-
f"📅 Запланировано на: **{moscow_datetime.strftime('%d.%m.%Y %H:%M')} (МСК)**\n"
|
|
630
|
-
f"👥 Сегмент: **{data.get('segment_display')}**\n\n"
|
|
631
|
-
f"💡 _Нажмите на название для копирования_",
|
|
632
|
-
parse_mode='Markdown'
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
# Очищаем состояние
|
|
636
|
-
await state.clear()
|
|
637
|
-
await state.set_state(AdminStates.admin_mode)
|
|
638
|
-
|
|
639
|
-
except Exception as e:
|
|
640
|
-
logger.error(f"Ошибка создания события: {e}")
|
|
641
|
-
await callback_query.message.edit_text(
|
|
642
|
-
f"❌ Ошибка создания события:\n`{str(e)}`",
|
|
643
|
-
parse_mode='Markdown'
|
|
644
|
-
)
|
|
645
|
-
await state.clear()
|
|
646
|
-
await state.set_state(AdminStates.admin_mode)
|
|
804
|
+
# Очищаем временные файлы и состояние
|
|
805
|
+
await cleanup_temp_files(state)
|
|
806
|
+
await state.set_state(AdminStates.admin_mode)
|
|
647
807
|
|
|
648
808
|
@admin_events_router.message(Command(commands=["список_событий", "list_events"]))
|
|
649
809
|
async def list_events_command(message: Message, state: FSMContext):
|
|
@@ -728,19 +888,34 @@ async def delete_event_command(message: Message, state: FSMContext):
|
|
|
728
888
|
event_name = parts[1].strip()
|
|
729
889
|
|
|
730
890
|
try:
|
|
731
|
-
#
|
|
732
|
-
response = supabase_client.client.table('scheduled_events').
|
|
733
|
-
'
|
|
734
|
-
|
|
891
|
+
# Сначала получаем событие чтобы узнать его ID
|
|
892
|
+
response = supabase_client.client.table('scheduled_events').select(
|
|
893
|
+
'id'
|
|
894
|
+
).eq('event_type', event_name).eq(
|
|
735
895
|
'event_category', 'admin_event'
|
|
736
896
|
).eq('status', 'pending').execute()
|
|
737
897
|
|
|
738
898
|
if response.data:
|
|
899
|
+
event_id = response.data[0]['id']
|
|
900
|
+
|
|
901
|
+
# Удаляем файлы из Storage
|
|
902
|
+
try:
|
|
903
|
+
await supabase_client.delete_event_files(event_id)
|
|
904
|
+
logger.info(f"🗑️ Удалены файлы события '{event_name}' (ID: {event_id}) из Storage")
|
|
905
|
+
except Exception as e:
|
|
906
|
+
logger.error(f"❌ Ошибка удаления файлов события из Storage: {e}")
|
|
907
|
+
|
|
908
|
+
# Отмечаем событие как отмененное
|
|
909
|
+
supabase_client.client.table('scheduled_events').update({
|
|
910
|
+
'status': 'cancelled'
|
|
911
|
+
}).eq('id', event_id).execute()
|
|
912
|
+
|
|
739
913
|
await message.answer(
|
|
740
|
-
f"✅ Событие `{event_name}` успешно
|
|
914
|
+
f"✅ Событие `{event_name}` успешно отменено\n"
|
|
915
|
+
f"_(файлы удалены из Storage)_",
|
|
741
916
|
parse_mode='Markdown'
|
|
742
917
|
)
|
|
743
|
-
logger.info(f"Отменено событие
|
|
918
|
+
logger.info(f"Отменено событие '{event_name}' (ID: {event_id})")
|
|
744
919
|
else:
|
|
745
920
|
await message.answer(
|
|
746
921
|
f"❌ Активное событие с названием `{event_name}` не найдено\n\n"
|