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.

@@ -1,14 +1,51 @@
1
1
  # Обработчики для создания админских событий
2
2
 
3
3
  import logging
4
- from datetime import datetime, time, timezone
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
- "📝 **Создание нового события**\n\n"
41
- "Введите название события:\n"
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
- if not event_message.strip():
204
- await message.answer("❌ Сообщение не может быть пустым. Попробуйте еще раз:")
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
- # Сохраняем сообщение и media_group_id если есть
208
- data_to_update = {'event_message': event_message}
209
-
210
- # Если это часть альбома (media group), сохраняем ID группы
211
- if message.media_group_id:
212
- data_to_update['media_group_id'] = message.media_group_id
213
- data_to_update['waiting_for_album'] = True
214
- logger.info(f"Обнаружен альбом с ID: {message.media_group_id}")
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
- await state.update_data(**data_to_update)
253
+ # Сохраняем сообщение
254
+ await state.update_data(event_message=event_message)
217
255
 
218
- # Если есть фото в сообщении, сохраняем его
219
- files = []
220
- if message.photo:
221
- import os
222
- from ..handlers.handlers import get_global_var
223
- bot = get_global_var('bot')
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
- if not data.get('waiting_for_album'):
280
- return
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
- if message.media_group_id != data.get('media_group_id'):
284
- return
315
+ # Сохраняем файлы
316
+ await state.update_data(files=files)
285
317
 
286
- bot = get_global_var('bot')
287
- files = data.get('files', [])
318
+ # Переходим к следующему этапу
319
+ await state.set_state(AdminStates.create_event_files)
288
320
 
289
- # Создаем временную папку если её нет
290
- temp_dir = "temp_event_files"
291
- os.makedirs(temp_dir, exist_ok=True)
321
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
322
+ [InlineKeyboardButton(text="➡️ Продолжить без файлов", callback_data="files:skip")]
323
+ ])
292
324
 
