import asyncio import logging import random import time import discord from discord.ext import commands logger = logging.getLogger("bcs.reactions") class ReactionCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot # Per-channel timestamp of last reaction self._last_reaction: dict[int, float] = {} @commands.Cog.listener() async def on_message(self, message: discord.Message): if message.author.bot or not message.guild: return cfg = self.bot.config.get("reactions", {}) if not cfg.get("enabled", False): return # Skip empty messages if not message.content or not message.content.strip(): return # Channel exclusion excluded = cfg.get("excluded_channels", []) if excluded: ch_name = getattr(message.channel, "name", "") if message.channel.id in excluded or ch_name in excluded: return # RNG gate chance = cfg.get("chance", 0.15) if random.random() > chance: return # Per-channel cooldown ch_id = message.channel.id cooldown = cfg.get("cooldown_seconds", 45) now = time.monotonic() if now - self._last_reaction.get(ch_id, 0) < cooldown: return # Fire and forget so we don't block anything asyncio.create_task(self._try_react(message, ch_id)) async def _try_react(self, message: discord.Message, ch_id: int): try: emoji = await self.bot.llm.pick_reaction( message.content, message.channel.name, ) if not emoji: return await message.add_reaction(emoji) self._last_reaction[ch_id] = time.monotonic() logger.info( "Reacted %s to %s in #%s: %s", emoji, message.author.display_name, message.channel.name, message.content[:60], ) except discord.HTTPException as e: # Invalid emoji or missing permissions — silently skip logger.debug("Reaction failed: %s", e) except Exception: logger.exception("Unexpected reaction error") async def setup(bot: commands.Bot): await bot.add_cog(ReactionCog(bot))