tg-flowmap 0.1.0__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.
- tg_flowmap/__init__.py +0 -0
- tg_flowmap/__main__.py +6 -0
- tg_flowmap/analyzer.py +5 -0
- tg_flowmap/ast_evaluator.py +13 -0
- tg_flowmap/cli.py +470 -0
- tg_flowmap/core/__init__.py +16 -0
- tg_flowmap/core/comparator.py +168 -0
- tg_flowmap/core/llm_exporter.py +106 -0
- tg_flowmap/core/models.py +142 -0
- tg_flowmap/dast/__init__.py +11 -0
- tg_flowmap/dast/auth.py +33 -0
- tg_flowmap/dast/capture.py +235 -0
- tg_flowmap/dast/comparison.py +289 -0
- tg_flowmap/dast/config.py +252 -0
- tg_flowmap/dast/crawler.py +1044 -0
- tg_flowmap/dast/fingerprint.py +54 -0
- tg_flowmap/dast/html_render.py +140 -0
- tg_flowmap/dast/logging_setup.py +47 -0
- tg_flowmap/dast/map_render.py +506 -0
- tg_flowmap/dast/media.py +93 -0
- tg_flowmap/dast/models.py +303 -0
- tg_flowmap/dast/navigation_executor.py +135 -0
- tg_flowmap/dast/navigation_graph.py +265 -0
- tg_flowmap/dast/navigator.py +287 -0
- tg_flowmap/dast/ratelimit.py +76 -0
- tg_flowmap/dast/reports.py +475 -0
- tg_flowmap/dast/route_planner.py +141 -0
- tg_flowmap/dast/safety.py +267 -0
- tg_flowmap/dast/scheduler.py +220 -0
- tg_flowmap/dast/screen_identity.py +259 -0
- tg_flowmap/dast/state.py +1015 -0
- tg_flowmap/dast/timeline_log.py +102 -0
- tg_flowmap/dast/tl_serial.py +66 -0
- tg_flowmap/dast/utf16.py +36 -0
- tg_flowmap/emoji_fetcher.py +13 -0
- tg_flowmap/models.py +19 -0
- tg_flowmap/parser.py +9 -0
- tg_flowmap/sast/__init__.py +17 -0
- tg_flowmap/sast/analyzer.py +472 -0
- tg_flowmap/sast/emoji.py +168 -0
- tg_flowmap/sast/evaluator.py +241 -0
- tg_flowmap/sast/parser.py +1302 -0
- tg_flowmap/visualizer/__init__.py +5 -0
- tg_flowmap/visualizer/html_builder.py +33 -0
- tg_flowmap/visualizer/templates/viewer.css +750 -0
- tg_flowmap/visualizer/templates/viewer.html +82 -0
- tg_flowmap/visualizer/templates/viewer.js +1659 -0
- tg_flowmap/visualizer.py +5 -0
- tg_flowmap-0.1.0.dist-info/METADATA +162 -0
- tg_flowmap-0.1.0.dist-info/RECORD +54 -0
- tg_flowmap-0.1.0.dist-info/WHEEL +5 -0
- tg_flowmap-0.1.0.dist-info/entry_points.txt +2 -0
- tg_flowmap-0.1.0.dist-info/licenses/LICENSE +21 -0
- tg_flowmap-0.1.0.dist-info/top_level.txt +1 -0
tg_flowmap/__init__.py
ADDED
|
File without changes
|
tg_flowmap/__main__.py
ADDED
tg_flowmap/analyzer.py
ADDED
tg_flowmap/cli.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from tg_flowmap.core.comparator import BotComparator
|
|
11
|
+
from tg_flowmap.core.models import BotMap
|
|
12
|
+
from tg_flowmap.sast.analyzer import GraphAnalyzer
|
|
13
|
+
from tg_flowmap.sast.parser import AstParser
|
|
14
|
+
from tg_flowmap.visualizer.html_builder import HtmlVisualizer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_env_token(target_path: Path) -> Optional[str]:
|
|
18
|
+
"""Tries to find BOT_TOKEN or TELEGRAM_BOT_TOKEN in env or .env files."""
|
|
19
|
+
for var in ("TELEGRAM_BOT_TOKEN", "BOT_TOKEN"):
|
|
20
|
+
val = os.environ.get(var)
|
|
21
|
+
if val and ":" in val:
|
|
22
|
+
return val.strip()
|
|
23
|
+
|
|
24
|
+
candidate_envs = [
|
|
25
|
+
Path.cwd() / ".env",
|
|
26
|
+
target_path / ".env" if target_path.is_dir() else target_path.parent / ".env",
|
|
27
|
+
target_path.parent / ".env",
|
|
28
|
+
Path(__file__).resolve().parent.parent / ".env",
|
|
29
|
+
]
|
|
30
|
+
for env_path in candidate_envs:
|
|
31
|
+
if env_path.exists():
|
|
32
|
+
try:
|
|
33
|
+
for line in env_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
34
|
+
line = line.strip()
|
|
35
|
+
if line.startswith("#") or "=" not in line:
|
|
36
|
+
continue
|
|
37
|
+
k, v = line.split("=", 1)
|
|
38
|
+
k = k.strip()
|
|
39
|
+
v = v.strip().strip("'\"")
|
|
40
|
+
if k in ("TELEGRAM_BOT_TOKEN", "BOT_TOKEN") and ":" in v:
|
|
41
|
+
return v
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_sast_markdown_report(bot_map: BotMap, target_path: Path) -> str:
|
|
48
|
+
stats = bot_map.stats
|
|
49
|
+
name = target_path.name
|
|
50
|
+
lines = [
|
|
51
|
+
f"# 🧭 TG-FLOWMAP SAST Report: {name}",
|
|
52
|
+
"",
|
|
53
|
+
f"- **Путь:** `{target_path.resolve()}`",
|
|
54
|
+
f"- **Всего экранов:** {stats.get('total_nodes', 0)}",
|
|
55
|
+
f"- **Всего переходов / связей:** {stats.get('total_edges', 0)}",
|
|
56
|
+
f"- **Тупиков (Dead-end):** {stats.get('dead_ends', 0)}",
|
|
57
|
+
f"- **Ошибок эмодзи:** {stats.get('broken_emojis', 0)}",
|
|
58
|
+
f"- **Экранов без кнопки «Назад»:** {stats.get('missing_backs', 0)}",
|
|
59
|
+
f"- **Скрытых команд (Orphan Commands):** {stats.get('orphan_commands', 0)}",
|
|
60
|
+
"",
|
|
61
|
+
"---",
|
|
62
|
+
"",
|
|
63
|
+
"## 🔍 Найденные дефекты и предупреждения",
|
|
64
|
+
"",
|
|
65
|
+
]
|
|
66
|
+
if bot_map.issues:
|
|
67
|
+
for idx, issue in enumerate(bot_map.issues, 1):
|
|
68
|
+
icon = "❌" if issue.severity == "error" else "⚠️" if issue.severity == "warning" else "ℹ️"
|
|
69
|
+
src_info = ""
|
|
70
|
+
if issue.source_file:
|
|
71
|
+
p = Path(issue.source_file).name
|
|
72
|
+
line = f":{issue.line_number}" if issue.line_number else ""
|
|
73
|
+
src_info = f" *({p}{line})*"
|
|
74
|
+
lines.append(f"{idx}. {icon} **[{issue.issue_type.upper()}]** {issue.message}{src_info}")
|
|
75
|
+
else:
|
|
76
|
+
lines.append("✅ *Дефектов и тупиков в навигации не обнаружено.*")
|
|
77
|
+
|
|
78
|
+
lines.extend([
|
|
79
|
+
"",
|
|
80
|
+
"---",
|
|
81
|
+
"",
|
|
82
|
+
"## 🗺️ Зарегистрированные экраны и клавиатуры",
|
|
83
|
+
"",
|
|
84
|
+
])
|
|
85
|
+
for node_id, node in sorted(bot_map.nodes.items(), key=lambda x: x[0]):
|
|
86
|
+
btns = []
|
|
87
|
+
if node.keyboard:
|
|
88
|
+
btns = [b.text for b in node.keyboard.all_buttons()]
|
|
89
|
+
btn_str = ", ".join(f"`{b}`" for b in btns) if btns else "*(без кнопок)*"
|
|
90
|
+
lines.append(f"- **`{node_id}`** ({node.title or 'screen'}): кнопки -> {btn_str}")
|
|
91
|
+
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_analysis(
|
|
96
|
+
target_path: Path,
|
|
97
|
+
output_html: Path | None = None,
|
|
98
|
+
output_json: Path | None = None,
|
|
99
|
+
strict: bool = False,
|
|
100
|
+
bot_token: str | None = None,
|
|
101
|
+
parser: AstParser | None = None,
|
|
102
|
+
output_md: Path | None = None,
|
|
103
|
+
) -> int:
|
|
104
|
+
if sys.platform == "win32":
|
|
105
|
+
try:
|
|
106
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
107
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
if not target_path.exists():
|
|
112
|
+
print(f"[ERROR] Целевой путь не существует: {target_path}", file=sys.stderr)
|
|
113
|
+
return 1
|
|
114
|
+
|
|
115
|
+
if parser is None:
|
|
116
|
+
parser = AstParser()
|
|
117
|
+
if target_path.is_file():
|
|
118
|
+
parser.parse_file(target_path)
|
|
119
|
+
else:
|
|
120
|
+
parser.parse_dir(target_path)
|
|
121
|
+
|
|
122
|
+
if parser.skipped_files:
|
|
123
|
+
print(f"⚠️ Предупреждение: пропущено файлов из-за синтаксических ошибок: {len(parser.skipped_files)}", file=sys.stderr)
|
|
124
|
+
for sp, err in parser.skipped_files:
|
|
125
|
+
print(f" - {sp.name}: {err}", file=sys.stderr)
|
|
126
|
+
|
|
127
|
+
analyzer = GraphAnalyzer(
|
|
128
|
+
parser.nodes,
|
|
129
|
+
parser.keyboards,
|
|
130
|
+
screen_aliases=parser.screen_aliases,
|
|
131
|
+
hub_return_nodes=parser.hub_return_nodes,
|
|
132
|
+
registered_commands=parser.registered_menu_commands,
|
|
133
|
+
has_set_my_commands=parser.has_set_my_commands,
|
|
134
|
+
skipped_files=parser.skipped_files,
|
|
135
|
+
)
|
|
136
|
+
bot_map = analyzer.build_map()
|
|
137
|
+
|
|
138
|
+
# Fetch Telegram Premium custom emojis if present
|
|
139
|
+
token = bot_token or load_env_token(target_path)
|
|
140
|
+
if parser.custom_emoji_ids:
|
|
141
|
+
if token:
|
|
142
|
+
from tg_flowmap.sast.emoji import fetch_custom_emojis
|
|
143
|
+
emojis = fetch_custom_emojis(token, parser.custom_emoji_ids)
|
|
144
|
+
bot_map.custom_emojis = emojis
|
|
145
|
+
else:
|
|
146
|
+
from tg_flowmap.sast.emoji import get_default_cache_dir, load_cached_emoji
|
|
147
|
+
cache_dir = get_default_cache_dir()
|
|
148
|
+
cached = {}
|
|
149
|
+
for eid in parser.custom_emoji_ids:
|
|
150
|
+
data_url = load_cached_emoji(eid, cache_dir)
|
|
151
|
+
if data_url:
|
|
152
|
+
cached[eid] = data_url
|
|
153
|
+
if cached:
|
|
154
|
+
bot_map.custom_emojis = cached
|
|
155
|
+
|
|
156
|
+
stats = bot_map.stats
|
|
157
|
+
print("=" * 60)
|
|
158
|
+
print(" 🧭 TG-FLOWMAP — Telegram Bot Analyzer (SAST)")
|
|
159
|
+
print("=" * 60)
|
|
160
|
+
print(f"Цель: {target_path.resolve()}")
|
|
161
|
+
print(f"Всего экранов: {stats.get('total_nodes', 0)}")
|
|
162
|
+
print(f"Всего связей: {stats.get('total_edges', 0)}")
|
|
163
|
+
print(f"Клавиатур найдено: {len(parser.keyboards)}")
|
|
164
|
+
print(f"Дубликатов экранов:{stats.get('duplicate_screens', 0)}")
|
|
165
|
+
if parser.custom_emoji_ids:
|
|
166
|
+
print(f"Премиум эмодзи: {len(parser.custom_emoji_ids)} (в кэше: {len(bot_map.custom_emojis)})")
|
|
167
|
+
print(f"Тупиков (Dead-end):{stats.get('dead_ends', 0)}")
|
|
168
|
+
print(f"Ошибок эмодзи: {stats.get('broken_emojis', 0)}")
|
|
169
|
+
print(f"Без кнопки Назад: {stats.get('missing_backs', 0)}")
|
|
170
|
+
print(f"Скрытых команд: {stats.get('orphan_commands', 0)}")
|
|
171
|
+
print("-" * 60)
|
|
172
|
+
|
|
173
|
+
if bot_map.issues:
|
|
174
|
+
print("\n🔍 НАЙДЕННЫЕ ДЕФЕКТЫ И ПРЕДУПРЕЖДЕНИЯ:")
|
|
175
|
+
for idx, issue in enumerate(bot_map.issues, 1):
|
|
176
|
+
icon = "❌" if issue.severity == "error" else "⚠️" if issue.severity == "warning" else "ℹ️"
|
|
177
|
+
src_info = ""
|
|
178
|
+
if issue.source_file:
|
|
179
|
+
p = Path(issue.source_file).name
|
|
180
|
+
line = f":{issue.line_number}" if issue.line_number else ""
|
|
181
|
+
src_info = f" ({p}{line})"
|
|
182
|
+
print(f" {idx}. {icon} [{issue.issue_type.upper()}] {issue.message}{src_info}")
|
|
183
|
+
print()
|
|
184
|
+
else:
|
|
185
|
+
print("\n✅ Дефектов и тупиков не обнаружено!")
|
|
186
|
+
|
|
187
|
+
if output_md:
|
|
188
|
+
output_md.parent.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
output_md.write_text(build_sast_markdown_report(bot_map, target_path), encoding="utf-8")
|
|
190
|
+
print(f"📄 Отчёт сохранён: {output_md.resolve()}")
|
|
191
|
+
|
|
192
|
+
if output_html:
|
|
193
|
+
output_html.parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
visualizer = HtmlVisualizer(bot_map)
|
|
195
|
+
visualizer.render(output_html)
|
|
196
|
+
print(f"📊 Интерактивная карта сохранена: {output_html.resolve()}")
|
|
197
|
+
|
|
198
|
+
if output_json:
|
|
199
|
+
output_json.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
output_json.write_text(json.dumps(bot_map.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
201
|
+
print(f"📦 JSON-структура сохранена: {output_json.resolve()}")
|
|
202
|
+
|
|
203
|
+
dead_ends = stats.get("dead_ends", 0)
|
|
204
|
+
broken_emojis = stats.get("broken_emojis", 0)
|
|
205
|
+
has_errors = any(i.severity == "error" for i in bot_map.issues) or (dead_ends > 0) or (broken_emojis > 0)
|
|
206
|
+
if strict and has_errors:
|
|
207
|
+
print("\n[STRICT MODE] Обнаружены критические ошибки! Выход с кодом 1.")
|
|
208
|
+
return 1
|
|
209
|
+
|
|
210
|
+
return 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def run_dast_scan(
|
|
214
|
+
bot_target: str,
|
|
215
|
+
output_dir: Path | None = None,
|
|
216
|
+
config_path: Path | None = None,
|
|
217
|
+
) -> int:
|
|
218
|
+
try:
|
|
219
|
+
import asyncio
|
|
220
|
+
import telethon
|
|
221
|
+
except ImportError as e:
|
|
222
|
+
print(
|
|
223
|
+
f"[ERROR] Для DAST режима требуется Telethon.\n"
|
|
224
|
+
f"Установите зависимость: pip install \"tg-flowmap[dast]\"\n({e})",
|
|
225
|
+
file=sys.stderr,
|
|
226
|
+
)
|
|
227
|
+
return 1
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
from tg_flowmap.dast.config import load_config, normalize_bot_username
|
|
231
|
+
from tg_flowmap.dast.reports import generate_reports
|
|
232
|
+
from tg_flowmap.dast.scheduler import run_scan
|
|
233
|
+
from tg_flowmap.dast.state import State
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(f"[ERROR] Ошибка инициализации DAST модулей: {e}", file=sys.stderr)
|
|
236
|
+
return 1
|
|
237
|
+
|
|
238
|
+
bot_name = normalize_bot_username(bot_target)
|
|
239
|
+
cfg_file = config_path or Path("config.json")
|
|
240
|
+
if not cfg_file.exists():
|
|
241
|
+
print(
|
|
242
|
+
f"[ERROR] Не найден конфигурационный файл: {cfg_file}\n"
|
|
243
|
+
f"Создайте config.json с параметрами api_id и api_hash (см. config.example.json)",
|
|
244
|
+
file=sys.stderr,
|
|
245
|
+
)
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
cfg = load_config(cfg_file)
|
|
249
|
+
if output_dir:
|
|
250
|
+
cfg.custom_output_dir = output_dir
|
|
251
|
+
|
|
252
|
+
print("=" * 60)
|
|
253
|
+
print(" 🤖 TG-FLOWMAP — Telegram Bot Crawler (DAST)")
|
|
254
|
+
print("=" * 60)
|
|
255
|
+
print(f"Целевой бот: {bot_name}")
|
|
256
|
+
print(f"Директория: {cfg.output_dir.resolve()}")
|
|
257
|
+
print("-" * 60)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
statuses = asyncio.run(run_scan(cfg, bots=[bot_name]))
|
|
261
|
+
bot_status = statuses.get(bot_name, "ok")
|
|
262
|
+
if "error" in bot_status.lower():
|
|
263
|
+
print(f"[WARN] Обход завершился со статусом: {bot_status}")
|
|
264
|
+
except KeyboardInterrupt:
|
|
265
|
+
print("\n[INFO] Обход прерван пользователем. Прогресс сохранён в БД.")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
print(f"[ERROR] Ошибка выполнения обхода: {e}", file=sys.stderr)
|
|
268
|
+
return 1
|
|
269
|
+
|
|
270
|
+
state = State(cfg.db_path)
|
|
271
|
+
try:
|
|
272
|
+
reports = generate_reports(cfg, state, bot_name)
|
|
273
|
+
finally:
|
|
274
|
+
state.close()
|
|
275
|
+
|
|
276
|
+
print(f"\n✅ Обход завершён!")
|
|
277
|
+
if reports.get("report"):
|
|
278
|
+
print(f"📄 Отчёт сохранён: {reports.get('report')}")
|
|
279
|
+
if reports.get("map_html"):
|
|
280
|
+
print(f"📊 Интерактивная карта: {reports.get('map_html')}")
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _load_target_data(target: str, out_dir: Path) -> dict:
|
|
285
|
+
p = Path(target)
|
|
286
|
+
if p.exists():
|
|
287
|
+
parser = AstParser()
|
|
288
|
+
if p.is_file():
|
|
289
|
+
parser.parse_file(p)
|
|
290
|
+
else:
|
|
291
|
+
parser.parse_dir(p)
|
|
292
|
+
analyzer = GraphAnalyzer(
|
|
293
|
+
parser.nodes,
|
|
294
|
+
parser.keyboards,
|
|
295
|
+
screen_aliases=parser.screen_aliases,
|
|
296
|
+
hub_return_nodes=parser.hub_return_nodes,
|
|
297
|
+
registered_commands=parser.registered_menu_commands,
|
|
298
|
+
has_set_my_commands=parser.has_set_my_commands,
|
|
299
|
+
skipped_files=parser.skipped_files,
|
|
300
|
+
)
|
|
301
|
+
bot_map = analyzer.build_map()
|
|
302
|
+
return {
|
|
303
|
+
"metrics": BotComparator.extract_metrics_from_bot_map(bot_map),
|
|
304
|
+
"screens": [
|
|
305
|
+
{
|
|
306
|
+
"screen_id": nid,
|
|
307
|
+
"title": n.title,
|
|
308
|
+
"buttons": [b.text for b in n.keyboard.all_buttons()] if n.keyboard else [],
|
|
309
|
+
}
|
|
310
|
+
for nid, n in bot_map.nodes.items()
|
|
311
|
+
],
|
|
312
|
+
}
|
|
313
|
+
clean = target.lstrip("@")
|
|
314
|
+
cand = out_dir / clean / "graph.json"
|
|
315
|
+
if cand.exists():
|
|
316
|
+
try:
|
|
317
|
+
graph_data = json.loads(cand.read_text(encoding="utf-8"))
|
|
318
|
+
nodes_data = graph_data.get("nodes") or graph_data.get("screens") or []
|
|
319
|
+
nodes = list(nodes_data.values()) if isinstance(nodes_data, dict) else (nodes_data if isinstance(nodes_data, list) else [])
|
|
320
|
+
edges = graph_data.get("edges") or []
|
|
321
|
+
total_buttons = sum(len(n.get("buttons", [])) for n in nodes)
|
|
322
|
+
screens_list = [
|
|
323
|
+
{
|
|
324
|
+
"screen_id": n.get("id") or n.get("screen_id"),
|
|
325
|
+
"title": (n.get("text_normalized") or n.get("text") or n.get("id") or "").splitlines()[0][:40]
|
|
326
|
+
if (n.get("text_normalized") or n.get("text"))
|
|
327
|
+
else (n.get("id") or n.get("screen_id")),
|
|
328
|
+
"buttons": n.get("buttons", []),
|
|
329
|
+
}
|
|
330
|
+
for n in nodes
|
|
331
|
+
]
|
|
332
|
+
return {
|
|
333
|
+
"metrics": {
|
|
334
|
+
"screens_count": len(nodes),
|
|
335
|
+
"buttons_count": total_buttons if total_buttons > 0 else len(edges),
|
|
336
|
+
"dead_ends": 0,
|
|
337
|
+
"missing_back": 0,
|
|
338
|
+
},
|
|
339
|
+
"screens": screens_list,
|
|
340
|
+
}
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
return {"metrics": {"screens_count": 0, "buttons_count": 0, "dead_ends": 0, "missing_back": 0}, "screens": []}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def run_compare(
|
|
347
|
+
target_a: str,
|
|
348
|
+
target_b: str,
|
|
349
|
+
output_dir: Path | None = None,
|
|
350
|
+
) -> int:
|
|
351
|
+
out_dir = output_dir or Path("output")
|
|
352
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
353
|
+
|
|
354
|
+
print("=" * 60)
|
|
355
|
+
print(" ⚔️ TG-FLOWMAP — Telegram Bot Benchmark & Comparison")
|
|
356
|
+
print("=" * 60)
|
|
357
|
+
print(f"Бот A (Исходный): {target_a}")
|
|
358
|
+
print(f"Бот B (Конкурент): {target_b}")
|
|
359
|
+
print("-" * 60)
|
|
360
|
+
|
|
361
|
+
data_a = _load_target_data(target_a, out_dir)
|
|
362
|
+
data_b = _load_target_data(target_b, out_dir)
|
|
363
|
+
|
|
364
|
+
path_a = Path(target_a)
|
|
365
|
+
path_b = Path(target_b)
|
|
366
|
+
|
|
367
|
+
comparator = BotComparator(
|
|
368
|
+
bot_a_name=path_a.name if path_a.exists() else target_a,
|
|
369
|
+
bot_a_data=data_a,
|
|
370
|
+
bot_b_name=path_b.name if path_b.exists() else target_b,
|
|
371
|
+
bot_b_data=data_b,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
exported = comparator.export(out_dir)
|
|
375
|
+
print(f"\n✅ Сравнение успешно завершено!")
|
|
376
|
+
print(f"📊 Отчёт сравнения сохранён: {exported['comparison'].resolve()}")
|
|
377
|
+
print(f"🤖 Промпт для нейросети (ChatGPT / Claude) готов: {exported['llm_prompt'].resolve()}")
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main() -> None:
|
|
382
|
+
if sys.platform == "win32":
|
|
383
|
+
try:
|
|
384
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
385
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
386
|
+
except Exception:
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
# Top-level parser
|
|
390
|
+
parser = argparse.ArgumentParser(
|
|
391
|
+
prog="tg-flowmap",
|
|
392
|
+
description="Unified Telegram Bot Analyzer: Static AST (SAST) + Dynamic Userbot Crawler (DAST) + LLM Benchmarker",
|
|
393
|
+
)
|
|
394
|
+
parser.add_argument("-v", "--version", action="version", version="%(prog)s 1.0.0")
|
|
395
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Доступные режимы анализа")
|
|
396
|
+
|
|
397
|
+
# SAST command
|
|
398
|
+
sast_parser = subparsers.add_parser("sast", aliases=["code", "scan"], help="Статический анализ кода бота (Zero-Runtime AST)")
|
|
399
|
+
sast_parser.add_argument("target", type=str, help="Путь к файлу бота (.py) или папке проекта")
|
|
400
|
+
sast_parser.add_argument("--strict", action="store_true", help="Возвращать код 1 при критических дефектах (для CI/CD)")
|
|
401
|
+
sast_parser.add_argument("--html", action="store_true", help="Сгенерировать интерактивную HTML-карту")
|
|
402
|
+
sast_parser.add_argument("-t", "--token", type=str, default=None, help="Токен бота для подгрузки оригинальных Premium эмодзи")
|
|
403
|
+
sast_parser.add_argument("-o", "--output-dir", type=str, default="output", help="Директория для отчетов (по умолчанию: output/)")
|
|
404
|
+
|
|
405
|
+
# DAST command
|
|
406
|
+
dast_parser = subparsers.add_parser("dast", aliases=["crawl", "bot"], help="Динамический реверс-инжиниринг через Telethon юзербота")
|
|
407
|
+
dast_parser.add_argument("bot", type=str, help="Имя или юзернейм целевого бота (например, @example_bot)")
|
|
408
|
+
dast_parser.add_argument("--config", type=str, default="config.json", help="Путь к config.json Telethon")
|
|
409
|
+
dast_parser.add_argument("-o", "--output-dir", type=str, default="output", help="Директория для отчетов (по умолчанию: output/)")
|
|
410
|
+
|
|
411
|
+
# COMPARE command
|
|
412
|
+
compare_parser = subparsers.add_parser("compare", aliases=["diff", "bench"], help="Сравнение двух ботов и генерация промпта для ИИ")
|
|
413
|
+
compare_parser.add_argument("target_a", type=str, help="Бот A (путь к коду или @bot)")
|
|
414
|
+
compare_parser.add_argument("target_b", type=str, help="Бот B (путь к коду или @bot конкурента)")
|
|
415
|
+
compare_parser.add_argument("-o", "--output-dir", type=str, default="output", help="Директория для отчетов (по умолчанию: output/)")
|
|
416
|
+
|
|
417
|
+
# Fast-path backward compatibility:
|
|
418
|
+
# If user called `tg-flowmap path/to/bot` directly without subcommand:
|
|
419
|
+
args_list = sys.argv[1:]
|
|
420
|
+
if args_list and args_list[0] not in (
|
|
421
|
+
"sast", "code", "scan", "dast", "crawl", "bot", "compare", "diff", "bench",
|
|
422
|
+
"-h", "--help", "-v", "--version"
|
|
423
|
+
):
|
|
424
|
+
# Auto-route to sast mode!
|
|
425
|
+
args_list = ["sast"] + args_list
|
|
426
|
+
|
|
427
|
+
args = parser.parse_args(args_list)
|
|
428
|
+
|
|
429
|
+
if not args.subcommand:
|
|
430
|
+
parser.print_help()
|
|
431
|
+
sys.exit(0)
|
|
432
|
+
|
|
433
|
+
if args.subcommand in ("sast", "code", "scan"):
|
|
434
|
+
target = Path(args.target)
|
|
435
|
+
out_dir = Path(getattr(args, "output_dir", "output"))
|
|
436
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
437
|
+
stem = (target.stem if target.is_file() else target.resolve().name) or "report"
|
|
438
|
+
out_md = out_dir / f"{stem}_report.md"
|
|
439
|
+
out_json = out_dir / f"{stem}_data.json"
|
|
440
|
+
out_html = out_dir / f"{stem}_map.html" if getattr(args, "html", False) else None
|
|
441
|
+
|
|
442
|
+
exit_code = run_analysis(
|
|
443
|
+
target_path=target,
|
|
444
|
+
output_md=out_md,
|
|
445
|
+
output_html=out_html,
|
|
446
|
+
output_json=out_json,
|
|
447
|
+
strict=args.strict,
|
|
448
|
+
bot_token=args.token,
|
|
449
|
+
)
|
|
450
|
+
sys.exit(exit_code)
|
|
451
|
+
|
|
452
|
+
elif args.subcommand in ("dast", "crawl", "bot"):
|
|
453
|
+
exit_code = run_dast_scan(
|
|
454
|
+
bot_target=args.bot,
|
|
455
|
+
output_dir=Path(args.output_dir),
|
|
456
|
+
config_path=Path(args.config) if args.config else None,
|
|
457
|
+
)
|
|
458
|
+
sys.exit(exit_code)
|
|
459
|
+
|
|
460
|
+
elif args.subcommand in ("compare", "diff", "bench"):
|
|
461
|
+
exit_code = run_compare(
|
|
462
|
+
target_a=args.target_a,
|
|
463
|
+
target_b=args.target_b,
|
|
464
|
+
output_dir=Path(args.output_dir),
|
|
465
|
+
)
|
|
466
|
+
sys.exit(exit_code)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if __name__ == "__main__":
|
|
470
|
+
main()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tg_flowmap.core.comparator import BotComparator
|
|
4
|
+
from tg_flowmap.core.llm_exporter import generate_llm_benchmark_prompt
|
|
5
|
+
from tg_flowmap.core.models import BotMap, Button, Edge, Issue, Keyboard, MessageNode
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Button",
|
|
9
|
+
"Keyboard",
|
|
10
|
+
"MessageNode",
|
|
11
|
+
"Edge",
|
|
12
|
+
"Issue",
|
|
13
|
+
"BotMap",
|
|
14
|
+
"BotComparator",
|
|
15
|
+
"generate_llm_benchmark_prompt",
|
|
16
|
+
]
|