smart-bot-factory 0.3.2__py3-none-any.whl → 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/admin/__init__.py +2 -0
- smart_bot_factory/admin/admin_events.py +932 -0
- smart_bot_factory/admin/admin_logic.py +59 -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 +218 -4
- smart_bot_factory/core/states.py +12 -0
- smart_bot_factory/creation/bot_builder.py +54 -2
- smart_bot_factory/handlers/handlers.py +10 -3
- smart_bot_factory/integrations/supabase_client.py +422 -37
- smart_bot_factory/utm_link_generator.py +13 -3
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.4.dist-info}/METADATA +3 -1
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.4.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.4.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.4.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -52,15 +52,65 @@ class SupabaseClient:
|
|
|
52
52
|
response = query.execute()
|
|
53
53
|
|
|
54
54
|
if response.data:
|
|
55
|
-
#
|
|
56
|
-
|
|
55
|
+
# Получаем текущие данные пользователя для мержинга UTM и сегментов
|
|
56
|
+
existing_user_query = self.client.table('sales_users').select(
|
|
57
|
+
'source', 'medium', 'campaign', 'content', 'term', 'segments'
|
|
58
|
+
).eq('telegram_id', user_data['telegram_id'])
|
|
59
|
+
|
|
60
|
+
if self.bot_id:
|
|
61
|
+
existing_user_query = existing_user_query.eq('bot_id', self.bot_id)
|
|
62
|
+
|
|
63
|
+
existing_response = existing_user_query.execute()
|
|
64
|
+
existing_utm = existing_response.data[0] if existing_response.data else {}
|
|
65
|
+
|
|
66
|
+
# Формируем данные для обновления
|
|
67
|
+
update_data = {
|
|
57
68
|
'username': user_data.get('username'),
|
|
58
69
|
'first_name': user_data.get('first_name'),
|
|
59
70
|
'last_name': user_data.get('last_name'),
|
|
60
71
|
'language_code': user_data.get('language_code'),
|
|
61
72
|
'updated_at': datetime.now().isoformat(),
|
|
62
73
|
'is_active': True
|
|
63
|
-
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Мержим UTM данные: обновляем только если новое значение не None
|
|
77
|
+
utm_fields = ['source', 'medium', 'campaign', 'content', 'term']
|
|
78
|
+
for field in utm_fields:
|
|
79
|
+
new_value = user_data.get(field)
|
|
80
|
+
if new_value is not None:
|
|
81
|
+
# Есть новое значение - обновляем
|
|
82
|
+
update_data[field] = new_value
|
|
83
|
+
if existing_utm.get(field) != new_value:
|
|
84
|
+
logger.info(f"📊 UTM обновление: {field} = '{new_value}' (было: '{existing_utm.get(field)}')")
|
|
85
|
+
else:
|
|
86
|
+
# Нового значения нет - сохраняем старое
|
|
87
|
+
update_data[field] = existing_utm.get(field)
|
|
88
|
+
|
|
89
|
+
# Обрабатываем сегменты с накоплением через запятую
|
|
90
|
+
new_segment = user_data.get('segment')
|
|
91
|
+
if new_segment:
|
|
92
|
+
existing_segments = existing_utm.get('segments', '') or ''
|
|
93
|
+
if existing_segments:
|
|
94
|
+
# Разбираем существующие сегменты
|
|
95
|
+
segments_list = [s.strip() for s in existing_segments.split(',') if s.strip()]
|
|
96
|
+
# Добавляем новый сегмент, если его еще нет
|
|
97
|
+
if new_segment not in segments_list:
|
|
98
|
+
segments_list.append(new_segment)
|
|
99
|
+
update_data['segments'] = ', '.join(segments_list)
|
|
100
|
+
logger.info(f"📊 Сегмент добавлен: '{new_segment}' (было: '{existing_segments}')")
|
|
101
|
+
else:
|
|
102
|
+
update_data['segments'] = existing_segments
|
|
103
|
+
logger.info(f"📊 Сегмент '{new_segment}' уже существует")
|
|
104
|
+
else:
|
|
105
|
+
# Первый сегмент
|
|
106
|
+
update_data['segments'] = new_segment
|
|
107
|
+
logger.info(f"📊 Первый сегмент добавлен: '{new_segment}'")
|
|
108
|
+
else:
|
|
109
|
+
# Нового сегмента нет - сохраняем старое значение
|
|
110
|
+
update_data['segments'] = existing_utm.get('segments')
|
|
111
|
+
|
|
112
|
+
# Обновляем пользователя
|
|
113
|
+
update_query = self.client.table('sales_users').update(update_data).eq('telegram_id', user_data['telegram_id'])
|
|
64
114
|
|
|
65
115
|
if self.bot_id:
|
|
66
116
|
update_query = update_query.eq('bot_id', self.bot_id)
|
|
@@ -83,13 +133,17 @@ class SupabaseClient:
|
|
|
83
133
|
'campaign': user_data.get('campaign'),
|
|
84
134
|
'content': user_data.get('content'),
|
|
85
135
|
'term': user_data.get('term'),
|
|
136
|
+
'segments': user_data.get('segment'), # Первый сегмент при создании
|
|
86
137
|
}
|
|
87
138
|
if self.bot_id:
|
|
88
139
|
user_insert_data['bot_id'] = self.bot_id
|
|
89
140
|
|
|
90
141
|
response = self.client.table('sales_users').insert(user_insert_data).execute()
|
|
91
142
|
|
|
92
|
-
|
|
143
|
+
if user_data.get('segment'):
|
|
144
|
+
logger.info(f"Создан новый пользователь {user_data['telegram_id']} с сегментом '{user_data.get('segment')}'{f' для bot_id {self.bot_id}' if self.bot_id else ''}")
|
|
145
|
+
else:
|
|
146
|
+
logger.info(f"Создан новый пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}")
|
|
93
147
|
return user_data['telegram_id']
|
|
94
148
|
|
|
95
149
|
except APIError as e:
|
|
@@ -730,47 +784,71 @@ class SupabaseClient:
|
|
|
730
784
|
async def get_funnel_stats(self, days: int = 7) -> Dict[str, Any]:
|
|
731
785
|
"""Получает статистику воронки продаж"""
|
|
732
786
|
try:
|
|
733
|
-
cutoff_date = datetime.now(
|
|
787
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
|
788
|
+
|
|
789
|
+
# Получаем ВСЕ уникальные пользователи из sales_users с фильтром по bot_id
|
|
790
|
+
users_query = self.client.table('sales_users').select('telegram_id')
|
|
734
791
|
|
|
735
|
-
# Общее количество сессий
|
|
736
|
-
sessions_response = self.client.table('sales_chat_sessions').select('id').gte(
|
|
737
|
-
'created_at', cutoff_date.isoformat()
|
|
738
|
-
)
|
|
739
792
|
if self.bot_id:
|
|
740
|
-
|
|
793
|
+
users_query = users_query.eq('bot_id', self.bot_id)
|
|
741
794
|
|
|
742
|
-
|
|
795
|
+
# Исключаем тестовых пользователей
|
|
796
|
+
users_query = users_query.neq('username', 'test_user')
|
|
743
797
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
'current_stage', 'quality_score'
|
|
747
|
-
).gte('created_at', cutoff_date.isoformat())
|
|
798
|
+
users_response = users_query.execute()
|
|
799
|
+
total_unique_users = len(users_response.data) if users_response.data else 0
|
|
748
800
|
|
|
801
|
+
# Получаем сессии с учетом bot_id за период
|
|
802
|
+
sessions_query = self.client.table('sales_chat_sessions').select(
|
|
803
|
+
'id', 'user_id', 'current_stage', 'lead_quality_score', 'created_at'
|
|
804
|
+
).gte('created_at', cutoff_date.isoformat())
|
|
805
|
+
|
|
749
806
|
if self.bot_id:
|
|
750
|
-
|
|
807
|
+
sessions_query = sessions_query.eq('bot_id', self.bot_id)
|
|
808
|
+
|
|
809
|
+
sessions_response = sessions_query.execute()
|
|
810
|
+
sessions = sessions_response.data
|
|
751
811
|
|
|
752
|
-
|
|
812
|
+
# Исключаем сессии тестовых пользователей
|
|
813
|
+
if sessions:
|
|
814
|
+
# Получаем telegram_id тестовых пользователей
|
|
815
|
+
test_users_query = self.client.table('sales_users').select('telegram_id').eq('username', 'test_user')
|
|
816
|
+
if self.bot_id:
|
|
817
|
+
test_users_query = test_users_query.eq('bot_id', self.bot_id)
|
|
818
|
+
|
|
819
|
+
test_users_response = test_users_query.execute()
|
|
820
|
+
test_user_ids = {user['telegram_id'] for user in test_users_response.data} if test_users_response.data else set()
|
|
821
|
+
|
|
822
|
+
# Фильтруем сессии
|
|
823
|
+
sessions = [s for s in sessions if s['user_id'] not in test_user_ids]
|
|
824
|
+
|
|
825
|
+
total_sessions = len(sessions)
|
|
826
|
+
|
|
827
|
+
# Группировка по этапам
|
|
753
828
|
|
|
829
|
+
# Группировка по этапам
|
|
754
830
|
stages = {}
|
|
755
831
|
quality_scores = []
|
|
756
832
|
|
|
757
|
-
for session in
|
|
833
|
+
for session in sessions:
|
|
758
834
|
stage = session.get('current_stage', 'unknown')
|
|
759
835
|
stages[stage] = stages.get(stage, 0) + 1
|
|
760
836
|
|
|
761
|
-
|
|
762
|
-
|
|
837
|
+
score = session.get('lead_quality_score', 5)
|
|
838
|
+
if score:
|
|
839
|
+
quality_scores.append(score)
|
|
763
840
|
|
|
764
|
-
avg_quality = sum(quality_scores) / len(quality_scores) if quality_scores else
|
|
841
|
+
avg_quality = sum(quality_scores) / len(quality_scores) if quality_scores else 5
|
|
765
842
|
|
|
766
843
|
return {
|
|
767
844
|
'total_sessions': total_sessions,
|
|
845
|
+
'total_unique_users': total_unique_users, # ✅ ВСЕ уникальные пользователи бота
|
|
768
846
|
'stages': stages,
|
|
769
|
-
'avg_quality': round(avg_quality,
|
|
847
|
+
'avg_quality': round(avg_quality, 1),
|
|
770
848
|
'period_days': days
|
|
771
849
|
}
|
|
772
|
-
|
|
773
|
-
except
|
|
850
|
+
|
|
851
|
+
except APIError as e:
|
|
774
852
|
logger.error(f"Ошибка получения статистики воронки: {e}")
|
|
775
853
|
return {
|
|
776
854
|
'total_sessions': 0,
|
|
@@ -782,25 +860,50 @@ class SupabaseClient:
|
|
|
782
860
|
async def get_events_stats(self, days: int = 7) -> Dict[str, int]:
|
|
783
861
|
"""Получает статистику событий"""
|
|
784
862
|
try:
|
|
785
|
-
cutoff_date = datetime.now(
|
|
863
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
|
786
864
|
|
|
787
|
-
|
|
788
|
-
|
|
865
|
+
# Получаем события с учетом bot_id через сессии
|
|
866
|
+
query = self.client.table('session_events').select(
|
|
867
|
+
'event_type', 'session_id'
|
|
789
868
|
).gte('created_at', cutoff_date.isoformat())
|
|
790
869
|
|
|
791
|
-
|
|
870
|
+
events_response = query.execute()
|
|
871
|
+
events = events_response.data if events_response.data else []
|
|
792
872
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
873
|
+
# Фильтруем события по bot_id через сессии
|
|
874
|
+
if self.bot_id and events:
|
|
875
|
+
# Получаем ID сессий этого бота
|
|
876
|
+
sessions_query = self.client.table('sales_chat_sessions').select('id', 'user_id').eq('bot_id', self.bot_id)
|
|
877
|
+
sessions_response = sessions_query.execute()
|
|
797
878
|
|
|
798
|
-
|
|
799
|
-
|
|
879
|
+
# Исключаем сессии тестовых пользователей
|
|
880
|
+
if sessions_response.data:
|
|
881
|
+
# Получаем telegram_id тестовых пользователей
|
|
882
|
+
test_users_query = self.client.table('sales_users').select('telegram_id').eq('username', 'test_user')
|
|
883
|
+
if self.bot_id:
|
|
884
|
+
test_users_query = test_users_query.eq('bot_id', self.bot_id)
|
|
885
|
+
|
|
886
|
+
test_users_response = test_users_query.execute()
|
|
887
|
+
test_user_ids = {user['telegram_id'] for user in test_users_response.data} if test_users_response.data else set()
|
|
888
|
+
|
|
889
|
+
# Фильтруем сессии: только не тестовые
|
|
890
|
+
bot_sessions = [s for s in sessions_response.data if s['user_id'] not in test_user_ids]
|
|
891
|
+
bot_session_ids = {session['id'] for session in bot_sessions}
|
|
892
|
+
else:
|
|
893
|
+
bot_session_ids = set()
|
|
894
|
+
|
|
895
|
+
# Фильтруем события
|
|
896
|
+
events = [event for event in events if event['session_id'] in bot_session_ids]
|
|
897
|
+
|
|
898
|
+
# Группируем по типам событий
|
|
899
|
+
event_counts = {}
|
|
900
|
+
for event in events:
|
|
901
|
+
event_type = event.get('event_type', 'unknown')
|
|
902
|
+
event_counts[event_type] = event_counts.get(event_type, 0) + 1
|
|
800
903
|
|
|
801
|
-
return
|
|
904
|
+
return event_counts
|
|
802
905
|
|
|
803
|
-
except
|
|
906
|
+
except APIError as e:
|
|
804
907
|
logger.error(f"Ошибка получения статистики событий: {e}")
|
|
805
908
|
return {}
|
|
806
909
|
|
|
@@ -919,4 +1022,286 @@ class SupabaseClient:
|
|
|
919
1022
|
|
|
920
1023
|
except Exception as e:
|
|
921
1024
|
logger.error(f"Ошибка получения последнего события для пользователя {user_id}, тип '{event_type}': {e}")
|
|
922
|
-
return None
|
|
1025
|
+
return None
|
|
1026
|
+
|
|
1027
|
+
async def get_all_segments(self) -> List[str]:
|
|
1028
|
+
"""
|
|
1029
|
+
Получает все уникальные сегменты из таблицы sales_users
|
|
1030
|
+
|
|
1031
|
+
Returns:
|
|
1032
|
+
List[str]: Список уникальных сегментов
|
|
1033
|
+
"""
|
|
1034
|
+
try:
|
|
1035
|
+
# Запрос всех непустых сегментов
|
|
1036
|
+
query = self.client.table('sales_users').select('segments').neq('segments', '')
|
|
1037
|
+
|
|
1038
|
+
if self.bot_id:
|
|
1039
|
+
query = query.eq('bot_id', self.bot_id)
|
|
1040
|
+
|
|
1041
|
+
response = query.execute()
|
|
1042
|
+
|
|
1043
|
+
# Собираем все уникальные сегменты
|
|
1044
|
+
all_segments = set()
|
|
1045
|
+
for row in response.data:
|
|
1046
|
+
segments_str = row.get('segments', '')
|
|
1047
|
+
if segments_str:
|
|
1048
|
+
# Разбираем сегменты через запятую
|
|
1049
|
+
segments = [s.strip() for s in segments_str.split(',') if s.strip()]
|
|
1050
|
+
all_segments.update(segments)
|
|
1051
|
+
|
|
1052
|
+
segments_list = sorted(list(all_segments))
|
|
1053
|
+
logger.info(f"Найдено {len(segments_list)} уникальных сегментов")
|
|
1054
|
+
|
|
1055
|
+
return segments_list
|
|
1056
|
+
|
|
1057
|
+
except Exception as e:
|
|
1058
|
+
logger.error(f"Ошибка получения сегментов: {e}")
|
|
1059
|
+
return []
|
|
1060
|
+
|
|
1061
|
+
async def get_users_by_segment(self, segment: str = None) -> List[Dict[str, Any]]:
|
|
1062
|
+
"""
|
|
1063
|
+
Получает пользователей по сегменту или всех пользователей
|
|
1064
|
+
|
|
1065
|
+
Args:
|
|
1066
|
+
segment: Название сегмента (если None - возвращает всех)
|
|
1067
|
+
|
|
1068
|
+
Returns:
|
|
1069
|
+
List[Dict]: Список пользователей с telegram_id
|
|
1070
|
+
"""
|
|
1071
|
+
try:
|
|
1072
|
+
query = self.client.table('sales_users').select('telegram_id, segments')
|
|
1073
|
+
|
|
1074
|
+
if self.bot_id:
|
|
1075
|
+
query = query.eq('bot_id', self.bot_id)
|
|
1076
|
+
|
|
1077
|
+
response = query.execute()
|
|
1078
|
+
|
|
1079
|
+
if segment is None:
|
|
1080
|
+
# Все пользователи
|
|
1081
|
+
logger.info(f"Получено {len(response.data)} всех пользователей")
|
|
1082
|
+
return response.data
|
|
1083
|
+
|
|
1084
|
+
# Фильтруем по сегменту
|
|
1085
|
+
users = []
|
|
1086
|
+
for row in response.data:
|
|
1087
|
+
segments_str = row.get('segments', '')
|
|
1088
|
+
if segments_str:
|
|
1089
|
+
segments = [s.strip() for s in segments_str.split(',') if s.strip()]
|
|
1090
|
+
if segment in segments:
|
|
1091
|
+
users.append(row)
|
|
1092
|
+
|
|
1093
|
+
logger.info(f"Найдено {len(users)} пользователей с сегментом '{segment}'")
|
|
1094
|
+
return users
|
|
1095
|
+
|
|
1096
|
+
except Exception as e:
|
|
1097
|
+
logger.error(f"Ошибка получения пользователей по сегменту '{segment}': {e}")
|
|
1098
|
+
return []
|
|
1099
|
+
|
|
1100
|
+
# =============================================================================
|
|
1101
|
+
# МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ СОБЫТИЙ В SUPABASE STORAGE
|
|
1102
|
+
# =============================================================================
|
|
1103
|
+
|
|
1104
|
+
async def upload_event_file(
|
|
1105
|
+
self,
|
|
1106
|
+
event_id: str,
|
|
1107
|
+
file_data: bytes,
|
|
1108
|
+
original_name: str,
|
|
1109
|
+
file_id: str
|
|
1110
|
+
) -> Dict[str, str]:
|
|
1111
|
+
"""
|
|
1112
|
+
Загружает файл события в Supabase Storage
|
|
1113
|
+
|
|
1114
|
+
Args:
|
|
1115
|
+
event_id: ID события из БД (используется как папка)
|
|
1116
|
+
file_data: Байты файла
|
|
1117
|
+
original_name: Оригинальное имя файла (для метаданных)
|
|
1118
|
+
file_id: Уникальный ID файла для хранения
|
|
1119
|
+
|
|
1120
|
+
Returns:
|
|
1121
|
+
Dict с storage_path и original_name
|
|
1122
|
+
"""
|
|
1123
|
+
try:
|
|
1124
|
+
bucket_name = 'admin-events'
|
|
1125
|
+
|
|
1126
|
+
# Формируем путь: admin-events/event_id/file_id.ext
|
|
1127
|
+
extension = original_name.split('.')[-1] if '.' in original_name else ''
|
|
1128
|
+
storage_name = f"{file_id}.{extension}" if extension else file_id
|
|
1129
|
+
storage_path = f"events/{event_id}/files/{storage_name}"
|
|
1130
|
+
|
|
1131
|
+
# Определяем MIME-type по оригинальному имени файла
|
|
1132
|
+
import mimetypes
|
|
1133
|
+
content_type, _ = mimetypes.guess_type(original_name)
|
|
1134
|
+
if not content_type:
|
|
1135
|
+
content_type = 'application/octet-stream'
|
|
1136
|
+
|
|
1137
|
+
# Загружаем в Storage
|
|
1138
|
+
self.client.storage.from_(bucket_name).upload(
|
|
1139
|
+
storage_path,
|
|
1140
|
+
file_data,
|
|
1141
|
+
file_options={"content-type": content_type}
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
logger.info(f"✅ Файл загружен в Storage: {storage_path}")
|
|
1145
|
+
|
|
1146
|
+
return {
|
|
1147
|
+
'storage_path': storage_path
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
except Exception as e:
|
|
1151
|
+
logger.error(f"❌ Ошибка загрузки файла в Storage: {e}")
|
|
1152
|
+
raise
|
|
1153
|
+
|
|
1154
|
+
async def download_event_file(self, event_id: str, storage_path: str) -> bytes:
|
|
1155
|
+
"""
|
|
1156
|
+
Скачивает файл события из Supabase Storage
|
|
1157
|
+
|
|
1158
|
+
Args:
|
|
1159
|
+
event_id: ID события
|
|
1160
|
+
storage_path: Полный путь к файлу в Storage
|
|
1161
|
+
|
|
1162
|
+
Returns:
|
|
1163
|
+
bytes: Содержимое файла
|
|
1164
|
+
"""
|
|
1165
|
+
try:
|
|
1166
|
+
bucket_name = 'admin-events'
|
|
1167
|
+
|
|
1168
|
+
# Скачиваем файл
|
|
1169
|
+
file_data = self.client.storage.from_(bucket_name).download(storage_path)
|
|
1170
|
+
|
|
1171
|
+
logger.info(f"✅ Файл скачан из Storage: {storage_path}")
|
|
1172
|
+
return file_data
|
|
1173
|
+
|
|
1174
|
+
except Exception as e:
|
|
1175
|
+
logger.error(f"❌ Ошибка скачивания файла из Storage: {e}")
|
|
1176
|
+
raise
|
|
1177
|
+
|
|
1178
|
+
async def delete_event_files(self, event_id: str):
|
|
1179
|
+
"""
|
|
1180
|
+
Удаляет ВСЕ файлы события из Supabase Storage
|
|
1181
|
+
|
|
1182
|
+
Args:
|
|
1183
|
+
event_id: ID события
|
|
1184
|
+
"""
|
|
1185
|
+
try:
|
|
1186
|
+
bucket_name = 'admin-events'
|
|
1187
|
+
event_path = f"events/{event_id}/files"
|
|
1188
|
+
|
|
1189
|
+
# Получаем список всех файлов в папке события
|
|
1190
|
+
files_list = self.client.storage.from_(bucket_name).list(event_path)
|
|
1191
|
+
|
|
1192
|
+
if not files_list:
|
|
1193
|
+
logger.info(f"ℹ️ Нет файлов для удаления в событии '{event_id}'")
|
|
1194
|
+
return
|
|
1195
|
+
|
|
1196
|
+
# Формируем пути для удаления
|
|
1197
|
+
file_paths = [f"{event_path}/{file['name']}" for file in files_list]
|
|
1198
|
+
|
|
1199
|
+
# Удаляем файлы
|
|
1200
|
+
self.client.storage.from_(bucket_name).remove(file_paths)
|
|
1201
|
+
|
|
1202
|
+
logger.info(f"✅ Удалено {len(file_paths)} файлов события '{event_id}' из Storage")
|
|
1203
|
+
|
|
1204
|
+
except Exception as e:
|
|
1205
|
+
logger.error(f"❌ Ошибка удаления файлов события из Storage: {e}")
|
|
1206
|
+
# Не прерываем выполнение, только логируем
|
|
1207
|
+
|
|
1208
|
+
async def save_admin_event(
|
|
1209
|
+
self,
|
|
1210
|
+
event_name: str,
|
|
1211
|
+
event_data: Dict[str, Any],
|
|
1212
|
+
scheduled_datetime: datetime
|
|
1213
|
+
) -> Dict[str, Any]:
|
|
1214
|
+
"""
|
|
1215
|
+
Сохраняет админское событие в таблицу scheduled_events
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
event_name: Название события
|
|
1219
|
+
event_data: Данные события (сегмент, сообщение, файлы)
|
|
1220
|
+
scheduled_datetime: Дата и время отправки (должно быть в UTC с timezone info)
|
|
1221
|
+
|
|
1222
|
+
Returns:
|
|
1223
|
+
Dict[str, Any]: {'id': str, 'event_type': str, ...} - все данные созданного события
|
|
1224
|
+
"""
|
|
1225
|
+
try:
|
|
1226
|
+
import json
|
|
1227
|
+
|
|
1228
|
+
# Убеждаемся что datetime в правильном формате для PostgreSQL
|
|
1229
|
+
# Если есть timezone info - используем, иначе предполагаем что это UTC
|
|
1230
|
+
if scheduled_datetime.tzinfo is None:
|
|
1231
|
+
logger.warning("⚠️ scheduled_datetime без timezone info, предполагаем UTC")
|
|
1232
|
+
from datetime import timezone
|
|
1233
|
+
scheduled_datetime = scheduled_datetime.replace(tzinfo=timezone.utc)
|
|
1234
|
+
|
|
1235
|
+
event_record = {
|
|
1236
|
+
'event_type': event_name,
|
|
1237
|
+
'event_category': 'admin_event',
|
|
1238
|
+
'user_id': None, # Для всех пользователей
|
|
1239
|
+
'event_data': json.dumps(event_data, ensure_ascii=False),
|
|
1240
|
+
'scheduled_at': scheduled_datetime.isoformat(),
|
|
1241
|
+
'status': 'pending'
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
response = self.client.table('scheduled_events').insert(event_record).execute()
|
|
1245
|
+
event = response.data[0]
|
|
1246
|
+
|
|
1247
|
+
logger.info(f"💾 Админское событие '{event_name}' сохранено в БД: {event['id']} на {scheduled_datetime.isoformat()}")
|
|
1248
|
+
return event
|
|
1249
|
+
|
|
1250
|
+
except Exception as e:
|
|
1251
|
+
logger.error(f"❌ Ошибка сохранения админского события: {e}")
|
|
1252
|
+
raise
|
|
1253
|
+
|
|
1254
|
+
async def get_admin_events(self, status: str = None) -> List[Dict[str, Any]]:
|
|
1255
|
+
"""
|
|
1256
|
+
Получает админские события
|
|
1257
|
+
|
|
1258
|
+
Args:
|
|
1259
|
+
status: Фильтр по статусу (pending, completed, cancelled)
|
|
1260
|
+
|
|
1261
|
+
Returns:
|
|
1262
|
+
List[Dict]: Список админских событий
|
|
1263
|
+
"""
|
|
1264
|
+
try:
|
|
1265
|
+
query = self.client.table('scheduled_events').select(
|
|
1266
|
+
'*'
|
|
1267
|
+
).eq('event_category', 'admin_event')
|
|
1268
|
+
|
|
1269
|
+
if status:
|
|
1270
|
+
query = query.eq('status', status)
|
|
1271
|
+
|
|
1272
|
+
response = query.order('scheduled_at', desc=False).execute()
|
|
1273
|
+
|
|
1274
|
+
logger.info(f"Найдено {len(response.data)} админских событий")
|
|
1275
|
+
return response.data
|
|
1276
|
+
|
|
1277
|
+
except Exception as e:
|
|
1278
|
+
logger.error(f"Ошибка получения админских событий: {e}")
|
|
1279
|
+
return []
|
|
1280
|
+
|
|
1281
|
+
async def check_event_name_exists(self, event_name: str) -> bool:
|
|
1282
|
+
"""
|
|
1283
|
+
Проверяет существует ли активное событие с таким названием
|
|
1284
|
+
|
|
1285
|
+
Args:
|
|
1286
|
+
event_name: Название события для проверки
|
|
1287
|
+
|
|
1288
|
+
Returns:
|
|
1289
|
+
bool: True если активное событие с таким именем существует
|
|
1290
|
+
"""
|
|
1291
|
+
try:
|
|
1292
|
+
response = self.client.table('scheduled_events').select(
|
|
1293
|
+
'id', 'event_type', 'status'
|
|
1294
|
+
).eq('event_category', 'admin_event').eq(
|
|
1295
|
+
'event_type', event_name
|
|
1296
|
+
).eq('status', 'pending').execute()
|
|
1297
|
+
|
|
1298
|
+
exists = len(response.data) > 0
|
|
1299
|
+
|
|
1300
|
+
if exists:
|
|
1301
|
+
logger.info(f"⚠️ Найдено активное событие с названием '{event_name}'")
|
|
1302
|
+
|
|
1303
|
+
return exists
|
|
1304
|
+
|
|
1305
|
+
except Exception as e:
|
|
1306
|
+
logger.error(f"Ошибка проверки названия события: {e}")
|
|
1307
|
+
return False
|
|
@@ -24,20 +24,24 @@ def get_user_input():
|
|
|
24
24
|
utm_content = input("utm_content (контент): ").strip()
|
|
25
25
|
utm_term = input("utm_term (ключевое слово): ").strip()
|
|
26
26
|
|
|
27
|
+
print("\n🎯 Сегментация (нажмите Enter для пропуска):")
|
|
28
|
+
segment = input("seg (сегмент): ").strip()
|
|
29
|
+
|
|
27
30
|
return {
|
|
28
31
|
'bot_username': bot_username,
|
|
29
32
|
'utm_source': utm_source,
|
|
30
33
|
'utm_medium': utm_medium,
|
|
31
34
|
'utm_campaign': utm_campaign,
|
|
32
35
|
'utm_content': utm_content,
|
|
33
|
-
'utm_term': utm_term
|
|
36
|
+
'utm_term': utm_term,
|
|
37
|
+
'segment': segment
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
def create_utm_string(utm_data):
|
|
37
|
-
"""Создает строку UTM параметров в формате source-vk_campaign-
|
|
41
|
+
"""Создает строку UTM параметров в формате source-vk_campaign-summer2025_seg-premium"""
|
|
38
42
|
utm_parts = []
|
|
39
43
|
|
|
40
|
-
# Маппинг полей базы данных на
|
|
44
|
+
# Маппинг полей базы данных на формат без префикса utm
|
|
41
45
|
field_mapping = {
|
|
42
46
|
'utm_source': 'source',
|
|
43
47
|
'utm_medium': 'medium',
|
|
@@ -51,6 +55,11 @@ def create_utm_string(utm_data):
|
|
|
51
55
|
if value:
|
|
52
56
|
utm_parts.append(f"{utm_field}-{value}")
|
|
53
57
|
|
|
58
|
+
# Добавляем сегмент, если указан
|
|
59
|
+
segment = utm_data.get('segment')
|
|
60
|
+
if segment:
|
|
61
|
+
utm_parts.append(f"seg-{segment}")
|
|
62
|
+
|
|
54
63
|
return "_".join(utm_parts)
|
|
55
64
|
|
|
56
65
|
def generate_telegram_link(bot_username, utm_string):
|
|
@@ -104,3 +113,4 @@ def main():
|
|
|
104
113
|
|
|
105
114
|
if __name__ == "__main__":
|
|
106
115
|
main()
|
|
116
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smart-bot-factory
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Библиотека для создания умных чат-ботов
|
|
5
5
|
Author-email: Kopatych <eserov73@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -21,10 +21,12 @@ Classifier: Topic :: Communications :: Chat
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Python: >=3.9
|
|
23
23
|
Requires-Dist: aiofiles>=23.0.0
|
|
24
|
+
Requires-Dist: aiogram-media-group>=0.5.1
|
|
24
25
|
Requires-Dist: aiogram>=3.4.1
|
|
25
26
|
Requires-Dist: click>=8.0.0
|
|
26
27
|
Requires-Dist: openai>=1.12.0
|
|
27
28
|
Requires-Dist: project-root-finder>=1.9
|
|
29
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
28
30
|
Requires-Dist: python-dotenv>=1.0.1
|
|
29
31
|
Requires-Dist: pytz>=2023.3
|
|
30
32
|
Requires-Dist: pyyaml>=6.0.2
|