Delete modules/auto_vc/auto_vc.py
This commit is contained in:
		
							parent
							
								
									1a89e21ebf
								
							
						
					
					
						commit
						42385d19f6
					
				@ -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))
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user