diff --git a/modules/auto_vc/auto_vc.py b/modules/auto_vc/auto_vc.py deleted file mode 100644 index 176b4c3..0000000 --- a/modules/auto_vc/auto_vc.py +++ /dev/null @@ -1,254 +0,0 @@ -# modules/auto_vc/auto_vc.py -import asyncio -import time -import discord -from discord.ext import commands - -def now() -> float: - return time.time() - -class AutoVCCog(commands.Cog): - """ - Auto-VC: - • When someone joins the trigger voice channel, create a new VC under the target category, - name it "{prefix} N", and move the member there. - • When an auto-VC is empty for `delay` seconds, delete it and renumber the remaining ones. - • Only channels created by this cog are managed (tracked in data_manager['vc_channels']). - - Admin commands: - /avc_status -> show current state - /avc_cleanup_now -> run a cleanup/renumber pass now - /avc_renumber -> renumber without deleting - """ - - def __init__(self, bot): - self.bot = bot - cfg = bot.config['DEFAULT'] - - # Config - self.trigger_id = int(cfg['trigger_channel_id']) - self.category_id = int(cfg['auto_vc_category_id']) - self.prefix = cfg['vc_name_prefix'] - self.delay = int(cfg.get('auto_vc_cleanup_delay', 30)) - self.modlog_channel_id = int(cfg.get('modlog_channel_id', '0')) if cfg.get('modlog_channel_id') else 0 - - # State - self.empty_since: dict[int, float] = {} # channel_id -> ts when became empty - self._vc_cooldowns: dict[int, float] = {} # user_id -> ts last created (anti-spam) - self._create_lock = asyncio.Lock() - - # Background sweeper - self._task = asyncio.create_task(self._sweeper()) - - # ------------- utilities ------------- - - def cog_unload(self): - try: - self._task.cancel() - except Exception: - pass - - def _prefixed(self, num: int) -> str: - return f"{self.prefix.strip()} {num}" - - def _vc_records(self, guild_id: int): - """Return list of tracked records for this guild from persistent store.""" - return [r for r in self.bot.data_manager.get('vc_channels') if r.get('guild_id') == guild_id] - - def _find_record(self, guild_id: int, channel_id: int): - for r in self._vc_records(guild_id): - if r.get('channel_id') == channel_id: - return r - return None - - async def _log(self, guild: discord.Guild, msg: str): - if not self.modlog_channel_id: - print(f"[AutoVC][{guild.name}] {msg}") - return - ch = guild.get_channel(self.modlog_channel_id) - if ch: - try: - await ch.send(msg) - except Exception: - print(f"[AutoVC][{guild.name}] (fallback) {msg}") - else: - print(f"[AutoVC][{guild.name}] {msg}") - - async def _renumber(self, guild: discord.Guild): - """Rename tracked channels to {prefix} 1..N in stable order (by creation_ts).""" - recs = sorted(self._vc_records(guild.id), key=lambda r: r.get('created_ts', 0)) - for i, rec in enumerate(recs, start=1): - ch = guild.get_channel(rec['channel_id']) - if not ch: - # prune dead record - self.bot.data_manager.remove('vc_channels', lambda x: x.get('channel_id') == rec['channel_id']) - continue - desired = self._prefixed(i) - if ch.name != desired: - try: - await ch.edit(name=desired, reason="Auto-VC renumber") - except Exception as e: - print("[auto_vc] rename failed:", repr(e)) - - async def _cleanup_pass(self, guild: discord.Guild): - """Delete empty tracked channels that exceeded delay and renumber.""" - cat = guild.get_channel(self.category_id) - if not cat: - return - - tracked_ids = {r['channel_id'] for r in self._vc_records(guild.id)} - now_ts = now() - to_delete: list[discord.VoiceChannel] = [] - - # Mark empties & collect deletions - for ch in cat.voice_channels: - if ch.id not in tracked_ids: - continue # unmanaged room - if len(ch.members) == 0: - started = self.empty_since.get(ch.id) - if started is None: - self.empty_since[ch.id] = now_ts - elif now_ts - started >= self.delay: - to_delete.append(ch) - else: - self.empty_since.pop(ch.id, None) - - # Delete idle channels - for ch in to_delete: - try: - await ch.delete(reason=f"Auto-VC idle > {self.delay}s") - await self._log(guild, f"🗑️ Deleted idle room: `{ch.name}`") - except Exception as e: - print("[auto_vc] delete failed:", repr(e)) - # purge record and emptiness stamp - self.bot.data_manager.remove('vc_channels', lambda r, cid=ch.id: r.get('channel_id') == cid) - self.empty_since.pop(ch.id, None) - - # purge records for channels that vanished by other means - for rec in list(self._vc_records(guild.id)): - if not guild.get_channel(rec['channel_id']): - self.bot.data_manager.remove('vc_channels', lambda r, cid=rec['channel_id']: r.get('channel_id') == cid) - - if to_delete: - await self._renumber(guild) - - # ------------- background worker ------------- - - async def _sweeper(self): - await self.bot.wait_until_ready() - while not self.bot.is_closed(): - try: - for guild in self.bot.guilds: - await self._cleanup_pass(guild) - except Exception as e: - print("[auto_vc] sweeper loop error:", repr(e)) - await asyncio.sleep(30) - - # ------------- channel creation ------------- - - async def _spawn_and_move(self, member: discord.Member): - guild = member.guild - cat = guild.get_channel(self.category_id) - if not cat: - await self._log(guild, "⚠️ auto_vc_category_id not found; cannot create rooms.") - return - - async with self._create_lock: - # Determine next index based on *tracked* channels - recs = sorted(self._vc_records(guild.id), key=lambda r: r.get('created_ts', 0)) - next_index = len(recs) + 1 - name = self._prefixed(next_index) - - # Create room - try: - new_ch = await cat.create_voice_channel(name, reason="Auto-VC spawn") - except discord.Forbidden: - await self._log(guild, "❌ Missing permission to create voice channels in the category.") - return - except Exception as e: - await self._log(guild, f"❌ Failed to create voice channel: {e}") - return - - # Persist record - self.bot.data_manager.add('vc_channels', { - 'guild_id': guild.id, - 'channel_id': new_ch.id, - 'created_ts': now() - }) - - # Move the user - try: - await member.move_to(new_ch, reason="Auto-VC move") - except discord.Forbidden: - await self._log(guild, "⚠️ I need **Move Members** and **Connect** permissions to move users.") - except Exception as e: - await self._log(guild, f"⚠️ Could not move {member} into `{name}`: {e}") - - # Start its idle timer if it immediately empties - if len(new_ch.members) == 0: - self.empty_since[new_ch.id] = now() - - # ------------- core flow ------------- - - @commands.Cog.listener() - async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): - guild = member.guild - - # Create on trigger join (with 5s per-user cooldown) - if after.channel and after.channel.id == self.trigger_id: - last = self._vc_cooldowns.get(member.id, 0.0) - if now() - last < 5.0: - return - self._vc_cooldowns[member.id] = now() - try: - await self._spawn_and_move(member) - except Exception as e: - print("[auto_vc] spawn/move failed:", repr(e)) - - # Mark empties immediately on leave - if before.channel: - ch = before.channel - if ch.category_id == self.category_id: - rec = self._find_record(guild.id, ch.id) - if rec and len(ch.members) == 0: - self.empty_since[ch.id] = now() - - # ------------- admin commands ------------- - - @commands.hybrid_command(name="avc_status", description="Show Auto-VC status for this guild") - @commands.has_permissions(manage_guild=True) - async def avc_status(self, ctx: commands.Context): - g = ctx.guild - recs = sorted(self._vc_records(g.id), key=lambda r: r.get('created_ts', 0)) - lines = [ - f"Trigger: <#{self.trigger_id}> | Category: <#{self.category_id}> | Prefix: `{self.prefix}` | Delay: {self.delay}s" - ] - for idx, rec in enumerate(recs, start=1): - ch = g.get_channel(rec['channel_id']) - if not ch: - state = "missing" - name = f"(deleted #{idx})" - else: - name = ch.name - state = f"{len(ch.members)} inside" if len(ch.members) else "empty" - t = self.empty_since.get(rec['channel_id']) - tail = f" | idle {int(now()-t)}s" if t and (not ch or (ch and not ch.members)) else "" - lines.append(f"- #{idx}: {name} — {state}{tail}") - msg = "Auto-VC status:\n" + "\n".join(lines) if lines else "No Auto-VC rooms tracked." - await ctx.reply(msg) - - @commands.hybrid_command(name="avc_cleanup_now", description="Run an immediate cleanup pass (delete idle rooms & renumber)") - @commands.has_permissions(manage_guild=True) - async def avc_cleanup_now(self, ctx: commands.Context): - await self._cleanup_pass(ctx.guild) - await ctx.reply("Cleanup pass complete.") - - @commands.hybrid_command(name="avc_renumber", description="Force a renumber of tracked rooms") - @commands.has_permissions(manage_guild=True) - async def avc_renumber(self, ctx: commands.Context): - await self._renumber(ctx.guild) - await ctx.reply("Renumbered.") - - -async def setup(bot): - await bot.add_cog(AutoVCCog(bot))