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.
- smart_bot_factory/admin/__init__.py +2 -0
- smart_bot_factory/admin/admin_events.py +757 -0
- smart_bot_factory/admin/admin_logic.py +54 -12
- smart_bot_factory/aiogram_calendar/__init__.py +6 -0
- smart_bot_factory/aiogram_calendar/common.py +70 -0
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +197 -0
- smart_bot_factory/aiogram_calendar/schemas.py +78 -0
- smart_bot_factory/aiogram_calendar/simple_calendar.py +180 -0
- smart_bot_factory/analytics/analytics_manager.py +42 -5
- smart_bot_factory/cli.py +1 -1
- smart_bot_factory/core/bot_utils.py +17 -16
- smart_bot_factory/core/decorators.py +215 -4
- smart_bot_factory/core/states.py +12 -0
- smart_bot_factory/creation/bot_builder.py +54 -2
- smart_bot_factory/handlers/handlers.py +2 -1
- smart_bot_factory/integrations/supabase_client.py +413 -37
- smart_bot_factory/utm_link_generator.py +13 -3
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/METADATA +2 -1
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/RECORD +22 -18
- smart_bot_factory/table/database_structure.sql +0 -57
- smart_bot_factory/table/schema.sql +0 -1094
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
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
|
-
)
|
|
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
|
|
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
|
@@ -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-
|
|
840
|
+
"""Парсит UTM-метки и сегмент из start параметра в формате source-vk_campaign-summer2025_seg-premium
|
|
846
841
|
|
|
847
842
|
Args:
|
|
848
|
-
start_param: строка вида 'source-vk_campaign-
|
|
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-
|
|
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-
|
|
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-
|
|
875
|
-
|
|
876
|
-
|
|
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 в
|
|
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
|
-
#
|
|
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
|
smart_bot_factory/core/states.py
CHANGED
|
@@ -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. СОЗДАЕМ НОВУЮ СЕССИЮ (автоматически закроет активные)
|