diff --git a/modules/pirate_cards/pirate_cards.py b/modules/pirate_cards/pirate_cards.py deleted file mode 100644 index 7e87798..0000000 --- a/modules/pirate_cards/pirate_cards.py +++ /dev/null @@ -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))