smart-bot-factory 0.1.3__py3-none-any.whl → 0.1.5__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 (37) hide show
  1. smart_bot_factory/__init__.py +0 -48
  2. smart_bot_factory/admin/admin_logic.py +11 -11
  3. smart_bot_factory/cli.py +299 -106
  4. smart_bot_factory/clients/__init__.py +33 -0
  5. smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +2 -0
  6. smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +95 -28
  7. smart_bot_factory/core/__init__.py +43 -22
  8. smart_bot_factory/core/bot_utils.py +268 -95
  9. smart_bot_factory/core/conversation_manager.py +542 -535
  10. smart_bot_factory/core/decorators.py +943 -229
  11. smart_bot_factory/core/globals.py +68 -0
  12. smart_bot_factory/core/message_sender.py +6 -6
  13. smart_bot_factory/core/router.py +172 -0
  14. smart_bot_factory/core/router_manager.py +165 -0
  15. smart_bot_factory/creation/__init__.py +1 -2
  16. smart_bot_factory/creation/bot_builder.py +116 -8
  17. smart_bot_factory/creation/bot_testing.py +74 -13
  18. smart_bot_factory/handlers/handlers.py +10 -2
  19. smart_bot_factory/integrations/__init__.py +1 -0
  20. smart_bot_factory/integrations/supabase_client.py +272 -2
  21. smart_bot_factory/utm_link_generator.py +106 -0
  22. smart_bot_factory-0.1.5.dist-info/METADATA +466 -0
  23. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/RECORD +26 -31
  24. smart_bot_factory/configs/growthmed-helper/env_example.txt +0 -1
  25. smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +0 -9
  26. smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +0 -582
  27. smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +0 -66
  28. smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +0 -232
  29. smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +0 -28
  30. smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +0 -7
  31. smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +0 -16
  32. smart_bot_factory/configs/growthmed-helper/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
  33. smart_bot_factory/uv.lock +0 -2004
  34. smart_bot_factory-0.1.3.dist-info/METADATA +0 -126
  35. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/WHEEL +0 -0
  36. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/entry_points.txt +0 -0
  37. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/licenses/LICENSE +0 -0
@@ -13,6 +13,12 @@
13
13
 
14
14
  ОТЧЕТЫ:
15
15
  bots/BOT_ID/reports/test_YYYYMMDD_HHMMSS.txt - подробные отчеты
