Delete modules/pirate_cards/pirate_cards.py
This commit is contained in:
		
							parent
							
								
									256cb33f0c
								
							
						
					
					
						commit
						461679f8eb
					
				@ -1,226 +0,0 @@
 | 
				
			|||||||
# modules/pirate_cards/pirate_cards.py
 | 
					 | 
				
			||||||
import asyncio
 | 
					 | 
				
			||||||
import discord
 | 
					 | 
				
			||||||
from discord.ext import commands
 | 
					 | 
				
			||||||
from datetime import datetime
 | 
					 | 
				
			||||||
from mod_perms import require_mod_ctx  # use your configured moderator roles
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class PirateCardsCog(commands.Cog):
 | 
					 | 
				
			||||||
    def __init__(self, bot):
 | 
					 | 
				
			||||||
        self.bot = bot
 | 
					 | 
				
			||||||
        cfg = bot.config['DEFAULT']
 | 
					 | 
				
			||||||
        self.pirates_channel_id = int(cfg['pirates_list_channel_id'])
 | 
					 | 
				
			||||||
        self.modlog_channel_id = int(cfg.get('modlog_channel_id', '0')) if cfg.get('modlog_channel_id') else 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # thresholds / samples (optional, with defaults)
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            self.group_threshold = int(cfg.get('threat_group_threshold', '3'))
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            self.group_threshold = 3
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            self.min_samples = int(cfg.get('threat_min_samples_for_stats', '3'))
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            self.min_samples = 3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # safe posting (don’t ping)
 | 
					 | 
				
			||||||
        self._no_mentions = discord.AllowedMentions.none()
 | 
					 | 
				
			||||||
        # serialize rebuilds per guild
 | 
					 | 
				
			||||||
        self._locks: dict[int, asyncio.Lock] = {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # -------- internals / helpers --------
 | 
					 | 
				
			||||||
    def _lock_for(self, guild_id: int) -> asyncio.Lock:
 | 
					 | 
				
			||||||
        self._locks.setdefault(guild_id, asyncio.Lock())
 | 
					 | 
				
			||||||
        return self._locks[guild_id]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @staticmethod
 | 
					 | 
				
			||||||
    def _esc(s: str) -> str:
 | 
					 | 
				
			||||||
        """Escape markdown & neutralize mentions for any user-sourced strings we show."""
 | 
					 | 
				
			||||||
        safe = discord.utils.escape_markdown(str(s))
 | 
					 | 
				
			||||||
        return safe.replace("@", "@\u200b")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _color_for_threat(self, threat: int, enc_count: int) -> discord.Color:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Gradient:
 | 
					 | 
				
			||||||
          - If too few samples: dark gray (unknown)
 | 
					 | 
				
			||||||
          - Else 0 -> green (0,255,0), 100 -> red (255,0,0)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if enc_count < self.min_samples:
 | 
					 | 
				
			||||||
            return discord.Color.dark_gray()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        t = max(0, min(100, int(threat))) / 100.0
 | 
					 | 
				
			||||||
        r = int(round(255 * t))
 | 
					 | 
				
			||||||
        g = int(round(255 * (1.0 - t)))
 | 
					 | 
				
			||||||
        return discord.Color.from_rgb(r, g, 0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _bucket(self, rate: float, samples: int) -> str:
 | 
					 | 
				
			||||||
        if samples < self.min_samples:
 | 
					 | 
				
			||||||
            return "unknown"
 | 
					 | 
				
			||||||
        if rate <= 0:
 | 
					 | 
				
			||||||
            return "never"
 | 
					 | 
				
			||||||
        if rate <= 0.25:
 | 
					 | 
				
			||||||
            return "rarely"
 | 
					 | 
				
			||||||
        if rate <= 0.60:
 | 
					 | 
				
			||||||
            return "sometimes"
 | 
					 | 
				
			||||||
        if rate <= 0.85:
 | 
					 | 
				
			||||||
            return "often"
 | 
					 | 
				
			||||||
        return "always"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _encounters_for(self, pirate: dict):
 | 
					 | 
				
			||||||
        acct_l = str(pirate.get('account_name', '')).lower()
 | 
					 | 
				
			||||||
        char_l = str(pirate.get('character_name', '')).lower()
 | 
					 | 
				
			||||||
        out = []
 | 
					 | 
				
			||||||
        for e in self.bot.data_manager.get('encounters'):
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                ident = str(e.get('identifier', '')).lower()
 | 
					 | 
				
			||||||
                if ident in (acct_l, char_l):
 | 
					 | 
				
			||||||
                    out.append(e)
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
        return out
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _get_card_record(self, account_lower: str):
 | 
					 | 
				
			||||||
        for r in self.bot.data_manager.get('pirate_cards'):
 | 
					 | 
				
			||||||
            if r.get('account_lower') == account_lower:
 | 
					 | 
				
			||||||
                return r
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def _modlog(self, guild: discord.Guild, content: str):
 | 
					 | 
				
			||||||
        if not self.modlog_channel_id:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        ch = guild.get_channel(self.modlog_channel_id)
 | 
					 | 
				
			||||||
        if ch:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                await ch.send(content, allowed_mentions=self._no_mentions)
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def _build_embed(self, pirate: dict) -> discord.Embed:
 | 
					 | 
				
			||||||
        encs = self._encounters_for(pirate)
 | 
					 | 
				
			||||||
        total = len(encs)
 | 
					 | 
				
			||||||
        # guard numeric fields
 | 
					 | 
				
			||||||
        def _i(v, d=0):
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                return int(v)
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                return d
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        group_rate = (sum(1 for e in encs if _i(e.get('group_size'), 0) >= self.group_threshold) / total) if total else 0.0
 | 
					 | 
				
			||||||
        destroy_rate = (sum(1 for e in encs if bool(e.get('destruction'))) / total) if total else 0.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # last encountered: date only (UTC)
 | 
					 | 
				
			||||||
        last_date = "—"
 | 
					 | 
				
			||||||
        if total:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                last_ts = max(int(float(e.get('timestamp', 0))) for e in encs)
 | 
					 | 
				
			||||||
                last_date = datetime.utcfromtimestamp(last_ts).strftime('%Y-%m-%d')
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                last_date = "—"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        groups_str = self._bucket(group_rate, total)
 | 
					 | 
				
			||||||
        destr_str = self._bucket(destroy_rate, total)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # threat & color
 | 
					 | 
				
			||||||
        threat = int(pirate.get('threat_level', 0))
 | 
					 | 
				
			||||||
        color = self._color_for_threat(threat, total)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        char = self._esc(pirate.get('character_name', 'Unknown'))
 | 
					 | 
				
			||||||
        acct = self._esc(pirate.get('account_name', 'Unknown#00000'))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        embed = discord.Embed(title=char, color=color)
 | 
					 | 
				
			||||||
        embed.add_field(name="Account name", value=f"*{acct}*", inline=False)
 | 
					 | 
				
			||||||
        embed.add_field(name="Threat", value=f"{threat}%", inline=True)
 | 
					 | 
				
			||||||
        embed.add_field(name="In groups", value=groups_str, inline=True)
 | 
					 | 
				
			||||||
        embed.add_field(name="Destructive", value=destr_str, inline=True)
 | 
					 | 
				
			||||||
        embed.set_footer(text=f"Encounters: {total} | Last: {last_date}")
 | 
					 | 
				
			||||||
        return embed
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def refresh_card_for_account(self, guild: discord.Guild, account_name: str):
 | 
					 | 
				
			||||||
        """Create or update a single pirate card by account name."""
 | 
					 | 
				
			||||||
        acct_l = str(account_name or "").lower()
 | 
					 | 
				
			||||||
        pirates = self.bot.data_manager.get('pirates')
 | 
					 | 
				
			||||||
        p = next((x for x in pirates if str(x.get('account_name', '')).lower() == acct_l), None)
 | 
					 | 
				
			||||||
        if not p:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        channel = guild.get_channel(self.pirates_channel_id)
 | 
					 | 
				
			||||||
        if not channel:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        embed = await self._build_embed(p)
 | 
					 | 
				
			||||||
        rec = self._get_card_record(acct_l)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if rec:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                msg = await channel.fetch_message(int(rec['message_id']))
 | 
					 | 
				
			||||||
                await msg.edit(embed=embed)
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                # fall-through to recreate
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            msg = await channel.send(embed=embed, allowed_mentions=self._no_mentions)
 | 
					 | 
				
			||||||
            self.bot.data_manager.add('pirate_cards', {
 | 
					 | 
				
			||||||
                'account_lower': acct_l,
 | 
					 | 
				
			||||||
                'message_id': msg.id
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def delete_card_for_account(self, guild: discord.Guild, account_name: str):
 | 
					 | 
				
			||||||
        """Delete a pirate card by account and remove its record."""
 | 
					 | 
				
			||||||
        acct_l = str(account_name or "").lower()
 | 
					 | 
				
			||||||
        channel = guild.get_channel(self.pirates_channel_id)
 | 
					 | 
				
			||||||
        rec = self._get_card_record(acct_l)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if rec and channel:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                msg = await channel.fetch_message(int(rec['message_id']))
 | 
					 | 
				
			||||||
                await msg.delete()
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # ensure record is gone regardless
 | 
					 | 
				
			||||||
        self.bot.data_manager.remove('pirate_cards', lambda r: r.get('account_lower') == acct_l)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def update_on_edit(self, guild: discord.Guild, old_account: str, new_account: str):
 | 
					 | 
				
			||||||
        """When an account name changes, carry over the card mapping and refresh."""
 | 
					 | 
				
			||||||
        old_l = str(old_account or "").lower()
 | 
					 | 
				
			||||||
        new_l = str(new_account or "").lower()
 | 
					 | 
				
			||||||
        rec = self._get_card_record(old_l)
 | 
					 | 
				
			||||||
        if rec:
 | 
					 | 
				
			||||||
            # replace mapping to new key
 | 
					 | 
				
			||||||
            self.bot.data_manager.remove('pirate_cards', lambda r: r.get('account_lower') == old_l)
 | 
					 | 
				
			||||||
            self.bot.data_manager.add('pirate_cards', {
 | 
					 | 
				
			||||||
                'account_lower': new_l,
 | 
					 | 
				
			||||||
                'message_id': rec['message_id']
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        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")
 | 
					 | 
				
			||||||
    async def pirate_cards_rebuild(self, ctx: commands.Context):
 | 
					 | 
				
			||||||
        if not await require_mod_ctx(ctx, "This command is restricted to moderators."):
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        if not ctx.guild:
 | 
					 | 
				
			||||||
            return await ctx.reply("Use this in a server.", ephemeral=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        async with self._lock_for(ctx.guild.id):
 | 
					 | 
				
			||||||
            guild = ctx.guild
 | 
					 | 
				
			||||||
            ch = guild.get_channel(self.pirates_channel_id)
 | 
					 | 
				
			||||||
            if not ch:
 | 
					 | 
				
			||||||
                return await ctx.reply("Configured pirates_list_channel_id not found.", ephemeral=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            count = 0
 | 
					 | 
				
			||||||
            for p in self.bot.data_manager.get('pirates'):
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    await self.refresh_card_for_account(guild, p.get('account_name', ''))
 | 
					 | 
				
			||||||
                    count += 1
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        is_slash = hasattr(ctx, "interaction") and ctx.interaction is not None
 | 
					 | 
				
			||||||
        await ctx.reply(f"Rebuilt/updated {count} pirate cards.", ephemeral=is_slash)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async def setup(bot):
 | 
					 | 
				
			||||||
    await bot.add_cog(PirateCardsCog(bot))
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user