From 9d28bfb97f54c07900adbc180fa9bd55e0feb37d Mon Sep 17 00:00:00 2001 From: esefxdz Date: Sun, 5 Jul 2026 14:42:33 +0300 Subject: [PATCH] roles cog added music function polished --- ai/ai_roleplay.py | 4 +- cogs/roles.py | 314 ++++++++++++++++++++++++++++++++++++++++++++++ main.py | 1 + music/music.py | 19 +++ 4 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 cogs/roles.py diff --git a/ai/ai_roleplay.py b/ai/ai_roleplay.py index d0a1e94..6ad62bd 100644 --- a/ai/ai_roleplay.py +++ b/ai/ai_roleplay.py @@ -323,7 +323,7 @@ class AIRoleplay(commands.Cog): persona = self.active_personas.get(cid, 'yuuka') prompt = self.get_prompt(cid) - # VISION SUPPORT + # VISION SUPPORT DOESNT WORK LOL user_content = [{"type": "text", "text": f'{message.author.display_name}: {content}'}] has_images = False @@ -354,7 +354,7 @@ class AIRoleplay(commands.Cog): api_msgs += list(mem) model_to_use = self.active_model # always MODEL_FLASH or MODEL_PRO, never swapped - #VISION SUPPORT END + #VISION SUPPORT END STILL DOESNT WORK LOL # ----- thinking mode (per channel, correct DeepSeek params) ----- # The "thinking" key must be sent EVERY request — the API defaults diff --git a/cogs/roles.py b/cogs/roles.py new file mode 100644 index 0000000..add6701 --- /dev/null +++ b/cogs/roles.py @@ -0,0 +1,314 @@ +""" +cogs/roles.py +───────────────────────────────────────────────────────────────────────────── +Custom decorative role cog. + +Channel behaviour +───────────────── +• The bot posts (or re-uses) a single persistent panel message in + ROLE_CHANNEL_ID containing a short explanation and a "Get my role" button. +• Any user message in that channel is deleted immediately so it stays clean. +• Clicking the button opens a DM wizard — fully concurrent across members. + +Wizard +────── +• Human tone, plain text, no embeds, no emojis. +• 120-second timeout per question. +• If the member already has a managed role, acknowledges it and asks if they + want to change it. +• Asks for: name → colour → hoist (display separately in member list). +• Role is zero-permissions, purely decorative. +""" + +import asyncio +import os +import re + +import discord +from discord.ext import commands + +# ── constants ──────────────────────────────────────────────────────────────── + +ROLE_TAG = "[custom]" +TIMEOUT = 120 # seconds per DM question + +COLOR_PRESETS: dict[str, int] = { + "red": 0xFF5555, + "orange": 0xFFB86C, + "yellow": 0xF1FA8C, + "green": 0x50FA7B, + "cyan": 0x8BE9FD, + "blue": 0x6272A4, + "pink": 0xFF79C6, + "purple": 0xBD93F9, + "white": 0xF8F8F2, + "black": 0x21222C, +} + +PANEL_TEXT = ( + "**custom roles**\n\n" + "you can give yourself a role with your own name and color. " + "it's purely decorative — no permissions, just looks.\n\n" + "click the button below and the bot will message you to set it up." +) + +# ── helpers ────────────────────────────────────────────────────────────────── + +def _parse_color(raw: str) -> int | None: + raw = raw.strip().lower() + if raw in COLOR_PRESETS: + return COLOR_PRESETS[raw] + raw = raw.lstrip("#") + if re.fullmatch(r"[0-9a-f]{6}", raw): + return int(raw, 16) + return None + + +def _find_managed_role(member: discord.Member) -> discord.Role | None: + for role in member.roles: + if ROLE_TAG in role.name: + return role + return None + + +def _color_presets_text() -> str: + return ", ".join(COLOR_PRESETS.keys()) + + +# ── persistent button view ──────────────────────────────────────────────────── + +class RoleWizardView(discord.ui.View): + """Persistent view that lives in the role channel panel message.""" + + def __init__(self, cog: "Roles"): + super().__init__(timeout=None) + self.cog = cog + + @discord.ui.button( + label="Get my role", + style=discord.ButtonStyle.secondary, + custom_id="roles:get_my_role", + ) + async def get_role_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + member: discord.Member = interaction.user # type: ignore + + # Acknowledge the interaction immediately so Discord doesn't show an error + await interaction.response.defer(ephemeral=True) + + if member.id in self.cog._active: + await interaction.followup.send( + "you already have a wizard open, check your DMs.", ephemeral=True + ) + return + + # Start wizard as a background task + self.cog.bot.loop.create_task(self.cog._wizard(member, interaction.guild)) + + +# ── cog ────────────────────────────────────────────────────────────────────── + +class Roles(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self._active: set[int] = set() + self._view = RoleWizardView(self) + + raw = os.getenv("ROLE_CHANNEL_ID", "") + self.role_channel_id: int | None = int(raw) if raw.isdigit() else None + + async def cog_load(self): + # Register the persistent view so button clicks survive restarts. + # This must happen here (before on_ready) so Discord can route + # button interactions that arrive before on_ready fires. + self.bot.add_view(self._view) + + # ── panel management ────────────────────────────────────────────────────── + + async def _ensure_panel(self): + """Post the explanation panel if it doesn't exist yet; otherwise leave it. + Must be called after the bot is ready so the channel cache is warm. + """ + if self.role_channel_id is None: + return + channel = self.bot.get_channel(self.role_channel_id) + if channel is None: + return + + # Look for an existing panel message from this bot + async for msg in channel.history(limit=50): + if msg.author == self.bot.user and msg.components: + return # panel already exists, leave it alone + + # Not found — post a fresh one + await channel.send(content=PANEL_TEXT, view=self._view) + + @commands.Cog.listener() + async def on_ready(self): + # Channel cache is warm now — safe to look up the panel channel + await self._ensure_panel() + + # ── keep role channel clean ─────────────────────────────────────────────── + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + if message.author.bot or not message.guild: + return + if self.role_channel_id is None or message.channel.id != self.role_channel_id: + return + try: + await message.delete() + except discord.HTTPException: + pass + + # ── wizard orchestration ────────────────────────────────────────────────── + + async def _wizard(self, member: discord.Member, guild: discord.Guild): + self._active.add(member.id) + try: + await self._run_wizard(member, guild) + finally: + self._active.discard(member.id) + + async def _run_wizard(self, member: discord.Member, guild: discord.Guild): + # Open DM channel + try: + dm = await member.create_dm() + except discord.Forbidden: + return + + def dm_check(m: discord.Message) -> bool: + return m.author.id == member.id and isinstance(m.channel, discord.DMChannel) + + async def ask(prompt: str) -> str | None: + """Send a DM prompt and return the stripped reply, or None on timeout/cancel.""" + await dm.send(prompt) + try: + reply = await self.bot.wait_for("message", check=dm_check, timeout=TIMEOUT) + except asyncio.TimeoutError: + await dm.send("took too long, wizard cancelled.") + return None + text = reply.content.strip() + if text.lower() == "cancel": + await dm.send("cancelled.") + return None + return text + + # ── greet + handle existing role ────────────────────────────────────── + existing = _find_managed_role(member) + + if existing: + raw_existing_name = existing.name.replace(ROLE_TAG + " ", "") + reply = await ask( + f"hey, you already have a role called \"{raw_existing_name}\". " + f"do you want to change it? say yes or no." + ) + if reply is None: + return + if reply.lower() not in ("yes", "y"): + await dm.send("alright, keeping your role as is.") + return + + else: + try: + await dm.send( + "hey, let's set up your custom role. " + "it won't give you any permissions, it's just for looks. " + "type \"cancel\" at any point to stop." + ) + except discord.Forbidden: + return + + # ── step 1: name ────────────────────────────────────────────────────── + name_raw = await ask("what do you want your role to be called?") + if name_raw is None: + return + if not (2 <= len(name_raw) <= 32): + await dm.send("that name needs to be between 2 and 32 characters. wizard cancelled.") + return + + # ── step 2: colour ──────────────────────────────────────────────────── + colour_raw = await ask( + f"what color? you can give me a hex code like #bd93f9, " + f"or just say one of these: {_color_presets_text()}." + ) + if colour_raw is None: + return + colour_int = _parse_color(colour_raw) + if colour_int is None: + await dm.send( + "couldn't figure out that color. use a hex code or one of the named options. " + "wizard cancelled." + ) + return + + # ── step 3: hoist (display separately) ─────────────────────────────── + hoist_reply = await ask( + "do you want to be listed separately from regular members in the member list? " + "say yes or no." + ) + if hoist_reply is None: + return + hoisted = hoist_reply.lower() in ("yes", "y") + + # ── apply ───────────────────────────────────────────────────────────── + full_name = f"{ROLE_TAG} {name_raw}" + colour_obj = discord.Colour(colour_int) + + try: + if existing: + await existing.edit( + name=full_name, + colour=colour_obj, + hoist=hoisted, + reason="custom role update", + ) + role = existing + else: + role = await guild.create_role( + name=full_name, + colour=colour_obj, + hoist=hoisted, + permissions=discord.Permissions.none(), + reason=f"custom decorative role for {member}", + ) + # Place just above @everyone + await guild.edit_role_positions({role: 1}) + await member.add_roles(role, reason="custom decorative role") + + except discord.Forbidden: + await dm.send( + "i don't have permission to manage roles. " + "ask an admin to give me the Manage Roles permission." + ) + return + except discord.HTTPException as e: + await dm.send(f"something went wrong: {e}") + return + + sep_note = " you'll show up separately in the member list." if hoisted else "" + await dm.send( + f"done. your role \"{name_raw}\" has been " + f"{'updated' if existing else 'set up'}." + f"{sep_note}" + ) + + # ── owner command to manually refresh the panel ─────────────────────────── + + @commands.command(name="rolepanel") + async def rolepanel(self, ctx: commands.Context): + """(Owner only) Re-post the role channel panel.""" + if ctx.author.id != int(os.getenv("OWNER_ID", "0")): + return + if self.role_channel_id is None: + return await ctx.reply("ROLE_CHANNEL_ID is not set.") + channel = self.bot.get_channel(self.role_channel_id) + if channel is None: + return await ctx.reply("can't find that channel.") + await channel.send(content=PANEL_TEXT, view=self._view) + await ctx.reply("panel posted.") + + +async def setup(bot: commands.Bot): + await bot.add_cog(Roles(bot)) diff --git a/main.py b/main.py index 96634e1..0ee9f6c 100644 --- a/main.py +++ b/main.py @@ -62,6 +62,7 @@ async def main(): await bot.load_extension('cogs.statchannel') await bot.load_extension('cogs.ffmpeg_things') await bot.load_extension('cogs.archive') + await bot.load_extension('cogs.roles') await bot.load_extension('cogs.currency_converter') await bot.load_extension('ai.ai_roleplay') await bot.load_extension('twitterbridge.leaks_cmd') diff --git a/music/music.py b/music/music.py index 3dba468..81daa95 100644 --- a/music/music.py +++ b/music/music.py @@ -76,6 +76,21 @@ class Music(commands.Cog): self._states[guild_id] = GuildState() return self._states[guild_id] + # ── channel status ──────────────────────────────────────────────────────── + async def _set_channel_status(self, player: wavelink.Player, track: wavelink.Playable | None): + """Set (or clear) the voice channel status to reflect the current track.""" + channel = player.channel + if channel is None: + return + try: + if track: + status = f"🎵 {track.title}" + else: + status = "" + await channel.set_status(status) + except Exception: + pass # silently ignore missing permissions or unsupported guild + # ── connect lavalink ───────────────────────────────────────────────────── @commands.Cog.listener() async def on_ready(self): @@ -94,6 +109,7 @@ class Music(commands.Cog): if st.loop == LOOP_TRACK and st.current: await player.play(st.current) + await self._set_channel_status(player, st.current) return if st.loop == LOOP_QUEUE and st.current: @@ -106,11 +122,13 @@ class Music(commands.Cog): next_track = st.queue.popleft() st.current = next_track await player.play(next_track) + await self._set_channel_status(player, next_track) elif st.autoplay: # wavelink built-in autoplay await player.set_autoplay(wavelink.AutoPlayMode.enabled) else: st.current = None + await self._set_channel_status(player, None) # ── !play ───────────────────────────────────────────────────────────────── @commands.command(aliases=["p"]) @@ -175,6 +193,7 @@ class Music(commands.Cog): st.current = track await vc.play(track) + await self._set_channel_status(vc, track) embed = discord.Embed( title="▶️ Now Playing",