tg-flowmap 0.1.0__tar.gz

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.
Files changed (60) hide show
  1. tg_flowmap-0.1.0/LICENSE +21 -0
  2. tg_flowmap-0.1.0/PKG-INFO +162 -0
  3. tg_flowmap-0.1.0/README.md +141 -0
  4. tg_flowmap-0.1.0/pyproject.toml +50 -0
  5. tg_flowmap-0.1.0/setup.cfg +4 -0
  6. tg_flowmap-0.1.0/tests/test_regressions.py +254 -0
  7. tg_flowmap-0.1.0/tg_flowmap/__init__.py +0 -0
  8. tg_flowmap-0.1.0/tg_flowmap/__main__.py +6 -0
  9. tg_flowmap-0.1.0/tg_flowmap/analyzer.py +5 -0
  10. tg_flowmap-0.1.0/tg_flowmap/ast_evaluator.py +13 -0
  11. tg_flowmap-0.1.0/tg_flowmap/cli.py +470 -0
  12. tg_flowmap-0.1.0/tg_flowmap/core/__init__.py +16 -0
  13. tg_flowmap-0.1.0/tg_flowmap/core/comparator.py +168 -0
  14. tg_flowmap-0.1.0/tg_flowmap/core/llm_exporter.py +106 -0
  15. tg_flowmap-0.1.0/tg_flowmap/core/models.py +142 -0
  16. tg_flowmap-0.1.0/tg_flowmap/dast/__init__.py +11 -0
  17. tg_flowmap-0.1.0/tg_flowmap/dast/auth.py +33 -0
  18. tg_flowmap-0.1.0/tg_flowmap/dast/capture.py +235 -0
  19. tg_flowmap-0.1.0/tg_flowmap/dast/comparison.py +289 -0
  20. tg_flowmap-0.1.0/tg_flowmap/dast/config.py +252 -0
  21. tg_flowmap-0.1.0/tg_flowmap/dast/crawler.py +1044 -0
  22. tg_flowmap-0.1.0/tg_flowmap/dast/fingerprint.py +54 -0
  23. tg_flowmap-0.1.0/tg_flowmap/dast/html_render.py +140 -0
  24. tg_flowmap-0.1.0/tg_flowmap/dast/logging_setup.py +47 -0
  25. tg_flowmap-0.1.0/tg_flowmap/dast/map_render.py +506 -0
  26. tg_flowmap-0.1.0/tg_flowmap/dast/media.py +93 -0
  27. tg_flowmap-0.1.0/tg_flowmap/dast/models.py +303 -0
  28. tg_flowmap-0.1.0/tg_flowmap/dast/navigation_executor.py +135 -0
  29. tg_flowmap-0.1.0/tg_flowmap/dast/navigation_graph.py +265 -0
  30. tg_flowmap-0.1.0/tg_flowmap/dast/navigator.py +287 -0
  31. tg_flowmap-0.1.0/tg_flowmap/dast/ratelimit.py +76 -0
  32. tg_flowmap-0.1.0/tg_flowmap/dast/reports.py +475 -0
  33. tg_flowmap-0.1.0/tg_flowmap/dast/route_planner.py +141 -0
  34. tg_flowmap-0.1.0/tg_flowmap/dast/safety.py +267 -0
  35. tg_flowmap-0.1.0/tg_flowmap/dast/scheduler.py +220 -0
  36. tg_flowmap-0.1.0/tg_flowmap/dast/screen_identity.py +259 -0
  37. tg_flowmap-0.1.0/tg_flowmap/dast/state.py +1015 -0
  38. tg_flowmap-0.1.0/tg_flowmap/dast/timeline_log.py +102 -0
  39. tg_flowmap-0.1.0/tg_flowmap/dast/tl_serial.py +66 -0
  40. tg_flowmap-0.1.0/tg_flowmap/dast/utf16.py +36 -0
  41. tg_flowmap-0.1.0/tg_flowmap/emoji_fetcher.py +13 -0
  42. tg_flowmap-0.1.0/tg_flowmap/models.py +19 -0
  43. tg_flowmap-0.1.0/tg_flowmap/parser.py +9 -0
  44. tg_flowmap-0.1.0/tg_flowmap/sast/__init__.py +17 -0
  45. tg_flowmap-0.1.0/tg_flowmap/sast/analyzer.py +472 -0
  46. tg_flowmap-0.1.0/tg_flowmap/sast/emoji.py +168 -0
  47. tg_flowmap-0.1.0/tg_flowmap/sast/evaluator.py +241 -0
  48. tg_flowmap-0.1.0/tg_flowmap/sast/parser.py +1302 -0
  49. tg_flowmap-0.1.0/tg_flowmap/visualizer/__init__.py +5 -0
  50. tg_flowmap-0.1.0/tg_flowmap/visualizer/html_builder.py +33 -0
  51. tg_flowmap-0.1.0/tg_flowmap/visualizer/templates/viewer.css +750 -0
  52. tg_flowmap-0.1.0/tg_flowmap/visualizer/templates/viewer.html +82 -0
  53. tg_flowmap-0.1.0/tg_flowmap/visualizer/templates/viewer.js +1659 -0
  54. tg_flowmap-0.1.0/tg_flowmap/visualizer.py +5 -0
  55. tg_flowmap-0.1.0/tg_flowmap.egg-info/PKG-INFO +162 -0
  56. tg_flowmap-0.1.0/tg_flowmap.egg-info/SOURCES.txt +58 -0
  57. tg_flowmap-0.1.0/tg_flowmap.egg-info/dependency_links.txt +1 -0
  58. tg_flowmap-0.1.0/tg_flowmap.egg-info/entry_points.txt +2 -0
  59. tg_flowmap-0.1.0/tg_flowmap.egg-info/requires.txt +13 -0
  60. tg_flowmap-0.1.0/tg_flowmap.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tg-flowmap contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: tg-flowmap
