"""GIF emote handler — parses emoji strings, fetches from CDN, sends reactions.""" import re import logging from typing import Optional import discord from discord.ext import commands import aiohttp import discordemojiparser as edp logger = logging.getLogger(__name__) # ─── Configuration ─────────────────────────────────────────────────────────── GIF_EMOTE_CDN = "https://media.discordapp.net/gifs/" REACTION_CHANCE = 0.05 # 5% chance to trigger on GIF emote mention REACTION_COOLDOWN = 60 # seconds per channel # ─── Emoji Parser ───────────────────────────────────────────────────────────── def parse_emoji(emoji_str: str) -> Optional[discord.Emoji]: """Parse an emoji string (e.g., :cat:) into a discord.Emoji object.""" emoji_str = emoji_str.strip() if not emoji_str.startswith(":") or not emoji_str.endswith(":"): return None emoji_name = emoji_str[1:-1] try: emoji_obj = edp.parse(emoji_name) except Exception as e: logger.debug(f"Failed to parse emoji '{emoji_name}': {e}") return None if not emoji_obj: return None guild_id = emoji_obj.get("guild_id") emoji_id = emoji_obj.get("id") if guild_id is None or emoji_id is None: return None try: guild = bot.get_guild(guild_id) if guild is None: return None return guild.get_emoji(emoji_id) except (discord.NotFound, AttributeError) as e: logger.debug(f"Emoji not found: guild={guild_id}, emoji={emoji_id}") return None # ─── Reaction Handler ──────────────────────────────────────────────────────── class GIFReactionHandler: """Handles GIF emote reactions on messages.""" def __init__(self, bot: commands.Bot) -> None: self.bot = bot self._cooldowns: dict[int, float] = {} # channel_id -> next_available async def handle_reaction(self, message: discord.Message) -> None: """Check if a message mentions a GIF emote and react accordingly.""" channel_id = message.channel.id # Enforce cooldown now = discord.utils.utcnow().timestamp() if channel_id in self._cooldowns: if now < self._cooldowns[channel_id]: return # Check if message contains a GIF emote if await self._check_and_react(message): self._cooldown(channel_id) async def _check_and_react(self, message: discord.Message) -> bool: """Check if message mentions a GIF emote and add reaction.""" # Look for emoji mentions in the message emoji_mentions = re.findall(r":(\w+):", message.content) for name in emoji_mentions: emoji_obj = parse_emoji(f":{name}:") if emoji_obj is None: continue # Determine the GIF URL gif_url = f"{GIF_EMOTE_CDN}{emoji_obj.id}.gif" # Add reaction try: await message.add_reaction(emoji_obj) return True except discord.Forbidden: pass return False def _cooldown(self, channel_id: int) -> None: """Set cooldown for a channel.""" self._cooldowns[channel_id] = discord.utils.utcnow().timestamp() + REACTION_COOLDOWN # ─── Bot Integration ───────────────────────────────────────────────────────── async def setup(bot: commands.Bot) -> None: """Register the GIF reaction handler with the bot.""" handler = GIFReactionHandler(bot) @bot.event async def on_message(message: discord.Message) -> None: """Check for GIF emote mentions in messages.""" if message.author == bot.user: return if message.author.bot: return await handler.handle_reaction(message)