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

@@ -19,14 +19,28 @@ class AnalyticsManager:
19
19
  stats = await self.supabase.get_funnel_stats(days)
20
20
 
21
21
  # Добавляем новых пользователей
22
- cutoff_date = datetime.now() - timedelta(days=days)
23
22
 
24
- # Запрос на новых пользователей
25
- response = self.supabase.client.table('sales_users').select('id').gte(
23
+ # Добавляем новых пользователей
24
+ cutoff_date = datetime.now() - timedelta(days=days)
25
+
26
+ # Запрос на новых пользователей С УЧЕТОМ bot_id
27
+ query = self.supabase.client.table('sales_users').select('id').gte(
26
28
  'created_at', cutoff_date.isoformat()
27
- ).execute()
29
+ )
28
30
 
31
+ # Фильтруем по bot_id если он указан
32
+ if self.supabase.bot_id:
33
+ query = query.eq('bot_id', self.supabase.bot_id)
34
+ logger.info(f"📊 Фильтр новых пользователей по bot_id: {self.supabase.bot_id}")
35
+
36
+ # Исключаем тестовых пользователей
37
+ query = query.neq('username', 'test_user')
38
+
39
+ response = query.execute()
40
+
29
41
  new_users = len(response.data) if response.data else 0
42
+
43
+ logger.info(f"🆕 Новых пользователей за {days} дней: {new_users}")
30
44
 
31
45
  # Обогащаем статистику
32
46
  stats['new_users'] = new_users