3
+ Version: 0.1.0
4
+ Summary: Static AST visualizer, flow mapper, and dead-end linter for Telegram bots
5
+ Author: w1bbex
6
+ License: MIT
7
+ Keywords: telegram,telegram-bot,ast,flowchart,visualizer,linter
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Provides-Extra: dast
12
+ Requires-Dist: telethon<1.45,>=1.36; extra == "dast"
13
+ Provides-Extra: cli
14
+ Requires-Dist: rich>=13.0; extra == "cli"
15
+ Requires-Dist: questionary>=2.0; extra == "cli"
16
+ Provides-Extra: full
17
+ Requires-Dist: tg-flowmap[cli,dast]; extra == "full"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # 🧭 tg-flowmap
23
+
24
+ > **Unified Telegram Bot Analysis & Benchmarking Suite: Static Code AST (SAST) + Dynamic Userbot Crawler (DAST) + AI UX Benchmarking.**
25
+
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
27
+ [![Python: 3.10+](https://img.shields.io/badge/Python-3.10+-brightgreen.svg)]()
28
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)]()
29
+ [![Tests: 259 passed](https://img.shields.io/badge/tests-258%20passed-brightgreen.svg)]()
30
+
31
+ `tg-flowmap` — комплексный инженерный инструмент для аудита, визуализации и сравнения Telegram-ботов:
32
+ 1. **SAST (Static AST)**: сканирует исходный код вашего бота **без запуска**, находит тупики, битые коллбеки, забытые команды меню и нарушения спецификации Telegram Bot API за 50 мс без единой зависимости.
33
+ 2. **DAST (Telethon Crawler)**: аккуратно обходит интерфейс **чужого бота-конкурента** юзерботом по графу Дейкстры (без лишних `/start`, со стилями кнопок Telethon 1.44 и защитой от бана).
34
+ 3. **AI UX Benchmarker**: сопоставляет вашего бота с ботом оппонента и формирует готовый промпт для ChatGPT / Claude (*«Сравни моего бота с конкурентом и дай 5 жестких UX-советов по воронке продаж»*).
35
+
36
+ ---
37
+
38
+ ## ⚡ Быстрый старт (Zero-Boilerplate)
39
+
40
+ Никаких сложных флагов. Все отчёты и данные автоматически сохраняются в `output/`.
41
+
42
+ ### 1. Установка
43
+
44
+ ```bash
45
+ # Базовый легковесный режим (SAST, 0 зависимостей):
46
+ pip install tg-flowmap
47
+
48
+ # Режим краулера (с Telethon для обхода внешних ботов):
49
+ pip install "tg-flowmap[dast]"
50
+
51
+ # Полный комплект (DAST + интерактивный терминальный интерфейс Rich/Questionary):
52
+ pip install "tg-flowmap[full]"
53
+ ```
54
+
55
+ ### 2. Анализ своего бота (SAST)
56
+
57
+ ```bash
58
+ tg-flowmap sast path/to/my_bot
59
+ ```
60
+ > Выводит аудит в консоль и автоматически сохраняет Markdown-отчёт: `output/my_bot_report.md`.
61
+ > *Хотите интерактивный холст? Просто добавьте флаг `--html`.*
62
+
63
+ ### 3. Исследование чужого бота (DAST)
64
+
65
+ ```bash
66
+ tg-flowmap dast @competitor_bot
67
+ ```
68
+ > Обходит интерфейс конкурента, сохраняет структуру меню, карточки и отчёт в `output/@competitor_bot/`.
69
+
70
+ ### 4. Ультимативное сравнение с конкурентом (AI Benchmark)
71
+
72
+ ```bash
73
+ tg-flowmap compare path/to/my_bot @competitor_bot
74
+ ```
75
+ > Генерирует сравнительную матрицу `output/comparison.md` и готовый к отправке в ИИ файл **`output/llm_prompt.md`**!
76
+
77
+ ### 💡 Быстрый интерактивный запуск на Windows (1-Click)
78
+
79
+ Дважды кликните **`run.bat`** (или перетащите файл/папку бота прямо на иконку `run.bat`):
80
+ - Откроется удобное меню с выбором режима и встроенным мастером настройки Telegram API (`config.json`).
81
+ - Пункт `[4]` сразу же строит карту для встроенного демо-бота `examples/sample_bot.py` и открывает её в браузере.
82
+
83
+ ---
84
+
85
+ ## 🔍 Интеллектуальный линтинг SAST v2
86
+
87
+ Статический анализатор проверяет десятки потенциальных дефектов архитектуры Telegram-интерфейса:
88
+
89
+ - ⚠️ **`[ORPHAN_COMMAND]`**: команда есть в коде (`/help`, `/settings`), но не зарегистрирована в системном меню через `set_my_commands` (пользователи о ней не узнают из autocomplete-подсказки).
90
+ - ⚠️ **`[GHOST_COMMAND]`**: пункт зарегистрирован в `set_my_commands`, но в кодовой базе нет обработчика (нажатие приведёт к тишине).
91
+ - ⚠️ **`[MISSING_BACK]`**: на вложенном экране отсутствует кнопка возврата («Назад» / «В меню»).
92
+ - ⚠️ **`[DEAD_END]`**: кнопка или переход ведут в тупик без последующих действий.
93
+ - ⚠️ **`[DANGLING_HANDLER]`**: обработчик существует, но ни одна кнопка или команда на него не ссылается.
94
+ - 🎨 **Telegram Premium Emojis**: автоматическое извлечение и валидация `icon_custom_emoji_id` с локальным кэшированием.
95
+ - 🐍 **Современный синтаксис**: полная поддержка Python 3.10+ `match / case`, aiogram 3 magic-фильтров (`F.data == ...`), Telebot и процедурных билдеров.
96
+
97
+ ---
98
+
99
+ ## 📊 Пример терминального вывода (SAST)
100
+
101
+ ```text
102
+ ============================================================
103
+ 🧭 TG-FLOWMAP — Telegram Bot Analyzer (SAST)
104
+ ============================================================
105
+ Цель: C:\Projects\my_shop_bot
106
+ Всего экранов: 54
107
+ Всего связей: 35
108
+ Клавиатур найдено: 31
109
+ Тупиков (Dead-end):0
110
+ Ошибок эмодзи: 0
111
+ Без кнопки Назад: 2
112
+ ------------------------------------------------------------
113
+
114
+ 🔍 НАЙДЕННЫЕ ДЕФЕКТЫ И ПРЕДУПРЕЖДЕНИЯ:
115
+ 1. ⚠️ [ORPHAN_COMMAND] Команда '/promo' объявлена в хэндлере, но отсутствует в 'set_my_commands'.
116
+ 2. ⚠️ [MISSING_BACK] В экране 'catalog:filter' нет кнопки возврата (« Назад / В меню »).
117
+ 3. ℹ️ [DANGLING_HANDLER] Обработчик 'admin_dump' определён в коде, но ни одна кнопка на него не ссылается.
118
+
119
+ 📄 Отчёт сохранён: output/my_shop_bot_report.md
120
+ 📦 JSON-структура сохранена: output/my_shop_bot_data.json
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 🤖 Пример сгенерированного AI-промпта (`llm_prompt.md`)
126
+
127
+ ```markdown
128
+ # Telegram Bot UX Benchmark: my_bot vs @competitor_bot
129
+
130
+ | Метрика | my_bot (Мой бот) | @competitor_bot (Конкурент) |
131
+ |---|---|---|
132
+ | Всего экранов | 14 | 32 |
133
+ | Всего кнопок | 28 | 64 |
134
+ | Тупиковые кнопки | 0 | 0 |
135
+ | Экраны без кнопки «Назад» | 0 | 2 |
136
+
137
+ [Структура экранов и кнопок обоих ботов...]
138
+
139
+ ### Запрос для нейросети:
140
+ "Ты — ведущий UX/CPO архитектор Telegram-ботов. На основе структуры выше проведи
141
+ глубокий аудит: где мой бот проигрывает конкуренту по скорости оформления заказа
142
+ и какие 5 конкретных правок меню поднимут конверсию?"
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 🛠️ Архитектура модулей
148
+
149
+ - `tg_flowmap/core/`: единая модель графа (`BotMap`), движок сравнения (`comparator.py`) и генератор промптов (`llm_exporter.py`).
150
+ - `tg_flowmap/sast/`: статический двухпроходный AST-парсер, вычисление выражений без `eval()`, двусторонний линтер Bot API.
151
+ - `tg_flowmap/dast/`: Telethon-краулер, навигация по Дейкстре, контекстный фильтр безопасности `safety.py`, SQLite state.
152
+ - `tg_flowmap/visualizer/`: компилятор автономных HTML5/SVG интерактивных карт (ForceAtlas2, ClearType DirectWrite).
153
+ - `tg_flowmap/cli.py`: единый CLI-интерфейс с поддержкой команд `sast`, `dast`, `compare`.
154
+
155
+ ---
156
+
157
+ ## 🧪 Тестирование
158
+
159
+ ```bash
160
+ pytest tests -v
161
+ ```
162
+ **258 passing tests** (SAST, DAST, Safety, State, Regression) с полным контролем бюджета памяти и производительности.
@@ -0,0 +1,141 @@
1
+ # 🧭 tg-flowmap
2
+
3
+ > **Unified Telegram Bot Analysis & Benchmarking Suite: Static Code AST (SAST) + Dynamic Userbot Crawler (DAST) + AI UX Benchmarking.**
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![Python: 3.10+](https://img.shields.io/badge/Python-3.10+-brightgreen.svg)]()
7
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)]()
8
+ [![Tests: 259 passed](https://img.shields.io/badge/tests-258%20passed-brightgreen.svg)]()
9
+
10
+ `tg-flowmap` — комплексный инженерный инструмент для аудита, визуализации и сравнения Telegram-ботов:
11
+ 1. **SAST (Static AST)**: сканирует исходный код вашего бота **без запуска**, находит тупики, битые коллбеки, забытые команды меню и нарушения спецификации Telegram Bot API за 50 мс без единой зависимости.
12
+ 2. **DAST (Telethon Crawler)**: аккуратно обходит интерфейс **чужого бота-конкурента** юзерботом по графу Дейкстры (без лишних `/start`, со стилями кнопок Telethon 1.44 и защитой от бана).
13
+ 3. **AI UX Benchmarker**: сопоставляет вашего бота с ботом оппонента и формирует готовый промпт для ChatGPT / Claude (*«Сравни моего бота с конкурентом и дай 5 жестких UX-советов по воронке продаж»*).
14
+
15
+ ---
16
+
17
+ ## ⚡ Быстрый старт (Zero-Boilerplate)
18
+
19
+ Никаких сложных флагов. Все отчёты и данные автоматически сохраняются в `output/`.
20
+
21
+ ### 1. Установка
22
+
23
+ ```bash
24
+ # Базовый легковесный режим (SAST, 0 зависимостей):
25
+ pip install tg-flowmap
26
+
27
+ # Режим краулера (с Telethon для обхода внешних ботов):
28
+ pip install "tg-flowmap[dast]"
29
+
30
+ # Полный комплект (DAST + интерактивный терминальный интерфейс Rich/Questionary):
31
+ pip install "tg-flowmap[full]"
32
+ ```
33
+
34
+ ### 2. Анализ своего бота (SAST)
35
+
36
+ ```bash
37
+ tg-flowmap sast path/to/my_bot
38
+ ```
39
+ > Выводит аудит в консоль и автоматически сохраняет Markdown-отчёт: `output/my_bot_report.md`.
40
+ > *Хотите интерактивный холст? Просто добавьте флаг `--html`.*
41
+
42
+ ### 3. Исследование чужого бота (DAST)
43
+
44
+ ```bash
45
+ tg-flowmap dast @competitor_bot
46
+ ```
47
+ > Обходит интерфейс конкурента, сохраняет структуру меню, карточки и отчёт в `output/@competitor_bot/`.
48
+
49
+ ### 4. Ультимативное сравнение с конкурентом (AI Benchmark)
50
+
51
+ ```bash
52
+ tg-flowmap compare path/to/my_bot @competitor_bot
53
+ ```
54
+ > Генерирует сравнительную матрицу `output/comparison.md` и готовый к отправке в ИИ файл **`output/llm_prompt.md`**!
55
+
56
+ ### 💡 Быстрый интерактивный запуск на Windows (1-Click)
57
+
58
+ Дважды кликните **`run.bat`** (или перетащите файл/папку бота прямо на иконку `run.bat`):
59
+ - Откроется удобное меню с выбором режима и встроенным мастером настройки Telegram API (`config.json`).
60
+ - Пункт `[4]` сразу же строит карту для встроенного демо-бота `examples/sample_bot.py` и открывает её в браузере.
61
+
62
+ ---
63
+
64
+ ## 🔍 Интеллектуальный линтинг SAST v2
65
+
66
+ Статический анализатор проверяет десятки потенциальных дефектов архитектуры Telegram-интерфейса:
67
+
68
+ - ⚠️ **`[ORPHAN_COMMAND]`**: команда есть в коде (`/help`, `/settings`), но не зарегистрирована в системном меню через `set_my_commands` (пользователи о ней не узнают из autocomplete-подсказки).
69
+ - ⚠️ **`[GHOST_COMMAND]`**: пункт зарегистрирован в `set_my_commands`, но в кодовой базе нет обработчика (нажатие приведёт к тишине).
70
+ - ⚠️ **`[MISSING_BACK]`**: на вложенном экране отсутствует кнопка возврата («Назад» / «В меню»).
71
+ - ⚠️ **`[DEAD_END]`**: кнопка или переход ведут в тупик без последующих действий.
72
+ - ⚠️ **`[DANGLING_HANDLER]`**: обработчик существует, но ни одна кнопка или команда на него не ссылается.
73
+ - 🎨 **Telegram Premium Emojis**: автоматическое извлечение и валидация `icon_custom_emoji_id` с локальным кэшированием.
74
+ - 🐍 **Современный синтаксис**: полная поддержка Python 3.10+ `match / case`, aiogram 3 magic-фильтров (`F.data == ...`), Telebot и процедурных билдеров.
75
+
76
+ ---
77
+
78
+ ## 📊 Пример терминального вывода (SAST)
79
+
80
+ ```text
81
+ ============================================================
82
+ 🧭 TG-FLOWMAP — Telegram Bot Analyzer (SAST)
83
+ ============================================================
84
+ Цель: C:\Projects\my_shop_bot
85
+ Всего экранов: 54
86
+ Всего связей: 35
87
+ Клавиатур найдено: 31
88
+ Тупиков (Dead-end):0
89
+ Ошибок эмодзи: 0
90
+ Без кнопки Назад: 2
91
+ ------------------------------------------------------------
92
+
93
+ 🔍 НАЙДЕННЫЕ ДЕФЕКТЫ И ПРЕДУПРЕЖДЕНИЯ:
94
+ 1. ⚠️ [ORPHAN_COMMAND] Команда '/promo' объявлена в хэндлере, но отсутствует в 'set_my_commands'.
95
+ 2. ⚠️ [MISSING_BACK] В экране 'catalog:filter' нет кнопки возврата (« Назад / В меню »).
96
+ 3. ℹ️ [DANGLING_HANDLER] Обработчик 'admin_dump' определён в коде, но ни одна кнопка на него не ссылается.
97
+
98
+ 📄 Отчёт сохранён: output/my_shop_bot_report.md
99
+ 📦 JSON-структура сохранена: output/my_shop_bot_data.json
100
+ ```
101
+
102
+ ---
103
+
104
+ ## 🤖 Пример сгенерированного AI-промпта (`llm_prompt.md`)
105
+
106
+ ```markdown
107
+ # Telegram Bot UX Benchmark: my_bot vs @competitor_bot
108
+
109
+ | Метрика | my_bot (Мой бот) | @competitor_bot (Конкурент) |
110
+ |---|---|---|
111
+ | Всего экранов | 14 | 32 |
112
+ | Всего кнопок | 28 | 64 |
113
+ | Тупиковые кнопки | 0 | 0 |
114
+ | Экраны без кнопки «Назад» | 0 | 2 |
115
+
116
+ [Структура экранов и кнопок обоих ботов...]
117
+
118
+ ### Запрос для нейросети:
119
+ "Ты — ведущий UX/CPO архитектор Telegram-ботов. На основе структуры выше проведи
120
+ глубокий аудит: где мой бот проигрывает конкуренту по скорости оформления заказа
121
+ и какие 5 конкретных правок меню поднимут конверсию?"
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 🛠️ Архитектура модулей
127
+
128
+ - `tg_flowmap/core/`: единая модель графа (`BotMap`), движок сравнения (`comparator.py`) и генератор промптов (`llm_exporter.py`).
129
+ - `tg_flowmap/sast/`: статический двухпроходный AST-парсер, вычисление выражений без `eval()`, двусторонний линтер Bot API.
130
+ - `tg_flowmap/dast/`: Telethon-краулер, навигация по Дейкстре, контекстный фильтр безопасности `safety.py`, SQLite state.
131
+ - `tg_flowmap/visualizer/`: компилятор автономных HTML5/SVG интерактивных карт (ForceAtlas2, ClearType DirectWrite).
132
+ - `tg_flowmap/cli.py`: единый CLI-интерфейс с поддержкой команд `sast`, `dast`, `compare`.
133
+
134
+ ---
135
+
136
+ ## 🧪 Тестирование
137
+
138
+ ```bash
139
+ pytest tests -v
140
+ ```
141
+ **258 passing tests** (SAST, DAST, Safety, State, Regression) с полным контролем бюджета памяти и производительности.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tg-flowmap"
7
+ version = "0.1.0"
8
+ description = "Static AST visualizer, flow mapper, and dead-end linter for Telegram bots"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "w1bbex" }]
13
+ keywords = ["telegram", "telegram-bot", "ast", "flowchart", "visualizer", "linter"]
14
+ dependencies = []
15
+
16
+ [project.optional-dependencies]
17
+ dast = [
18
+ "telethon>=1.36,<1.45",
19
+ ]
20
+ cli = [
21
+ "rich>=13.0",
22
+ "questionary>=2.0",
23
+ ]
24
+ full = [
25
+ "tg-flowmap[dast,cli]",
26
+ ]
27
+ dev = [
28
+ "pytest>=8.0",
29
+ ]
30
+
31
+ [project.scripts]
32
+ tg-flowmap = "tg_flowmap.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["."]
36
+ include = ["tg_flowmap*"]
37
+
38
+ [tool.setuptools]
39
+ include-package-data = true
40
+
41
+ [tool.setuptools.package-data]
42
+ tg_flowmap = [
43
+ "visualizer/templates/*",
44
+ ]
45
+
46
+ [tool.pytest.ini_options]
47
+ pythonpath = ["."]
48
+ addopts = "--basetemp=.pytest_tmp -p no:cacheprovider"
49
+
50
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import json
6
+ from pathlib import Path
7
+ from unittest.mock import AsyncMock, MagicMock
8
+
9
+ import pytest
10
+
11
+ from tg_flowmap.core.comparator import BotComparator
12
+ from tg_flowmap.dast.auth import create_client
13
+ from tg_flowmap.dast.capture import extract_buttons
14
+ from tg_flowmap.dast.config import Config
15
+ from tg_flowmap.dast.crawler import BotCrawler
16
+ from tg_flowmap.dast.models import ButtonRef, CallbackResult, PressOutcome
17
+ from tg_flowmap.dast.navigation_graph import NavigationGraph
18
+ from tg_flowmap.dast.safety import SafetyFilter, WALLET_REGEXES
19
+ from tg_flowmap.dast.screen_identity import buttons_signature, match_button_instance
20
+ from tg_flowmap.dast.state import State
21
+ from tg_flowmap.dast.timeline_log import ActionLogger, TimelineWriter
22
+ from tg_flowmap.dast.utf16 import utf16_length
23
+ from tg_flowmap.sast.analyzer import GraphAnalyzer
24
+ from tg_flowmap.sast.emoji import fetch_custom_emojis, load_cached_emoji
25
+ import run
26
+
27
+
28
+ def test_run_wizard_no_name_error(tmp_path: Path) -> None:
29
+ """Verifies that run.py functions use cfg_path and do not contain undefined cfg_file references."""
30
+ source = inspect.getsource(run.run_telegram_config_wizard)
31
+ assert "os.startfile(cfg_file)" not in source
32
+ assert "os.startfile(cfg_path)" in source
33
+
34
+
35
+ def test_screen_identity_buttons_signature_none_url_comparison() -> None:
36
+ """Verifies that buttons with None URL do not crash sorted() when compared with string URLs."""
37
+ btn1 = {"row": 0, "col": 0, "label": "Btn", "button_type": "url", "url": None}
38
+ btn2 = {"row": 0, "col": 0, "label": "Btn", "button_type": "url", "url": "https://example.com"}
39
+ sig = buttons_signature([btn1, btn2])
40
+ assert len(sig) == 1
41
+ assert len(sig[0]) == 2
42
+
43
+
44
+ def test_screen_identity_row_col_none_fallback() -> None:
45
+ """Verifies that explicit None in row/col does not cause TypeError in int()."""
46
+ btn = {"row": None, "col": None, "label": "Test", "button_type": "callback"}
47
+ sig = buttons_signature([btn])
48
+ assert len(sig) == 1
49
+
50
+ matched, conf, _ = match_button_instance({"row": None, "col": None, "label": "Test", "button_type": "callback"}, [btn])
51
+ assert matched is not None
52
+ assert conf == 1.0
53
+
54
+
55
+ def test_dast_config_to_dict_json_serializable(tmp_path: Path) -> None:
56
+ """Verifies that custom_output_dir Path does not break json serialization in to_dict() and save()."""
57
+ cfg = Config(root=tmp_path, custom_output_dir=tmp_path / "custom")
58
+ d = cfg.to_dict()
59
+ assert isinstance(d["custom_output_dir"], str)
60
+ dumped = json.dumps(d)
61
+ assert "custom" in dumped
62
+
63
+ save_file = tmp_path / "cfg.json"
64
+ cfg.save(save_file)
65
+ assert save_file.exists()
66
+
67
+
68
+ def test_capture_empty_callback_data_preserved() -> None:
69
+ """Verifies that b'' callback_data is preserved and not replaced by None."""
70
+ class DummyButton:
71
+ text = "Separator"
72
+ data = b""
73
+
74
+ class DummyRow:
75
+ buttons = [DummyButton()]
76
+
77
+ class DummyReplyMarkup:
78
+ rows = [DummyRow()]
79
+
80
+ buttons = extract_buttons(DummyReplyMarkup(), message_id=1)
81
+ assert len(buttons) == 1
82
+ assert buttons[0].callback_data == b""
83
+
84
+
85
+ def test_emoji_load_cached_invalid_ntfs_chars(tmp_path: Path) -> None:
86
+ """Verifies that invalid filename characters in emoji_id do not throw WinError 123."""
87
+ res = load_cached_emoji("$ICONS[star]*:?", tmp_path)
88
+ assert res is None
89
+
90
+
91
+ def test_emoji_fetch_custom_emojis_filters_non_digits(tmp_path: Path) -> None:
92
+ """Verifies that non-digit emoji IDs are filtered out without making requests."""
93
+ res = fetch_custom_emojis("test_token", ["$VAR_ICON", "star*"], cache_dir=tmp_path)
94
+ assert res == {}
95
+
96
+
97
+ def test_comparator_extract_metrics_nodes_list() -> None:
98
+ """Verifies that extract_metrics_from_bot_map supports bot_map with nodes as a list (DAST format)."""
99
+ bot_map = {
100
+ "nodes": [
101
+ {
102
+ "id": "screen_1",
103
+ "keyboard": {
104
+ "rows": [[{"label": "Btn1"}], [{"label": "Btn2"}]]
105
+ }
106
+ }
107
+ ],
108
+ "issues": [{"issue_type": "dead_end"}],
109
+ }
110
+ metrics = BotComparator.extract_metrics_from_bot_map(bot_map)
111
+ assert metrics["screens_count"] == 1
112
+ assert metrics["buttons_count"] == 2
113
+ assert metrics["dead_ends"] == 1
114
+
115
+
116
+ def test_safety_manual_answers_without_at_prefix() -> None:
117
+ """Verifies manual_answer lookup works regardless of whether @ prefix is present."""
118
+ manual = {
119
+ "testbot": {
120
+ "screens": [{"match": "Enter promo", "answer": "12345"}]
121
+ }
122
+ }
123
+ ans = SafetyFilter.manual_answer(manual, "@testbot", "Enter promo")
124
+ assert ans == "12345"
125
+
126
+ ans2 = SafetyFilter.manual_answer(manual, "testbot", "Enter promo")
127
+ assert ans2 == "12345"
128
+
129
+
130
+ def test_safety_tron_regex_case_sensitivity() -> None:
131
+ """Verifies that TRON wallet regex requires capital T and is case-sensitive."""
132
+ tron_pat = WALLET_REGEXES[1]
133
+ valid_tron = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
134
+ lowercase_false_positive = "t1234567890abcdef1234567890abcdef1"
135
+
136
+ assert tron_pat.search(valid_tron) is not None
137
+ assert tron_pat.search(lowercase_false_positive) is None
138
+
139
+
140
+ def test_crawler_continue_on_stale_button(tmp_path: Path) -> None:
141
+ async def _run():
142
+ db_file = tmp_path / "state.db"
143
+ state = State(db_file)
144
+ cfg = Config()
145
+ writer = TimelineWriter(tmp_path / "timeline.jsonl")
146
+ logger = ActionLogger(state, writer)
147
+ safety = SafetyFilter(cfg)
148
+ nav = MagicMock()
149
+ nav.press_button = AsyncMock(
150
+ return_value=PressOutcome(kind="answer_only", callback=CallbackResult(message="Alert", alert=True))
151
+ )
152
+ crawler = BotCrawler(cfg, state, logger, safety, nav, {}, "test_bot", media_dir=tmp_path / "media")
153
+
154
+ crawler._current_msg = MagicMock(id=123)
155
+ crawler._current_msg_rowid = 1
156
+ crawler._current_screen_id = "screen_1"
157
+
158
+ state._exec(
159
+ "INSERT INTO messages (id, bot, chat_id, message_id, event_type, captured_at) VALUES (?, ?, ?, ?, ?, ?)",
160
+ (1, "test_bot", 12345, 123, "new", "2026-01-01T00:00:00Z"),
161
+ )
162
+ state._exec(
163
+ "INSERT INTO buttons (id, bot, screen_id, row, col, label, button_type, callback_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
164
+ (1, "test_bot", "screen_1", 0, 0, "Stale Button", "callback", b"stale"),
165
+ )
166
+ state._exec(
167
+ "INSERT INTO buttons (id, bot, screen_id, row, col, label, button_type, callback_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
168
+ (2, "test_bot", "screen_1", 0, 1, "Valid Button", "callback", b"valid"),
169
+ )
170
+ state._exec(
171
+ "INSERT INTO button_instances (bot, message_rowid, row, col, label, callback_data, action_id, button_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
172
+ ("test_bot", 1, 0, 1, "Valid Button", "valid", "act_valid", "callback"),
173
+ )
174
+
175
+ item = {"screen_id": "screen_1", "path": [], "depth": 1}
176
+ await crawler._explore(item)
177
+
178
+ assert nav.press_button.call_count == 1
179
+
180
+ asyncio.run(_run())
181
+
182
+
183
+ def test_navigation_graph_persists_unstable_edges_to_sqlite(tmp_path: Path) -> None:
184
+ db_file = tmp_path / "state.db"
185
+ state = State(db_file)
186
+ graph = NavigationGraph(state=state, bot="test_bot")
187
+
188
+ graph.record_transition("scr_a", "act_1", "scr_b", "edit", success=True)
189
+ graph.record_transition("scr_a", "act_1", "scr_c", "edit", success=True)
190
+
191
+ edges_in_db = state.graph_edges_all("test_bot")
192
+ assert len(edges_in_db) == 2
193
+ for e in edges_in_db:
194
+ assert e["is_stable"] == 0
195
+ assert e["confidence"] <= 0.5
196
+
197
+
198
+ def test_comparator_dast_routing_and_screens_fallback(tmp_path: Path) -> None:
199
+ dast_a = {
200
+ "summary": {"screens": 5, "buttons": 12, "routes": 4, "dead_ends": 1, "max_depth": 3},
201
+ "nodes": [{"id": "s1"}, {"id": "s2"}],
202
+ }
203
+ dast_b = {
204
+ "summary": {"screens": 3, "buttons": 8, "routes": 2, "dead_ends": 0, "max_depth": 2},
205
+ "nodes": [{"id": "s1"}],
206
+ }
207
+ comp = BotComparator("BotA", dast_a, "BotB", dast_b)
208
+ metrics_a, metrics_b = comp._resolve_metrics()
209
+
210
+ assert metrics_a["buttons_count"] == 12
211
+ assert metrics_b["buttons_count"] == 8
212
+
213
+ out = comp.export(tmp_path)
214
+ prompt_content = out["llm_prompt"].read_text(encoding="utf-8")
215
+ assert "BotA" in prompt_content
216
+ assert "BotB" in prompt_content
217
+
218
+
219
+ def test_analyzer_dynamic_callback_placeholder() -> None:
220
+ analyzer = GraphAnalyzer({}, {})
221
+ target, kind = analyzer._resolve_target("$user_id")
222
+ assert kind == "dynamic"
223
+ assert target == "dynamic:$user_id"
224
+
225
+
226
+ def test_emoji_tgs_and_webm_support(tmp_path: Path) -> None:
227
+ (tmp_path / "12345.tgs").write_bytes(b"tgs_data")
228
+ (tmp_path / "67890.webm").write_bytes(b"webm_data")
229
+
230
+ res_tgs = load_cached_emoji("12345", tmp_path)
231
+ assert res_tgs is not None
232
+ assert res_tgs.startswith("data:application/x-tgsticker;base64,")
233
+
234
+ res_webm = load_cached_emoji("67890", tmp_path)
235
+ assert res_webm is not None
236
+ assert res_webm.startswith("data:video/webm;base64,")
237
+
238
+
239
+ def test_utf16_surrogatepass() -> None:
240
+ lone_surrogate = "\ud800"
241
+ length = utf16_length(lone_surrogate)
242
+ assert length == 1
243
+
244
+
245
+ def test_auth_put_your_api_id() -> None:
246
+ async def _run():
247
+ cfg = Config()
248
+ cfg.api_id = "PUT_YOUR_API_ID"
249
+ cfg.api_hash = "valid_hash_123"
250
+
251
+ with pytest.raises(RuntimeError, match="API credentials \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b"):
252
+ await create_client(cfg)
253
+
254
+ asyncio.run(_run())
File without changes
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from tg_flowmap.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from tg_flowmap.sast.analyzer import GraphAnalyzer
4
+
5
+ __all__ = ["GraphAnalyzer"]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from tg_flowmap.sast.evaluator import (
4
+ _ast_to_value,
5
+ evaluate_ast_node,
6
+ extract_module_constants,
7
+ )
8
+
9
+ __all__ = [
10
+ "_ast_to_value",
11
+ "evaluate_ast_node",
12
+ "extract_module_constants",
13
+ ]