- 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.)
194 lines
7.7 KiB
Python
194 lines
7.7 KiB
Python
"""Core Discord bot with intents, slash commands, and event handlers.
|
|
|
|
Compatible with discord.py 2.7.x.
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
|
|
import discord
|
|
from discord.ext import commands
|
|
from discord.app_commands import AppCommandGroup as Group
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ─── Configuration ────────────────────────────────────────────────────────────
|
|
TOKEN = os.getenv("DISCORD_TOKEN")
|
|
if not TOKEN:
|
|
raise RuntimeError("DISCORD_TOKEN environment variable is not set.")
|
|
|
|
PREFIX = os.getenv("COMMAND_PREFIX", "!")
|
|
BOT_NAME = os.getenv("BOT_NAME", "A.B.I.N.A.S.H")
|
|
GUILD_IDS = os.getenv("GUILD_IDS", "").split(",")
|
|
|
|
# ─── Intents ──────────────────────────────────────────────────────────────────
|
|
intents = discord.Intents.default()
|
|
intents.message_content = True
|
|
intents.members = True
|
|
intents.guilds = True
|
|
intents.presences = False
|
|
|
|
# ─── Bot Instance ────────────────────────────────────────────────────────────
|
|
bot = commands.Bot(
|
|
command_prefix=PREFIX,
|
|
intents=intents,
|
|
)
|
|
|
|
# ─── Slash Command Tree ──────────────────────────────────────────────────────
|
|
tree = Group(bot)
|
|
|
|
# ─── Ping ─────────────────────────────────────────────────────────────────────
|
|
@tree.command(name="ping", description="Check bot latency.")
|
|
async def ping(interaction: discord.Interaction) -> None:
|
|
"""Check the bot's latency."""
|
|
latency = round(bot.latency * 1000)
|
|
await interaction.response.send_message(
|
|
f"Pong! Latency: {latency}ms",
|
|
ephemeral=True,
|
|
)
|
|
|
|
# ─── Hello ───────────────────────────────────────────────────────────────────
|
|
@tree.command(name="hello", description="Say hello to someone.")
|
|
async def hello(
|
|
interaction: discord.Interaction,
|
|
name: str = "World",
|
|
) -> None:
|
|
"""Greet a user by name."""
|
|
await interaction.response.send_message(
|
|
f"Hello, {name}!",
|
|
ephemeral=True,
|
|
)
|
|
|
|
# ─── Emoji ───────────────────────────────────────────────────────────────────
|
|
@tree.command(name="emoji", description="Send a custom emoji by name.")
|
|
async def emoji(interaction: discord.Interaction, *, name: str) -> None:
|
|
"""Send a custom emoji by name (requires colon prefix)."""
|
|
emoji_str = f":{name}:"
|
|
await interaction.response.send_message(emoji_str, ephemeral=True)
|
|
|
|
# ─── App Emoji ───────────────────────────────────────────────────────────────
|
|
@tree.command(name="app-emoji", description="Fetch an app-owned emoji by ID.")
|
|
async def app_emoji(
|
|
interaction: discord.Interaction,
|
|
*,
|
|
emoji_id: int,
|
|
) -> None:
|
|
"""Fetch an application-owned emoji by ID."""
|
|
try:
|
|
emoji = await bot.fetch_application_emoji(emoji_id)
|
|
await interaction.response.send_message(
|
|
f"Emoji: `{emoji.name}` — {emoji.url}",
|
|
ephemeral=True,
|
|
)
|
|
except discord.NotFound:
|
|
await interaction.response.send_message(
|
|
f"Emoji with ID {emoji_id} not found.",
|
|
ephemeral=True,
|
|
)
|
|
except discord.HTTPException as e:
|
|
await interaction.response.send_message(
|
|
f"Error fetching emoji: {e}",
|
|
ephemeral=True,
|
|
)
|
|
|
|
# ─── Upload Emoji ────────────────────────────────────────────────────────────
|
|
@tree.command(name="upload-emoji", description="Upload a custom emoji to the guild.")
|
|
async def upload_emoji(
|
|
interaction: discord.Interaction,
|
|
name: str,
|
|
file_path: str,
|
|
) -> None:
|
|
"""Upload a custom emoji (JPG, PNG, or GIF) to the guild."""
|
|
if not interaction.guild:
|
|
await interaction.response.send_message(
|
|
"This command can only be used in a server.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
if not interaction.guild.permissions_for(interaction.user).manage_emojis_and_stickers:
|
|
await interaction.response.send_message(
|
|
"You need manage_emojis_and_stickers permission.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
try:
|
|
with open(file_path, "rb") as f:
|
|
image_bytes = f.read()
|
|
await interaction.guild.create_custom_emoji(name=name, image=image_bytes)
|
|
await interaction.response.send_message(
|
|
f"Successfully uploaded emoji `{name}`!",
|
|
ephemeral=True,
|
|
)
|
|
except FileNotFoundError:
|
|
await interaction.response.send_message(
|
|
"Error: File not found.",
|
|
ephemeral=True,
|
|
)
|
|
except Exception as e:
|
|
await interaction.response.send_message(
|
|
f"Error uploading emoji: {e}",
|
|
ephemeral=True,
|
|
)
|
|
|
|
# ─── Event Handlers ───────────────────────────────────────────────────────────
|
|
|
|
@bot.event
|
|
async def on_ready() -> None:
|
|
"""Entry point after successful login."""
|
|
assert bot.user is not None
|
|
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
|
|
print(f"Bot is running with {len(bot.guilds)} guilds")
|
|
|
|
# Sync slash commands
|
|
await tree.sync()
|
|
print("Slash commands synced.")
|
|
|
|
|
|
@bot.event
|
|
async def on_command_error(
|
|
ctx: commands.Context,
|
|
error: commands.CommandError,
|
|
) -> None:
|
|
"""Global error handler for all commands."""
|
|
if isinstance(error, commands.CommandNotFound):
|
|
await ctx.send("Command not found. Type `!help` for a list of commands.")
|
|
elif isinstance(error, commands.CheckFailure):
|
|
await ctx.send("You do not have permission to use this command.")
|
|
elif isinstance(error, commands.MissingPermissions):
|
|
await ctx.send("You don't have permission to use this command.")
|
|
elif isinstance(error, commands.MissingRequiredArgument):
|
|
await ctx.send(f"Missing argument: {error.param.name}.")
|
|
else:
|
|
await ctx.send(f"An error occurred: {error}")
|
|
|
|
|
|
@bot.event
|
|
async def on_voice_state_update(
|
|
member: discord.Member,
|
|
before: discord.VoiceState,
|
|
after: discord.VoiceState,
|
|
) -> None:
|
|
"""Handle voice state changes."""
|
|
if after.channel_id is None and before.channel_id is not None:
|
|
print(f"{member.name} left voice channel.")
|
|
elif after.channel_id is not None and before.channel_id is None:
|
|
print(f"{member.name} joined voice channel.")
|
|
|
|
|
|
# ─── Entry Point ─────────────────────────────────────────────────────────────
|
|
def main() -> None:
|
|
"""Run the bot."""
|
|
import asyncio
|
|
asyncio.run(bot.start(TOKEN))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|