0.3.9.2.a5

performance improvements, stability, and primarily settings-handling improvements.
  - Due to the container transition, some settings handling became quietly broken or defunct.
This commit is contained in:
Franz Rolfsvaag 2025-08-10 20:23:09 +02:00
parent 9bdb286d38
commit 5368d21be4
11 changed files with 209 additions and 138 deletions

2
bot.py
View File

@ -8,7 +8,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.2.a4"
VERSION = "0.3.9.2.a5"
# ---------- Env & config loading ----------

View File

@ -2,26 +2,13 @@
import re
import discord
from discord.ext import commands
from modules.common.settings import cfg # ENV-first config helper
def _parse_ids(raw: str):
ids = []
if not raw:
return ids
for tok in re.split(r'[,\s]+', raw.strip()):
try:
ids.append(int(tok))
except Exception:
pass
return ids
def get_mod_role_ids(bot: commands.Bot):
cfg = bot.config['DEFAULT']
# read individually; allow comma-separated in any field for flexibility
keys = ["admin_role_id", "field_mod_role_id", "intel_mod_role_id", "moderator_role_id"]
ids = []
for k in keys:
raw = cfg.get(k, "")
for tok in re.split(r"[,\s]+", raw.strip()):
if not tok:
continue
try:
@ -30,6 +17,22 @@ def get_mod_role_ids(bot: commands.Bot):
pass
return ids
def get_mod_role_ids(bot: commands.Bot):
# Read from ENV/INI via helper; allow comma-separated lists in any field
reader = cfg(bot)
keys = ["admin_role_id", "field_mod_role_id", "intel_mod_role_id", "moderator_role_id"]
collected = []
for k in keys:
collected.extend(_parse_ids(reader.get(k, "")))
# dedupe while preserving order
seen = set()
unique = []
for i in collected:
if i not in seen:
seen.add(i)
unique.append(i)
return unique
def is_moderator_member(member: discord.Member, bot: commands.Bot) -> bool:
if not isinstance(member, discord.Member):
return False

View File

@ -3,10 +3,13 @@ import asyncio
import time
import discord
from discord.ext import commands
from modules.common.settings import cfg # ENV-first config helper
def now() -> float:
return time.time()
class AutoVCCog(commands.Cog):
"""
Auto-VC:
@ -23,14 +26,14 @@ class AutoVCCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
cfg = bot.config['DEFAULT']
r = cfg(bot)
# Config
self.trigger_id = int(cfg['trigger_channel_id'])
self.category_id = int(cfg['auto_vc_category_id'])
self.prefix = cfg['vc_name_prefix']
self.delay = int(cfg.get('auto_vc_cleanup_delay', 30))
self.modlog_channel_id = int(cfg.get('modlog_channel_id', '0')) if cfg.get('modlog_channel_id') else 0
# Config (ENV/INI via helper; safe defaults)
self.trigger_id = r.int('trigger_channel_id', 0)
self.category_id = r.int('auto_vc_category_id', 0)
self.prefix = r.get('vc_name_prefix', 'Room')
self.delay = r.int('auto_vc_cleanup_delay', 30)
self.modlog_channel_id = r.int('modlog_channel_id', 0)
# State
self.empty_since: dict[int, float] = {} # channel_id -> ts when became empty
@ -92,6 +95,8 @@ class AutoVCCog(commands.Cog):
async def _cleanup_pass(self, guild: discord.Guild):
"""Delete empty tracked channels that exceeded delay and renumber."""
if not self.category_id:
return
cat = guild.get_channel(self.category_id)
if not cat:
return
@ -148,6 +153,9 @@ class AutoVCCog(commands.Cog):
async def _spawn_and_move(self, member: discord.Member):
guild = member.guild
if not self.category_id:
await self._log(guild, "⚠️ auto_vc_category_id not configured; cannot create rooms.")
return
cat = guild.get_channel(self.category_id)
if not cat:
await self._log(guild, "⚠️ auto_vc_category_id not found; cannot create rooms.")
@ -195,7 +203,7 @@ class AutoVCCog(commands.Cog):
guild = member.guild
# Create on trigger join (with 5s per-user cooldown)
if after.channel and after.channel.id == self.trigger_id:
if self.trigger_id and after.channel and after.channel.id == self.trigger_id:
last = self._vc_cooldowns.get(member.id, 0.0)
if now() - last < 5.0:
return
@ -206,7 +214,7 @@ class AutoVCCog(commands.Cog):
print("[auto_vc] spawn/move failed:", repr(e))
# Mark empties immediately on leave
if before.channel:
if before.channel and self.category_id:
ch = before.channel
if ch.category_id == self.category_id:
rec = self._find_record(guild.id, ch.id)
@ -221,7 +229,7 @@ class AutoVCCog(commands.Cog):
g = ctx.guild
recs = sorted(self._vc_records(g.id), key=lambda r: r.get('created_ts', 0))
lines = [
f"Trigger: <#{self.trigger_id}> | Category: <#{self.category_id}> | 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"
]
for idx, rec in enumerate(recs, start=1):
ch = g.get_channel(rec['channel_id'])

View File

@ -1,7 +1,10 @@
# modules/common/boot_notice.py
import os
import discord
import aiohttp
import xml.etree.ElementTree as ET
from modules.common.settings import cfg
async def _fetch_latest_subject_sha(rss_url: str) -> tuple[str | None, str | None]:
"""Best-effort: read latest commit subject + short sha from a Gitea RSS feed."""
@ -23,6 +26,7 @@ async def _fetch_latest_subject_sha(rss_url: str) -> tuple[str | None, str | Non
except Exception:
return None, None
async def post_boot_notice(bot):
"""
Posts a boot status message to the configured modlog channel.
@ -53,16 +57,15 @@ async def post_boot_notice(bot):
if not line:
return # nothing to say
try:
ch_id = int(bot.config['DEFAULT'].get('modlog_channel_id', "0") or 0)
except Exception:
ch_id = 0
if not ch_id:
# Read modlog channel from ENV/INI via helper
modlog_channel_id = cfg(bot).int('modlog_channel_id', 0)
if not modlog_channel_id:
return
# Find channel across guilds
ch = None
for g in bot.guilds:
ch = g.get_channel(ch_id)
ch = g.get_channel(modlog_channel_id)
if ch:
break
if not ch:

