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:
# 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 ----------

View File

@ -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))

View File

@ -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."""

View File

@ -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

View File

@ -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

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)
# 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."):

View File

@ -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

View File

@ -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() ==============