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>
This commit is contained in:
64
tui/app.py
64
tui/app.py
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
tui.py — Textual TUI for the ULP credential monitor.
|
||||
tui.py - Textual TUI for the ULP credential monitor.
|
||||
|
||||
Layout (main screen):
|
||||
┌──────────────────────────────────┬──────────────────────────────────┐
|
||||
@@ -14,13 +14,13 @@ Layout (main screen):
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Additional screens (push/pop via keybindings):
|
||||
• SearchScreen — full-text search across hits DB [s]
|
||||
• HitsDBScreen — paginated recent / severity viewer [h]
|
||||
• KeywordsScreen — live-edit TARGET_KEYWORDS regex list [k]
|
||||
• SearchScreen - full-text search across hits DB [s]
|
||||
• HitsDBScreen - paginated recent / severity viewer [h]
|
||||
• KeywordsScreen - live-edit TARGET_KEYWORDS regex list [k]
|
||||
|
||||
Architecture:
|
||||
- The entire bot backend runs as a Textual Worker (asyncio task inside the
|
||||
TUI event loop — no threading needed).
|
||||
TUI event loop - no threading needed).
|
||||
- A second Worker runs _bus_consumer(), reading events from tui_events.queue
|
||||
and dispatching to the right panel.
|
||||
- Channel add/remove from the UI immediately re-registers Telethon handlers
|
||||
@@ -29,7 +29,7 @@ Architecture:
|
||||
into the download panel's RichLog.
|
||||
- StatsPanel polls database.stats() every 10 s via set_interval().
|
||||
- Keyword changes are applied in-memory immediately (scorer caches rebuilt);
|
||||
NOT auto-persisted to config.py — a notice banner reminds the user.
|
||||
NOT auto-persisted to config.py - a notice banner reminds the user.
|
||||
- Live patterns are recompiled from config.TARGET_KEYWORDS on every message
|
||||
so keyword changes take effect without a handler restart.
|
||||
"""
|
||||
@@ -88,7 +88,7 @@ def _now() -> str:
|
||||
|
||||
class DownloadPanel(Vertical):
|
||||
"""
|
||||
Left panel — two sub-logs stacked vertically:
|
||||
Left panel - two sub-logs stacked vertically:
|
||||
• top: tdl raw output (stripped ANSI), scrolling
|
||||
• bottom: our own structured status entries
|
||||
"""
|
||||
@@ -158,7 +158,7 @@ class DownloadPanel(Vertical):
|
||||
# ─── Hits panel ───────────────────────────────────────────────────────────────
|
||||
|
||||
class HitsPanel(Vertical):
|
||||
"""Right panel — scrollable color-coded hit log with live counter badge."""
|
||||
"""Right panel - scrollable color-coded hit log with live counter badge."""
|
||||
|
||||
hit_count: reactive[int] = reactive(0)
|
||||
|
||||
@@ -208,7 +208,7 @@ class HitsPanel(Vertical):
|
||||
|
||||
class StatsPanel(Horizontal):
|
||||
"""
|
||||
Slim bar — shows live DB stats, refreshed every 10 s.
|
||||
Slim bar - shows live DB stats, refreshed every 10 s.
|
||||
Also refreshed immediately whenever a new hit arrives.
|
||||
"""
|
||||
|
||||
@@ -233,14 +233,14 @@ class StatsPanel(Horizontal):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("📊 DB Stats", id="stat-label")
|
||||
yield Static("🔴 —", classes="stat-critical", id="stat-critical")
|
||||
yield Static("🟠 —", classes="stat-high", id="stat-high")
|
||||
yield Static("🟡 —", classes="stat-medium", id="stat-medium")
|
||||
yield Static("🟢 —", classes="stat-low", id="stat-low")
|
||||
yield Static("total: —", id="stat-total")
|
||||
yield Static("unique: —", id="stat-unique")
|
||||
yield Static("dupes: —", id="stat-dupes")
|
||||
yield Static("sources: —", id="stat-sources")
|
||||
yield Static("🔴 - ", classes="stat-critical", id="stat-critical")
|
||||
yield Static("🟠 - ", classes="stat-high", id="stat-high")
|
||||
yield Static("🟡 - ", classes="stat-medium", id="stat-medium")
|
||||
yield Static("🟢 - ", classes="stat-low", id="stat-low")
|
||||
yield Static("total: - ", id="stat-total")
|
||||
yield Static("unique: - ", id="stat-unique")
|
||||
yield Static("dupes: - ", id="stat-dupes")
|
||||
yield Static("sources: - ", id="stat-sources")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.set_interval(10, self.refresh_stats)
|
||||
@@ -266,7 +266,7 @@ class StatsPanel(Horizontal):
|
||||
|
||||
class ChannelPanel(Vertical):
|
||||
"""
|
||||
Bottom panel — live-editable channel list.
|
||||
Bottom panel - live-editable channel list.
|
||||
|
||||
Changes are applied immediately (Telethon handlers are re-registered).
|
||||
To make them permanent, edit config.py's WATCHED_CHANNELS manually.
|
||||
@@ -314,7 +314,7 @@ class ChannelPanel(Vertical):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(
|
||||
"📡 Channels — changes apply immediately | edit config.py to persist",
|
||||
"📡 Channels - changes apply immediately | edit config.py to persist",
|
||||
classes="panel-title",
|
||||
)
|
||||
with Horizontal(classes="controls"):
|
||||
@@ -524,7 +524,7 @@ class HitsDBScreen(Screen):
|
||||
status,
|
||||
)
|
||||
self.query_one("#db-status", Label).update(
|
||||
f" {len(rows)} row(s) — {label}"
|
||||
f" {len(rows)} row(s) - {label}"
|
||||
)
|
||||
|
||||
def _load_recent(self) -> None:
|
||||
@@ -560,7 +560,7 @@ class KeywordsScreen(Screen):
|
||||
• scorer's domain caches are rebuilt
|
||||
• The bot handler recompiles patterns on the next message automatically
|
||||
|
||||
Changes are NOT written back to config.py — a notice banner says so.
|
||||
Changes are NOT written back to config.py - a notice banner says so.
|
||||
"""
|
||||
|
||||
BINDINGS = [Binding("escape", "dismiss", "Back")]
|
||||
@@ -601,7 +601,7 @@ class KeywordsScreen(Screen):
|
||||
yield Header()
|
||||
yield Label("🔑 Keyword / Pattern Editor", classes="screen-title")
|
||||
yield Label(
|
||||
"⚠ Changes are in-memory only — copy patterns to config.py to persist across restarts.",
|
||||
"⚠ Changes are in-memory only - copy patterns to config.py to persist across restarts.",
|
||||
classes="notice",
|
||||
)
|
||||
with Horizontal(id="kw-controls"):
|
||||
@@ -671,7 +671,7 @@ class KeywordsScreen(Screen):
|
||||
except Exception as e:
|
||||
log.warning(f"Could not rebuild scorer caches: {e}")
|
||||
bus.post(bus.EvStatus(
|
||||
f"Keywords updated — {len(config.TARGET_KEYWORDS)} pattern(s) active"
|
||||
f"Keywords updated - {len(config.TARGET_KEYWORDS)} pattern(s) active"
|
||||
))
|
||||
|
||||
def action_dismiss(self) -> None:
|
||||
@@ -721,7 +721,7 @@ class MonitorApp(App):
|
||||
# The bot backend runs in its own thread with its own asyncio event
|
||||
# loop, completely isolated from Textual. Telethon spawns background
|
||||
# tasks via asyncio.ensure_future() and calls connect() which returns
|
||||
# only after its receiver loop is scheduled — both of these deadlock
|
||||
# only after its receiver loop is scheduled - both of these deadlock
|
||||
# inside Textual's managed loop. Running in a dedicated thread
|
||||
# sidesteps all of that.
|
||||
#
|
||||
@@ -767,7 +767,7 @@ class MonitorApp(App):
|
||||
"""
|
||||
Called every 100 ms by set_interval(). Drains all pending events
|
||||
from the thread-safe queue and dispatches them to the right widget.
|
||||
Runs on Textual's event loop — safe to call widget methods directly.
|
||||
Runs on Textual's event loop - safe to call widget methods directly.
|
||||
"""
|
||||
q = bus.get_bus()
|
||||
if q is None:
|
||||
@@ -854,7 +854,7 @@ class MonitorApp(App):
|
||||
|
||||
async def _bot_main(self) -> None:
|
||||
"""
|
||||
Full bot backend — runs inside the bot thread's own event loop.
|
||||
Full bot backend - runs inside the bot thread's own event loop.
|
||||
Telethon is free to schedule background tasks without interfering
|
||||
with Textual's loop.
|
||||
"""
|
||||
@@ -870,7 +870,7 @@ class MonitorApp(App):
|
||||
patterns = compile_patterns(config.TARGET_KEYWORDS)
|
||||
|
||||
bus.post(bus.EvStatus(
|
||||
f"Starting — {len(config.WATCHED_CHANNELS)} channel(s), "
|
||||
f"Starting - {len(config.WATCHED_CHANNELS)} channel(s), "
|
||||
f"{len(patterns)} pattern(s)"
|
||||
))
|
||||
|
||||
@@ -894,9 +894,9 @@ class MonitorApp(App):
|
||||
await user_client.connect()
|
||||
log.info("[bot] user_client connected, checking auth...")
|
||||
if not await user_client.is_user_authorized():
|
||||
log.error("[bot] user_client not authorized — run: python main.py --no-tui")
|
||||
log.error("[bot] user_client not authorized - run: python main.py --no-tui")
|
||||
bus.post(bus.EvStatus(
|
||||
"Not authorized — run --no-tui once to complete login",
|
||||
"Not authorized - run --no-tui once to complete login",
|
||||
level="error",
|
||||
))
|
||||
return
|
||||
@@ -962,7 +962,7 @@ class MonitorApp(App):
|
||||
log.info(f"[bot] Handler registered for {len(channels)} channel(s)")
|
||||
bus.post(bus.EvStatus(f"Watching {len(channels)} channel(s)"))
|
||||
|
||||
# Channel-change event — lives on this (bot) loop.
|
||||
# Channel-change event - lives on this (bot) loop.
|
||||
# Textual signals it thread-safely via _signal_channel_changed().
|
||||
_ch_changed = asyncio.Event()
|
||||
self._bot_loop_channel_event = _ch_changed
|
||||
@@ -971,7 +971,7 @@ class MonitorApp(App):
|
||||
bus.post(bus.EvStatus("Live listener active"))
|
||||
|
||||
await backfill_all(user_client, bot_client, patterns)
|
||||
bus.post(bus.EvStatus("Backfill complete — monitoring live"))
|
||||
bus.post(bus.EvStatus("Backfill complete - monitoring live"))
|
||||
|
||||
async def _watch_channels():
|
||||
while True:
|
||||
@@ -1009,7 +1009,7 @@ class MonitorApp(App):
|
||||
# ─── Entry point ──────────────────────────────────────────────────────────────
|
||||
|
||||
def run_tui() -> None:
|
||||
# Do NOT call bus.init_bus() here — the Queue must be created inside
|
||||
# Do NOT call bus.init_bus() here - the Queue must be created inside
|
||||
# Textual's event loop (see MonitorApp.on_mount). Calling it here
|
||||
# would bind the Queue to the outer loop which is discarded when
|
||||
# App.run() creates a new one.
|
||||
|
||||
Reference in New Issue
Block a user