View File

@ -0,0 +1,34 @@
# modules/common/settings.py
import os
class ConfigView:
"""Unified read: ENV first, then optional bot.config['DEFAULT'], then fallback."""
def __init__(self, bot):
self._env = os.environ
self._default = {}
try:
self._default = getattr(bot, "config", {}).get("DEFAULT", {}) or {}
except Exception:
pass
def get(self, key: str, default: str = "") -> str:
v = self._env.get(key.upper(), "")
if not v:
v = self._default.get(key, "")
v = (v or "").strip().strip('"').strip("'")
return v if v else default
def int(self, key: str, default: int = 0) -> int:
try:
return int(self.get(key, ""))
except Exception:
return default
def bool(self, key: str, default: bool = False) -> bool:
v = self.get(key, "")
if not v:
return default
return v.lower() in ("1", "true", "yes", "on")
def cfg(bot) -> ConfigView:
return ConfigView(bot)

View File

@ -1,3 +1,4 @@
# modules/nick_nudge/nick_nudge.py
import asyncio
import time
from typing import Optional, Tuple
@ -7,6 +8,7 @@ from discord import app_commands
from mod_perms import is_moderator_userid
from modules.common.emoji_accept import is_accept
from modules.common.settings import cfg # ENV-first config helper
CHECK = '' # approved/verified
CROSS = '' # reject / no
@ -14,12 +16,14 @@ PENDING = '✔️' # heavy check mark = pending claim
ACCEPT = {CHECK, '🫡'}
NO_MENTIONS = discord.AllowedMentions.none()
def _ts_rel(ts: Optional[float] = None) -> str:
"""Discord relative timestamp like <t:12345:R>."""
if ts is None:
ts = time.time()
return f"<t:{int(ts)}:R>"
class NickNudgeCog(commands.Cog):
"""
Handles:
@ -32,14 +36,14 @@ class NickNudgeCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
cfg = bot.config['DEFAULT']
self.modlog_channel_id = int(cfg['modlog_channel_id'])
self.mod_channel_id = int(cfg['mod_channel_id']) # same review channel as pirate reports
r = cfg(bot)
# Config via helper (ENV -> optional INI fallback)
self.modlog_channel_id = r.int('modlog_channel_id', 0)
self.mod_channel_id = r.int('mod_channel_id', 0) # same review channel as pirate reports
# Optional DM nudge loop retained
try:
self.loop_enabled = cfg.getboolean('nick_nudge_loop_enabled')
except Exception:
self.loop_enabled = False
self.loop_enabled = r.bool('nick_nudge_loop_enabled', False)
self._task = asyncio.create_task(self._nudge_loop()) if self.loop_enabled else None
# ---------- utils ----------
@ -52,6 +56,8 @@ class NickNudgeCog(commands.Cog):
pass
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:
@ -85,7 +91,7 @@ class NickNudgeCog(commands.Cog):
- source: "claim" or "nick_same"
Stores in data_manager['nick_reviews'] a record keyed by the review message_id.
"""
if not guild:
if not guild or not self.mod_channel_id:
return
# If a pending review already exists for this user in this guild, do nothing
for r in self.bot.data_manager.get('nick_reviews'):
@ -135,12 +141,12 @@ class NickNudgeCog(commands.Cog):
await self.bot.wait_until_ready()
while not self.bot.is_closed():
try:
now = time.time()
now_t = time.time()
for guild in self.bot.guilds:
for member in guild.members:
if member.bot or not member.joined_at:
continue
if (now - member.joined_at.timestamp()) < 24*3600:
if (now_t - member.joined_at.timestamp()) < 24 * 3600:
continue
# If they already have a server nick OR already claimed/verified, skip nudging
dm = self.bot.data_manager
@ -162,7 +168,7 @@ class NickNudgeCog(commands.Cog):
'message_id': int(msg.id),
'user_id': int(member.id),
'guild_id': int(guild.id),
'ts': now
'ts': now_t
})
self.bot.data_manager.add('nick_nudged', int(member.id))
await self._modlog(guild, f"📨 Sent nickname nudge to {member.mention}")
@ -221,7 +227,7 @@ class NickNudgeCog(commands.Cog):
# 2) Handle moderator review reactions in mod channel
if payload.guild_id and str(payload.emoji) in (CHECK, CROSS) and payload.user_id != self.bot.user.id:
if payload.channel_id != self.mod_channel_id:
if payload.channel_id != self.mod_channel_id or not self.mod_channel_id:
return
guild = self.bot.get_guild(payload.guild_id)
if not guild:
@ -239,7 +245,7 @@ class NickNudgeCog(commands.Cog):
member = guild.get_member(int(review['user_id']))
if not member:
# mark closed missing
self.bot.data_manager.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status':'closed_missing'}), r)[1])
self.bot.data_manager.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'closed_missing'}), r)[1])
return
# Fetch and edit the review message content (best-effort)
@ -276,9 +282,7 @@ class NickNudgeCog(commands.Cog):
pass
# Modlog
await self._modlog(guild,
f"✅ Nickname **verified** for {member.mention} by {approver}{_ts_rel(now_ts)}."
)
await self._modlog(guild, f"✅ Nickname **verified** for {member.mention} by {approver}{_ts_rel(now_ts)}.")
# Refresh roles / card
rr = self.bot.get_cog('ReactionRoleCog')
@ -313,9 +317,7 @@ class NickNudgeCog(commands.Cog):
except Exception:
pass
await self._modlog(guild,
f"❌ Nickname **rejected** for {member.mention} by {approver}{_ts_rel(now_ts)}."
)
await self._modlog(guild, f"❌ Nickname **rejected** for {member.mention} by {approver}{_ts_rel(now_ts)}.")
# Refresh roles / card
rr = self.bot.get_cog('ReactionRoleCog')
@ -331,5 +333,6 @@ class NickNudgeCog(commands.Cog):
except Exception:
pass
async def setup(bot):
await bot.add_cog(NickNudgeCog(bot))

