diff --git a/bot.py b/bot.py index df951a1..db6b384 100644 --- a/bot.py +++ b/bot.py @@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice # Version consists of: # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update) -VERSION = "0.3.9.7.a3" +VERSION = "0.3.9.7.a4" # ---------- Env loading ---------- diff --git a/modules/auto_vc/auto_vc.py b/modules/auto_vc/auto_vc.py index 41aa9fd..d1f7797 100644 --- a/modules/auto_vc/auto_vc.py +++ b/modules/auto_vc/auto_vc.py @@ -4,12 +4,11 @@ import time import discord from discord.ext import commands from modules.common.settings import cfg # ENV-first config helper - +from mod_perms import require_mod_ctx # <- use project mod perms def now() -> float: return time.time() - class AutoVCCog(commands.Cog): """ Auto-VC: @@ -20,8 +19,8 @@ class AutoVCCog(commands.Cog): Admin commands: /avc_status -> show current state - /avc_cleanup_now -> run a cleanup/renumber pass now - /avc_renumber -> renumber without deleting + /avc_cleanup_now -> [MOD] run a cleanup/renumber pass now + /avc_renumber -> [MOD] renumber without deleting """ def __init__(self, bot): @@ -39,6 +38,7 @@ class AutoVCCog(commands.Cog): 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() + self._ops_lock = asyncio.Lock() # serialize admin ops vs sweeper # Background sweeper self._task = asyncio.create_task(self._sweeper()) @@ -143,8 +143,10 @@ class AutoVCCog(commands.Cog): await self.bot.wait_until_ready() while not self.bot.is_closed(): try: - for guild in self.bot.guilds: - await self._cleanup_pass(guild) + # Serialize with admin ops + async with self._ops_lock: + 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) @@ -224,7 +226,7 @@ class AutoVCCog(commands.Cog): # ------------- admin commands ------------- @commands.hybrid_command(name="avc_status", description="Show Auto-VC status for this guild") - @commands.has_permissions(manage_guild=True) + @commands.guild_only() 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)) @@ -245,18 +247,26 @@ class AutoVCCog(commands.Cog): 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) + @commands.hybrid_command(name="avc_cleanup_now", description="[MOD] Run an immediate cleanup pass (delete idle rooms & renumber)") + @commands.guild_only() async def avc_cleanup_now(self, ctx: commands.Context): - await self._cleanup_pass(ctx.guild) - await ctx.reply("Cleanup pass complete.") + if not await require_mod_ctx(ctx, "This command is restricted to moderators."): + return + # serialize vs sweeper and other admin ops + async with self._ops_lock: + await self._cleanup_pass(ctx.guild) + await self._log(ctx.guild, f"🧹 Cleanup pass invoked by {ctx.author.mention}") + await ctx.reply("Cleanup pass complete.", ephemeral=hasattr(ctx, "interaction") and ctx.interaction is not None) - @commands.hybrid_command(name="avc_renumber", description="Force a renumber of tracked rooms") - @commands.has_permissions(manage_guild=True) + @commands.hybrid_command(name="avc_renumber", description="[MOD] Force a renumber of tracked rooms") + @commands.guild_only() async def avc_renumber(self, ctx: commands.Context): - await self._renumber(ctx.guild) - await ctx.reply("Renumbered.") - + if not await require_mod_ctx(ctx, "This command is restricted to moderators."): + return + async with self._ops_lock: + await self._renumber(ctx.guild) + await self._log(ctx.guild, f"🔢 Renumber invoked by {ctx.author.mention}") + await ctx.reply("Renumbered.", ephemeral=hasattr(ctx, "interaction") and ctx.interaction is not None) async def setup(bot): await bot.add_cog(AutoVCCog(bot)) diff --git a/modules/nick_nudge/nick_nudge.py b/modules/nick_nudge/nick_nudge.py index d0fe2ff..dc7cf83 100644 --- a/modules/nick_nudge/nick_nudge.py +++ b/modules/nick_nudge/nick_nudge.py @@ -396,7 +396,7 @@ class NickNudgeCog(commands.Cog): # ---------- Mod commands to manipulate nickname reviews ---------- - @app_commands.command(name="clear_nick_reviews", description="Delete all PENDING nickname review records for this server.") + @app_commands.command(name="clear_nick_reviews", description="[MOD] Delete all PENDING nickname review records for this server.") async def clear_nick_reviews(self, interaction: discord.Interaction): """Moderator-only. Clears all 'pending' entries in data_manager['nick_reviews'] for this guild.""" # Must be used in a guild @@ -437,7 +437,7 @@ class NickNudgeCog(commands.Cog): ephemeral=True ) - @app_commands.command(name="recreate_nick_reviews", description="Scan and recreate any missing pending nickname reviews for this server.") + @app_commands.command(name="recreate_nick_reviews", description="[MOD] Scan and recreate any missing pending nickname reviews for this server.") async def recreate_nick_reviews(self, interaction: discord.Interaction): """Moderator-only bulk fixer for 'grey checkmark' users (claimed but no pending review).""" if not interaction.guild: @@ -486,7 +486,7 @@ class NickNudgeCog(commands.Cog): ephemeral=True ) - @app_commands.command(name="recreate", description="Recreate a missing pending nickname review for one user.") + @app_commands.command(name="recreate", description="[MOD] Recreate a missing pending nickname review for one user.") @app_commands.describe(user="Member to recreate review for") async def recreate_nick_review(self, interaction: discord.Interaction, user: discord.Member): """Moderator-only single-user fixer.""" diff --git a/modules/pirate_cards/pirate_cards.py b/modules/pirate_cards/pirate_cards.py index 37088b7..9bbd883 100644 --- a/modules/pirate_cards/pirate_cards.py +++ b/modules/pirate_cards/pirate_cards.py @@ -196,7 +196,7 @@ class PirateCardsCog(commands.Cog): await self.refresh_card_for_account(guild, new_account) # -------- command (mod-gated via require_mod_ctx) -------- - @commands.hybrid_command(name="pirate_cards_rebuild", description="Rebuild pirate cards for all known pirates") + @commands.hybrid_command(name="pirate_cards_rebuild", description="[MOD] Rebuild pirate cards for all known pirates") async def pirate_cards_rebuild(self, ctx: commands.Context): if not await require_mod_ctx(ctx, "This command is restricted to moderators."): return diff --git a/modules/pirate_report/pirate_report.py b/modules/pirate_report/pirate_report.py index df49929..f243de8 100644 --- a/modules/pirate_report/pirate_report.py +++ b/modules/pirate_report/pirate_report.py @@ -538,7 +538,7 @@ class PirateReportCog(commands.Cog): return await interaction.response.send_message("Use this in a server.", ephemeral=True) await interaction.response.send_modal(ReportModal(self)) - @app_commands.command(name="edit_pirate", description="Edit a pirate entry (opens a form)") + @app_commands.command(name="edit_pirate", description="[MOD] Edit a pirate entry (opens a form)") async def edit_pirate(self, interaction: discord.Interaction): if not await require_mod_interaction(interaction): return diff --git a/modules/pirates_list/pirates_list.py b/modules/pirates_list/pirates_list.py index a6feac0..ac849a6 100644 --- a/modules/pirates_list/pirates_list.py +++ b/modules/pirates_list/pirates_list.py @@ -211,7 +211,7 @@ class PiratesListCog(commands.Cog): dm.remove("pirates_list_posts", lambda r, mid=m.id: r.get("message_id") == mid) # Manual refresh command (hybrid: works as /pirates_list_refresh and !pirates_list_refresh) - @commands.hybrid_command(name="pirates_list_refresh", description="Rebuild the compact pirates list") + @commands.hybrid_command(name="pirates_list_refresh", description="[MOD] Rebuild the compact pirates list") @commands.cooldown(1, 10, commands.BucketType.guild) # tiny anti-spam async def pirates_list_refresh(self, ctx: commands.Context): if not await require_mod_ctx(ctx, "This command is restricted to moderators."): diff --git a/modules/power/power.py b/modules/power/power.py index 5508ef8..0ef41ca 100644 --- a/modules/power/power.py +++ b/modules/power/power.py @@ -91,7 +91,7 @@ class PowerActionsCog(commands.Cog): power = app_commands.Group(name="power", description="Administrative power actions (mod-only)") - @power.command(name="restart", description="Restart the bot (mod-only). Provide a descriptive reason.") + @power.command(name="restart", description="[MOD] Restart the bot. Provide a descriptive reason.") @app_commands.describe(reason="Explain why a restart is necessary (be specific).") async def restart(self, interaction: discord.Interaction, reason: str): # Mods only diff --git a/modules/status/status_rotator.py b/modules/status/status_rotator.py index a748cc1..b8ac36d 100644 --- a/modules/status/status_rotator.py +++ b/modules/status/status_rotator.py @@ -309,7 +309,7 @@ class StatusRotatorCog(commands.Cog): if not ver: return None # Occasionally include version (kept as its own generator for randomness) - return f"Running {ver}" + return f"Running v{ver}" # ============== setup() ============== @@ -317,4 +317,4 @@ async def setup(bot: commands.Bot): cog = StatusRotatorCog(bot) await bot.add_cog(cog) print("[status] StatusRotatorCog loaded; rotating presence every", - max(1, cfg(bot).int('status_interval_min', 5)), "minute(s).") + max(1, cfg(bot).int('status_interval_min', 5)), "minute(s).") \ No newline at end of file