ia_bot/bot.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

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