- 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
4.1 KiB
tui/app.py
Textual TUI frontend. Entry point: run_tui().
Entry point
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 ownasyncio.new_event_loop()
Runs_bot_main()— Telethon is completely isolated from Textual's loop. - TUI drain →
set_interval(0.1, _drain_bus)— pollsqueue.Queueevery 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
DataTablewith columns: Sev, Time, URL, Username, Password, Source, File - Submit with
↵or Search button;Escapeto 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_DOMAINSandORG_DOMAINS - Bot handler recompiles patterns on the next incoming message automatically
- Changes are in-memory only — copy to
config.pyto 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()