Files
stealergram/tui/app.md
anti 48f486ac97 Initial commit: ULPgrammer
- Core Telegram monitoring pipeline (scraper, processor, notifier, downloaders)
- Textual TUI frontend with thread-safe event bus
- SQLite persistence, severity scoring, dedup cache
- Fixed ULP parser: handles https:// truncation, port+path URLs, semicolon separator
- Test suite: 88 tests across scorer, cache, database, processor
2026-04-02 01:58:49 -03:00

131 lines
4.1 KiB
Markdown

# tui/app.py
Textual TUI frontend. Entry point: `run_tui()`.
## Entry point
```python
from tui.app import run_tui
run_tui() # called by main.py
```
---
## Screen hierarchy
```
MonitorApp (App)
├── [default screen]
│ ├── Header
│ ├── #top-row (Horizontal)
│ │ ├── DownloadPanel #dl-panel
│ │ └── HitsPanel #hits-panel
│ ├── StatsPanel #stats-panel
│ ├── ChannelPanel #ch-panel
│ └── Footer
├── SearchScreen (push/pop via 's')
├── HitsDBScreen (push/pop via 'h')
└── KeywordsScreen (push/pop via 'k')
```
---
## MonitorApp
### Threading model
- **Bot backend** → `threading.Thread(daemon=True)` with its own `asyncio.new_event_loop()`
Runs `_bot_main()` — Telethon is completely isolated from Textual's loop.
- **TUI drain** → `set_interval(0.1, _drain_bus)` — polls `queue.Queue` every 100ms on Textual's loop.
### Key methods
| Method | Description |
|--------|-------------|
| `on_mount()` | Calls `bus.init_bus()`, starts bot thread, sets drain interval |
| `_drain_bus()` | Drains all pending events from `queue.Queue`, dispatches to widgets |
| `_run_bot_thread()` | Thread entry: creates event loop, runs `_bot_main()` |
| `_bot_main()` | Async bot backend: connect, auth, backfill, live handler loop |
| `_signal_channel_changed()` | Thread-safely sets the bot loop's `asyncio.Event` via `call_soon_threadsafe` |
### Keybindings
| Key | Action |
|-----|--------|
| `s` | Push `SearchScreen` |
| `h` | Push `HitsDBScreen` |
| `k` | Push `KeywordsScreen` |
| `c` | Clear download + hits logs |
| `r` | Force-refresh stats bar |
| `q` / `ctrl+c` | Quit |
---
## Widgets
### DownloadPanel
Left panel. Two `RichLog` widgets separated by a dashed line:
- **top** (`#tdl-out`): raw tdl output lines (ANSI stripped)
- **bottom** (`#dl-log`): structured download status entries
Methods: `tdl_line(line)`, `queued(filename, size_mb, source, password)`, `status(filename, state, via)`, `clear_logs()`
States for `status()`: `queued` · `downloading` · `done_tdl` · `done_tel` · `failed`
### HitsPanel
Right panel. Single `RichLog` with color-coded hit entries.
Reactive `hit_count` updates the panel title badge automatically.
Methods: `add_hit(severity, raw, source, filename, reasons)`, `clear_log()`
### StatsPanel
Slim horizontal bar. Polls `utils.database.stats()` every 10s via `set_interval`.
Also refreshed immediately on each `EvHit` event.
### ChannelPanel
Bottom panel. `ListView` + `Input` + buttons.
Add/remove posts `EvChannelAdded` / `EvChannelRemoved` onto the bus.
Changes apply immediately (handler re-registered). Not persisted to `config.py` automatically.
---
## Screens
### SearchScreen (`s`)
- Text input → queries `utils.database.search(keyword)`
- Results in a `DataTable` with columns: Sev, Time, URL, Username, Password, Source, File
- Submit with `↵` or Search button; `Escape` to dismiss
### HitsDBScreen (`h`)
- Toolbar buttons + number keys filter by severity
- `r` → recent 50, `1`→CRITICAL, `2`→HIGH, `3`→MEDIUM, `4`→LOW
- Calls `utils.database.recent()` / `by_severity()`
### KeywordsScreen (`k`)
- Live-edit `config.TARGET_KEYWORDS`
- Validates regex before adding
- On change: rebuilds `utils.scorer.EMPLOYEE_DOMAINS` and `ORG_DOMAINS`
- Bot handler recompiles patterns on the next incoming message automatically
- **Changes are in-memory only** — copy to `config.py` to persist
---
## Bot auth flow (`_bot_main`)
```
await bot_client.connect()
await bot_client.is_user_authorized()? → sign_in(bot_token=...)
await user_client.connect()
await user_client.is_user_authorized()? → log error + return (must run --no-tui first)
warm_entity_cache()
_make_handler(channels) ← registers NewMessage handler
backfill_all()
run_until_disconnected() ┐
_watch_channels() ┘ gathered
```
Channel-change signal path:
```
ChannelPanel button → EvChannel* on bus → _drain_bus → _signal_channel_changed()
→ call_soon_threadsafe(asyncio.Event.set) → _watch_channels() wakes → _make_handler()
```