- 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.)
125 lines
4.2 KiB
Python
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)
|