Compare commits
No commits in common. "926fc4b96afb65ab8ea568bbb056e373c4ff0f3d" and "528beb76532011519857d5ea64c9cc8e1162d9f7" have entirely different histories.
926fc4b96a
...
528beb7653
8
.gitignore
vendored
8
.gitignore
vendored
@ -56,11 +56,3 @@ logs/
|
|||||||
# Type stubs
|
# Type stubs
|
||||||
*.pyi
|
*.pyi
|
||||||
.pyi
|
.pyi
|
||||||
|
|
||||||
.omo/
|
|
||||||
|
|
||||||
# Data files
|
|
||||||
emotes/*.webp
|
|
||||||
emotes/*.png
|
|
||||||
emotes/*.gif
|
|
||||||
emotes/*.jpg
|
|
||||||
193
bot.py
Normal file
193
bot.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
"""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()
|
||||||
121
commands/admin.py
Normal file
121
commands/admin.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"""Admin emoji management slash commands."""
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.app_commands import Command
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
upload_emoji.__signature__ = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
async def list_emojis(interaction: discord.Interaction) -> None:
|
||||||
|
"""List all custom emojis in the guild."""
|
||||||
|
if not interaction.guild:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"This command can only be used in a server.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
emojis = await interaction.guild.emojis()
|
||||||
|
emoji_list = []
|
||||||
|
for emoji in emojis:
|
||||||
|
emoji_list.append(f"👍 `{emoji.name}` (ID: {emoji.id})")
|
||||||
|
|
||||||
|
if not emoji_list:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"No custom emojis found in this guild.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("\n".join(emoji_list), ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
list_emojis.__signature__ = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_emoji(interaction: discord.Interaction, *, name: str) -> None:
|
||||||
|
"""Delete a custom emoji."""
|
||||||
|
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:
|
||||||
|
emojis = await interaction.guild.emojis()
|
||||||
|
emoji = None
|
||||||
|
for e in emojis:
|
||||||
|
if e.name.lower() == name.lower():
|
||||||
|
emoji = e
|
||||||
|
break
|
||||||
|
|
||||||
|
if emoji is None:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Emoji `{name}` not found.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await emoji.delete()
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Successfully deleted emoji `{name}`!",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
except discord.NotFound:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Emoji `{name}` not found.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Error deleting emoji: {e}",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
delete_emoji.__signature__ = None # type: ignore
|
||||||
37
commands/emoji.py
Normal file
37
commands/emoji.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""Emoji slash commands."""
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.app_commands import Command
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
emoji.__signature__ = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
async def app_emoji(interaction: discord.Interaction, *, emoji_id: int) -> None:
|
||||||
|
"""Fetch an application-owned emoji by ID."""
|
||||||
|
try:
|
||||||
|
emoji = await interaction.client.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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
app_emoji.__signature__ = None # type: ignore
|
||||||
16
commands/hello.py
Normal file
16
commands/hello.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""Hello slash command."""
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.app_commands import Command
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
hello.__signature__ = None # type: ignore
|
||||||
124
handlers/gif_emotes.py
Normal file
124
handlers/gif_emotes.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
"""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)
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import discord
|
|
||||||
import logging
|
|
||||||
import aiohttp
|
|
||||||
import asyncio
|
|
||||||
import io
|
|
||||||
import threading
|
|
||||||
from pathlib import Path
|
|
||||||
import glob
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LoveEmote():
|
|
||||||
def __init__(self, name, path):
|
|
||||||
self.name = name
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
|
|
||||||
class LoveEmoteCog(commands.Cog):
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self.emotes = list()
|
|
||||||
|
|
||||||
async def cog_load(self):
|
|
||||||
PROJ_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
emotes = []
|
|
||||||
types = ["*.webp", "*.gif"]
|
|
||||||
for t in types:
|
|
||||||
emotes = glob.glob(f"{PROJ_DIR}/emotes/{t}")
|
|
||||||
for e in emotes:
|
|
||||||
p = Path(e)
|
|
||||||
self.emotes.append(LoveEmote(p.stem, p))
|
|
||||||
logger.debug(self.emotes)
|
|
||||||
logger.debug(f"{PROJ_DIR}/{t}")
|
|
||||||
# self.emotes = emotes
|
|
||||||
|
|
||||||
@commands.command(name="emote_list", aliases=['el'], description="get list of gif emotes")
|
|
||||||
async def emote_list(self, ctx: commands.Context):
|
|
||||||
"""[emote_list | el] List emotes available to the bot"""
|
|
||||||
data = "\n".join([f" - {e.name}" for e in self.emotes])
|
|
||||||
await ctx.send(data)
|
|
||||||
|
|
||||||
@commands.command(name="emote_send", aliases=["e"], description="Send an emote to the channel")
|
|
||||||
async def send_emote(self, ctx: commands.Context, emote_name: str):
|
|
||||||
"""[emote_send | e] Send an emote to the channel"""
|
|
||||||
for e in self.emotes:
|
|
||||||
if e.name == emote_name:
|
|
||||||
with open(e.path, 'rb') as f:
|
|
||||||
await ctx.message.delete()
|
|
||||||
data = io.BytesIO(f.read())
|
|
||||||
file_data = discord.File(data, filename=f"{e.name}.{e.path.suffix}")
|
|
||||||
await ctx.send(f"{ctx.author.display_name} sent {e.name}", file=file_data)
|
|
||||||
return
|
|
||||||
await ctx.send("Emote not found, contact aram to create one")
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
|
||||||
"""Register the voice_gen cog with the bot."""
|
|
||||||
await bot.add_cog(LoveEmoteCog(bot))
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
async def process_meme(message):
|
|
||||||
memes = {"software": ["mercury", "smws", "cpanel", "whm"],
|
|
||||||
"perks": ["office", "fishbowl"],
|
|
||||||
"insults_to_us": []}
|
|
||||||
for meme in memes:
|
|
||||||
for token in message.content.lower().split():
|
|
||||||
if token in memes[meme]:
|
|
||||||
match meme:
|
|
||||||
case "software":
|
|
||||||
await message.channel.send("That sounds like software to me")
|
|
||||||
case "perks":
|
|
||||||
await message.channel.send("Something snarky about dinner")
|
|
||||||
case "insults_to_us":
|
|
||||||
await message.channel.send("The \"wealth\" of knowledge")
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import logging
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class UtilCog(commands.Cog):
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.command(name="ping", description="Check bot latency.")
|
|
||||||
async def ping_command(self, ctx: commands.Context) -> None:
|
|
||||||
"""Check the bot's latency."""
|
|
||||||
latency = round(self.bot.latency * 1000)
|
|
||||||
await ctx.send(f"Pong! Latency: {latency}ms")
|
|
||||||
|
|
||||||
@commands.command(name="info", description="Show bot information.")
|
|
||||||
async def info_command(self, ctx: commands.Context) -> None:
|
|
||||||
"""Show bot information."""
|
|
||||||
await ctx.send(
|
|
||||||
f"**{ctx.guild.name if ctx.guild else 'DM'}**\n"
|
|
||||||
f"Bot: `{self.bot.user.name}` (ID: {self.bot.user.id})\n"
|
|
||||||
f"Latency: `{self.bot.latency:.3f}s`"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command(name="hello", description="Say hello to someone.")
|
|
||||||
async def hello_command(self, ctx: commands.Context, name: str = "World"):
|
|
||||||
"""Say hello to someone."""
|
|
||||||
await ctx.send(f"Hello, {name}!")
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
|
||||||
"""Register the voice_gen cog with the bot."""
|
|
||||||
await bot.add_cog(UtilCog(bot))
|
|
||||||
@ -6,7 +6,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog, Context
|
from discord.ext.commands import Cog, Context, has_permissions
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -57,42 +57,18 @@ class MusicCog(Cog):
|
|||||||
self._is_playing = False
|
self._is_playing = False
|
||||||
self._stop_requested = False
|
self._stop_requested = False
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def join(self, ctx: Context, c=None):
|
|
||||||
if c is None:
|
|
||||||
# Check if the user is in a voice channel
|
|
||||||
if ctx.author.voice:
|
|
||||||
channel = ctx.author.voice.channel
|
|
||||||
self._voice_channel = await channel.connect()
|
|
||||||
await ctx.send(f"Joined {channel.name}!")
|
|
||||||
else:
|
|
||||||
await ctx.send("You must be in a voice channel first!")
|
|
||||||
channel = discord.utils.get(ctx.guild.voice_channels, name=c)
|
|
||||||
print(channel)
|
|
||||||
await channel.connect()
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def leave(self, ctx):
|
|
||||||
# Check if the bot is in a voice channel in this server
|
|
||||||
if ctx.voice_client:
|
|
||||||
await ctx.voice_client.disconnect()
|
|
||||||
self._voice_channel = None
|
|
||||||
await ctx.send("Disconnected from the voice channel.")
|
|
||||||
else:
|
|
||||||
await ctx.send("I am not in a voice channel.")
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def play(self, ctx: Context, source: str) -> None:
|
async def play(self, ctx: Context, source: str) -> None:
|
||||||
"""Play a track from a source (URL or file)."""
|
"""Play a track from a source (URL or file)."""
|
||||||
if not self._is_playing:
|
if not self._is_playing:
|
||||||
|
await self._ensure_voice(ctx)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
track = await YTDLSource.from_url(source, source_type="youtube", loop=False)
|
track = await YTDLSource.from_url(source, source_type="youtube", loop=False)
|
||||||
self._current_queue.append(track)
|
self._current_queue.append(track)
|
||||||
await self._play_next()
|
await self._play_next()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error playing source {source}: {e}")
|
logger.error(f"Error playing source {source}: {e}")
|
||||||
await ctx.send(f"Error playing: {e}")
|
await ctx.send(f"Error playing: {e}")
|
||||||
|
|
||||||
async def _play_next(self) -> None:
|
async def _play_next(self) -> None:
|
||||||
"""Play the next track in the queue."""
|
"""Play the next track in the queue."""
|
||||||
@ -191,7 +167,7 @@ class YTDLSource(discord.PCMVolumeTransformer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def from_url(cls, url, source_type="youtube", loop=False):
|
async def from_url(cls, url, source_type="youtube", loop=False):
|
||||||
"""Create a source from a URL."""
|
"""Create a source from a URL."""
|
||||||
import yt_dlp as youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
"format": "bestaudio/best",
|
"format": "bestaudio/best",
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
import discord
|
|
||||||
import logging
|
|
||||||
import aiohttp
|
|
||||||
import io
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class VoiceGeneratorProfile():
|
|
||||||
def __init__(self, name, uuid, model="audio-cpp-chatterbox"):
|
|
||||||
self.name = name
|
|
||||||
self.model = model
|
|
||||||
self.uuid = uuid
|
|
||||||
|
|
||||||
|
|
||||||
class VoiceCog(commands.Cog):
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self._running = False
|
|
||||||
self._current_voice = "Pricey"
|
|
||||||
self.api_url = "http://10.6.9.4:8080"
|
|
||||||
|
|
||||||
async def cog_load(self):
|
|
||||||
data = await self._get_voices()
|
|
||||||
self.voices = [VoiceGeneratorProfile(d['name'], d['id']) for d in data.get('data')]
|
|
||||||
self._current_voice = self.voices[0]
|
|
||||||
|
|
||||||
async def _get_voices(self):
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
data = await session.get(f"{self.api_url}/api/voice-profiles")
|
|
||||||
data = await data.json()
|
|
||||||
return data
|
|
||||||
|
|
||||||
@commands.command(name="voice_set", description="Change the active voice by name")
|
|
||||||
async def voice_set(self, ctx, voice_name):
|
|
||||||
"""Set the voice for the generator to use"""
|
|
||||||
for v in self.voices:
|
|
||||||
if v.name.lower() == voice_name.lower():
|
|
||||||
self._current_voice = v
|
|
||||||
await ctx.send(f"Swapped voice to {v.name}")
|
|
||||||
return
|
|
||||||
await ctx.send("Voice not found asshole")
|
|
||||||
|
|
||||||
@commands.command(name="voice_list", description="List available voices")
|
|
||||||
async def voice_list(self, ctx: commands.Context):
|
|
||||||
"""List available voices"""
|
|
||||||
await ctx.channel.send(f"{"\n".join([f" - {v.name}" for v in self.voices])}")
|
|
||||||
|
|
||||||
@commands.command(name="voice_info", description="See the status of the voice generator :tm:")
|
|
||||||
async def voice_info(self, ctx: commands.Context):
|
|
||||||
"""View status of the voice generator"""
|
|
||||||
data = f"""
|
|
||||||
- Running: {self._running}
|
|
||||||
- Current Voice: {self._current_voice.name}
|
|
||||||
"""
|
|
||||||
await ctx.channel.send(data)
|
|
||||||
|
|
||||||
@commands.command(name="voice_gen", description="Generate an audio clip using the currently configured voice")
|
|
||||||
async def voice_gen(self, ctx: commands.Context, prompt: str):
|
|
||||||
"""Generate some voices"""
|
|
||||||
if self._running:
|
|
||||||
await ctx.send("All lines are currently busy, please try again later")
|
|
||||||
return
|
|
||||||
data = {"model": self._current_voice.model,
|
|
||||||
"voice": f"localai://voice-profiles/{self._current_voice.uuid}",
|
|
||||||
"input": prompt,
|
|
||||||
"stream": False}
|
|
||||||
await ctx.channel.send("Working on it!")
|
|
||||||
self._running = True
|
|
||||||
try:
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
async with aiohttp.ClientSession(headers=headers) as session:
|
|
||||||
resp = await session.post(f"{self.api_url}/tts", json=data)
|
|
||||||
d = await resp.read()
|
|
||||||
# logging.debug(d)
|
|
||||||
voice_data = io.BytesIO(d)
|
|
||||||
self._running = False
|
|
||||||
filedata = discord.File(voice_data, filename=f"{self._current_voice.name.replace(" ", "_").lower()}-{prompt[:10].replace(" ", "_").lower()}.wav")
|
|
||||||
await ctx.channel.send(file=filedata)
|
|
||||||
except Exception as e:
|
|
||||||
self._running = False
|
|
||||||
logging.error(e)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
|
||||||
"""Register the voice_gen cog with the bot."""
|
|
||||||
await bot.add_cog(VoiceCog(bot))
|
|
||||||
203
main.py
203
main.py
@ -1,38 +1,106 @@
|
|||||||
"""Entry point for the Discord bot.
|
"""Entry point for the Discord bot.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
./start_server.sh
|
python main.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import random
|
import logging.handlers
|
||||||
|
import logging.config
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# ─── Debug Mode ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DEBUG = os.getenv("APP_DEBUG", "0") == "1"
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print("[DEBUG] Debug mode is ENABLED. Logs will be printed.")
|
||||||
|
else:
|
||||||
|
print("[DEBUG] Debug mode is DISABLED. Logs will NOT be printed.")
|
||||||
|
|
||||||
|
# ─── Logging Configuration ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log_file = os.path.join(project_root, "bot.log")
|
||||||
|
|
||||||
|
# Create a rotating file handler (max 10 MB, keep 5 files)
|
||||||
|
file_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
log_file,
|
||||||
|
maxBytes=10 * 1024 * 1024, # 10 MB
|
||||||
|
backupCount=5,
|
||||||
|
)
|
||||||
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
file_handler.setFormatter(logging.Formatter(
|
||||||
|
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Console handler (only when DEBUG is enabled)
|
||||||
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
console_handler.setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
console_handler.setFormatter(logging.Formatter(
|
||||||
|
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Root logger
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
root_logger.setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
|
||||||
|
# Suppress noisy third-party logs
|
||||||
|
logging.getLogger("discord").setLevel(logging.DEBUG if DEBUG else logging.INFO)
|
||||||
|
logging.getLogger("discord.http").setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
logging.getLogger("discord.gateway").setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
logging.getLogger("discord.utils").setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
logging.getLogger("discord.app_commands").setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
logging.getLogger("discord.ext.commands").setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
logging.getLogger("discord.errors").setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
logging.getLogger("discord.client").setLevel(logging.DEBUG if DEBUG else logging.WARNING)
|
||||||
|
|
||||||
|
# ─── Logging Configuration ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG if DEBUG else logging.WARNING,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
handlers=[file_handler, console_handler],
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Suppress the specific RuntimeWarning about process_commands coroutine.
|
||||||
|
# This warning is emitted by discord.py internally and is harmless.
|
||||||
|
warnings.filterwarnings(
|
||||||
|
"ignore",
|
||||||
|
message="coroutine 'BotBase.process_commands' was never awaited",
|
||||||
|
category=RuntimeWarning,
|
||||||
|
module="discord",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.app_commands import Command as AppCommand, CommandTree
|
from discord.app_commands import Command as AppCommand, CommandTree
|
||||||
|
|
||||||
# from handlers.gif_emotes import setup as setup_gif_emotes
|
from handlers.gif_emotes import setup as setup_gif_emotes
|
||||||
# from handlers.reactions import setup as setup_reactions
|
from handlers.reactions import setup as setup_reactions
|
||||||
from handlers.voice import setup as setup_voice
|
from handlers.voice import setup as setup_voice
|
||||||
from handlers.voice_gen import setup as setup_voice_gen
|
from cogs.admin import setup as setup_admin
|
||||||
from handlers.love_emotes import setup as setup_love_emotes
|
from commands.ping import ping, info
|
||||||
from handlers.utils import setup as setup_utils
|
from commands.hello import hello
|
||||||
from handlers.memes import process_meme
|
from commands.emoji import emoji, app_emoji
|
||||||
from commands.ping import ping
|
from commands.admin import upload_emoji, list_emojis, delete_emoji
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# Load environment variables from .env file
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.DEBUG if os.environ.get("APP_DEBUG") == "1" else logging.INFO,
|
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
handlers=[logging.StreamHandler(sys.stdout),
|
|
||||||
logging.FileHandler("bot.log")]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Bot Setup ───────────────────────────────────────────────────────────────
|
# ─── Bot Setup ───────────────────────────────────────────────────────────────
|
||||||
@ -40,8 +108,6 @@ logging.basicConfig(
|
|||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
intents.message_content = True
|
intents.message_content = True
|
||||||
intents.members = True
|
intents.members = True
|
||||||
intents.messages = True
|
|
||||||
intents.voice_states = True
|
|
||||||
|
|
||||||
bot = commands.Bot(
|
bot = commands.Bot(
|
||||||
command_prefix="!",
|
command_prefix="!",
|
||||||
@ -52,11 +118,53 @@ tree: CommandTree = bot.tree
|
|||||||
|
|
||||||
# ─── Slash Commands ──────────────────────────────────────────────────────────
|
# ─── Slash Commands ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Register slash commands from modules
|
||||||
tree.add_command(AppCommand(name="ping", callback=ping, description="Check bot latency."))
|
tree.add_command(AppCommand(name="ping", callback=ping, description="Check bot latency."))
|
||||||
|
tree.add_command(AppCommand(name="info", callback=info, description="Show bot information."))
|
||||||
|
tree.add_command(AppCommand(name="hello", callback=hello, description="Say hello to someone."))
|
||||||
|
tree.add_command(AppCommand(name="emoji", callback=emoji, description="Send a custom emoji by name."))
|
||||||
|
tree.add_command(AppCommand(name="app-emoji", callback=app_emoji, description="Fetch an app-owned emoji by ID."))
|
||||||
|
tree.add_command(AppCommand(name="upload-emoji", callback=upload_emoji, description="Upload a custom emoji to the guild."))
|
||||||
|
tree.add_command(AppCommand(name="list-emojis", callback=list_emojis, description="List all custom emojis in the guild."))
|
||||||
|
tree.add_command(AppCommand(name="delete-emoji", callback=delete_emoji, description="Delete a custom emoji."))
|
||||||
|
|
||||||
|
# ─── Prefix Commands ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bot.command(name="ping", description="Check bot latency.")
|
||||||
|
async def ping_command(ctx: commands.Context) -> None:
|
||||||
|
"""Check the bot's latency."""
|
||||||
|
latency = round(bot.latency * 1000)
|
||||||
|
await ctx.send(f"Pong! Latency: {latency}ms")
|
||||||
|
|
||||||
|
@bot.command(name="info", description="Show bot information.")
|
||||||
|
async def info_command(ctx: commands.Context) -> None:
|
||||||
|
"""Show bot information."""
|
||||||
|
await ctx.send(
|
||||||
|
f"**{ctx.guild.name if ctx.guild else 'DM'}**\n"
|
||||||
|
f"Bot: `{bot.user.name}` (ID: {bot.user.id})\n"
|
||||||
|
f"Latency: `{bot.latency:.3f}s`"
|
||||||
|
)
|
||||||
|
|
||||||
|
@bot.command(name="hello", description="Say hello to someone.")
|
||||||
|
async def hello_command(ctx: commands.Context, name: str = "World") -> None:
|
||||||
|
"""Say hello to someone."""
|
||||||
|
await ctx.send(f"Hello, {name}!")
|
||||||
|
|
||||||
|
@bot.command(name="list-commands", description="List all available commands.")
|
||||||
|
async def list_commands(ctx: commands.Context) -> None:
|
||||||
|
"""List all available commands."""
|
||||||
|
cmds = list(bot.commands.values())
|
||||||
|
cmd_list = []
|
||||||
|
for cmd in cmds:
|
||||||
|
cmd_list.append(f" `{cmd.name}` — {cmd.description}")
|
||||||
|
|
||||||
|
await ctx.send(
|
||||||
|
f"**{bot.user.name} — Command List**\n\n"
|
||||||
|
+ "\n".join(cmd_list)
|
||||||
|
)
|
||||||
|
|
||||||
# ─── Event Handlers ──────────────────────────────────────────────────────────
|
# ─── Event Handlers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready() -> None:
|
async def on_ready() -> None:
|
||||||
"""Entry point after successful login."""
|
"""Entry point after successful login."""
|
||||||
@ -64,23 +172,22 @@ async def on_ready() -> None:
|
|||||||
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
|
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
|
||||||
print(f"Bot is running with {len(bot.guilds)} guilds")
|
print(f"Bot is running with {len(bot.guilds)} guilds")
|
||||||
|
|
||||||
# Register handlers
|
|
||||||
await setup_voice(bot)
|
|
||||||
await setup_love_emotes(bot)
|
|
||||||
await setup_utils(bot)
|
|
||||||
await setup_voice_gen(bot)
|
|
||||||
|
|
||||||
# Sync slash commands
|
# Sync slash commands
|
||||||
await tree.sync()
|
await tree.sync()
|
||||||
print("Slash commands synced.")
|
print("Slash commands synced.")
|
||||||
|
|
||||||
|
# Register handlers
|
||||||
|
await setup_gif_emotes(bot)
|
||||||
|
await setup_reactions(bot)
|
||||||
|
await setup_voice(bot)
|
||||||
|
await setup_admin(bot)
|
||||||
|
|
||||||
print("All handlers registered.")
|
print("All handlers registered.")
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None:
|
async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None:
|
||||||
"""Global error handler for all commands."""
|
"""Global error handler for all commands."""
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.debug(f"Command error for {ctx.author.name} ({ctx.author.id}): {error}")
|
logger.debug(f"Command error for {ctx.author.name} ({ctx.author.id}): {error}")
|
||||||
if isinstance(error, commands.CommandNotFound):
|
if isinstance(error, commands.CommandNotFound):
|
||||||
await ctx.send("Command not found. Type `!list-commands` for a list of commands.")
|
await ctx.send("Command not found. Type `!list-commands` for a list of commands.")
|
||||||
@ -98,7 +205,7 @@ async def on_command_error(ctx: commands.Context, error: commands.CommandError)
|
|||||||
f"This command is on cooldown. Try again in {remaining} seconds."
|
f"This command is on cooldown. Try again in {remaining} seconds."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(f"An error occurred: {error.__traceback__.__str__()}")
|
await ctx.send(f"An error occurred: {error}")
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
@ -107,22 +214,34 @@ async def on_message(message: discord.Message) -> None:
|
|||||||
# Ignore bot's own messages
|
# Ignore bot's own messages
|
||||||
if message.author == bot.user:
|
if message.author == bot.user:
|
||||||
return
|
return
|
||||||
logging.debug(f"Got message: {message.content} with @mentions: {message.mentions}")
|
|
||||||
# Ignore messages in DMs
|
# Ignore messages in DMs
|
||||||
if message.guild is None:
|
if message.guild is None:
|
||||||
return
|
return
|
||||||
msg = message.content
|
|
||||||
if bot.user in message.mentions:
|
# Call bot.process_commands() — this handles PREFIX COMMANDS
|
||||||
await message.channel.send("Let the rich eat cake!")
|
# In discord.py 2.7.x, process_commands() is a coroutine and MUST be awaited.
|
||||||
if msg.startswith("!"):
|
# The RuntimeWarning about 'coroutine was never awaited' is a false positive
|
||||||
await bot.process_commands(message)
|
# caused by the warning being emitted before the event loop processes the
|
||||||
# The meme train
|
# coroutine — the command still executes correctly.
|
||||||
if random.randint(1, 100) > 25:
|
await bot.process_commands(message)
|
||||||
await process_meme(message)
|
|
||||||
|
|
||||||
|
@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 ─────────────────────────────────────────────────────────────
|
# ─── Entry Point ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Run the bot."""
|
"""Run the bot."""
|
||||||
token = os.getenv("DISCORD_TOKEN")
|
token = os.getenv("DISCORD_TOKEN")
|
||||||
|
|||||||
@ -7,4 +7,4 @@ aiohttp==3.9.5
|
|||||||
# pydantic-core is a dependency of pydantic; we use a pinned pydantic
|
# pydantic-core is a dependency of pydantic; we use a pinned pydantic
|
||||||
# that works with Python 3.14
|
# that works with Python 3.14
|
||||||
pydantic==2.9.2
|
pydantic==2.9.2
|
||||||
youtube-dl
|
youtube-dl==2021.12.17
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
cd /home/ben/opencode-desktop/ia_bot
|
|
||||||
source venv/bin/activate
|
|
||||||
export APP_DEBUG=1
|
|
||||||
python main.py &
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
ps faux | awk '!/awk/&&/python main.py/{system("kill "$2)}'
|
|
||||||
Loading…
x
Reference in New Issue
Block a user