View File

@ -3,24 +3,22 @@ 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
from mod_perms import require_mod_ctx
from modules.common.settings import cfg # ENV-first config helper
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
r = cfg(bot)
# 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
# IDs / config (ENV -> optional INI fallback)
self.pirates_channel_id = r.int('pirates_list_channel_id', 0)
self.modlog_channel_id = r.int('modlog_channel_id', 0)
# thresholds / samples (with defaults)
self.group_threshold = r.int('threat_group_threshold', 3)
self.min_samples = r.int('threat_min_samples_for_stats', 3)
# safe posting (dont ping)
self._no_mentions = discord.AllowedMentions.none()
@ -97,7 +95,7 @@ class PirateCardsCog(commands.Cog):
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)
@ -222,5 +220,6 @@ class PirateCardsCog(commands.Cog):
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))

View File

@ -1,9 +1,11 @@
# modules/pirate_report/pirate_report.py
import re
import time
from datetime import datetime, timezone
import discord
from discord.ext import commands
from discord import app_commands
from modules.common.settings import cfg
from mod_perms import (
is_moderator_member,
@ -341,20 +343,25 @@ class EncounterModal(discord.ui.Modal, title="Log Pirate Encounter"):
class PirateReportCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
cfg = bot.config['DEFAULT']
self.mod_channel = int(cfg['mod_channel_id'])
self.modlog_channel_id = int(cfg['modlog_channel_id'])
r = cfg(bot)
# Optional threat weights (normalized elsewhere if you added them)
# Channels
self.mod_channel = r.int('mod_channel_id', 0)
self.modlog_channel_id = r.int('modlog_channel_id', 0)
# Threat config
self.group_threshold = r.int('threat_group_threshold', 3)
def _f(key: str, default: float) -> float:
try:
self.group_threshold = int(cfg.get('threat_group_threshold', '3'))
return float(r.get(key, str(default)))
except Exception:
self.group_threshold = 3
# Defaults if not already present in your earlier version:
self.w_kill = float(cfg.get('threat_w_kill', '0.35'))
self.w_destruction = float(cfg.get('threat_w_destruction', '0.30'))
self.w_group = float(cfg.get('threat_w_group', '0.20'))
self.w_skill = float(cfg.get('threat_w_skill', '0.15'))
return default
self.w_kill = _f('threat_w_kill', 0.35)
self.w_destruction = _f('threat_w_destruction', 0.30)
self.w_group = _f('threat_w_group', 0.20)
self.w_skill = _f('threat_w_skill', 0.15)
async def _refresh_pirates_list(self, guild: discord.Guild):
plist = self.bot.get_cog('PiratesListCog')
@ -362,6 +369,8 @@ class PirateReportCog(commands.Cog):
await plist.refresh_list(guild)
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:
@ -563,11 +572,8 @@ async def setup(bot):
cog = PirateReportCog(bot)
await bot.add_cog(cog)
try:
home_gid = int(bot.config['DEFAULT'].get('home_guild_id', '0'))
except Exception:
home_gid = 0
# Register commands either globally or to a specific guild if configured
home_gid = cfg(bot).int('home_guild_id', 0)
if home_gid:
guild_obj = discord.Object(id=home_gid)
bot.tree.add_command(cog.report, guild=guild_obj)

View File

@ -1,6 +1,8 @@
# modules/reaction_role/reaction_role.py
import discord
from discord.ext import commands
from modules.common.emoji_accept import is_accept
from modules.common.settings import cfg # ENV-first helper
CHECKMARK = ''
@ -16,21 +18,16 @@ class ReactionRoleCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
cfg = bot.config['DEFAULT']
r = cfg(bot)
def _i(key):
try:
v = cfg.get(key)
return int(v) if v else 0
except Exception:
return 0
# Message + role IDs from ENV/INI (default 0 if unset)
self.rules_msg_id = r.int('rules_message_id', 0)
self.engage_msg_id = r.int('engagement_message_id', 0)
self.nick_msg_id = r.int('nickname_message_id', 0)
self.rules_msg_id = _i('rules_message_id')
self.engage_msg_id = _i('engagement_message_id')
self.nick_msg_id = _i('nickname_message_id')
self.rules_role = _i('rules_role_id')
self.engage_role = _i('engagement_role_id')
self.full_access_role = _i('full_access_role_id')
self.rules_role = r.int('rules_role_id', 0)
self.engage_role = r.int('engagement_role_id', 0)
self.full_access_role = r.int('full_access_role_id', 0)
# ---- helpers ----
def _has_rules(self, member_id: int) -> bool:
@ -215,5 +212,6 @@ class ReactionRoleCog(commands.Cog):
await self.maybe_apply_full_access(member)
async def setup(bot):
await bot.add_cog(ReactionRoleCog(bot))

View File

@ -6,6 +6,7 @@ from typing import List, Dict, Tuple, Optional
import discord
from discord.ext import commands
from discord import app_commands
from modules.common.settings import cfg
# Accept both for backward compatibility; display uses "Refiner"
VALID_ROLES = {"crawler_owner", "carrier_owner", "refiner_owner", "lsr_owner"}
@ -560,12 +561,12 @@ class _StartView(discord.ui.View):
class SpicePayCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.sessions: Dict[tuple, Dict] = {}
self-sessions: Dict[tuple, Dict] = {}
cfg = bot.config['DEFAULT']
r = cfg(bot)
def _f(key, default):
try:
return float(cfg.get(key, str(default)))
return float(r.get(key, str(default)))
except Exception:
return float(default)
self.base_weight = _f('spicepay_base_weight', 25.0)
@ -575,7 +576,8 @@ class SpicePayCog(commands.Cog):
def _i(key):
try:
return int(cfg.get(key)) if cfg.get(key) else None
v = r.get(key, "")
return int(v) if v else None
except Exception:
return None
self.emoji_sand_id = _i('emoji_sand_id')
@ -679,7 +681,7 @@ class SpicePayCog(commands.Cog):
f"- Base weight: **{self.base_weight} × active %**\n"
f"- Carrier bonus: **+{self.carrier_bonus}**\n"
f"- Crawler bonus: **+{self.crawler_bonus}**\n\n"
"_Edit these in `settings.conf` under `[DEFAULT]` and restart the bot._"
"_Set via environment variables or your INI. Restart the bot after changing._"
)
await interaction.response.send_message(txt, ephemeral=True)
@ -899,11 +901,7 @@ async def setup(bot):
cog = SpicePayCog(bot)
await bot.add_cog(cog)
try:
home_gid = int(bot.config['DEFAULT'].get('home_guild_id', '0'))
except Exception:
home_gid = 0
home_gid = cfg(bot).int('home_guild_id', 0)
if home_gid:
guild_obj = discord.Object(id=home_gid)
bot.tree.add_command(cog.spicepay, guild=guild_obj)

