0.4.1.0.a8
- Converted all commands to slash-only commands
This commit is contained in:
		
							parent
							
								
									0349c36880
								
							
						
					
					
						commit
						77f92abe19
					
				
							
								
								
									
										2
									
								
								bot.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								bot.py
									
									
									
									
									
								
							@ -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; doesn’t trigger auto update)
 | 
					# Major.Enhancement.Minor.Patch.Test  (Test is alphanumeric; doesn’t trigger auto update)
 | 
				
			||||||
VERSION = "0.4.1.0.a7"
 | 
					VERSION = "0.4.1.0.a8"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ---------- Env loading ----------
 | 
					# ---------- Env loading ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,9 @@ import asyncio
 | 
				
			|||||||
import time
 | 
					import time
 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
from discord.ext import commands
 | 
					from discord.ext import commands
 | 
				
			||||||
 | 
					from discord import app_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
 | 
					from mod_perms import require_mod_ctx, require_mod_interaction  # <- use project mod perms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def now() -> float:
 | 
					def now() -> float:
 | 
				
			||||||
    return time.time()
 | 
					    return time.time()
 | 
				
			||||||
@ -225,10 +226,12 @@ class AutoVCCog(commands.Cog):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # ------------- admin commands -------------
 | 
					    # ------------- admin commands -------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @commands.hybrid_command(name="avc_status", description="Show Auto-VC status for this guild")
 | 
					    @app_commands.command(name="avc_status", description="Show Auto-VC status for this guild")
 | 
				
			||||||
    @commands.guild_only()
 | 
					    async def avc_status(self, interaction: discord.Interaction):
 | 
				
			||||||
    async def avc_status(self, ctx: commands.Context):
 | 
					        if not interaction.guild:
 | 
				
			||||||
        g = ctx.guild
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        g = interaction.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))
 | 
				
			||||||
        lines = [
 | 
					        lines = [
 | 
				
			||||||
            f"Trigger: <#{self.trigger_id or 0}> | Category: <#{self.category_id or 0}> | Prefix: `{self.prefix}` | Delay: {self.delay}s"
 | 
					            f"Trigger: <#{self.trigger_id or 0}> | Category: <#{self.category_id or 0}> | Prefix: `{self.prefix}` | Delay: {self.delay}s"
 | 
				
			||||||
@ -244,29 +247,35 @@ class AutoVCCog(commands.Cog):
 | 
				
			|||||||
            t = self.empty_since.get(rec['channel_id'])
 | 
					            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 ""
 | 
					            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}")
 | 
					            lines.append(f"- #{idx}: {name} — {state}{tail}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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 interaction.response.send_message(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @commands.hybrid_command(name="avc_cleanup_now", description="[MOD] Run an immediate cleanup pass (delete idle rooms & renumber)")
 | 
					    @app_commands.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, interaction: discord.Interaction):
 | 
				
			||||||
    async def avc_cleanup_now(self, ctx: commands.Context):
 | 
					        if not interaction.guild:
 | 
				
			||||||
        if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
 | 
					        if not await require_mod_interaction(interaction, "This command is restricted to moderators."):
 | 
				
			||||||
            return
 | 
					            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="[MOD] Force a renumber of tracked rooms")
 | 
					        await interaction.response.defer(ephemeral=True)
 | 
				
			||||||
    @commands.guild_only()
 | 
					 | 
				
			||||||
    async def avc_renumber(self, ctx: commands.Context):
 | 
					 | 
				
			||||||
        if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        async with self._ops_lock:
 | 
					        async with self._ops_lock:
 | 
				
			||||||
            await self._renumber(ctx.guild)
 | 
					            await self._cleanup_pass(interaction.guild)
 | 
				
			||||||
        await self._log(ctx.guild, f"🔢 Renumber invoked by {ctx.author.mention}")
 | 
					        await self._log(interaction.guild, f"🧹 Cleanup pass invoked by {interaction.user.mention}")
 | 
				
			||||||
        await ctx.reply("Renumbered.", ephemeral=hasattr(ctx, "interaction") and ctx.interaction is not None)
 | 
					        await interaction.followup.send("Cleanup pass complete.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @app_commands.command(name="avc_renumber", description="[MOD] Force a renumber of tracked rooms")
 | 
				
			||||||
 | 
					    async def avc_renumber(self, interaction: discord.Interaction):
 | 
				
			||||||
 | 
					        if not interaction.guild:
 | 
				
			||||||
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
 | 
					        if not await require_mod_interaction(interaction, "This command is restricted to moderators."):
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await interaction.response.defer(ephemeral=True)
 | 
				
			||||||
 | 
					        async with self._ops_lock:
 | 
				
			||||||
 | 
					            await self._renumber(interaction.guild)
 | 
				
			||||||
 | 
					        await self._log(interaction.guild, f"🔢 Renumber invoked by {interaction.user.mention}")
 | 
				
			||||||
 | 
					        await interaction.followup.send("Renumbered.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def setup(bot):
 | 
					async def setup(bot):
 | 
				
			||||||
    await bot.add_cog(AutoVCCog(bot))
 | 
					    await bot.add_cog(AutoVCCog(bot))
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,9 @@
 | 
				
			|||||||
import asyncio
 | 
					import asyncio
 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
from discord.ext import commands
 | 
					from discord.ext import commands
 | 
				
			||||||
 | 
					from discord import app_commands
 | 
				
			||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
from mod_perms import require_mod_ctx
 | 
					from mod_perms import require_mod_ctx, require_mod_interaction
 | 
				
			||||||
from modules.common.settings import cfg  # ENV-first config helper
 | 
					from modules.common.settings import cfg  # ENV-first config helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -196,18 +197,18 @@ 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="[MOD] Rebuild pirate cards for all known pirates")
 | 
					    @app_commands.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, interaction: discord.Interaction):
 | 
				
			||||||
        if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
 | 
					        if not interaction.guild:
 | 
				
			||||||
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
 | 
					        if not await require_mod_interaction(interaction, "This command is restricted to moderators."):
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        if not ctx.guild:
 | 
					 | 
				
			||||||
            return await ctx.reply("Use this in a server.", ephemeral=True)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        async with self._lock_for(ctx.guild.id):
 | 
					        await interaction.response.defer(ephemeral=True)
 | 
				
			||||||
            guild = ctx.guild
 | 
					        guild = interaction.guild
 | 
				
			||||||
        ch = guild.get_channel(self.pirates_channel_id)
 | 
					        ch = guild.get_channel(self.pirates_channel_id)
 | 
				
			||||||
        if not ch:
 | 
					        if not ch:
 | 
				
			||||||
                return await ctx.reply("Configured pirates_list_channel_id not found.", ephemeral=True)
 | 
					            return await interaction.followup.send("Configured pirates_list_channel_id not found.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        count = 0
 | 
					        count = 0
 | 
				
			||||||
        for p in self.bot.data_manager.get('pirates'):
 | 
					        for p in self.bot.data_manager.get('pirates'):
 | 
				
			||||||
@ -217,9 +218,7 @@ class PirateCardsCog(commands.Cog):
 | 
				
			|||||||
            except Exception:
 | 
					            except Exception:
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        is_slash = hasattr(ctx, "interaction") and ctx.interaction is not None
 | 
					        await interaction.followup.send(f"Rebuilt/updated {count} pirate cards.", ephemeral=True)
 | 
				
			||||||
        await ctx.reply(f"Rebuilt/updated {count} pirate cards.", ephemeral=is_slash)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def setup(bot):
 | 
					async def setup(bot):
 | 
				
			||||||
    await bot.add_cog(PirateCardsCog(bot))
 | 
					    await bot.add_cog(PirateCardsCog(bot))
 | 
				
			||||||
 | 
				
			|||||||
@ -518,18 +518,23 @@ class PirateReportCog(commands.Cog):
 | 
				
			|||||||
        return (matches[0], None)
 | 
					        return (matches[0], None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Remove pirate (mod-only)
 | 
					    # Remove pirate (mod-only)
 | 
				
			||||||
    @commands.hybrid_command(name='remove_pirate', description='[MOD] Remove an approved pirate entry')
 | 
					    @app_commands.command(name="remove_pirate", description="[MOD] Remove an approved pirate entry")
 | 
				
			||||||
    async def remove_pirate(self, ctx, account_name: str):
 | 
					    @app_commands.describe(account_name="Account name to remove")
 | 
				
			||||||
        if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
 | 
					    async def remove_pirate(self, interaction: discord.Interaction, account_name: str):
 | 
				
			||||||
 | 
					        if not interaction.guild:
 | 
				
			||||||
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
 | 
					        if not await require_mod_interaction(interaction, "This command is restricted to moderators."):
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        acct_lower = account_name.strip().lower()
 | 
					        acct_lower = account_name.strip().lower()
 | 
				
			||||||
        dm = self.bot.data_manager
 | 
					        dm = self.bot.data_manager
 | 
				
			||||||
        if not any(p['account_name'].lower() == acct_lower for p in dm.get('pirates')):
 | 
					        if not any(p['account_name'].lower() == acct_lower for p in dm.get('pirates')):
 | 
				
			||||||
            return await ctx.reply("Pirate not found.")
 | 
					            return await interaction.response.send_message("Pirate not found.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dm.remove('pirates', lambda p: p['account_name'].lower() == acct_lower)
 | 
					        dm.remove('pirates', lambda p: p['account_name'].lower() == acct_lower)
 | 
				
			||||||
        await self._modlog(ctx.guild, f"🗑️ Removed pirate {account_name} by {ctx.author.mention}")
 | 
					        await self._modlog(interaction.guild, f"🗑️ Removed pirate {account_name} by {interaction.user.mention}")
 | 
				
			||||||
        await self._refresh_pirates_list(ctx.guild)
 | 
					        await self._refresh_pirates_list(interaction.guild)
 | 
				
			||||||
        await ctx.reply("Removed.", ephemeral=hasattr(ctx, "interaction") and ctx.interaction is not None)
 | 
					        await interaction.response.send_message("Removed.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Modal launchers
 | 
					    # Modal launchers
 | 
				
			||||||
    @app_commands.command(name="report", description="Submit a pirate report for moderator review (opens a form)")
 | 
					    @app_commands.command(name="report", description="Submit a pirate report for moderator review (opens a form)")
 | 
				
			||||||
@ -551,10 +556,15 @@ class PirateReportCog(commands.Cog):
 | 
				
			|||||||
        await interaction.response.send_modal(EncounterModal(self))
 | 
					        await interaction.response.send_modal(EncounterModal(self))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ---- Migration: convert encounter identifiers to accounts (mod-only) ----
 | 
					    # ---- Migration: convert encounter identifiers to accounts (mod-only) ----
 | 
				
			||||||
    @commands.hybrid_command(name='encounters_migrate_ids', description='[MOD] Migrate encounter identifiers to account names')
 | 
					    @app_commands.command(name="encounters_migrate_ids", description="[MOD] Migrate encounter identifiers to account names")
 | 
				
			||||||
    async def encounters_migrate_ids(self, ctx: commands.Context):
 | 
					    async def encounters_migrate_ids(self, interaction: discord.Interaction):
 | 
				
			||||||
        if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
 | 
					        if not interaction.guild:
 | 
				
			||||||
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
 | 
					        if not await require_mod_interaction(interaction, "This command is restricted to moderators."):
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await interaction.response.defer(ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dm = self.bot.data_manager
 | 
					        dm = self.bot.data_manager
 | 
				
			||||||
        pirates = dm.get('pirates')
 | 
					        pirates = dm.get('pirates')
 | 
				
			||||||
        by_char = {}
 | 
					        by_char = {}
 | 
				
			||||||
@ -562,10 +572,7 @@ class PirateReportCog(commands.Cog):
 | 
				
			|||||||
            by_char.setdefault(p['character_name'].lower(), []).append(p)
 | 
					            by_char.setdefault(p['character_name'].lower(), []).append(p)
 | 
				
			||||||
        by_acct = {p['account_name'].lower(): p for p in pirates}
 | 
					        by_acct = {p['account_name'].lower(): p for p in pirates}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        changed = 0
 | 
					        changed = ambiguous = missing = already = 0
 | 
				
			||||||
        ambiguous = 0
 | 
					 | 
				
			||||||
        missing = 0
 | 
					 | 
				
			||||||
        already = 0
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for e in dm.get('encounters'):
 | 
					        for e in dm.get('encounters'):
 | 
				
			||||||
            ident = e.get('identifier', '')
 | 
					            ident = e.get('identifier', '')
 | 
				
			||||||
@ -583,26 +590,27 @@ class PirateReportCog(commands.Cog):
 | 
				
			|||||||
                ambiguous += 1
 | 
					                ambiguous += 1
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
            acct = matches[0]['account_name']
 | 
					            acct = matches[0]['account_name']
 | 
				
			||||||
            # update this one entry atomically
 | 
					
 | 
				
			||||||
            def pred(x, ts=e['timestamp'], rid=e['reporter_id'], ident_old=ident):
 | 
					            def pred(x, ts=e['timestamp'], rid=e['reporter_id'], ident_old=ident):
 | 
				
			||||||
                return x.get('timestamp') == ts and x.get('reporter_id') == rid and x.get('identifier') == ident_old
 | 
					                return x.get('timestamp') == ts and x.get('reporter_id') == rid and x.get('identifier') == ident_old
 | 
				
			||||||
            def upd(x, acct_new=acct):
 | 
					            def upd(x, acct_new=acct):
 | 
				
			||||||
                x['identifier'] = acct_new
 | 
					                x['identifier'] = acct_new
 | 
				
			||||||
                return x
 | 
					                return x
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ok = dm.update('encounters', pred, upd)
 | 
					            ok = dm.update('encounters', pred, upd)
 | 
				
			||||||
            if ok:
 | 
					            if ok:
 | 
				
			||||||
                changed += 1
 | 
					                changed += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await ctx.reply(
 | 
					        await interaction.followup.send(
 | 
				
			||||||
            f"Migration complete.\n"
 | 
					            "Migration complete.\n"
 | 
				
			||||||
            f"- Updated to accounts: **{changed}**\n"
 | 
					            f"- Updated to accounts: **{changed}**\n"
 | 
				
			||||||
            f"- Already accounts: **{already}**\n"
 | 
					            f"- Already accounts: **{already}**\n"
 | 
				
			||||||
            f"- Ambiguous character names (skipped): **{ambiguous}**\n"
 | 
					            f"- Ambiguous character names (skipped): **{ambiguous}**\n"
 | 
				
			||||||
            f"- Not found in pirates list (skipped): **{missing}**",
 | 
					            f"- Not found in pirates list (skipped): **{missing}**",
 | 
				
			||||||
            ephemeral=hasattr(ctx, "interaction") and ctx.interaction is not None
 | 
					            ephemeral=True
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await self._refresh_pirates_list(ctx.guild)
 | 
					        await self._refresh_pirates_list(interaction.guild)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Moderator reaction handling (atomic claim)
 | 
					    # Moderator reaction handling (atomic claim)
 | 
				
			||||||
    @commands.Cog.listener()
 | 
					    @commands.Cog.listener()
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,8 @@
 | 
				
			|||||||
import asyncio
 | 
					import asyncio
 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
from discord.ext import commands
 | 
					from discord.ext import commands
 | 
				
			||||||
from mod_perms import require_mod_ctx
 | 
					from discord import app_commands
 | 
				
			||||||
 | 
					from mod_perms import require_mod_ctx, require_mod_interaction
 | 
				
			||||||
from modules.common.settings import cfg as _cfg
 | 
					from modules.common.settings import cfg as _cfg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -211,19 +212,17 @@ 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="[MOD] Rebuild the compact pirates list")
 | 
					    @app_commands.command(name="pirates_list_refresh", description="[MOD] Rebuild the compact pirates list")
 | 
				
			||||||
    @commands.cooldown(1, 10, commands.BucketType.guild)  # tiny anti-spam
 | 
					    @app_commands.checks.cooldown(1, 10)  # guild-scope anti-spam analogue
 | 
				
			||||||
    async def pirates_list_refresh(self, ctx: commands.Context):
 | 
					    async def pirates_list_refresh(self, interaction: discord.Interaction):
 | 
				
			||||||
        if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
 | 
					        if not interaction.guild:
 | 
				
			||||||
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
 | 
					        if not await require_mod_interaction(interaction, "This command is restricted to moderators."):
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        if not ctx.guild:
 | 
					 | 
				
			||||||
            return await ctx.reply("Use this in a server.", ephemeral=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await self.refresh_list(ctx.guild)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        is_slash = hasattr(ctx, "interaction") and ctx.interaction is not None
 | 
					 | 
				
			||||||
        await ctx.reply("Pirates list refreshed.", ephemeral=is_slash)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await interaction.response.defer(ephemeral=True)
 | 
				
			||||||
 | 
					        await self.refresh_list(interaction.guild)
 | 
				
			||||||
 | 
					        await interaction.followup.send("Pirates list refreshed.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def setup(bot: commands.Bot):
 | 
					async def setup(bot: commands.Bot):
 | 
				
			||||||
    await bot.add_cog(PiratesListCog(bot))
 | 
					    await bot.add_cog(PiratesListCog(bot))
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
# modules/reaction_role/reaction_role.py
 | 
					# modules/reaction_role/reaction_role.py
 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
from discord.ext import commands
 | 
					from discord.ext import commands
 | 
				
			||||||
 | 
					from discord import app_commands
 | 
				
			||||||
from modules.common.emoji_accept import is_accept
 | 
					from modules.common.emoji_accept import is_accept
 | 
				
			||||||
from modules.common.settings import cfg  # ENV-first helper
 | 
					from modules.common.settings import cfg  # ENV-first helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -94,22 +95,22 @@ class ReactionRoleCog(commands.Cog):
 | 
				
			|||||||
            return False
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ---- commands (hybrid = prefix + slash) ----
 | 
					    # ---- commands (hybrid = prefix + slash) ----
 | 
				
			||||||
    @commands.hybrid_command(name='nick_same', description='Claim that your global display name matches your in-game name (triggers mod review)')
 | 
					    @app_commands.command(name="nick_same", description="Claim that your global display name matches your in-game name (triggers mod review)")
 | 
				
			||||||
    async def nick_same(self, ctx: commands.Context):
 | 
					    async def nick_same(self, interaction: discord.Interaction):
 | 
				
			||||||
        member = ctx.author if isinstance(ctx.author, discord.Member) else None
 | 
					        if not interaction.guild or not isinstance(interaction.user, discord.Member):
 | 
				
			||||||
        if not member or not ctx.guild:
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
            return await ctx.reply("Use this in a server.", ephemeral=True)
 | 
					
 | 
				
			||||||
 | 
					        member: discord.Member = interaction.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Atomic path handled inside NickNudge
 | 
					 | 
				
			||||||
        nn = self.bot.get_cog('NickNudgeCog')
 | 
					        nn = self.bot.get_cog('NickNudgeCog')
 | 
				
			||||||
        if nn and hasattr(nn, 'ensure_pending_and_maybe_open'):
 | 
					        if nn and hasattr(nn, 'ensure_pending_and_maybe_open'):
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                await nn.ensure_pending_and_maybe_open(ctx.guild, member, source="nick_same")
 | 
					                await nn.ensure_pending_and_maybe_open(interaction.guild, member, source="nick_same")
 | 
				
			||||||
            except Exception:
 | 
					            except Exception:
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await self.maybe_apply_full_access(member)
 | 
					        await self.maybe_apply_full_access(member)
 | 
				
			||||||
        await ctx.reply("Thanks — your nickname claim was sent for moderator review.", ephemeral=True)
 | 
					        await interaction.response.send_message("Thanks — your nickname claim was sent for moderator review.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ---- listeners ----
 | 
					    # ---- listeners ----
 | 
				
			||||||
    @commands.Cog.listener()
 | 
					    @commands.Cog.listener()
 | 
				
			||||||
 | 
				
			|||||||
@ -6,22 +6,15 @@ import discord
 | 
				
			|||||||
COUNTER_KEY_PREFIX = "cmd::"
 | 
					COUNTER_KEY_PREFIX = "cmd::"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _key_from_app(cmd: discord.app_commands.Command) -> str:
 | 
					def _key_from_app(cmd: discord.app_commands.Command) -> str:
 | 
				
			||||||
    name = getattr(cmd, "qualified_name", None) or getattr(cmd, "name", "unknown")
 | 
					    return f"{COUNTER_KEY_PREFIX}{getattr(cmd, 'qualified_name', None) or getattr(cmd, 'name', 'unknown')}"
 | 
				
			||||||
    return f"{COUNTER_KEY_PREFIX}{name}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _key_from_ctx(ctx: commands.Context) -> str:
 | 
					 | 
				
			||||||
    c = getattr(ctx, "command", None)
 | 
					 | 
				
			||||||
    name = getattr(c, "qualified_name", None) or getattr(c, "name", "unknown")
 | 
					 | 
				
			||||||
    return f"{COUNTER_KEY_PREFIX}{name}"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UsageStatsCog(commands.Cog):
 | 
					class UsageStatsCog(commands.Cog):
 | 
				
			||||||
    """Count command runs once: app for slash, prefix for non-interaction Context."""
 | 
					    """Slash-only metrics; count once per successful app command completion."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, bot: commands.Bot):
 | 
					    def __init__(self, bot: commands.Bot):
 | 
				
			||||||
        self.bot = bot
 | 
					        self.bot = bot
 | 
				
			||||||
        print("[usage] UsageStatsCog init")
 | 
					        print("[usage] UsageStatsCog init (slash-only)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # -------- slash / app-commands --------
 | 
					 | 
				
			||||||
    @commands.Cog.listener()
 | 
					    @commands.Cog.listener()
 | 
				
			||||||
    async def on_app_command_completion(self, interaction: discord.Interaction, command: discord.app_commands.Command):
 | 
					    async def on_app_command_completion(self, interaction: discord.Interaction, command: discord.app_commands.Command):
 | 
				
			||||||
        dm = getattr(self.bot, "data_manager", None)
 | 
					        dm = getattr(self.bot, "data_manager", None)
 | 
				
			||||||
@ -34,30 +27,10 @@ class UsageStatsCog(commands.Cog):
 | 
				
			|||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            print("[usage] app !! incr failed:", repr(e))
 | 
					            print("[usage] app !! incr failed:", repr(e))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # -------- prefix (and hybrid-as-prefix) --------
 | 
					 | 
				
			||||||
    @commands.Cog.listener()
 | 
					 | 
				
			||||||
    async def on_command_completion(self, ctx: commands.Context):
 | 
					 | 
				
			||||||
        # If this Context came from a slash interaction (hybrid invoked via slash),
 | 
					 | 
				
			||||||
        # DO NOT count here—the app listener already did.
 | 
					 | 
				
			||||||
        if getattr(ctx, "interaction", None):
 | 
					 | 
				
			||||||
            print("[usage] px  ~~ skip: ctx.interaction is set (slash path already counted)")
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        dm = getattr(self.bot, "data_manager", None)
 | 
					 | 
				
			||||||
        if not dm:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            key = _key_from_ctx(ctx)
 | 
					 | 
				
			||||||
            newv = dm.incr_counter(key, 1)
 | 
					 | 
				
			||||||
            print(f"[usage] px  ++ {key} -> {newv}")
 | 
					 | 
				
			||||||
        except Exception as e:
 | 
					 | 
				
			||||||
            print("[usage] px  !! incr failed:", repr(e))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async def setup(bot: commands.Bot):
 | 
					async def setup(bot: commands.Bot):
 | 
				
			||||||
    # Avoid double registration if extensions are discovered/reloaded twice
 | 
					 | 
				
			||||||
    if getattr(bot, "_usage_stats_loaded", False):
 | 
					    if getattr(bot, "_usage_stats_loaded", False):
 | 
				
			||||||
        print("[usage] UsageStatsCog already loaded; skipping duplicate add")
 | 
					        print("[usage] UsageStatsCog already loaded; skipping duplicate add")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    await bot.add_cog(UsageStatsCog(bot))
 | 
					    await bot.add_cog(UsageStatsCog(bot))
 | 
				
			||||||
    bot._usage_stats_loaded = True
 | 
					    bot._usage_stats_loaded = True
 | 
				
			||||||
    print("[usage] UsageStatsCog loaded")
 | 
					    print("[usage] UsageStatsCog loaded (slash-only)")
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import time
 | 
				
			|||||||
from typing import Optional, Set, Tuple
 | 
					from typing import Optional, Set, Tuple
 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
from discord.ext import commands
 | 
					from discord.ext import commands
 | 
				
			||||||
 | 
					from discord import app_commands
 | 
				
			||||||
from modules.common.emoji_accept import is_accept
 | 
					from modules.common.emoji_accept import is_accept
 | 
				
			||||||
from modules.common.settings import cfg  # ENV-first helper
 | 
					from modules.common.settings import cfg  # ENV-first helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -494,20 +495,18 @@ class UserCardsCog(commands.Cog):
 | 
				
			|||||||
            await asyncio.sleep(12 * 60 * 60)  # twice a day
 | 
					            await asyncio.sleep(12 * 60 * 60)  # twice a day
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ---------- mod command: rescan + live reconcile ----------
 | 
					    # ---------- mod command: rescan + live reconcile ----------
 | 
				
			||||||
 | 
					    @app_commands.command(name="usercards_rescan", description="[MOD] Re-check all users and refresh cards")
 | 
				
			||||||
 | 
					    @app_commands.default_permissions(manage_guild=True)
 | 
				
			||||||
 | 
					    @app_commands.checks.has_permissions(manage_guild=True)
 | 
				
			||||||
 | 
					    async def usercards_rescan(self, interaction: discord.Interaction):
 | 
				
			||||||
 | 
					        if not interaction.guild:
 | 
				
			||||||
 | 
					            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @commands.hybrid_command(
 | 
					        await interaction.response.defer(ephemeral=True)
 | 
				
			||||||
        name="usercards_rescan",
 | 
					 | 
				
			||||||
        description="[MOD] Re-check all users and refresh cards"
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @commands.has_permissions(manage_guild=True)
 | 
					 | 
				
			||||||
    async def usercards_rescan(self, ctx: commands.Context):
 | 
					 | 
				
			||||||
        g = ctx.guild
 | 
					 | 
				
			||||||
        if not g:
 | 
					 | 
				
			||||||
            return await ctx.reply("Use this in a server.", ephemeral=True)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        g = interaction.guild
 | 
				
			||||||
        rchg, echg, nadd, nrem = await self._reconcile_agreements(g)
 | 
					        rchg, echg, nadd, nrem = await self._reconcile_agreements(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Rebuild cards
 | 
					 | 
				
			||||||
        updated = 0
 | 
					        updated = 0
 | 
				
			||||||
        for m in g.members:
 | 
					        for m in g.members:
 | 
				
			||||||
            if not m.bot:
 | 
					            if not m.bot:
 | 
				
			||||||
@ -517,13 +516,12 @@ class UserCardsCog(commands.Cog):
 | 
				
			|||||||
                except Exception:
 | 
					                except Exception:
 | 
				
			||||||
                    pass
 | 
					                    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await ctx.reply(
 | 
					        await interaction.followup.send(
 | 
				
			||||||
            f"Reconciled from messages. Changes — Rules: **{rchg}**, RoE: **{echg}**, "
 | 
					            f"Reconciled from messages. Changes — Rules: **{rchg}**, RoE: **{echg}**, "
 | 
				
			||||||
            f"Nickname (added): **{nadd}**, Nickname (removed): **{nrem}**. "
 | 
					            f"Nickname (added): **{nadd}**, Nickname (removed): **{nrem}**. "
 | 
				
			||||||
            f"Refreshed cards for **{updated}** members.",
 | 
					            f"Refreshed cards for **{updated}** members.",
 | 
				
			||||||
            ephemeral=True
 | 
					            ephemeral=True
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
async def setup(bot):
 | 
					async def setup(bot):
 | 
				
			||||||
    await bot.add_cog(UserCardsCog(bot))
 | 
					    await bot.add_cog(UserCardsCog(bot))
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user