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
This commit is contained in:
114
tui/events.py
Normal file
114
tui/events.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
tui_events.py — Thread-safe event bus between the bot backend and the TUI.
|
||||
|
||||
The bot backend runs in a dedicated thread with its own asyncio event loop
|
||||
(completely isolated from Textual's loop). Events are posted via a standard
|
||||
queue.Queue (thread-safe), and the TUI consumer polls it from Textual's loop
|
||||
using asyncio.get_event_loop().run_in_executor() bridging.
|
||||
|
||||
post() is safe to call from any thread or any asyncio loop.
|
||||
"""
|
||||
|
||||
import queue
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
# Thread-safe queue — works across the bot thread and Textual's thread.
|
||||
_queue: queue.Queue | None = None
|
||||
_queue_lock = threading.Lock()
|
||||
|
||||
# Set to True when the TUI is running so tdl pipes output instead of
|
||||
# writing directly to the terminal.
|
||||
tui_active: bool = False
|
||||
|
||||
|
||||
def init_bus() -> queue.Queue:
|
||||
"""Call once from MonitorApp.on_mount() to create the queue."""
|
||||
global _queue, tui_active
|
||||
_queue = queue.Queue()
|
||||
tui_active = True
|
||||
return _queue
|
||||
|
||||
|
||||
def get_bus() -> queue.Queue | None:
|
||||
return _queue
|
||||
|
||||
|
||||
def post(event: Any) -> None:
|
||||
"""Fire-and-forget from any thread. Silently drops if bus not up."""
|
||||
if _queue is not None:
|
||||
try:
|
||||
_queue.put_nowait(event)
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
|
||||
# ─── Event types ──────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class EvDownloadQueued:
|
||||
"""A file has been accepted and is waiting for tdl."""
|
||||
batch_id: str
|
||||
filename: str
|
||||
size_mb: float
|
||||
source: str
|
||||
password: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvDownloadStarted:
|
||||
"""tdl has begun transferring this file."""
|
||||
batch_id: str
|
||||
filename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvDownloadDone:
|
||||
"""File fully downloaded (tdl or Telethon fallback)."""
|
||||
batch_id: str
|
||||
filename: str
|
||||
via: str # "tdl" | "telethon"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvDownloadFailed:
|
||||
"""All download attempts failed."""
|
||||
batch_id: str
|
||||
filename: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvTdlOutput:
|
||||
"""A line of output from tdl's stdout/stderr (TUI mode only)."""
|
||||
line: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvHit:
|
||||
"""A scored credential hit to display in the hits panel."""
|
||||
severity: str
|
||||
raw: str
|
||||
source: str
|
||||
filename: str
|
||||
reasons: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvChannelAdded:
|
||||
"""A channel was added to the live watch list."""
|
||||
channel: str | int
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvChannelRemoved:
|
||||
"""A channel was removed from the live watch list."""
|
||||
channel: str | int
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvStatus:
|
||||
"""Generic one-line status message (startup, errors, etc.)."""
|
||||
text: str
|
||||
level: str = "info" # "info" | "warning" | "error"
|
||||
Reference in New Issue
Block a user