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, ) -> bool: if message.author.id != friend_id: return False 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 await asyncio.sleep(human_jitter()) 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