Delete modules/pirates_list/pirates_list.py
This commit is contained in:
parent
e599fde099
commit
5b2aa79b9f
@ -1,256 +0,0 @@
|
|||||||
# modules/pirates_list/pirates_list.py
|
|
||||||
import asyncio
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from mod_perms import require_mod_ctx # ctx-aware mod gate
|
|
||||||
|
|
||||||
|
|
||||||
class PiratesListCog(commands.Cog):
|
|
||||||
"""
|
|
||||||
Maintains a compact, alphabetized pirates list in the configured channel.
|
|
||||||
|
|
||||||
Entry format:
|
|
||||||
- **{Character}** (*{Account}*) [{Threat}%]
|
|
||||||
- In group: {never/rarely/...}. Destructive: {never/rarely/...}. Encounters: N. Last: <t:UNIX:R>
|
|
||||||
|
|
||||||
Posts are chunked to stay <2000 chars and previous posts are deleted on refresh.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
cfg = bot.config["DEFAULT"]
|
|
||||||
self.list_channel_id = int(cfg["pirates_list_channel_id"])
|
|
||||||
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
|
|
||||||
|
|
||||||
# serialize refreshes per guild
|
|
||||||
self._locks = {}
|
|
||||||
self._no_mentions = discord.AllowedMentions.none()
|
|
||||||
|
|
||||||
def _lock_for(self, guild_id: int):
|
|
||||||
import asyncio
|
|
||||||
self._locks.setdefault(guild_id, asyncio.Lock())
|
|
||||||
return self._locks[guild_id]
|
|
||||||
|
|
||||||
# send settings: never ping on posted content
|
|
||||||
self._no_mentions = discord.AllowedMentions.none()
|
|
||||||
|
|
||||||
# ----------------- utils -----------------
|
|
||||||
|
|
||||||
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 and neuter @mentions in user-provided strings."""
|
|
||||||
safe = discord.utils.escape_markdown(str(s))
|
|
||||||
return safe.replace("@", "@\u200b")
|
|
||||||
|
|
||||||
def _encounters_for(self, pirate: dict):
|
|
||||||
"""Return encounters matching either char name or account (case-insensitive), defensively."""
|
|
||||||
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 _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 _format_entry(self, p: dict) -> str:
|
|
||||||
encs = self._encounters_for(p)
|
|
||||||
total = len(encs)
|
|
||||||
|
|
||||||
def _safe_int(v, default=0):
|
|
||||||
try:
|
|
||||||
return int(v)
|
|
||||||
except Exception:
|
|
||||||
return default
|
|
||||||
|
|
||||||
group_rate = (
|
|
||||||
sum(1 for e in encs if _safe_int(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
|
|
||||||
)
|
|
||||||
groups_str = self._bucket(group_rate, total)
|
|
||||||
destr_str = self._bucket(destroy_rate, total)
|
|
||||||
|
|
||||||
last_ts = None
|
|
||||||
if total:
|
|
||||||
try:
|
|
||||||
last_ts = max(int(float(e.get("timestamp", 0))) for e in encs)
|
|
||||||
except Exception:
|
|
||||||
last_ts = None
|
|
||||||
|
|
||||||
char = self._esc(p.get("character_name", "Unknown"))
|
|
||||||
acct = self._esc(p.get("account_name", "Unknown#00000"))
|
|
||||||
threat = int(p.get("threat_level", 0))
|
|
||||||
last_str = f"<t:{last_ts}:R>" if last_ts else "—"
|
|
||||||
|
|
||||||
line1 = f"- **{char}** (*{acct}*) [{threat}%]"
|
|
||||||
line2 = f" - In group: {groups_str}. Destructive: {destr_str}. Encounters: {total}. Last: {last_str}"
|
|
||||||
return f"{line1}\n{line2}"
|
|
||||||
|
|
||||||
async def refresh_list(self, guild: discord.Guild):
|
|
||||||
"""Edit list messages in place; only send extra messages when we need more chunks (new pirates)."""
|
|
||||||
# ---- serialize per guild ----
|
|
||||||
lock = getattr(self, "_locks", {}).get(guild.id)
|
|
||||||
if lock is None:
|
|
||||||
# tiny fallback if you didn't add _lock_for()
|
|
||||||
import asyncio as _asyncio
|
|
||||||
if not hasattr(self, "_locks"):
|
|
||||||
self._locks = {}
|
|
||||||
self._locks[guild.id] = _asyncio.Lock()
|
|
||||||
lock = self._locks[guild.id]
|
|
||||||
|
|
||||||
async with lock:
|
|
||||||
channel = guild.get_channel(self.list_channel_id)
|
|
||||||
if not channel:
|
|
||||||
print("[pirates_list] list channel not found:", self.list_channel_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
dm = self.bot.data_manager
|
|
||||||
allow = getattr(self, "_no_mentions", discord.AllowedMentions.none())
|
|
||||||
|
|
||||||
# ---- load & prune existing posts for this guild/channel ----
|
|
||||||
records = [r for r in dm.get("pirates_list_posts")
|
|
||||||
if r.get("guild_id") == guild.id and r.get("channel_id") == self.list_channel_id]
|
|
||||||
|
|
||||||
# fetch messages (drop any that vanished)
|
|
||||||
msgs, kept_records = [], []
|
|
||||||
for r in records:
|
|
||||||
try:
|
|
||||||
m = await channel.fetch_message(int(r["message_id"]))
|
|
||||||
msgs.append(m)
|
|
||||||
kept_records.append(r)
|
|
||||||
except Exception:
|
|
||||||
# prune dead record
|
|
||||||
dm.remove("pirates_list_posts", lambda x, mid=r["message_id"]: x.get("message_id") == mid)
|
|
||||||
|
|
||||||
records = kept_records # only live ones, in stored order
|
|
||||||
|
|
||||||
# ---- build fresh, sorted contents ----
|
|
||||||
pirates = sorted(
|
|
||||||
dm.get("pirates"),
|
|
||||||
key=lambda x: (
|
|
||||||
str(x.get("character_name", "")).lower(),
|
|
||||||
str(x.get("account_name", "")).lower()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Empty state
|
|
||||||
if not pirates:
|
|
||||||
placeholder = "_No verified pirates yet._"
|
|
||||||
if msgs:
|
|
||||||
# edit first, delete the rest
|
|
||||||
if msgs[0].content != placeholder:
|
|
||||||
try:
|
|
||||||
await msgs[0].edit(content=placeholder, allowed_mentions=allow)
|
|
||||||
except Exception as e:
|
|
||||||
print("[pirates_list] edit placeholder failed:", repr(e))
|
|
||||||
# remove extra posts/records
|
|
||||||
for extra in msgs[1:]:
|
|
||||||
try:
|
|
||||||
await extra.delete()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
dm.remove("pirates_list_posts", lambda r, mid=extra.id: r.get("message_id") == mid)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
m = await channel.send(placeholder, allowed_mentions=allow)
|
|
||||||
dm.add("pirates_list_posts", {
|
|
||||||
"guild_id": guild.id,
|
|
||||||
"channel_id": self.list_channel_id,
|
|
||||||
"message_id": m.id
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
print("[pirates_list] send placeholder failed:", repr(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Chunk into <2000 char blocks (~1900 for margin)
|
|
||||||
chunks, current = [], ""
|
|
||||||
for p in pirates:
|
|
||||||
entry = self._format_entry(p)
|
|
||||||
if len(current) + len(entry) + 1 > 1900:
|
|
||||||
chunks.append(current.rstrip())
|
|
||||||
current = entry + "\n"
|
|
||||||
else:
|
|
||||||
current += entry + "\n"
|
|
||||||
if current.strip():
|
|
||||||
chunks.append(current.rstrip())
|
|
||||||
|
|
||||||
# ---- edit existing messages with new content ----
|
|
||||||
common = min(len(msgs), len(chunks))
|
|
||||||
for i in range(common):
|
|
||||||
if msgs[i].content != chunks[i]:
|
|
||||||
try:
|
|
||||||
await msgs[i].edit(content=chunks[i], allowed_mentions=allow)
|
|
||||||
except Exception as e:
|
|
||||||
print("[pirates_list] edit block failed:", repr(e))
|
|
||||||
|
|
||||||
# ---- if we need *more* messages (usually after adding a pirate), send them ----
|
|
||||||
if len(chunks) > len(msgs):
|
|
||||||
for i in range(len(msgs), len(chunks)):
|
|
||||||
try:
|
|
||||||
m = await channel.send(chunks[i], allowed_mentions=allow)
|
|
||||||
dm.add("pirates_list_posts", {
|
|
||||||
"guild_id": guild.id,
|
|
||||||
"channel_id": self.list_channel_id,
|
|
||||||
"message_id": m.id
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
print("[pirates_list] send block failed:", repr(e))
|
|
||||||
|
|
||||||
# ---- if we need fewer messages (e.g., pirate removed), delete extras ----
|
|
||||||
elif len(chunks) < len(msgs):
|
|
||||||
extras = msgs[len(chunks):]
|
|
||||||
for m in extras:
|
|
||||||
try:
|
|
||||||
await m.delete()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
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.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."):
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(PiratesListCog(bot))
|
|
Loading…
Reference in New Issue
Block a user