smart-bot-factory 0.3.3__py3-none-any.whl → 0.3.4__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/decorators.py +13 -10
- smart_bot_factory/handlers/handlers.py +8 -2
- smart_bot_factory/integrations/supabase_client.py +32 -23
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.4.dist-info}/METADATA +2 -1
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.4.dist-info}/RECORD +10 -10
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.4.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.4.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.3.dist-info → smart_bot_factory-0.3.4.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"
|
|
@@ -29,6 +29,11 @@ async def cancel_handler(message: Message, state: FSMContext):
|
|
|
29
29
|
# Получаем текущий state
|
|
30
30
|
current_state = await state.get_state()
|
|
31
31
|
|
|
32
|
+
# Очищаем временные файлы если это создание события
|
|
33
|
+
if current_state and current_state.startswith('AdminStates:create_event'):
|
|
34
|
+
from .admin_events import cleanup_temp_files
|
|
35
|
+
await cleanup_temp_files(state)
|
|
36
|
+
|
|
32
37
|
# Очищаем state
|
|
33
38
|
await state.clear()
|
|
34
39
|
|
|
@@ -1747,15 +1747,15 @@ async def process_admin_event(event: Dict):
|
|
|
1747
1747
|
for file_info in files_metadata:
|
|
1748
1748
|
try:
|
|
1749
1749
|
file_bytes = await supabase_client.download_event_file(
|
|
1750
|
-
|
|
1751
|
-
|
|
1750
|
+
event_id=event_id,
|
|
1751
|
+
storage_path=file_info['storage_path']
|
|
1752
1752
|
)
|
|
1753
1753
|
|
|
1754
1754
|
# Сохраняем в соответствующую папку
|
|
1755
1755
|
if file_info['stage'] == 'with_message':
|
|
1756
|
-
file_path = temp_with_msg / file_info['
|
|
1756
|
+
file_path = temp_with_msg / file_info['original_name']
|
|
1757
1757
|
else:
|
|
1758
|
-
file_path = temp_after_msg / file_info['
|
|
1758
|
+
file_path = temp_after_msg / file_info['original_name']
|
|
1759
1759
|
|
|
1760
1760
|
with open(file_path, 'wb') as f:
|
|
1761
1761
|
f.write(file_bytes)
|
|
@@ -1794,8 +1794,11 @@ async def process_admin_event(event: Dict):
|
|
|
1794
1794
|
media_group = []
|
|
1795
1795
|
first_file = True
|
|
1796
1796
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1797
|
+
# Сортируем файлы по порядку
|
|
1798
|
+
sorted_files = sorted(files_with_msg, key=lambda x: x.get('order', 0))
|
|
1799
|
+
|
|
1800
|
+
for file_info in sorted_files:
|
|
1801
|
+
file_path = temp_with_msg / file_info['original_name']
|
|
1799
1802
|
|
|
1800
1803
|
if file_info['type'] == 'photo':
|
|
1801
1804
|
media = InputMediaPhoto(
|
|
@@ -1824,7 +1827,7 @@ async def process_admin_event(event: Dict):
|
|
|
1824
1827
|
files_after = [f for f in files_metadata if f['stage'] == 'after_message']
|
|
1825
1828
|
|
|
1826
1829
|
for file_info in files_after:
|
|
1827
|
-
file_path = temp_after_msg / file_info['
|
|
1830
|
+
file_path = temp_after_msg / file_info['original_name']
|
|
1828
1831
|
|
|
1829
1832
|
if file_info['type'] == 'document':
|
|
1830
1833
|
await bot.send_document(chat_id=telegram_id, document=FSInputFile(file_path))
|
|
@@ -1848,10 +1851,10 @@ async def process_admin_event(event: Dict):
|
|
|
1848
1851
|
shutil.rmtree(temp_after_msg, ignore_errors=True)
|
|
1849
1852
|
logger.info("🗑️ Временные папки очищены")
|
|
1850
1853
|
|
|
1851
|
-
|
|
1854
|
+
# 4.2. Удаляем файлы из Supabase Storage
|
|
1852
1855
|
try:
|
|
1853
|
-
await supabase_client.delete_event_files(
|
|
1854
|
-
logger.info(f"🗑️ Файлы события '{
|
|
1856
|
+
await supabase_client.delete_event_files(event_id)
|
|
1857
|
+
logger.info(f"🗑️ Файлы события '{event_id}' удалены из Storage")
|
|
1855
1858
|
except Exception as e:
|
|
1856
1859
|
logger.error(f"❌ Ошибка удаления из Storage: {e}")
|
|
1857
1860
|
|
|
@@ -222,12 +222,18 @@ async def voice_handler(message: Message, state: FSMContext):
|
|
|
222
222
|
# Распознаем через Whisper
|
|
223
223
|
recognized_text = await openai_client.transcribe_audio(str(file_path))
|
|
224
224
|
|
|
225
|
-
# Удаляем временный файл
|
|
225
|
+
# Удаляем временный файл и папку
|
|
226
226
|
try:
|
|
227
227
|
os.remove(file_path)
|
|
228
228
|
logger.info(f"🗑️ Временный файл удален: {file_path}")
|
|
229
|
+
|
|
230
|
+
# Проверяем, пуста ли папка
|
|
231
|
+
if not any(temp_dir.iterdir()):
|
|
232
|
+
temp_dir.rmdir()
|
|
233
|
+
logger.info(f"🗑️ Временная папка удалена: {temp_dir}")
|
|
234
|
+
|
|
229
235
|
except Exception as e:
|
|
230
|
-
logger.warning(f"⚠️ Не удалось удалить
|
|
236
|
+
logger.warning(f"⚠️ Не удалось удалить временные файлы: {e}")
|
|
231
237
|
|
|
232
238
|
if not recognized_text:
|
|
233
239
|
await processing_msg.edit_text("❌ Не удалось распознать голос. Попробуйте еще раз.")
|
|
@@ -1101,27 +1101,36 @@ class SupabaseClient:
|
|
|
1101
1101
|
# МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ СОБЫТИЙ В SUPABASE STORAGE
|
|
1102
1102
|
# =============================================================================
|
|
1103
1103
|
|
|
1104
|
-
async def upload_event_file(
|
|
1104
|
+
async def upload_event_file(
|
|
1105
|
+
self,
|
|
1106
|
+
event_id: str,
|
|
1107
|
+
file_data: bytes,
|
|
1108
|
+
original_name: str,
|
|
1109
|
+
file_id: str
|
|
1110
|
+
) -> Dict[str, str]:
|
|
1105
1111
|
"""
|
|
1106
1112
|
Загружает файл события в Supabase Storage
|
|
1107
1113
|
|
|
1108
1114
|
Args:
|
|
1109
|
-
|
|
1115
|
+
event_id: ID события из БД (используется как папка)
|
|
1110
1116
|
file_data: Байты файла
|
|
1111
|
-
|
|
1117
|
+
original_name: Оригинальное имя файла (для метаданных)
|
|
1118
|
+
file_id: Уникальный ID файла для хранения
|
|
1112
1119
|
|
|
1113
1120
|
Returns:
|
|
1114
|
-
Dict с storage_path
|
|
1121
|
+
Dict с storage_path и original_name
|
|
1115
1122
|
"""
|
|
1116
1123
|
try:
|
|
1117
1124
|
bucket_name = 'admin-events'
|
|
1118
1125
|
|
|
1119
|
-
# Формируем путь: admin-events/
|
|
1120
|
-
|
|
1126
|
+
# Формируем путь: admin-events/event_id/file_id.ext
|
|
1127
|
+
extension = original_name.split('.')[-1] if '.' in original_name else ''
|
|
1128
|
+
storage_name = f"{file_id}.{extension}" if extension else file_id
|
|
1129
|
+
storage_path = f"events/{event_id}/files/{storage_name}"
|
|
1121
1130
|
|
|
1122
|
-
# Определяем MIME-type
|
|
1131
|
+
# Определяем MIME-type по оригинальному имени файла
|
|
1123
1132
|
import mimetypes
|
|
1124
|
-
content_type, _ = mimetypes.guess_type(
|
|
1133
|
+
content_type, _ = mimetypes.guess_type(original_name)
|
|
1125
1134
|
if not content_type:
|
|
1126
1135
|
content_type = 'application/octet-stream'
|
|
1127
1136
|
|
|
@@ -1142,20 +1151,19 @@ class SupabaseClient:
|
|
|
1142
1151
|
logger.error(f"❌ Ошибка загрузки файла в Storage: {e}")
|
|
1143
1152
|
raise
|
|
1144
1153
|
|
|
1145
|
-
async def download_event_file(self,
|
|
1154
|
+
async def download_event_file(self, event_id: str, storage_path: str) -> bytes:
|
|
1146
1155
|
"""
|
|
1147
1156
|
Скачивает файл события из Supabase Storage
|
|
1148
1157
|
|
|
1149
1158
|
Args:
|
|
1150
|
-
|
|
1151
|
-
|
|
1159
|
+
event_id: ID события
|
|
1160
|
+
storage_path: Полный путь к файлу в Storage
|
|
1152
1161
|
|
|
1153
1162
|
Returns:
|
|
1154
1163
|
bytes: Содержимое файла
|
|
1155
1164
|
"""
|
|
1156
1165
|
try:
|
|
1157
1166
|
bucket_name = 'admin-events'
|
|
1158
|
-
storage_path = f"{event_name}/{file_name}"
|
|
1159
1167
|
|
|
1160
1168
|
# Скачиваем файл
|
|
1161
1169
|
file_data = self.client.storage.from_(bucket_name).download(storage_path)
|
|
@@ -1167,30 +1175,31 @@ class SupabaseClient:
|
|
|
1167
1175
|
logger.error(f"❌ Ошибка скачивания файла из Storage: {e}")
|
|
1168
1176
|
raise
|
|
1169
1177
|
|
|
1170
|
-
async def delete_event_files(self,
|
|
1178
|
+
async def delete_event_files(self, event_id: str):
|
|
1171
1179
|
"""
|
|
1172
1180
|
Удаляет ВСЕ файлы события из Supabase Storage
|
|
1173
1181
|
|
|
1174
1182
|
Args:
|
|
1175
|
-
|
|
1183
|
+
event_id: ID события
|
|
1176
1184
|
"""
|
|
1177
1185
|
try:
|
|
1178
1186
|
bucket_name = 'admin-events'
|
|
1187
|
+
event_path = f"events/{event_id}/files"
|
|
1179
1188
|
|
|
1180
1189
|
# Получаем список всех файлов в папке события
|
|
1181
|
-
files_list = self.client.storage.from_(bucket_name).list(
|
|
1190
|
+
files_list = self.client.storage.from_(bucket_name).list(event_path)
|
|
1182
1191
|
|
|
1183
1192
|
if not files_list:
|
|
1184
|
-
logger.info(f"ℹ️ Нет файлов для удаления в событии '{
|
|
1193
|
+
logger.info(f"ℹ️ Нет файлов для удаления в событии '{event_id}'")
|
|
1185
1194
|
return
|
|
1186
1195
|
|
|
1187
1196
|
# Формируем пути для удаления
|
|
1188
|
-
file_paths = [f"{
|
|
1197
|
+
file_paths = [f"{event_path}/{file['name']}" for file in files_list]
|
|
1189
1198
|
|
|
1190
1199
|
# Удаляем файлы
|
|
1191
1200
|
self.client.storage.from_(bucket_name).remove(file_paths)
|
|
1192
1201
|
|
|
1193
|
-
logger.info(f"✅ Удалено {len(file_paths)} файлов события '{
|
|
1202
|
+
logger.info(f"✅ Удалено {len(file_paths)} файлов события '{event_id}' из Storage")
|
|
1194
1203
|
|
|
1195
1204
|
except Exception as e:
|
|
1196
1205
|
logger.error(f"❌ Ошибка удаления файлов события из Storage: {e}")
|
|
@@ -1201,7 +1210,7 @@ class SupabaseClient:
|
|
|
1201
1210
|
event_name: str,
|
|
1202
1211
|
event_data: Dict[str, Any],
|
|
1203
1212
|
scheduled_datetime: datetime
|
|
1204
|
-
) -> str:
|
|
1213
|
+
) -> Dict[str, Any]:
|
|
1205
1214
|
"""
|
|
1206
1215
|
Сохраняет админское событие в таблицу scheduled_events
|
|
1207
1216
|
|
|
@@ -1211,7 +1220,7 @@ class SupabaseClient:
|
|
|
1211
1220
|
scheduled_datetime: Дата и время отправки (должно быть в UTC с timezone info)
|
|
1212
1221
|
|
|
1213
1222
|
Returns:
|
|
1214
|
-
str:
|
|
1223
|
+
Dict[str, Any]: {'id': str, 'event_type': str, ...} - все данные созданного события
|
|
1215
1224
|
"""
|
|
1216
1225
|
try:
|
|
1217
1226
|
import json
|
|
@@ -1233,10 +1242,10 @@ class SupabaseClient:
|
|
|
1233
1242
|
}
|
|
1234
1243
|
|
|
1235
1244
|
response = self.client.table('scheduled_events').insert(event_record).execute()
|
|
1236
|
-
|
|
1245
|
+
event = response.data[0]
|
|
1237
1246
|
|
|
1238
|
-
logger.info(f"💾 Админское событие '{event_name}' сохранено в БД: {
|
|
1239
|
-
return
|
|
1247
|
+
logger.info(f"💾 Админское событие '{event_name}' сохранено в БД: {event['id']} на {scheduled_datetime.isoformat()}")
|
|
1248
|
+
return event
|
|
1240
1249
|
|
|
1241
1250
|
except Exception as e:
|
|
1242
1251
|
logger.error(f"❌ Ошибка сохранения админского события: {e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smart-bot-factory
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Библиотека для создания умных чат-ботов
|
|
5
5
|
Author-email: Kopatych <eserov73@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Communications :: Chat
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Python: >=3.9
|
|
23
23
|
Requires-Dist: aiofiles>=23.0.0
|
|
24
|
+
Requires-Dist: aiogram-media-group>=0.5.1
|
|
24
25
|
Requires-Dist: aiogram>=3.4.1
|
|
25
26
|
Requires-Dist: click>=8.0.0
|
|
26
27
|
Requires-Dist: openai>=1.12.0
|
|
@@ -4,8 +4,8 @@ smart_bot_factory/config.py,sha256=kB3G2hGMrrCSOAvlrddf8x43bgfgEaQqOCKOeELAzfM,1
|
|
|
4
4
|
smart_bot_factory/setup_checker.py,sha256=fqRzyptyMzcb2I0boUaj0rWLdarqaN6ViX6suwTEeWc,20123
|
|
5
5
|
smart_bot_factory/utm_link_generator.py,sha256=wYPdYYK555YfOP22eJXAcEv_D66_t50Dhg0-wkLVzVk,4502
|
|
6
6
|
smart_bot_factory/admin/__init__.py,sha256=vdsMTpt_LiXkY-awFu_X9e2Zt7CV50PwmsWkFbk6whk,488
|
|
7
|
-
smart_bot_factory/admin/admin_events.py,sha256=
|
|
8
|
-
smart_bot_factory/admin/admin_logic.py,sha256=
|
|
7
|
+
smart_bot_factory/admin/admin_events.py,sha256=QCosyTbJgrU8daWSK_bQgf8UZoJSIrV6xyO0R3XV2j0,43289
|
|
8
|
+
smart_bot_factory/admin/admin_logic.py,sha256=vPkNk86bdPsjNUNlZ3qfKtbRr9UuJy2oG54cYUGGNmg,23107
|
|
9
9
|
smart_bot_factory/admin/admin_manager.py,sha256=xlyG9mIjPmtUhS4E9lp36T7o5Kfp5PZpJ-r1QjnSn5g,6394
|
|
10
10
|
smart_bot_factory/admin/admin_migration.sql,sha256=kleMPJBSe2Z7ZZz7rNyOX_yoh4GZivGesqAX90U5PGs,5667
|
|
11
11
|
smart_bot_factory/admin/admin_tester.py,sha256=PGFpf7fmD5Wxea31xR2ZM_A_QpvrB73gsbxvUrHQBkg,6463
|
|
@@ -32,7 +32,7 @@ smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt
|
|
|
32
32
|
smart_bot_factory/configs/growthmed-october-24/welcome_file/Чек лист по 152ФЗ и 323ФЗ для медицины.pdf,sha256=BiAiQHNnQXJPMsks9AeL6s0beEjRFkRMJLMlAn4WorA,5284954
|
|
33
33
|
smart_bot_factory/core/bot_utils.py,sha256=Be334BPspBgHNO9OpEiPlrCc0Gmy7_8sox5WpV8XHic,45174
|
|
34
34
|
smart_bot_factory/core/conversation_manager.py,sha256=eoHL7MCEz68DRvTVwRwZgf2PWwGv4T6J9D-I-thETi8,28289
|
|
35
|
-
smart_bot_factory/core/decorators.py,sha256=
|
|
35
|
+
smart_bot_factory/core/decorators.py,sha256=oSsX1yCULoUfVf2aNynBmvap7zDEh5-cEcW4yoRv3dc,93903
|
|
36
36
|
smart_bot_factory/core/message_sender.py,sha256=J4b6n8nXVjqf-qzL6URRSvc-FVnQfShwujVSM6qv26w,32232
|
|
37
37
|
smart_bot_factory/core/router.py,sha256=03fbysaj0LR96p4-8iiml8dTmEHCkR-AaTposSv8q8o,11898
|
|
38
38
|
smart_bot_factory/core/router_manager.py,sha256=dUwesog-oHk1U2EDdS8p0e4MTSkwtx5_qXn6nrJ9l9I,9700
|
|
@@ -41,9 +41,9 @@ smart_bot_factory/creation/__init__.py,sha256=IgDk8GDS3pg7Pw_Et41J33ZmeZIU5dRwQd
|
|
|
41
41
|
smart_bot_factory/creation/bot_builder.py,sha256=yGRmOPD7qCMbhcBiltHWISoKxWx8eqjDSnZXpwhqnUs,43115
|
|
42
42
|
smart_bot_factory/creation/bot_testing.py,sha256=JDWXyJfZmbgo-DLdAPk8Sd9FiehtHHa4sLD17lBrTOc,55669
|
|
43
43
|
smart_bot_factory/event/__init__.py,sha256=hPL449RULIOB-OXv1ZbGNiHctAYaOMUqhSWGPrDHYBM,212
|
|
44
|
-
smart_bot_factory/handlers/handlers.py,sha256=
|
|
44
|
+
smart_bot_factory/handlers/handlers.py,sha256=ymPCQPjA0uQc9deiwiVrMhxpBsqdKsAfeajHzK74seA,60739
|
|
45
45
|
smart_bot_factory/integrations/openai_client.py,sha256=fwaJpwojFdLBWChcFWpFGOHK9upG-nCIwDochkCRRlY,24291
|
|
46
|
-
smart_bot_factory/integrations/supabase_client.py,sha256=
|
|
46
|
+
smart_bot_factory/integrations/supabase_client.py,sha256=Rv0sZHXGSfm3UWodmaR1N-X5-2JbmynPWJKY0a0k_Tk,63557
|
|
47
47
|
smart_bot_factory/message/__init__.py,sha256=-ehDZweUc3uKgmLLxFVsD-KWrDtnHpHms7pCrDelWo0,1950
|
|
48
48
|
smart_bot_factory/router/__init__.py,sha256=5gEbpG3eylOyow5NmidzGUy0K-AZq7RhYLVu9OaUT6c,270
|
|
49
49
|
smart_bot_factory/supabase/__init__.py,sha256=XmZP6yM9ffERM5ddAWyJnrNzEhCYtMu3AcjVCi1rOf8,179
|
|
@@ -52,8 +52,8 @@ smart_bot_factory/utils/__init__.py,sha256=UhsJXEHfrIK8h1AHsroHSwAriijk-LvnqLyvg
|
|
|
52
52
|
smart_bot_factory/utils/debug_routing.py,sha256=BOoDhKBg7UXe5uHQxRk3TSfPfLPOFqt0N7lAo6kjCOo,4719
|
|
53
53
|
smart_bot_factory/utils/prompt_loader.py,sha256=JSn7CsWnToSbHYtURdeuZn7ectyDqQGrPGHN2ixIGkw,19930
|
|
54
54
|
smart_bot_factory/utils/user_prompt_loader.py,sha256=dk6P0X_3UcNqxjRtuIvb0LcPrp03zIIsstZwdmeCPaE,2519
|
|
55
|
-
smart_bot_factory-0.3.
|
|
56
|
-
smart_bot_factory-0.3.
|
|
57
|
-
smart_bot_factory-0.3.
|
|
58
|
-
smart_bot_factory-0.3.
|
|
59
|
-
smart_bot_factory-0.3.
|
|
55
|
+
smart_bot_factory-0.3.4.dist-info/METADATA,sha256=lO_csJRkPwV6c8SUmBUEN4WBl9X8LTx_lSt_FALl6J0,31991
|
|
56
|
+
smart_bot_factory-0.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
57
|
+
smart_bot_factory-0.3.4.dist-info/entry_points.txt,sha256=ybKEAI0WSb7WoRiey7QE-HHfn88UGV7nxLDxXq7b7SU,50
|
|
58
|
+
smart_bot_factory-0.3.4.dist-info/licenses/LICENSE,sha256=OrK3cwdUTzNzIhJvSPtJaVMoYIyC_sSx5EFE_FDMvGs,1092
|
|
59
|
+
smart_bot_factory-0.3.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|