smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.9__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.

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