16
+
17
+ ФОРМАТ СЦЕНАРИЕВ:
18
+ expected_keywords поддерживает синонимы:
19
+ - Одно слово: ["привет"] - проверяется только это слово
20
+ - Синонимы: [["привет", "здравствуйте", "добро пожаловать"]] - достаточно найти любое из слов
21
+ - Смешанный формат: ["привет", ["здравствуйте", "добро пожаловать"]] - комбинация одиночных слов и синонимов
16
22
  """
17
23
 
18
24
  import asyncio
@@ -73,8 +79,25 @@ class TestStep:
73
79
  def __init__(self, user_input: str, expected_keywords: List[str],
74
80
  forbidden_keywords: List[str] = None):
75
81
  self.user_input = user_input
76
- self.expected_keywords = [kw.lower() for kw in expected_keywords]
82
+ # Поддержка синонимов: если элемент списка - список, то это синонимы
83
+ self.expected_keywords = self._process_keywords(expected_keywords)
77
84
  self.forbidden_keywords = [kw.lower() for kw in (forbidden_keywords or [])]
85
+
86
+ def _process_keywords(self, keywords: List) -> List:
87
+ """Обрабатывает ключевые слова, поддерживая синонимы"""
88
+ processed = []
89
+ for kw in keywords:
90
+ if isinstance(kw, list):
91
+ # Это группа синонимов
92
+ synonyms = [s.lower() for s in kw if isinstance(s, str)]
93
+ processed.append(synonyms)
94
+ elif isinstance(kw, str):
95
+ # Одно слово
96
+ processed.append([kw.lower()])
97
+ else:
98
+ # Игнорируем неверные типы
99
+ continue
100
+ return processed
78
101
 
79
102
 
80
103
  class TestScenario:
@@ -343,7 +366,14 @@ class BotTester:
343
366
  logging.info(f"💬 Ввод пользователя: '{step.user_input}'")
344
367
 
345
368
  if step.expected_keywords:
346
- logging.info(f"🎯 Ожидаемые слова: {step.expected_keywords}")
369
+ # Форматируем ожидаемые ключевые слова для логов
370
+ expected_display = []
371
+ for group in step.expected_keywords:
372
+ if len(group) == 1:
373
+ expected_display.append(group[0])
374
+ else:
375
+ expected_display.append(f"[{'/'.join(group)}]")
376
+ logging.info(f"🎯 Ожидаемые слова: {expected_display}")
347
377
  if step.forbidden_keywords:
348
378
  logging.info(f"🚫 Запрещенные слова: {step.forbidden_keywords}")
349
379
 
@@ -358,14 +388,25 @@ class BotTester:
358
388
  logging.info(f"🤖 Ответ бота: '{response_preview}'")
359
389
  logging.info(f"⏱️ Время обработки: {step_duration}мс")
360
390
 
361
- # Проверяем ожидаемые ключевые слова
362
- missing_keywords = []
391
+ # Проверяем ожидаемые ключевые слова (с поддержкой синонимов)
392
+ missing_keyword_groups = []
363
393
  found_expected = []
364
- for keyword in step.expected_keywords:
365
- if keyword.lower() in clean_response.lower():
366
- found_expected.append(keyword)
394
+ for keyword_group in step.expected_keywords:
395
+ # keyword_group - это либо список синонимов, либо список с одним словом
396
+ found_in_group = False
397
+ found_synonym = None
398
+
399
+ for synonym in keyword_group:
400
+ if synonym in clean_response.lower():
401
+ found_in_group = True
402
+ found_synonym = synonym
403
+ break
404
+
405
+ if found_in_group:
406
+ found_expected.append(found_synonym)
367
407
  else:
368
- missing_keywords.append(keyword)
408
+ # Если группа синонимов не найдена, добавляем всю группу в missing
409
+ missing_keyword_groups.append(keyword_group)
369
410
 
370
411
  # Проверяем запрещенные ключевые слова
371
412
  found_forbidden = []
@@ -376,22 +417,34 @@ class BotTester:
376
417
  # Выводим результаты проверки
377
418
  if found_expected:
378
419
  logging.info(f"✅ Найденные ожидаемые: {found_expected}")
379
- if missing_keywords:
380
- logging.info(f"❌ НЕ найденные ожидаемые: {missing_keywords}")
420
+ if missing_keyword_groups:
421
+ # Показываем пропущенные группы синонимов в удобном формате
422
+ missing_display = []
423
+ for group in missing_keyword_groups:
424
+ if len(group) == 1:
425
+ missing_display.append(group[0])
426
+ else:
427
+ missing_display.append(f"[{'/'.join(group)}]")
428
+ logging.info(f"❌ НЕ найденные ожидаемые: {missing_display}")
381
429
  if found_forbidden:
382
430
  logging.info(f"🚫 Найденные запрещенные: {found_forbidden}")
383
431
 
384
432
  # Определяем результат шага
385
- passed = len(missing_keywords) == 0 and len(found_forbidden) == 0
433
+ passed = len(missing_keyword_groups) == 0 and len(found_forbidden) == 0
386
434
  status_icon = "✅" if passed else "❌"
387
435
  status_text = "ПРОЙДЕН" if passed else "ПРОВАЛЕН"
388
436
  logging.info(f"🎯 Результат шага {step_num}: {status_icon} {status_text}")
389
437
 
438
+ # Преобразуем missing_keyword_groups в плоский список для обратной совместимости
439
+ missing_keywords_flat = []
440
+ for group in missing_keyword_groups:
441
+ missing_keywords_flat.extend(group)
442
+
390
443
  step_result = StepResult(
391
444
  step=step,
392
445
  bot_response=clean_response,
393
446
  passed=passed,
394
- missing_keywords=missing_keywords,
447
+ missing_keywords=missing_keywords_flat,
395
448
  found_forbidden=found_forbidden
396
449
  )
397
450
 
@@ -813,10 +866,18 @@ class ReportGenerator:
813
866
  step_num = i + 1
814
867
  status = "✅" if step_result.passed else "❌"
815
868
 
869
+ # Форматируем ожидаемые ключевые слова с учетом синонимов
870
+ expected_display = []
871
+ for group in step_result.step.expected_keywords:
872
+ if len(group) == 1:
873
+ expected_display.append(group[0])
874
+ else:
875
+ expected_display.append(f"[{'/'.join(group)}]")
876
+
816
877
  report_lines.extend([
817
878
  f"ШАГ {step_num} {status}:",
818
879
  f" Ввод: \"{step_result.step.user_input}\"",
819
- f" Ожидаемые: {step_result.step.expected_keywords}",
880
+ f" Ожидаемые: {expected_display}",
820
881
  f" Запрещенные: {step_result.step.forbidden_keywords}",
821
882
  ""
822
883
  ])
@@ -134,8 +134,16 @@ async def user_start_handler(message: Message, state: FSMContext):
134
134
  await state.set_state(UserStates.waiting_for_message)
135
135
 
136
136
  # 6. ОТПРАВЛЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ
137
- await send_message(message, welcome_message)
138
- logger.info(f"Приветственное сообщение отправлено пользователю {message.from_user.id}")
137
+ try:
138
+ await send_message(message, welcome_message)
139
+ logger.info(f"Приветственное сообщение отправлено пользователю {message.from_user.id}")
140
+ except Exception as e:
141
+ if "Forbidden: bot was blocked by the user" in str(e):
142
+ logger.warning(f"🚫 Бот заблокирован пользователем {message.from_user.id}")
143
+ return
144
+ else:
145
+ logger.error(f"❌ Ошибка отправки приветственного сообщения: {e}")
146
+ raise
139
147
 
140
148
  # 7. ЕСЛИ ЕСТЬ ФАЙЛ ОТПРАВЛЯЕМ ВМЕСТЕ С ПОДПИСЬЮ
141
149
  logging.info(f"📎 Попытка отправки приветственного файла для сессии {session_id}")
@@ -7,3 +7,4 @@ from .openai_client import OpenAIClient
7
7
  __all__ = ['OpenAIClient']
8
8
 
9
9
 
10
+
@@ -421,7 +421,7 @@ class SupabaseClient:
421
421
  logger.error(f"Ошибка при завершении диалогов: {e}")
422
422
  return 0
423
423
 
424
- async def get_admin_conversation(self, admin_id: int) -> Optional[Dict[str, Any]]:
424
+ async def get_admin_active_conversation(self, admin_id: int) -> Optional[Dict[str, Any]]:
425
425
  """Получает активный диалог админа"""
426
426
  try:
427
427
  response = self.client.table('admin_user_conversations').select(
@@ -589,4 +589,274 @@ class SupabaseClient:
589
589
 
590
590
  except APIError as e:
591
591
  logger.error(f"Ошибка при архивировании сессий: {e}")
592
- raise
592
+ raise
593
+
594
+ async def get_sent_files(self, user_id: int) -> List[str]:
595
+ """Получает список отправленных файлов для пользователя
596
+
597
+ Args:
598
+ user_id: Telegram ID пользователя
599
+
600
+ Returns:
601
+ List[str]: Список имен файлов, разделенных запятой
602
+ """
603
+ try:
604
+ query = self.client.table('sales_users').select('files').eq('telegram_id', user_id)
605
+
606
+ if self.bot_id:
607
+ query = query.eq('bot_id', self.bot_id)
608
+
609
+ response = query.execute()
610
+
611
+ if response.data and response.data[0].get('files'):
612
+ files_str = response.data[0]['files']
613
+ return [f.strip() for f in files_str.split(',') if f.strip()]
614
+
615
+ return []
616
+
617
+ except Exception as e:
618
+ logger.error(f"Ошибка получения отправленных файлов для пользователя {user_id}: {e}")
619
+ return []
620
+
621
+ async def get_sent_directories(self, user_id: int) -> List[str]:
622
+ """Получает список отправленных каталогов для пользователя
623
+
624
+ Args:
625
+ user_id: Telegram ID пользователя
626
+
627
+ Returns:
628
+ List[str]: Список путей каталогов, разделенных запятой
629
+ """
630
+ try:
631
+ query = self.client.table('sales_users').select('directories').eq('telegram_id', user_id)
632
+
633
+ if self.bot_id:
634
+ query = query.eq('bot_id', self.bot_id)
635
+
636
+ response = query.execute()
637
+
638
+ if response.data and response.data[0].get('directories'):
639
+ dirs_str = response.data[0]['directories']
640
+ return [d.strip() for d in dirs_str.split(',') if d.strip()]
641
+
642
+ return []
643
+
644
+ except Exception as e:
645
+ logger.error(f"Ошибка получения отправленных каталогов для пользователя {user_id}: {e}")
646
+ return []
647
+
648
+ async def add_sent_files(self, user_id: int, files_list: List[str]):
649
+ """Добавляет файлы в список отправленных для пользователя
650
+
651
+ Args:
652
+ user_id: Telegram ID пользователя
653
+ files_list: Список имен файлов для добавления
654
+ """
655
+ try:
656
+ logger.info(f"Добавление файлов для пользователя {user_id}: {files_list}")
657
+
658
+ # Получаем текущий список
659
+ current_files = await self.get_sent_files(user_id)
660
+ logger.info(f"Текущие файлы в БД: {current_files}")
661
+
662
+ # Объединяем с новыми файлами (без дубликатов)
663
+ all_files = list(set(current_files + files_list))
664
+ logger.info(f"Объединенный список файлов: {all_files}")
665
+
666
+ # Сохраняем обратно
667
+ files_str = ', '.join(all_files)
668
+ logger.info(f"Сохраняем строку: {files_str}")
669
+
670
+ query = self.client.table('sales_users').update({
671
+ 'files': files_str
672
+ }).eq('telegram_id', user_id)
673
+
674
+ if self.bot_id:
675
+ query = query.eq('bot_id', self.bot_id)
676
+ logger.info(f"Фильтр по bot_id: {self.bot_id}")
677
+
678
+ response = query.execute()
679
+ logger.info(f"Ответ от БД: {response.data}")
680
+
681
+ logger.info(f"✅ Добавлено {len(files_list)} файлов для пользователя {user_id}")
682
+
683
+ except Exception as e:
684
+ logger.error(f"❌ Ошибка добавления отправленных файлов для пользователя {user_id}: {e}")
685
+ logger.exception("Полный стек ошибки:")
686
+
687
+ async def add_sent_directories(self, user_id: int, dirs_list: List[str]):
688
+ """Добавляет каталоги в список отправленных для пользователя
689
+
690
+ Args:
691
+ user_id: Telegram ID пользователя
692
+ dirs_list: Список путей каталогов для добавления
693
+ """
694
+ try:
695
+ logger.info(f"Добавление каталогов для пользователя {user_id}: {dirs_list}")
696
+
697
+ # Получаем текущий список
698
+ current_dirs = await self.get_sent_directories(user_id)
699
+ logger.info(f"Текущие каталоги в БД: {current_dirs}")
700
+
701
+ # Объединяем с новыми каталогами (без дубликатов)
702
+ all_dirs = list(set(current_dirs + dirs_list))
703
+ logger.info(f"Объединенный список каталогов: {all_dirs}")
704
+
705
+ # Сохраняем обратно
706
+ dirs_str = ', '.join(all_dirs)
707
+ logger.info(f"Сохраняем строку: {dirs_str}")
708
+
709
+ query = self.client.table('sales_users').update({
710
+ 'directories': dirs_str
711
+ }).eq('telegram_id', user_id)
712
+
713
+ if self.bot_id:
714
+ query = query.eq('bot_id', self.bot_id)
715
+ logger.info(f"Фильтр по bot_id: {self.bot_id}")
716
+
717
+ response = query.execute()
718
+ logger.info(f"Ответ от БД: {response.data}")
719
+
720
+ logger.info(f"✅ Добавлено {len(dirs_list)} каталогов для пользователя {user_id}")
721
+
722
+ except Exception as e:
723
+ logger.error(f"❌ Ошибка добавления отправленных каталогов для пользователя {user_id}: {e}")
724
+ logger.exception("Полный стек ошибки:")
725
+
726
+ # =============================================================================
727
+ # МЕТОДЫ ДЛЯ АНАЛИТИКИ
728
+ # =============================================================================
729
+
730
+ async def get_funnel_stats(self, days: int = 7) -> Dict[str, Any]:
731
+ """Получает статистику воронки продаж"""
732
+ try:
733
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=days)
734
+
735
+ # Общее количество сессий
736
+ sessions_response = self.client.table('sales_chat_sessions').select('id').gte(
737
+ 'created_at', cutoff_date.isoformat()
738
+ )
739
+ if self.bot_id:
740
+ sessions_response = sessions_response.eq('bot_id', self.bot_id)
741
+
742
+ total_sessions = len(sessions_response.execute().data)
743
+
744
+ # Статистика по этапам
745
+ stages_response = self.client.table('sales_chat_sessions').select(
746
+ 'current_stage', 'quality_score'
747
+ ).gte('created_at', cutoff_date.isoformat())
748
+
749
+ if self.bot_id:
750
+ stages_response = stages_response.eq('bot_id', self.bot_id)
751
+
752
+ sessions_data = stages_response.execute().data
753
+
754
+ stages = {}
755
+ quality_scores = []
756
+
757
+ for session in sessions_data:
758
+ stage = session.get('current_stage', 'unknown')
759
+ stages[stage] = stages.get(stage, 0) + 1
760
+
761
+ if session.get('quality_score'):
762
+ quality_scores.append(session['quality_score'])
763
+
764
+ avg_quality = sum(quality_scores) / len(quality_scores) if quality_scores else 0
765
+
766
+ return {
767
+ 'total_sessions': total_sessions,
768
+ 'stages': stages,
769
+ 'avg_quality': round(avg_quality, 2),
770
+ 'period_days': days
771
+ }
772
+
773
+ except Exception as e:
774
+ logger.error(f"Ошибка получения статистики воронки: {e}")
775
+ return {
776
+ 'total_sessions': 0,
777
+ 'stages': {},
778
+ 'avg_quality': 0,
779
+ 'period_days': days
780
+ }
781
+
782
+ async def get_events_stats(self, days: int = 7) -> Dict[str, int]:
783
+ """Получает статистику событий"""
784
+ try:
785
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=days)
786
+
787
+ response = self.client.table('scheduled_events').select(
788
+ 'event_type', 'status'
789
+ ).gte('created_at', cutoff_date.isoformat())
790
+
791
+ events_data = response.execute().data
792
+
793
+ stats = {}
794
+ for event in events_data:
795
+ event_type = event.get('event_type', 'unknown')
796
+ status = event.get('status', 'unknown')
797
+
798
+ key = f"{event_type}_{status}"
799
+ stats[key] = stats.get(key, 0) + 1
800
+
801
+ return stats
802
+
803
+ except Exception as e:
804
+ logger.error(f"Ошибка получения статистики событий: {e}")
805
+ return {}
806
+
807
+ async def get_user_last_message_info(self, user_id: int) -> Optional[Dict[str, Any]]:
808
+ """Получает информацию о последнем сообщении пользователя"""
809
+ try:
810
+ # Получаем последнее сообщение пользователя
811
+ response = self.client.table('sales_messages').select(
812
+ 'id', 'created_at', 'session_id', 'sender_type'
813
+ ).eq('sender_telegram_id', user_id).order('created_at', desc=True).limit(1).execute()
814
+
815
+ if not response.data:
816
+ return None
817
+
818
+ last_message = response.data[0]
819
+
820
+ # Получаем информацию о сессии
821
+ session_response = self.client.table('sales_chat_sessions').select(
822
+ 'id', 'current_stage', 'updated_at'
823
+ ).eq('id', last_message['session_id']).execute()
824
+
825
+ if not session_response.data:
826
+ return None
827
+
828
+ session = session_response.data[0]
829
+
830
+ return {
831
+ 'last_message_at': last_message['created_at'],
832
+ 'session_id': last_message['session_id'],
833
+ 'current_stage': session['current_stage'],
834
+ 'session_updated_at': session['updated_at']
835
+ }
836
+
837
+ except Exception as e:
838
+ logger.error(f"Ошибка получения информации о последнем сообщении пользователя {user_id}: {e}")
839
+ return None
840
+
841
+ async def check_user_stage_changed(self, user_id: int, original_session_id: str) -> bool:
842
+ """Проверяет, изменился ли этап пользователя с момента планирования события"""
843
+ try:
844
+ # Получаем текущую информацию о сессии
845
+ response = self.client.table('sales_chat_sessions').select(
846
+ 'id', 'current_stage'
847
+ ).eq('user_telegram_id', user_id).order('created_at', desc=True).limit(1).execute()
848
+
849
+ if not response.data:
850
+ return False
851
+
852
+ current_session = response.data[0]
853
+
854
+ # Если сессия изменилась - этап точно изменился
855
+ if current_session['id'] != original_session_id:
856
+ return True
857
+
858
+ return False
859
+
860
+ except Exception as e:
861
+ logger.error(f"Ошибка проверки изменения этапа пользователя {user_id}: {e}")
862
+ return False
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Генератор UTM-ссылок для Telegram ботов
4
+ Создает ссылки в формате: @https://t.me/bot?start=source-vk_campaign-summer2025
5
+ """
6
+
7
+ def get_user_input():
8
+ """Получает данные от пользователя через консоль"""
9
+ print("🔗 Генератор UTM-ссылок для Telegram")
10
+ print("=" * 50)
11
+
12
+ # Основные параметры
13
+ bot_username = input("Введите username бота (без @): ").strip()
14
+ if not bot_username:
15
+ print("❌ Username бота обязателен!")
16
+ return None
17
+
18
+ print("\n📊 Введите UTM-метки (нажмите Enter для пропуска):")
19
+
20
+ # UTM параметры (соответствуют полям в базе данных)
21
+ utm_source = input("utm_source (источник): ").strip()
22
+ utm_medium = input("utm_medium (канал): ").strip()
23
+ utm_campaign = input("utm_campaign (кампания): ").strip()
24
+ utm_content = input("utm_content (контент): ").strip()
25
+ utm_term = input("utm_term (ключевое слово): ").strip()
26
+
27
+ return {
28
+ 'bot_username': bot_username,
29
+ 'utm_source': utm_source,
30
+ 'utm_medium': utm_medium,
31
+ 'utm_campaign': utm_campaign,
32
+ 'utm_content': utm_content,
33
+ 'utm_term': utm_term
34
+ }
35
+
36
+ def create_utm_string(utm_data):
37
+ """Создает строку UTM параметров в формате source-vk_campaign-summer2025"""
38
+ utm_parts = []
39
+
40
+ # Маппинг полей базы данных на новый формат (без utm, в нижнем регистре)
41
+ field_mapping = {
42
+ 'utm_source': 'source',
43
+ 'utm_medium': 'medium',
44
+ 'utm_campaign': 'campaign',
45
+ 'utm_content': 'content',
46
+ 'utm_term': 'term'
47
+ }
48
+
49
+ for db_field, utm_field in field_mapping.items():
50
+ value = utm_data.get(db_field)
51
+ if value:
52
+ utm_parts.append(f"{utm_field}-{value}")
53
+
54
+ return "_".join(utm_parts)
55
+
56
+ def generate_telegram_link(bot_username, utm_string):
57
+ """Генерирует полную ссылку на Telegram бота"""
58
+ return f"https://t.me/{bot_username}?start={utm_string}"
59
+
60
+ def check_size_and_validate(utm_string):
61
+ """Проверяет размер строки после start= и валидирует"""
62
+ MAX_SIZE = 64
63
+
64
+ if len(utm_string) > MAX_SIZE:
65
+ return False, f"Строка слишком большая: {len(utm_string)} символов (максимум {MAX_SIZE})"
66
+
67
+ return True, f"Размер OK: {len(utm_string)} символов"
68
+
69
+
70
+ def main():
71
+ """Основная функция"""
72
+ try:
73
+ # Получаем данные от пользователя
74
+ data = get_user_input()
75
+ if not data:
76
+ return
77
+
78
+ # Создаем UTM строку
79
+ utm_string = create_utm_string(data)
80
+
81
+ if not utm_string:
82
+ print("❌ Не указано ни одной UTM-метки!")
83
+ return
84
+
85
+ # Проверяем размер
86
+ is_valid, size_message = check_size_and_validate(utm_string)
87
+
88
+ print(f"\n📏 {size_message}")
89
+
90
+ if not is_valid:
91
+ print("❌ Ссылка превышает максимальный размер!")
92
+ print("💡 Сократите значения UTM-меток или уберите менее важные")
93
+ return
94
+
95
+ # Генерируем и выводим ссылку
96
+ telegram_link = generate_telegram_link(data['bot_username'], utm_string)
97
+
98
+ print(f"\n✅ Сгенерированная ссылка:")
99
+ print(f"🔗 {telegram_link}")
100
+ except KeyboardInterrupt:
101
+ print("\n\n👋 Отменено пользователем")
102
+ except Exception as e:
103
+ print(f"\n❌ Ошибка: {e}")
104
+
105
+ if __name__ == "__main__":
106
+ main()