From 926fc4b96afb65ab8ea568bbb056e373c4ff0f3d Mon Sep 17 00:00:00 2001 From: Benjamyn Date: Sun, 30 Aug 2026 16:54:49 +1000 Subject: [PATCH] Beeeg refactor --- .gitignore | 8 ++ bot.py | 193 -------------------------------------- commands/admin.py | 121 ------------------------ commands/emoji.py | 37 -------- commands/hello.py | 16 ---- emotes/.gitkeep | 0 handlers/gif_emotes.py | 124 ------------------------ handlers/love_emotes.py | 59 ++++++++++++ handlers/memes.py | 14 +++ handlers/utils.py | 34 +++++++ handlers/voice.py | 44 +++++++-- handlers/voice_gen.py | 87 +++++++++++++++++ main.py | 203 +++++++++------------------------------- requirements.txt | 2 +- start_server.sh | 3 +- 15 files changed, 281 insertions(+), 664 deletions(-) delete mode 100644 bot.py delete mode 100644 commands/admin.py delete mode 100644 commands/emoji.py delete mode 100644 commands/hello.py create mode 100644 emotes/.gitkeep delete mode 100644 handlers/gif_emotes.py create mode 100644 handlers/love_emotes.py create mode 100644 handlers/memes.py create mode 100644 handlers/utils.py create mode 100644 handlers/voice_gen.py diff --git a/.gitignore b/.gitignore index 54b0164..f06304c 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,11 @@ logs/ # Type stubs *.pyi .pyi + +.omo/ + +# Data files +emotes/*.webp +emotes/*.png +emotes/*.gif +emotes/*.jpg \ No newline at end of file diff --git a/bot.py b/bot.py deleted file mode 100644 index 4c81f67..0000000 --- a/bot.py +++ /dev/null @@ -1,193 +0,0 @@ -"""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() diff --git a/commands/admin.py b/commands/admin.py deleted file mode 100644 index f4b787d..0000000 --- a/commands/admin.py +++ /dev/null @@ -1,121 +0,0 @@ -"""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 diff --git a/commands/emoji.py b/commands/emoji.py deleted file mode 100644 index 26af1e6..0000000 --- a/commands/emoji.py +++ /dev/null @@ -1,37 +0,0 @@ -"""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 diff --git a/commands/hello.py b/commands/hello.py deleted file mode 100644 index 3ff6543..0000000 --- a/commands/hello.py +++ /dev/null @@ -1,16 +0,0 @@ -"""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 diff --git a/emotes/.gitkeep b/emotes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/handlers/gif_emotes.py b/handlers/gif_emotes.py deleted file mode 100644 index 5a49c07..0000000 --- a/handlers/gif_emotes.py +++ /dev/null @@ -1,124 +0,0 @@ -"""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) diff --git a/handlers/love_emotes.py b/handlers/love_emotes.py new file mode 100644 index 0000000..f4d1eb1 --- /dev/null +++ b/handlers/love_emotes.py @@ -0,0 +1,59 @@ +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)) diff --git a/handlers/memes.py b/handlers/memes.py new file mode 100644 index 0000000..c89cde0 --- /dev/null +++ b/handlers/memes.py @@ -0,0 +1,14 @@ +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") \ No newline at end of file diff --git a/handlers/utils.py b/handlers/utils.py new file mode 100644 index 0000000..ba96512 --- /dev/null +++ b/handlers/utils.py @@ -0,0 +1,34 @@ +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)) diff --git a/handlers/voice.py b/handlers/voice.py index 17ab0be..dba9f44 100644 --- a/handlers/voice.py +++ b/handlers/voice.py @@ -6,7 +6,7 @@ from typing import Optional import discord from discord.ext import commands -from discord.ext.commands import Cog, Context, has_permissions +from discord.ext.commands import Cog, Context logger = logging.getLogger(__name__) @@ -57,18 +57,42 @@ class MusicCog(Cog): self._is_playing = 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: """Play a track from a source (URL or file).""" if not self._is_playing: - await self._ensure_voice(ctx) - try: - track = await YTDLSource.from_url(source, source_type="youtube", loop=False) - self._current_queue.append(track) - await self._play_next() - except Exception as e: - logger.error(f"Error playing source {source}: {e}") - await ctx.send(f"Error playing: {e}") + try: + track = await YTDLSource.from_url(source, source_type="youtube", loop=False) + self._current_queue.append(track) + await self._play_next() + except Exception as e: + logger.error(f"Error playing source {source}: {e}") + await ctx.send(f"Error playing: {e}") async def _play_next(self) -> None: """Play the next track in the queue.""" @@ -167,7 +191,7 @@ class YTDLSource(discord.PCMVolumeTransformer): @classmethod async def from_url(cls, url, source_type="youtube", loop=False): """Create a source from a URL.""" - import youtube_dl + import yt_dlp as youtube_dl ydl_opts = { "format": "bestaudio/best", diff --git a/handlers/voice_gen.py b/handlers/voice_gen.py new file mode 100644 index 0000000..0ffd6a9 --- /dev/null +++ b/handlers/voice_gen.py @@ -0,0 +1,87 @@ +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)) diff --git a/main.py b/main.py index 94f242a..8515980 100644 --- a/main.py +++ b/main.py @@ -1,106 +1,38 @@ """Entry point for the Discord bot. Usage: - python main.py + ./start_server.sh """ import asyncio import os import sys import logging -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 random import discord from discord.ext import commands from discord.app_commands import Command as AppCommand, CommandTree -from handlers.gif_emotes import setup as setup_gif_emotes -from handlers.reactions import setup as setup_reactions +# from handlers.gif_emotes import setup as setup_gif_emotes +# from handlers.reactions import setup as setup_reactions from handlers.voice import setup as setup_voice -from cogs.admin import setup as setup_admin -from commands.ping import ping, info -from commands.hello import hello -from commands.emoji import emoji, app_emoji -from commands.admin import upload_emoji, list_emojis, delete_emoji +from handlers.voice_gen import setup as setup_voice_gen +from handlers.love_emotes import setup as setup_love_emotes +from handlers.utils import setup as setup_utils +from handlers.memes import process_meme +from commands.ping import ping +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 ─────────────────────────────────────────────────────────────── @@ -108,6 +40,8 @@ from commands.admin import upload_emoji, list_emojis, delete_emoji intents = discord.Intents.default() intents.message_content = True intents.members = True +intents.messages = True +intents.voice_states = True bot = commands.Bot( command_prefix="!", @@ -118,53 +52,11 @@ tree: CommandTree = bot.tree # ─── Slash Commands ────────────────────────────────────────────────────────── -# Register slash commands from modules 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 ────────────────────────────────────────────────────────── + @bot.event async def on_ready() -> None: """Entry point after successful login.""" @@ -172,22 +64,23 @@ async def on_ready() -> None: print(f"Logged in as {bot.user} (ID: {bot.user.id})") 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 await tree.sync() 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.") @bot.event async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None: """Global error handler for all commands.""" + logger = logging.getLogger(__name__) logger.debug(f"Command error for {ctx.author.name} ({ctx.author.id}): {error}") if isinstance(error, commands.CommandNotFound): await ctx.send("Command not found. Type `!list-commands` for a list of commands.") @@ -205,7 +98,7 @@ async def on_command_error(ctx: commands.Context, error: commands.CommandError) f"This command is on cooldown. Try again in {remaining} seconds." ) else: - await ctx.send(f"An error occurred: {error}") + await ctx.send(f"An error occurred: {error.__traceback__.__str__()}") @bot.event @@ -214,34 +107,22 @@ async def on_message(message: discord.Message) -> None: # Ignore bot's own messages if message.author == bot.user: return - + logging.debug(f"Got message: {message.content} with @mentions: {message.mentions}") # Ignore messages in DMs if message.guild is None: return - - # Call bot.process_commands() — this handles PREFIX COMMANDS - # In discord.py 2.7.x, process_commands() is a coroutine and MUST be awaited. - # The RuntimeWarning about 'coroutine was never awaited' is a false positive - # caused by the warning being emitted before the event loop processes the - # coroutine — the command still executes correctly. - await bot.process_commands(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.") - + msg = message.content + if bot.user in message.mentions: + await message.channel.send("Let the rich eat cake!") + if msg.startswith("!"): + await bot.process_commands(message) + # The meme train + if random.randint(1, 100) > 25: + await process_meme(message) # ─── Entry Point ───────────────────────────────────────────────────────────── + def main() -> None: """Run the bot.""" token = os.getenv("DISCORD_TOKEN") diff --git a/requirements.txt b/requirements.txt index e2a5a4a..c2e539a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ aiohttp==3.9.5 # pydantic-core is a dependency of pydantic; we use a pinned pydantic # that works with Python 3.14 pydantic==2.9.2 -youtube-dl==2021.12.17 +youtube-dl diff --git a/start_server.sh b/start_server.sh index fd81038..4b84393 100755 --- a/start_server.sh +++ b/start_server.sh @@ -1,4 +1,5 @@ #!/bin/bash cd /home/ben/opencode-desktop/ia_bot source venv/bin/activate -APP_DEBUG=1 python main.py & +export APP_DEBUG=1 +python main.py &