Add a live status line to the stats panel
Shows what's happening right now underneath the table (connecting, resolving the friend, discovering targets, fetching channel history, waiting out a jittered delay before reacting, sending a reaction, removing a reaction during --undo, etc). Status updates are cheap and DB-free, using the last fetched stats for re-rendering.
This commit is contained in:
@@ -42,6 +42,10 @@ async def scan_channel_backlog(
|
||||
channel_id = getattr(channel, "id", None)
|
||||
if channel_id is None:
|
||||
return
|
||||
chan_label = getattr(channel, "name", None) or target.name
|
||||
|
||||
if display is not None:
|
||||
display.set_status(f"Checking {chan_label} for previously found, unreacted message(s)...")
|
||||
|
||||
found_messages = []
|
||||
pending_ids = await asyncio.to_thread(storage.get_pending_found_message_ids, channel_id, emoji)
|
||||
@@ -57,6 +61,9 @@ async def scan_channel_backlog(
|
||||
newest_id_seen = int(state["newest_id_seen"]) if state and state.get("newest_id_seen") else None
|
||||
after = discord.Object(id=newest_id_seen) if newest_id_seen else cutoff
|
||||
|
||||
if display is not None:
|
||||
display.set_status(f"Fetching message history for {chan_label}...")
|
||||
|
||||
try:
|
||||
async for message in channel.history(after=after, oldest_first=True, limit=None):
|
||||
if newest_id_seen is None or message.id > newest_id_seen:
|
||||
@@ -68,6 +75,7 @@ async def scan_channel_backlog(
|
||||
storage.record_message_found, message.id, channel_id, target.target_id, friend_id
|
||||
)
|
||||
if display is not None:
|
||||
display.set_status(f"Scanning {chan_label} — {len(found_messages)} found so far...")
|
||||
await display.refresh()
|
||||
|
||||
await asyncio.to_thread(storage.set_scan_state, channel_id, target.target_id, newest_id_seen, True)
|
||||
@@ -78,5 +86,7 @@ async def scan_channel_backlog(
|
||||
log.exception("HTTP error scanning channel %s", channel_id)
|
||||
return
|
||||
|
||||
for message in found_messages:
|
||||
for i, message in enumerate(found_messages, start=1):
|
||||
if display is not None:
|
||||
display.set_status(f"Reacting to message {i}/{len(found_messages)} in {chan_label}...")
|
||||
await react_fn(message, target)
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
|
||||
from rich.console import Console
|
||||
from rich.console import Console, Group
|
||||
from rich.live import Live
|
||||
from rich.panel import Panel
|
||||
from rich.progress_bar import ProgressBar
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from bot.storage import Storage
|
||||
|
||||
_TYPE_LABELS = {"guild": "server", "group_dm": "group", "dm": "DM"}
|
||||
|
||||
|
||||
def _render(stats: List[dict], friend_label: Optional[str] = None) -> Panel:
|
||||
def _render(stats: List[dict], friend_label: Optional[str] = None, status: Optional[str] = None) -> Panel:
|
||||
table = Table(expand=True)
|
||||
table.add_column("Target")
|
||||
table.add_column("Type")
|
||||
@@ -40,8 +41,12 @@ def _render(stats: List[dict], friend_label: Optional[str] = None) -> Panel:
|
||||
if not stats:
|
||||
table.add_row("(no targets discovered yet)", "-", "-", "-", "-")
|
||||
|
||||
content = table
|
||||
if status:
|
||||
content = Group(table, Text(f"▸ {status}", style="dim italic"))
|
||||
|
||||
who = f" for {friend_label}" if friend_label else ""
|
||||
return Panel(table, title=f"Reaction stats{who} — total: {total_reacted}/{total_found}", border_style="cyan")
|
||||
return Panel(content, title=f"Reaction stats{who} — total: {total_reacted}/{total_found}", border_style="cyan")
|
||||
|
||||
|
||||
class StatsDisplay:
|
||||
@@ -52,8 +57,10 @@ class StatsDisplay:
|
||||
self.console = console
|
||||
self.storage = storage
|
||||
self.friend_label: Optional[str] = None
|
||||
self.status: str = "Starting..."
|
||||
self._last_stats: List[dict] = []
|
||||
self._live = Live(
|
||||
_render([]),
|
||||
_render([], None, self.status),
|
||||
console=console,
|
||||
refresh_per_second=4,
|
||||
transient=False,
|
||||
@@ -69,10 +76,16 @@ class StatsDisplay:
|
||||
def set_friend(self, label: str) -> None:
|
||||
self.friend_label = label
|
||||
|
||||
def set_status(self, text: str) -> None:
|
||||
"""Update the "what's happening right now" line without hitting the
|
||||
DB — cheap enough to call for every jittered wait / API call."""
|
||||
self.status = text
|
||||
self._live.update(_render(self._last_stats, self.friend_label, self.status))
|
||||
|
||||
def refresh_sync(self) -> None:
|
||||
stats = self.storage.get_stats()
|
||||
self._live.update(_render(stats, self.friend_label))
|
||||
self._last_stats = self.storage.get_stats()
|
||||
self._live.update(_render(self._last_stats, self.friend_label, self.status))
|
||||
|
||||
async def refresh(self) -> None:
|
||||
stats = await asyncio.to_thread(self.storage.get_stats)
|
||||
self._live.update(_render(stats, self.friend_label))
|
||||
self._last_stats = await asyncio.to_thread(self.storage.get_stats)
|
||||
self._live.update(_render(self._last_stats, self.friend_label, self.status))
|
||||
|
||||
@@ -46,8 +46,14 @@ async def react_to_message(
|
||||
if already_reacted:
|
||||
return False
|
||||
|
||||
await asyncio.sleep(human_jitter())
|
||||
chan_label = getattr(message.channel, "name", None) or target.name
|
||||
delay = human_jitter()
|
||||
if display is not None:
|
||||
display.set_status(f"Waiting {delay:.1f}s before reacting in {chan_label}...")
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
if display is not None:
|
||||
display.set_status(f"Sending reaction in {chan_label}...")
|
||||
try:
|
||||
await message.add_reaction(emoji)
|
||||
except discord.HTTPException:
|
||||
|
||||
11
bot/undo.py
11
bot/undo.py
@@ -24,6 +24,9 @@ async def undo_all_reactions(client: discord.Client, storage: Storage, display:
|
||||
emoji = row["emoji"]
|
||||
should_clear_log = True
|
||||
|
||||
if display is not None:
|
||||
display.set_status(f"Removing reaction from message {message_id} ({i}/{total})...")
|
||||
|
||||
try:
|
||||
channel = client.get_channel(channel_id) or await client.fetch_channel(channel_id)
|
||||
message = await channel.fetch_message(message_id)
|
||||
@@ -41,11 +44,17 @@ async def undo_all_reactions(client: discord.Client, storage: Storage, display:
|
||||
if display is not None:
|
||||
await display.refresh()
|
||||
|
||||
await asyncio.sleep(human_jitter())
|
||||
delay = human_jitter()
|
||||
if display is not None:
|
||||
display.set_status(f"Waiting {delay:.1f}s before next removal...")
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
if display is not None:
|
||||
display.set_status("Clearing channel resume points and found-message log...")
|
||||
cleared = await asyncio.to_thread(storage.clear_scan_state)
|
||||
await asyncio.to_thread(storage.reset_message_counts)
|
||||
if display is not None:
|
||||
display.set_status("Undo complete.")
|
||||
await display.refresh()
|
||||
log.info(
|
||||
"Undo complete: removed %d/%d reaction(s), cleared %d channel resume point(s) — "
|
||||
|
||||
Reference in New Issue
Block a user