ia_bot/handlers/gif_emotes.py
Benjamyn 43ca943cb8 feat: A.B.I.N.A.S.H Discord bot with GIF emotes, slash commands, and music
- Core bot with slash command tree (discord.py 2.7.x compatible)
- Commands: /ping, /info, /hello, /emoji, /app-emoji
- Admin: /upload-emoji, /list-emojis, /delete-emoji
- GIF emote detection and reaction handling
- Message reactions with percentage-based triggers and cooldowns
- Voice/music cog with YTDLSource, skip, volume, pause, resume, stop
- Global error handler and voice state change logging
- Environment config via .env (DISCORD_TOKEN, COMMAND_PREFIX, etc.)
2026-08-27 16:05:27 +10:00

125 lines
4.2 KiB
Python

"""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)