0.3.9.7.a4

- Fixed permissions module not affecting certain features
- Added clear `[MOD]` labels to moderator-only commands for clarity
This commit is contained in:
Franz Rolfsvaag 2025-08-11 09:53:35 +02:00
parent 4f0e000c93
commit 21f6150842
8 changed files with 36 additions and 26 deletions

2
bot.py
View File

@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice
# Version consists of: # Version consists of:
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update) # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update)
VERSION = "0.3.9.7.a3" VERSION = "0.3.9.7.a4"
# ---------- Env loading ---------- # ---------- Env loading ----------

View File

@ -4,12 +4,11 @@ import time
import discord import discord
from discord.ext import commands from discord.ext import commands
from modules.common.settings import cfg # ENV-first config helper from modules.common.settings import cfg # ENV-first config helper
from mod_perms import require_mod_ctx # <- use project mod perms
def now() -> float: def now() -> float:
return time.time() return time.time()
class AutoVCCog(commands.Cog): class AutoVCCog(commands.Cog):
""" """
Auto-VC: Auto-VC:
@ -20,8 +19,8 @@ class AutoVCCog(commands.Cog):
Admin commands: Admin commands:
/avc_status -> show current state /avc_status -> show current state
/avc_cleanup_now -> run a cleanup/renumber pass now /avc_cleanup_now -> [MOD] run a cleanup/renumber pass now
/avc_renumber -> renumber without deleting /avc_renumber -> [MOD] renumber without deleting
""" """
def __init__(self, bot): 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.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._vc_cooldowns: dict[int, float] = {} # user_id -> ts last created (anti-spam)
self._create_lock = asyncio.Lock() self._create_lock = asyncio.Lock()
self._ops_lock = asyncio.Lock() # serialize admin ops vs sweeper
# Background sweeper # Background sweeper
self._task = asyncio.create_task(self._sweeper()) self._task = asyncio.create_task(self._sweeper())
@ -143,8 +143,10 @@ class AutoVCCog(commands.Cog):
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
while not self.bot.is_closed(): while not self.bot.is_closed():
try: try:
for guild in self.bot.guilds: # Serialize with admin ops
await self._cleanup_pass(guild) async with self._ops_lock:
for guild in self.bot.guilds:
await self._cleanup_pass(guild)
except Exception as e: except Exception as e:
print("[auto_vc] sweeper loop error:", repr(e)) print("[auto_vc] sweeper loop error:", repr(e))
await asyncio.sleep(30) await asyncio.sleep(30)
@ -224,7 +226,7 @@ class AutoVCCog(commands.Cog):
# ------------- admin commands ------------- # ------------- admin commands -------------
@commands.hybrid_command(name="avc_status", description="Show Auto-VC status for this guild") @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): async def avc_status(self, ctx: commands.Context):
g = ctx.guild g = ctx.guild
recs = sorted(self._vc_records(g.id), key=lambda r: r.get('created_ts', 0)) 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." msg = "Auto-VC status:\n" + "\n".join(lines) if lines else "No Auto-VC rooms tracked."
await ctx.reply(msg) await ctx.reply(msg)
@commands.hybrid_command(name="avc_cleanup_now", description="Run an immediate cleanup pass (delete idle rooms & renumber)") @commands.hybrid_command(name="avc_cleanup_now", description="[MOD] Run an immediate cleanup pass (delete idle rooms & renumber)")
@commands.has_permissions(manage_guild=True) @commands.guild_only()
async def avc_cleanup_now(self, ctx: commands.Context): async def avc_cleanup_now(self, ctx: commands.Context):
await self._cleanup_pass(ctx.guild) if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
await ctx.reply("Cleanup pass complete.") 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.hybrid_command(name="avc_renumber", description="[MOD] Force a renumber of tracked rooms")
@commands.has_permissions(manage_guild=True) @commands.guild_only()
async def avc_renumber(self, ctx: commands.Context): async def avc_renumber(self, ctx: commands.Context):
await self._renumber(ctx.guild) if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
await ctx.reply("Renumbered.") 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): async def setup(bot):
await bot.add_cog(AutoVCCog(bot)) await bot.add_cog(AutoVCCog(bot))

View File

@ -396,7 +396,7 @@ class NickNudgeCog(commands.Cog):
# ---------- Mod commands to manipulate nickname reviews ---------- # ---------- 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): async def clear_nick_reviews(self, interaction: discord.Interaction):
"""Moderator-only. Clears all 'pending' entries in data_manager['nick_reviews'] for this guild.""" """Moderator-only. Clears all 'pending' entries in data_manager['nick_reviews'] for this guild."""
# Must be used in a guild # Must be used in a guild
@ -437,7 +437,7 @@ class NickNudgeCog(commands.Cog):
ephemeral=True 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): async def recreate_nick_reviews(self, interaction: discord.Interaction):
"""Moderator-only bulk fixer for 'grey checkmark' users (claimed but no pending review).""" """Moderator-only bulk fixer for 'grey checkmark' users (claimed but no pending review)."""
if not interaction.guild: if not interaction.guild:
@ -486,7 +486,7 @@ class NickNudgeCog(commands.Cog):
ephemeral=True 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") @app_commands.describe(user="Member to recreate review for")
async def recreate_nick_review(self, interaction: discord.Interaction, user: discord.Member): async def recreate_nick_review(self, interaction: discord.Interaction, user: discord.Member):
"""Moderator-only single-user fixer.""" """Moderator-only single-user fixer."""

View File

@ -196,7 +196,7 @@ class PirateCardsCog(commands.Cog):
await self.refresh_card_for_account(guild, new_account) await self.refresh_card_for_account(guild, new_account)
# -------- command (mod-gated via require_mod_ctx) -------- # -------- 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): async def pirate_cards_rebuild(self, ctx: commands.Context):
if not await require_mod_ctx(ctx, "This command is restricted to moderators."): if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
return return

View File

@ -538,7 +538,7 @@ class PirateReportCog(commands.Cog):
return await interaction.response.send_message("Use this in a server.", ephemeral=True) return await interaction.response.send_message("Use this in a server.", ephemeral=True)
await interaction.response.send_modal(ReportModal(self)) 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): async def edit_pirate(self, interaction: discord.Interaction):
if not await require_mod_interaction(interaction): if not await require_mod_interaction(interaction):
return return

View File

@ -211,7 +211,7 @@ class PiratesListCog(commands.Cog):
dm.remove("pirates_list_posts", lambda r, mid=m.id: r.get("message_id") == mid) 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) # 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 @commands.cooldown(1, 10, commands.BucketType.guild) # tiny anti-spam
async def pirates_list_refresh(self, ctx: commands.Context): async def pirates_list_refresh(self, ctx: commands.Context):
if not await require_mod_ctx(ctx, "This command is restricted to moderators."): if not await require_mod_ctx(ctx, "This command is restricted to moderators."):

View File

@ -91,7 +91,7 @@ class PowerActionsCog(commands.Cog):
power = app_commands.Group(name="power", description="Administrative power actions (mod-only)") 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).") @app_commands.describe(reason="Explain why a restart is necessary (be specific).")
async def restart(self, interaction: discord.Interaction, reason: str): async def restart(self, interaction: discord.Interaction, reason: str):
# Mods only # Mods only

View File

@ -309,7 +309,7 @@ class StatusRotatorCog(commands.Cog):
if not ver: if not ver:
return None return None
# Occasionally include version (kept as its own generator for randomness) # Occasionally include version (kept as its own generator for randomness)
return f"Running {ver}" return f"Running v{ver}"
# ============== setup() ============== # ============== setup() ==============