293
- # Скачиваем фото
294
- photo = message.photo[-1]
295
- file = await bot.get_file(photo.file_id)
296
- file_path = os.path.join(temp_dir, f"{photo.file_id}.jpg")
297
- await bot.download_file(file.file_path, file_path)
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
- files.append({
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
- await state.update_data(files=files)
308
- logger.info(f"Фото из альбома сохранено: {file_path} (with_message, всего: {len(files)})")
348
+ # Проверяем текст
349
+ if not event_message.strip():
350
+ await message.answer("❌ Сообщение не может быть пустым. Попробуйте еще раз:")
351
+ return
309
352
 
310
- # Автоматически переходим к следующему этапу через 2 секунды после последнего фото
311
- import asyncio
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
- if data.get('waiting_for_album') and data.get('media_group_id') == message.media_group_id:
317
- # Все фото получены, переходим к добавлению доп файлов
318
- await state.update_data(waiting_for_album=False)
319
- await state.set_state(AdminStates.create_event_files)
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
- files_count = len(data.get('files', []))
322
- keyboard = InlineKeyboardMarkup(inline_keyboard=[
323
- [InlineKeyboardButton(text="➡️ Продолжить", callback_data="files:skip")]
324
- ])
365
+ # Создаем временную папку
366
+ ensure_temp_dir()
325
367
 
326
- await message.answer(
327
- f"✅ **Сообщение и {files_count} фото сохранены!**\n\n"
328
- "📎 **Дополнительные файлы**\n\n"
329
- "Теперь вы можете отправить:\n"
330
- "📄 PDF документы\n"
331
- "📁 Файлы любых форматов\n"
332
- "🎥 Дополнительные видео\n\n"
333
- "💡 _Можно отправить несколько файлов по очереди_\n\n"
334
- "Или нажмите кнопку, если дополнительных файлов нет:",
335
- reply_markup=keyboard,
336
- parse_mode='Markdown'
337
- )
338
-
339
- @admin_events_router.callback_query(F.data == "album:done", AdminStates.create_event_message)
340
- async def finish_album_collection(callback_query: CallbackQuery, state: FSMContext):
341
- """Завершение сбора фото из альбома"""
342
- await state.update_data(waiting_for_album=False)
343
- await state.set_state(AdminStates.create_event_files)
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
- data = await state.get_data()
346
- files_count = len(data.get('files', []))
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 callback_query.message.edit_text(
353
- f"✅ **Сообщение и {files_count} фото сохранены!**\n\n"
410
+ await message.answer(
411
+ "✅ **Сообщение сохранено!**\n\n"
354
412
  "📎 **Дополнительные файлы**\n\n"
355
413
  "Теперь вы можете отправить:\n"
356
414
  "📄 PDF документы\n"
357
415
  "📁 Файлы любых форматов\n"
358
- "🎥 Дополнительные видео\n\n"
359
- "💡 _Эти файлы будут отправлены после сообщения_\n\n"
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
- temp_dir = "temp_event_files"
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(temp_dir, f"{message.document.file_id}_{message.document.file_name}")
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
- file_path = os.path.join(temp_dir, f"{photo.file_id}.jpg")
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': f"{photo.file_id}.jpg",
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
- file_path = os.path.join(temp_dir, f"{message.video.file_id}.mp4")
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': f"{message.video.file_id}.mp4",
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
- for file_info in files_with_msg:
467
- if file_info['type'] == 'photo':
468
- media = InputMediaPhoto(
469
- media=FSInputFile(file_info['file_path']),
470
- caption=data.get('event_message') if first_file else None,
471
- parse_mode='Markdown' if first_file else None
472
- )
473
- media_group.append(media)
474
- elif file_info['type'] == 'video':
475
- media = InputMediaVideo(
476
- media=FSInputFile(file_info['file_path']),
477
- caption=data.get('event_message') if first_file else None,
478
- parse_mode='Markdown' if first_file else None
479
- )
480
- media_group.append(media)
481
- first_file = False
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
- await callback_query.message.answer_media_group(media_group)
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.edit_text(
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
- # Загружаем файлы в Supabase Storage
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
- os.remove(file_info['file_path'])
596
- logger.info(f"🗑️ Удален временный файл: {file_info['file_path']}")
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.warning(f"⚠️ Не удалось удалить временный файл: {e}")
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
- logger.info(f"✅ Загружено {len(uploaded_files)} файлов в Storage для события '{event_name}'")
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"❌ Ошибка загрузки файлов в Storage: {e}")
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
- event_data = {
613
- 'segment': data.get('segment'),
614
- 'message': data.get('event_message'),
615
- 'files': uploaded_files # Сохраняем только метаданные
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
- try:
619
- # Сохраняем в БД (в UTC)
620
- event_id = await supabase_client.save_admin_event(
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
- # Удаляем по названию (только pending события)
732
- response = supabase_client.client.table('scheduled_events').update({
733
- 'status': 'cancelled'
734
- }).eq('event_type', event_name).eq(
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"Отменено событие с названием '{event_name}'")
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
- event_name=event_name,
1751
- file_name=file_info['name']
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['name']
1756
+ file_path = temp_with_msg / file_info['original_name']
1757
1757
  else:
1758
- file_path = temp_after_msg / file_info['name']
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
- for file_info in files_with_msg:
1798
- file_path = temp_with_msg / file_info['name']
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['name']
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
- # 4.2. Удаляем файлы из Supabase Storage
1854
+ # 4.2. Удаляем файлы из Supabase Storage
1852
1855
  try:
1853
- await supabase_client.delete_event_files(event_name)
1854
- logger.info(f"🗑️ Файлы события '{event_name}' удалены из Storage")
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"⚠️ Не удалось удалить временный файл: {e}")
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(self, event_name: str, file_data: bytes, file_name: str) -> Dict[str, str]:
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
- event_name: Название события (используется как папка)
1115
+ event_id: ID события из БД (используется как папка)
1110
1116
  file_data: Байты файла
1111
- file_name: Имя файла
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/{event_name}/{file_name}
1120
- storage_path = f"{event_name}/{file_name}"
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(file_name)
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, event_name: str, file_name: str) -> bytes:
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
- event_name: Название события
1151
- file_name: Имя файла
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, event_name: str):
1178
+ async def delete_event_files(self, event_id: str):
1171
1179
  """
1172
1180
  Удаляет ВСЕ файлы события из Supabase Storage
1173
1181
 
1174
1182
  Args:
1175
- event_name: Название события (папка с файлами)
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(event_name)
1190
+ files_list = self.client.storage.from_(bucket_name).list(event_path)
1182
1191
 
1183
1192
  if not files_list:
1184
- logger.info(f"ℹ️ Нет файлов для удаления в событии '{event_name}'")
1193
+ logger.info(f"ℹ️ Нет файлов для удаления в событии '{event_id}'")
1185
1194
  return
1186
1195
 
1187
1196
  # Формируем пути для удаления
1188
- file_paths = [f"{event_name}/{file['name']}" for file in files_list]
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)} файлов события '{event_name}' из Storage")
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: ID созданного события
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
- event_id = response.data[0]['id']
1245
+ event = response.data[0]
1237
1246
 
1238
- logger.info(f"💾 Админское событие '{event_name}' сохранено в БД: {event_id} на {scheduled_datetime.isoformat()}")
1239
- return event_id
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
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=yU52QWc1GC-E-WAA_A3HAXpY3zMG96eGq97FtHC5ZB0,34804
8
- smart_bot_factory/admin/admin_logic.py,sha256=zyaklL-s0LihPGHVbguOB9F0NMaggHAyQGukco_kTtA,22831
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=gJWaJVyCoBSsy4EsaJTv4OPVK-Efv1pQLm54Odgu4Lk,93678
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=jMKD04v81BbCNLl_00rAM_rgPDH1mMdHZlLI8ZhXpc8,60438
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=XznvAXqcMBqHw2_-t91Exyomru-mLP6-BlYuEeoUJSY,63082
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.3.dist-info/METADATA,sha256=kyf-bct-Sf1zvA6PwlUtTOGJ17kV7q8FuqtrjvNvXRM,31949
56
- smart_bot_factory-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- smart_bot_factory-0.3.3.dist-info/entry_points.txt,sha256=ybKEAI0WSb7WoRiey7QE-HHfn88UGV7nxLDxXq7b7SU,50
58
- smart_bot_factory-0.3.3.dist-info/licenses/LICENSE,sha256=OrK3cwdUTzNzIhJvSPtJaVMoYIyC_sSx5EFE_FDMvGs,1092
59
- smart_bot_factory-0.3.3.dist-info/RECORD,,
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,,