@@ -135,7 +149,7 @@ class AnalyticsManager:
135
149
  lines = [
136
150
  f"📊 ВОРОНКА ЗА {stats['period_days']} ДНЕЙ",
137
151
  "",
138
- f"👥 Всего пользователей: {stats['total_sessions']}",
152
+ f"👥 Всего пользователей: {stats.get('total_unique_users', 0)}",
139
153
  f"🆕 Новых: {stats.get('new_users', 0)}",
140
154
  "",
141
155
  "📈 ЭТАПЫ ВОРОНКИ:"
@@ -161,6 +175,29 @@ class AnalyticsManager:
161
175
 
162
176
  return "\n".join(lines)
163
177
 
178
+ def format_events_stats(self, events: Dict[str, int]) -> str:
179
+ """Форматирует статистику событий для отображения"""
180
+ if not events:
181
+ return "🔥 События: нет данных"
182
+
183
+ # Эмодзи для событий
184
+ event_emojis = {
185
+ 'телефон': '📱',
186
+ 'консультация': '💬',
187
+ 'покупка': '💰',
188
+ 'отказ': '❌'
189
+ }
190
+
191
+ lines = ["🔥 СОБЫТИЯ:"]
192
+
193
+ for event_type, count in events.items():
194
+ emoji = event_emojis.get(event_type, '🔔')
195
+ # Экранируем потенциально проблемные символы
196
+ safe_event_type = event_type.replace('_', ' ').title()
197
+ lines.append(f"{emoji} {safe_event_type}: {count}")
198
+
199
+ return "\n".join(lines)
200
+
164
201
  def format_events_stats(self, events: Dict[str, int]) -> str:
165
202
  """Форматирует статистику событий для отображения"""
166
203
  if not events:
smart_bot_factory/cli.py CHANGED
@@ -67,7 +67,7 @@ def run(bot_id: str):
67
67
 
68
68
  # Загружаем .env файл
69
69
  load_dotenv(env_file)
70
- click.echo(f"⚙️ Загружен .env файл: {env_file}")
70
+ click.echo(f"📄 Загружен .env файл: {env_file}")
71
71
 
72
72
  # Устанавливаем переменные окружения
73
73
  os.environ["BOT_ID"] = bot_id
@@ -15,13 +15,8 @@ from aiogram.utils.media_group import MediaGroupBuilder
15
15
 
16
16
  from pathlib import Path
17
17
  from ..core.decorators import (
18
- execute_event_handler,
19
- save_immediate_event,
20
- schedule_task_for_later_with_db,
21
- schedule_global_handler_for_later_with_db,
22
18
  execute_scheduled_task_from_event,
23
19
  execute_global_handler_from_event,
24
- update_event_result
25
20
  )
26
21
 
27
22
  # Функция для получения глобальных переменных
@@ -842,20 +837,20 @@ OpenAI API: {'✅' if openai_status else '❌'}
842
837
 
843
838
 
844
839
  def parse_utm_from_start_param(start_param: str) -> dict:
845
- """Парсит UTM-метки из start параметра в формате source-vk_campaign-summer2025
840
+ """Парсит UTM-метки и сегмент из start параметра в формате source-vk_campaign-summer2025_seg-premium
846
841
 
847
842
  Args:
848
- start_param: строка вида 'source-vk_campaign-summer2025' или полная ссылка
843
+ start_param: строка вида 'source-vk_campaign-summer2025_seg-premium' или полная ссылка
849
844
 
850
845
  Returns:
851
- dict: {'utm_source': 'vk', 'utm_campaign': 'summer2025'}
846
+ dict: {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'premium'}
852
847
 
853
848
  Examples:
854
- >>> parse_utm_from_start_param('source-vk_campaign-summer2025')
855
- {'utm_source': 'vk', 'utm_campaign': 'summer2025'}
849
+ >>> parse_utm_from_start_param('source-vk_campaign-summer2025_seg-premium')
850
+ {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'premium'}
856
851
 
857
- >>> parse_utm_from_start_param('https://t.me/bot?start=source-vk_campaign-summer2025')
858
- {'utm_source': 'vk', 'utm_campaign': 'summer2025'}
852
+ >>> parse_utm_from_start_param('https://t.me/bot?start=source-vk_campaign-summer2025_seg-vip')
853
+ {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'vip'}
859
854
  """
860
855
  import re
861
856
  from urllib.parse import unquote
@@ -871,16 +866,22 @@ def parse_utm_from_start_param(start_param: str) -> dict:
871
866
  else:
872
867
  return {}
873
868
 
874
- # Парсим новый формат: source-vk_campaign-summer2025
875
- if '_' in start_param and '-' in start_param:
876
- parts = start_param.split('_')
869
+ # Парсим новый формат: source-vk_campaign-summer2025_seg-premium
870
+ # Поддерживает как комбинированные параметры, так и одиночные (например, только seg-prem)
871
+ if '-' in start_param:
872
+ # Разделяем по _ (если есть несколько параметров) или используем весь параметр
873
+ parts = start_param.split('_') if '_' in start_param else [start_param]
874
+
877
875
  for part in parts:
878
876
  if '-' in part:
879
877
  key, value = part.split('-', 1)
880
- # Преобразуем source в utm_source
878
+ # Преобразуем source/medium/campaign/content/term в utm_*
881
879
  if key in ['source', 'medium', 'campaign', 'content', 'term']:
882
880
  key = 'utm_' + key
883
881
  utm_data[key] = value
882
+ # Обрабатываем seg как segment
883
+ elif key == 'seg':
884
+ utm_data['segment'] = value
884
885
 
885
886
  except Exception as e:
886
887
  print(f"Ошибка парсинга UTM параметров: {e}")
@@ -1325,9 +1325,9 @@ async def get_pending_events_in_next_minute(limit: int = 100) -> list:
1325
1325
  return []
1326
1326
 
1327
1327
  async def background_event_processor():
1328
- """Фоновый процессор для всех типов событий (проверяет БД каждую минуту)"""
1328
+ """Фоновый процессор для ВСЕХ типов событий включая админские (проверяет БД каждую минуту)"""
1329
1329
 
1330
- logger.info("🔄 Запуск фонового процессора событий")
1330
+ logger.info("🔄 Запуск фонового процессора событий (user_event, scheduled_task, global_handler, admin_event)")
1331
1331
 
1332
1332
  while True:
1333
1333
  try:
@@ -1344,7 +1344,37 @@ async def background_event_processor():
1344
1344
  user_id = event.get('user_id')
1345
1345
  session_id = event.get('session_id')
1346
1346
 
1347
- # Для user_event - проверяем once_only
1347
+ # ========== ОБРАБОТКА АДМИНСКИХ СОБЫТИЙ ==========
1348
+ if event_category == 'admin_event':
1349
+ try:
1350
+ # Обрабатываем и получаем результат
1351
+ result = await process_admin_event(event)
1352
+
1353
+ # Сохраняем результат в result_data
1354
+ import json
1355
+ supabase_client = get_supabase_client()
1356
+ supabase_client.client.table('scheduled_events').update({
1357
+ 'status': 'completed',
1358
+ 'executed_at': datetime.now(timezone.utc).isoformat(),
1359
+ 'result_data': json.dumps(result, ensure_ascii=False) if result else None
1360
+ }).eq('id', event['id']).execute()
1361
+
1362
+ logger.info(f"✅ Админское событие {event['id']} выполнено")
1363
+ continue
1364
+
1365
+ except Exception as e:
1366
+ logger.error(f"❌ Ошибка обработки админского события {event['id']}: {e}")
1367
+
1368
+ # Обновляем статус на failed
1369
+ supabase_client = get_supabase_client()
1370
+ supabase_client.client.table('scheduled_events').update({
1371
+ 'status': 'failed',
1372
+ 'last_error': str(e),
1373
+ 'executed_at': datetime.now(timezone.utc).isoformat()
1374
+ }).eq('id', event['id']).execute()
1375
+ continue
1376
+
1377
+ # ========== ОБРАБОТКА USER СОБЫТИЙ ==========
1348
1378
  if event_category == 'user_event':
1349
1379
  router_manager = get_router_manager()
1350
1380
  if router_manager:
@@ -1657,4 +1687,185 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
1657
1687
 
1658
1688
  except Exception as e:
1659
1689
  logger.error(f"❌ Ошибка проверки дублирования для '{event_type}': {e}")
1660
- return False
1690
+ return False
1691
+
1692
+
1693
+ async def process_admin_event(event: Dict):
1694
+ """
1695
+ Обрабатывает одно админское событие - скачивает файлы из Storage и отправляет пользователям
1696
+
1697
+ Args:
1698
+ event: Событие из БД с данными для отправки
1699
+ """
1700
+ import json
1701
+ import os
1702
+ import shutil
1703
+ from pathlib import Path
1704
+ from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaDocument, InputMediaVideo
1705
+
1706
+ event_id = event['id']
1707
+ event_name = event['event_type']
1708
+ event_data_str = event['event_data']
1709
+
1710
+ try:
1711
+ event_data = json.loads(event_data_str)
1712
+ except Exception as e:
1713
+ logger.error(f"❌ Не удалось распарсить event_data для события {event_id}: {e}")
1714
+ return {
1715
+ 'success_count': 0,
1716
+ 'failed_count': 0,
1717
+ 'total_users': 0,
1718
+ 'error': f'Ошибка парсинга event_data: {str(e)}'
1719
+ }
1720
+
1721
+ segment = event_data.get('segment')
1722
+ message_text = event_data.get('message')
1723
+ files_metadata = event_data.get('files', [])
1724
+
1725
+ logger.info(f"📨 Обработка события '{event_name}': сегмент='{segment}', файлов={len(files_metadata)}")
1726
+
1727
+ # Получаем клиенты
1728
+ supabase_client = get_supabase_client()
1729
+ if not supabase_client:
1730
+ logger.error("❌ Supabase клиент не найден")
1731
+ return {'success_count': 0, 'failed_count': 0, 'total_users': 0, 'error': 'Нет Supabase клиента'}
1732
+
1733
+ from ..handlers.handlers import get_global_var
1734
+ bot = get_global_var('bot')
1735
+ if not bot:
1736
+ logger.error("❌ Бот не найден")
1737
+ return {'success_count': 0, 'failed_count': 0, 'total_users': 0, 'error': 'Нет бота'}
1738
+
1739
+ # Создаем временные папки
1740
+ temp_with_msg = Path("temp_with_msg")
1741
+ temp_after_msg = Path("temp_after_msg")
1742
+ temp_with_msg.mkdir(exist_ok=True)
1743
+ temp_after_msg.mkdir(exist_ok=True)
1744
+
1745
+ try:
1746
+ # 1. Скачиваем файлы из Storage
1747
+ for file_info in files_metadata:
1748
+ try:
1749
+ file_bytes = await supabase_client.download_event_file(
1750
+ event_name=event_name,
1751
+ file_name=file_info['name']
1752
+ )
1753
+
1754
+ # Сохраняем в соответствующую папку
1755
+ if file_info['stage'] == 'with_message':
1756
+ file_path = temp_with_msg / file_info['name']
1757
+ else:
1758
+ file_path = temp_after_msg / file_info['name']
1759
+
1760
+ with open(file_path, 'wb') as f:
1761
+ f.write(file_bytes)
1762
+
1763
+ logger.info(f"📥 Скачан файл: {file_path}")
1764
+
1765
+ except Exception as e:
1766
+ logger.error(f"❌ Ошибка скачивания файла {file_info['name']}: {e}")
1767
+ raise
1768
+
1769
+ # 2. Получаем пользователей
1770
+ users = await supabase_client.get_users_by_segment(segment)
1771
+
1772
+ if not users:
1773
+ logger.warning(f"⚠️ Нет пользователей для сегмента '{segment}'")
1774
+ return {
1775
+ 'success_count': 0,
1776
+ 'failed_count': 0,
1777
+ 'total_users': 0,
1778
+ 'segment': segment or 'Все',
1779
+ 'warning': 'Нет пользователей'
1780
+ }
1781
+
1782
+ success_count = 0
1783
+ failed_count = 0
1784
+
1785
+ # 3. Отправляем каждому пользователю
1786
+ for user in users:
1787
+ telegram_id = user['telegram_id']
1788
+
1789
+ try:
1790
+ # 3.1. Отправляем медиа-группу с сообщением
1791
+ files_with_msg = [f for f in files_metadata if f['stage'] == 'with_message']
1792
+
1793
+ if files_with_msg:
1794
+ media_group = []
1795
+ first_file = True
1796
+
1797
+ for file_info in files_with_msg:
1798
+ file_path = temp_with_msg / file_info['name']
1799
+
1800
+ if file_info['type'] == 'photo':
1801
+ media = InputMediaPhoto(
1802
+ media=FSInputFile(file_path),
1803
+ caption=message_text if first_file else None,
1804
+ parse_mode='Markdown' if first_file else None
1805
+ )
1806
+ media_group.append(media)
1807
+ elif file_info['type'] == 'video':
1808
+ media = InputMediaVideo(
1809
+ media=FSInputFile(file_path),
1810
+ caption=message_text if first_file else None,
1811
+ parse_mode='Markdown' if first_file else None
1812
+ )
1813
+ media_group.append(media)
1814
+
1815
+ first_file = False
1816
+
1817
+ if media_group:
1818
+ await bot.send_media_group(chat_id=telegram_id, media=media_group)
1819
+ else:
1820
+ # Только текст без файлов
1821
+ await bot.send_message(chat_id=telegram_id, text=message_text, parse_mode='Markdown')
1822
+
1823
+ # 3.2. Отправляем файлы после сообщения
1824
+ files_after = [f for f in files_metadata if f['stage'] == 'after_message']
1825
+
1826
+ for file_info in files_after:
1827
+ file_path = temp_after_msg / file_info['name']
1828
+
1829
+ if file_info['type'] == 'document':
1830
+ await bot.send_document(chat_id=telegram_id, document=FSInputFile(file_path))
1831
+ elif file_info['type'] == 'photo':
1832
+ await bot.send_photo(chat_id=telegram_id, photo=FSInputFile(file_path))
1833
+ elif file_info['type'] == 'video':
1834
+ await bot.send_video(chat_id=telegram_id, video=FSInputFile(file_path))
1835
+
1836
+ success_count += 1
1837
+ logger.info(f"✅ Отправлено пользователю {telegram_id}")
1838
+
1839
+ except Exception as e:
1840
+ logger.error(f"❌ Ошибка отправки пользователю {telegram_id}: {e}")
1841
+ failed_count += 1
1842
+
1843
+ logger.info(f"📊 Результат '{event_name}': успешно={success_count}, ошибок={failed_count}")
1844
+
1845
+ # 4. Очистка после успешной отправки
1846
+ # 4.1. Удаляем локальные временные файлы
1847
+ shutil.rmtree(temp_with_msg, ignore_errors=True)
1848
+ shutil.rmtree(temp_after_msg, ignore_errors=True)
1849
+ logger.info("🗑️ Временные папки очищены")
1850
+
1851
+ # 4.2. Удаляем файлы из Supabase Storage
1852
+ try:
1853
+ await supabase_client.delete_event_files(event_name)
1854
+ logger.info(f"🗑️ Файлы события '{event_name}' удалены из Storage")
1855
+ except Exception as e:
1856
+ logger.error(f"❌ Ошибка удаления из Storage: {e}")
1857
+
1858
+ return {
1859
+ 'success_count': success_count,
1860
+ 'failed_count': failed_count,
1861
+ 'total_users': len(users),
1862
+ 'segment': segment or 'Все пользователи',
1863
+ 'files_count': len(files_metadata)
1864
+ }
1865
+
1866
+ except Exception as e:
1867
+ # В случае ошибки все равно чистим временные файлы
1868
+ shutil.rmtree(temp_with_msg, ignore_errors=True)
1869
+ shutil.rmtree(temp_after_msg, ignore_errors=True)
1870
+ logger.error(f"❌ Критическая ошибка обработки события: {e}")
1871
+ raise
@@ -7,8 +7,20 @@ from aiogram.fsm.state import State, StatesGroup
7
7
  class UserStates(StatesGroup):
8
8
  waiting_for_message = State()
9
9
  admin_chat = State() # пользователь в диалоге с админом
10
+
11
+ voice_confirmation = State() # ожидание подтверждения распознанного текста
12
+ voice_editing = State() # редактирование распознанного текста
10
13
 
11
14
  class AdminStates(StatesGroup):
12
15
  admin_mode = State()
13
16
  in_conversation = State()
17
+
18
+ # Состояния для создания события
19
+ create_event_name = State()
20
+ create_event_date = State()
21
+ create_event_time = State()
22
+ create_event_segment = State()
23
+ create_event_message = State()
24
+ create_event_files = State()
25
+ create_event_confirm = State()
14
26
 
@@ -601,6 +601,53 @@ class BotBuilder:
601
601
  """Получает менеджер роутеров событий"""
602
602
  return self.router_manager
603
603
 
604
+ async def _setup_bot_commands(self, bot):
605
+ """Устанавливает меню команд для бота (разные для админов и пользователей)"""
606
+ from aiogram.types import BotCommand, BotCommandScopeDefault, BotCommandScopeChat
607
+
608
+ try:
609
+ # Команды для обычных пользователей
610
+ user_commands = [
611
+ BotCommand(command="start", description="🚀 Начать/перезапустить бота"),
612
+ BotCommand(command="help", description="❓ Помощь"),
613
+ ]
614
+
615
+ # Устанавливаем для всех пользователей по умолчанию
616
+ await bot.set_my_commands(user_commands, scope=BotCommandScopeDefault())
617
+ logger.info("✅ Установлены команды для обычных пользователей")
618
+
619
+ # Команды для админов (включая команды пользователей + админские)
620
+ admin_commands = [
621
+ BotCommand(command="start", description="🚀 Начать/перезапустить бота"),
622
+ BotCommand(command="help", description="❓ Помощь"),
623
+ BotCommand(command="cancel", description="❌ Отменить текущее действие"),
624
+ BotCommand(command="admin", description="👑 Админ панель"),
625
+ BotCommand(command="stats", description="📊 Статистика"),
626
+ BotCommand(command="chat", description="💬 Начать чат с пользователем"),
627
+ BotCommand(command="chats", description="👥 Активные чаты"),
628
+ BotCommand(command="stop", description="⛔ Остановить текущий чат"),
629
+ BotCommand(command="history", description="📜 История сообщений"),
630
+ BotCommand(command="create_event", description="📝 Создать событие"),
631
+ BotCommand(command="list_events", description="📋 Список событий"),
632
+ BotCommand(command="delete_event", description="🗑️ Удалить событие"),
633
+ ]
634
+
635
+ # Устанавливаем для каждого админа персональные команды
636
+ for admin_id in self.config.ADMIN_TELEGRAM_IDS:
637
+ try:
638
+ await bot.set_my_commands(
639
+ admin_commands,
640
+ scope=BotCommandScopeChat(chat_id=admin_id)
641
+ )
642
+ logger.info(f"✅ Установлены админские команды для {admin_id}")
643
+ except Exception as e:
644
+ logger.warning(f"⚠️ Не удалось установить команды для админа {admin_id}: {e}")
645
+
646
+ logger.info(f"✅ Меню команд настроено ({len(self.config.ADMIN_TELEGRAM_IDS)} админов)")
647
+
648
+ except Exception as e:
649
+ logger.error(f"❌ Ошибка установки команд бота: {e}")
650
+
604
651
  async def start(self):
605
652
  """
606
653
  Запускает бота (аналог main.py)
@@ -620,6 +667,9 @@ class BotBuilder:
620
667
  storage = MemoryStorage()
621
668
  dp = Dispatcher(storage=storage)
622
669
 
670
+ # Устанавливаем меню команд для бота
671
+ await self._setup_bot_commands(bot)
672
+
623
673
  # Инициализируем базу данных
624
674
  await self.supabase_client.initialize()
625
675
 
@@ -702,6 +752,7 @@ class BotBuilder:
702
752
  # Теперь импортируем и настраиваем обработчики
703
753
  from ..handlers.handlers import setup_handlers
704
754
  from ..admin.admin_logic import setup_admin_handlers
755
+ from ..admin.admin_events import setup_admin_events_handlers
705
756
  from ..core.bot_utils import setup_utils_handlers
706
757
 
707
758
  # Подключаем пользовательские Telegram роутеры ПЕРВЫМИ (высший приоритет)
@@ -715,6 +766,7 @@ class BotBuilder:
715
766
  # Настраиваем стандартные обработчики (меньший приоритет)
716
767
  setup_utils_handlers(dp) # Утилитарные команды (/status, /help)
717
768
  setup_admin_handlers(dp) # Админские команды (/админ, /стат, /чат)
769
+ setup_admin_events_handlers(dp) # Админские события (/создать_событие)
718
770
  setup_handlers(dp) # Основные пользовательские обработчики
719
771
 
720
772
  # Устанавливаем глобальные переменные в модуле бота для удобного доступа
@@ -739,11 +791,11 @@ class BotBuilder:
739
791
  logger.info(f" 👑 Админов настроено: {len(self.config.ADMIN_TELEGRAM_IDS)}")
740
792
  logger.info(f" 📝 Загружено промптов: {len(self.config.PROMPT_FILES)}")
741
793
 
742
- # Запускаем фоновый процессор событий
794
+ # Запускаем единый фоновый процессор для всех событий
743
795
  from ..core.decorators import background_event_processor
744
796
  import asyncio
745
797
  asyncio.create_task(background_event_processor())
746
- logger.info("✅ Фоновый процессор событий запущен (проверка БД каждую минуту)")
798
+ logger.info("✅ Фоновый процессор событий запущен (user_event, scheduled_task, global_handler, admin_event)")
747
799
 
748
800
  # Четкое сообщение о запуске
749
801
  print(f"\n🤖 БОТ {self.bot_id.upper()} УСПЕШНО ЗАПУЩЕН!")
@@ -373,7 +373,8 @@ async def user_start_handler(message: Message, state: FSMContext):
373
373
  'medium': utm_data.get('utm_medium'),
374
374
  'campaign': utm_data.get('utm_campaign'),
375
375
  'content': utm_data.get('utm_content'),
376
- 'term': utm_data.get('utm_term')
376
+ 'term': utm_data.get('utm_term'),
377
+ 'segment': utm_data.get('segment')
377
378
  }
378
379
 
379
380
  # 4. СОЗДАЕМ НОВУЮ СЕССИЮ (автоматически закроет активные)