Files
stealergram/tui/app.md
anti 741e6bb0d3 Rename to stealergram, add pyproject.toml, purge em-dashes
- Rename project to stealergram throughout
- Add pyproject.toml (replaces requirements.txt split, folds pytest.ini)
- Replace all em-dashes with hyphens across all source files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 10:06:30 -04:00

4.0 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 backendthreading.Thread(daemon=True) with its own asyncio.new_event_loop()
    Runs _bot_main() - Telethon is completely isolated from Textual's loop.
  • TUI drainset_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()