Stats panel: a persistent spinner marks whichever target is currently being scanned/reacted to, and jittered waits show a live-ticking countdown bar instead of a static "waiting Xs" line. Both the status line and countdown row are always rendered (blank when idle) so their appearing/disappearing no longer shifts the table's height. Backlog: channel_scan_state now tracks oldest_covered_at alongside newest_id_seen. If days_back is widened after a channel already has a resume checkpoint, the forward-only diff would never look back far enough to notice — now the newly-exposed gap gets backfilled first.
82 lines
2.2 KiB
Python
82 lines
2.2 KiB
Python
import asyncio
|
|
import logging
|
|
import random
|
|
from typing import Optional
|
|
|
|
import discord
|
|
|
|
from bot.discovery import Target
|
|
from bot.display import StatsDisplay
|
|
from bot.storage import Storage
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def human_jitter() -> float:
|
|
"""Gaussian delay centered a few seconds in, clamped to a natural-looking range."""
|
|
delay = random.gauss(4.0, 1.5)
|
|
return max(1.5, min(delay, 8.0))
|
|
|
|
|
|
async def react_to_message(
|
|
storage: Storage,
|
|
message: discord.Message,
|
|
target: Target,
|
|
friend_id: int,
|
|
emoji: str,
|
|
source: str,
|
|
display: Optional[StatsDisplay] = None,
|
|
count_found: bool = True,
|
|
) -> bool:
|
|
if message.author.id != friend_id:
|
|
return False
|
|
|
|
if count_found:
|
|
await asyncio.to_thread(
|
|
storage.record_message_found, message.id, message.channel.id, target.target_id, friend_id
|
|
)
|
|
if display is not None:
|
|
await display.refresh()
|
|
|
|
already_reacted = any(
|
|
reaction.me and str(reaction.emoji) == emoji for reaction in message.reactions
|
|
)
|
|
if not already_reacted:
|
|
already_reacted = await asyncio.to_thread(storage.has_reacted, message.id, emoji)
|
|
if already_reacted:
|
|
return False
|
|
|
|
chan_label = getattr(message.channel, "name", None) or target.name
|
|
delay = human_jitter()
|
|
if display is not None:
|
|
display.start_countdown(delay, f"reacting in {chan_label}")
|
|
await asyncio.sleep(delay)
|
|
|
|
if display is not None:
|
|
display.clear_countdown()
|
|
display.set_status(f"Sending reaction in {chan_label}...")
|
|
try:
|
|
await message.add_reaction(emoji)
|
|
except discord.HTTPException:
|
|
log.exception("Failed to react to message %s in channel %s", message.id, message.channel.id)
|
|
return False
|
|
|
|
inserted = await asyncio.to_thread(
|
|
storage.record_reaction,
|
|
message.id,
|
|
message.channel.id,
|
|
target.target_id,
|
|
friend_id,
|
|
emoji,
|
|
source,
|
|
message.created_at,
|
|
)
|
|
if inserted:
|
|
log.info(
|
|
"Reacted to message in %s [%s] (%s): %s",
|
|
target.name, target.target_type, source, message.jump_url,
|
|
)
|
|
if display is not None:
|
|
await display.refresh()
|
|
return inserted
|