View File

@ -1,9 +1,11 @@
# modules/user_cards/user_cards.py
import asyncio
import time
from typing import Optional, Set, Tuple
import discord
from discord.ext import commands
from modules.common.emoji_accept import is_accept
from modules.common.settings import cfg # ENV-first helper
CHECK = '' # verified
CROSS = '' # not done
@ -11,6 +13,7 @@ PENDING = '✔️' # claimed / pending review
ACCEPT = {CHECK, '🫡'}
NO_MENTIONS = discord.AllowedMentions.none()
class UserCardsCog(commands.Cog):
"""
Per-user status cards with live reconcile and offline review triggers.
@ -25,34 +28,35 @@ class UserCardsCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
cfg = bot.config['DEFAULT']
self.userslist_channel_id = int(cfg['userslist_channel_id'])
self.modlog_channel_id = int(cfg['modlog_channel_id'])
self.mod_channel_id = int(cfg.get('mod_channel_id', '0') or 0)
r = cfg(bot)
# Channels / IDs from ENV/INI
self.userslist_channel_id = r.int('userslist_channel_id', 0)
self.modlog_channel_id = r.int('modlog_channel_id', 0)
self.mod_channel_id = r.int('mod_channel_id', 0)
# reaction-role authoritative messages/roles
self.rules_msg_id = int(cfg['rules_message_id'])
self.engage_msg_id = int(cfg['engagement_message_id'])
self.nick_msg_id = int(cfg['nickname_message_id'])
self.rules_role_id = int(cfg['rules_role_id'])
self.engage_role_id = int(cfg['engagement_role_id'])
self.full_access_role_id = int(cfg['full_access_role_id'])
self.rules_msg_id = r.int('rules_message_id', 0)
self.engage_msg_id = r.int('engagement_message_id', 0)
self.nick_msg_id = r.int('nickname_message_id', 0)
self.rules_role_id = r.int('rules_role_id', 0)
self.engage_role_id = r.int('engagement_role_id', 0)
self.full_access_role_id = r.int('full_access_role_id', 0)
self._refresh_locks = {} # per-user locks to avoid racey double-posts
# Optional periodic refresh (twice a day)
try:
self.cron_enabled = cfg.getboolean('user_cards_cron_enabled')
except Exception:
self.cron_enabled = False
self.cron_enabled = r.bool('user_cards_cron_enabled', False)
self._cron_task = asyncio.create_task(self._periodic_refresh()) if self.cron_enabled else None
self._startup_task = asyncio.create_task(self._startup_reconcile())
def cog_unload(self):
for t in (self._cron_task, self._startup_task):
if t:
try: t.cancel()
except Exception: pass
try:
t.cancel()
except Exception:
pass
# ---------- status helpers ----------
@ -126,7 +130,7 @@ class UserCardsCog(commands.Cog):
if member.avatar:
embed.set_thumbnail(url=member.avatar.url)
# NEW: stable identity so we can find/edit the right card later
# Stable identity so we can find/edit the right card later
embed.set_footer(text=f"UID:{member.id}")
return embed
@ -137,7 +141,7 @@ class UserCardsCog(commands.Cog):
2) If not found, search the channel by footer marker and edit that.
3) If still not found, post a new one, then delete any stragglers with the same marker.
"""
if not member or not member.guild:
if not member or not member.guild or not self.userslist_channel_id:
return
async with self._lock_for(member.id):
@ -183,11 +187,13 @@ class UserCardsCog(commands.Cog):
return
# 4) Post fresh card
try:
new_msg = await channel.send(embed=embed, allowed_mentions=NO_MENTIONS)
except Exception:
return
# 5) Clean up any other messages that look like this user's card
try:
# Find any *other* occurrences with the same footer marker and delete them
marker = f"UID:{member.id}"
async for m in channel.history(limit=400, oldest_first=False):
if m.id == new_msg.id or m.author.id != self.bot.user.id or not m.embeds:
@ -229,10 +235,14 @@ class UserCardsCog(commands.Cog):
return None
async def _log(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=NO_MENTIONS)
except Exception: pass
try:
await ch.send(content, allowed_mentions=NO_MENTIONS)
except Exception:
pass
self.bot.data_manager.add('modlog', {'guild_id': guild.id, 'content': content})
# ---------- RR message lookup & reactor collection ----------
@ -249,6 +259,8 @@ class UserCardsCog(commands.Cog):
dm.add('rr_msg_channels', {'guild_id': guild_id, 'message_id': int(message_id), 'channel_id': int(channel_id)})
async def _get_message_by_id(self, guild: discord.Guild, message_id: int) -> Optional[discord.Message]:
if not message_id:
return None
ch_id = self._get_cached_msg_channel_id(guild.id, message_id)
if ch_id:
ch = guild.get_channel(ch_id)
@ -362,7 +374,7 @@ class UserCardsCog(commands.Cog):
except Exception:
pass
# --- New part: open reviews for *any* unreviewed claimers (startup/offline) ---
# --- Open reviews for *any* unreviewed claimers (startup/offline) ---
nn = self.bot.get_cog('NickNudgeCog')
verified_set = set(dm.get('nick_verified'))
@ -431,7 +443,7 @@ class UserCardsCog(commands.Cog):
await self._log(member.guild, f"📝 User joined: {member.mention} (ID: {member.id})")
await self.refresh_card(member)
@commands.Cog.listener()
@commands.Cog.listener())
async def on_member_update(self, before: discord.Member, after: discord.Member):
if before.nick != after.nick or before.roles != after.roles:
await self.refresh_card(after)
@ -443,8 +455,10 @@ class UserCardsCog(commands.Cog):
for g in self.bot.guilds:
m = g.get_member(after.id)
if m:
try: await self.refresh_card(m)
except Exception: pass
try:
await self.refresh_card(m)
except Exception:
pass
# ---------- periodic + startup ----------
@ -458,16 +472,20 @@ class UserCardsCog(commands.Cog):
for g in list(self.bot.guilds):
for m in g.members:
if not m.bot:
try: await self.refresh_card(m)
except Exception: pass
try:
await self.refresh_card(m)
except Exception:
pass
async def _periodic_refresh(self):
await self.bot.wait_until_ready()
while not self.bot.is_closed():
try:
for g in self.bot.guilds:
try: await self._reconcile_agreements(g)
except Exception: pass
try:
await self._reconcile_agreements(g)
except Exception:
pass
for m in g.members:
if not m.bot:
await self.refresh_card(m)
@ -506,5 +524,6 @@ class UserCardsCog(commands.Cog):
ephemeral=True
)
async def setup(bot):
await bot.add_cog(UserCardsCog(bot))