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.
- tg_flowmap-0.1.0/LICENSE +21 -0
- tg_flowmap-0.1.0/PKG-INFO +162 -0
- tg_flowmap-0.1.0/README.md +141 -0
- tg_flowmap-0.1.0/pyproject.toml +50 -0
- tg_flowmap-0.1.0/setup.cfg +4 -0
- tg_flowmap-0.1.0/tests/test_regressions.py +254 -0
- tg_flowmap-0.1.0/tg_flowmap/__init__.py +0 -0
- tg_flowmap-0.1.0/tg_flowmap/__main__.py +6 -0
- tg_flowmap-0.1.0/tg_flowmap/analyzer.py +5 -0
- tg_flowmap-0.1.0/tg_flowmap/ast_evaluator.py +13 -0
- tg_flowmap-0.1.0/tg_flowmap/cli.py +470 -0
- tg_flowmap-0.1.0/tg_flowmap/core/__init__.py +16 -0
- tg_flowmap-0.1.0/tg_flowmap/core/comparator.py +168 -0
- tg_flowmap-0.1.0/tg_flowmap/core/llm_exporter.py +106 -0
- tg_flowmap-0.1.0/tg_flowmap/core/models.py +142 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/__init__.py +11 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/auth.py +33 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/capture.py +235 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/comparison.py +289 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/config.py +252 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/crawler.py +1044 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/fingerprint.py +54 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/html_render.py +140 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/logging_setup.py +47 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/map_render.py +506 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/media.py +93 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/models.py +303 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/navigation_executor.py +135 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/navigation_graph.py +265 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/navigator.py +287 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/ratelimit.py +76 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/reports.py +475 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/route_planner.py +141 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/safety.py +267 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/scheduler.py +220 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/screen_identity.py +259 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/state.py +1015 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/timeline_log.py +102 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/tl_serial.py +66 -0
- tg_flowmap-0.1.0/tg_flowmap/dast/utf16.py +36 -0
- tg_flowmap-0.1.0/tg_flowmap/emoji_fetcher.py +13 -0
- tg_flowmap-0.1.0/tg_flowmap/models.py +19 -0
- tg_flowmap-0.1.0/tg_flowmap/parser.py +9 -0
- tg_flowmap-0.1.0/tg_flowmap/sast/__init__.py +17 -0
- tg_flowmap-0.1.0/tg_flowmap/sast/analyzer.py +472 -0
- tg_flowmap-0.1.0/tg_flowmap/sast/emoji.py +168 -0
- tg_flowmap-0.1.0/tg_flowmap/sast/evaluator.py +241 -0
- tg_flowmap-0.1.0/tg_flowmap/sast/parser.py +1302 -0
- tg_flowmap-0.1.0/tg_flowmap/visualizer/__init__.py +5 -0
- tg_flowmap-0.1.0/tg_flowmap/visualizer/html_builder.py +33 -0
- tg_flowmap-0.1.0/tg_flowmap/visualizer/templates/viewer.css +750 -0
- tg_flowmap-0.1.0/tg_flowmap/visualizer/templates/viewer.html +82 -0
- tg_flowmap-0.1.0/tg_flowmap/visualizer/templates/viewer.js +1659 -0
- tg_flowmap-0.1.0/tg_flowmap/visualizer.py +5 -0
- tg_flowmap-0.1.0/tg_flowmap.egg-info/PKG-INFO +162 -0
- tg_flowmap-0.1.0/tg_flowmap.egg-info/SOURCES.txt +58 -0
- tg_flowmap-0.1.0/tg_flowmap.egg-info/dependency_links.txt +1 -0
- tg_flowmap-0.1.0/tg_flowmap.egg-info/entry_points.txt +2 -0
- tg_flowmap-0.1.0/tg_flowmap.egg-info/requires.txt +13 -0
- tg_flowmap-0.1.0/tg_flowmap.egg-info/top_level.txt +1 -0
tg_flowmap-0.1.0/LICENSE
ADDED
|
@@ -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)
|
|
27
|
+
[]()
|
|
28
|
+
[]()
|
|
29
|
+
[]()
|
|
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)
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
8
|
+
[]()
|
|
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,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
|