0.3.9.3.a2
Fixes nick review lock, preventing re-reviews to be sent out despite genuine
This commit is contained in:
parent
268966a4ae
commit
c09f36162d
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.3.9.3.a1"
|
VERSION = "0.3.9.3.a2"
|
||||||
|
|
||||||
# ---------- Env loading ----------
|
# ---------- Env loading ----------
|
||||||
|
|
||||||
|
@ -31,23 +31,25 @@ def _ts_rel(ts: Optional[float] = None) -> str:
|
|||||||
|
|
||||||
class NickNudgeCog(commands.Cog):
|
class NickNudgeCog(commands.Cog):
|
||||||
"""
|
"""
|
||||||
Handles:
|
Nickname review flow:
|
||||||
• DM nickname nudge loop (optional; unchanged behavior)
|
- Atomic transition to 'pending' and open exactly ONE review.
|
||||||
• Nickname *review* workflow for claims:
|
- Mods: ✅ approve -> mark verified; ❌ reject -> clear claim.
|
||||||
- Atomic transition to pending + open exactly ONE review
|
- If a verified user changes their nickname, verification is revoked automatically.
|
||||||
- Mods react: ✅ -> mark verified; ❌ -> clear claim and revoke Full Access
|
Data keys used in data_manager:
|
||||||
• Stores review mapping in data_manager['nick_reviews']
|
• agreed_nickname: [user_id]
|
||||||
|
• nick_claim_pending: [user_id]
|
||||||
|
• nick_verified: [user_id]
|
||||||
|
• nick_reviews: [{ message_id, guild_id, user_id, status, ... }]
|
||||||
|
• nick_verified_name: [{ guild_id, user_id, nick, ts }]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
r = cfg(bot)
|
r = cfg(bot)
|
||||||
|
|
||||||
# Config via helper (ENV -> optional INI fallback)
|
|
||||||
self.modlog_channel_id = r.int('modlog_channel_id', 0)
|
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
|
self.mod_channel_id = r.int('mod_channel_id', 0)
|
||||||
|
|
||||||
# Optional DM nudge loop retained
|
|
||||||
self.loop_enabled = r.bool('nick_nudge_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
|
self._task = asyncio.create_task(self._nudge_loop()) if self.loop_enabled else None
|
||||||
|
|
||||||
@ -72,10 +74,6 @@ class NickNudgeCog(commands.Cog):
|
|||||||
self.bot.data_manager.add('modlog', {'ts': time.time(), 'guild_id': guild.id, 'content': content})
|
self.bot.data_manager.add('modlog', {'ts': time.time(), 'guild_id': guild.id, 'content': content})
|
||||||
|
|
||||||
async def _find_last_nick_change(self, guild: discord.Guild, member: discord.Member) -> Tuple[Optional[str], Optional[str]]:
|
async def _find_last_nick_change(self, guild: discord.Guild, member: discord.Member) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""
|
|
||||||
Best-effort: look up last nickname change via audit logs.
|
|
||||||
Returns (before_nick, after_nick) or (None, None) if not found/allowed.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
async for entry in guild.audit_logs(limit=10, action=discord.AuditLogAction.member_update):
|
async for entry in guild.audit_logs(limit=10, action=discord.AuditLogAction.member_update):
|
||||||
if entry.target.id != member.id or not entry.changes:
|
if entry.target.id != member.id or not entry.changes:
|
||||||
@ -129,13 +127,9 @@ class NickNudgeCog(commands.Cog):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ---------- public API (kept; called by ensure_pending...) ----------
|
# ---------- public API ----------
|
||||||
|
|
||||||
async def start_nick_review(self, guild: discord.Guild, member: discord.Member, source: str = "claim"):
|
async def start_nick_review(self, guild: discord.Guild, member: discord.Member, source: str = "claim"):
|
||||||
"""
|
|
||||||
Create a nickname review entry for this member in the mod channel.
|
|
||||||
Stores in data_manager['nick_reviews'] a record keyed by the review message_id.
|
|
||||||
"""
|
|
||||||
if not guild or not self.mod_channel_id:
|
if not guild or not self.mod_channel_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -143,10 +137,9 @@ class NickNudgeCog(commands.Cog):
|
|||||||
if not mod_ch:
|
if not mod_ch:
|
||||||
return
|
return
|
||||||
|
|
||||||
before_n, after_n = await self._find_last_nick_change(guild, member)
|
before_n, _ = await self._find_last_nick_change(guild, member)
|
||||||
now_ts = int(time.time())
|
now_ts = int(time.time())
|
||||||
|
|
||||||
# Compose review text
|
|
||||||
title = "📝 **Nickname Verification Request**"
|
title = "📝 **Nickname Verification Request**"
|
||||||
who = f"User: {member.mention} (`{member.id}`)"
|
who = f"User: {member.mention} (`{member.id}`)"
|
||||||
change = f"Claimed {_ts_rel(now_ts)}"
|
change = f"Claimed {_ts_rel(now_ts)}"
|
||||||
@ -162,7 +155,6 @@ class NickNudgeCog(commands.Cog):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Persist review mapping
|
|
||||||
self.bot.data_manager.add('nick_reviews', {
|
self.bot.data_manager.add('nick_reviews', {
|
||||||
'message_id': int(msg.id),
|
'message_id': int(msg.id),
|
||||||
'guild_id': int(guild.id),
|
'guild_id': int(guild.id),
|
||||||
@ -174,7 +166,6 @@ class NickNudgeCog(commands.Cog):
|
|||||||
'ts': now_ts
|
'ts': now_ts
|
||||||
})
|
})
|
||||||
|
|
||||||
# Log to modlog
|
|
||||||
await self._modlog(guild, f"🔎 Nickname review opened for {member.mention} — {method} — {_ts_rel(now_ts)}.")
|
await self._modlog(guild, f"🔎 Nickname review opened for {member.mention} — {method} — {_ts_rel(now_ts)}.")
|
||||||
|
|
||||||
# ---------- DM nudge loop (unchanged) ----------
|
# ---------- DM nudge loop (unchanged) ----------
|
||||||
@ -190,7 +181,6 @@ class NickNudgeCog(commands.Cog):
|
|||||||
continue
|
continue
|
||||||
if (now_t - member.joined_at.timestamp()) < 24 * 3600:
|
if (now_t - member.joined_at.timestamp()) < 24 * 3600:
|
||||||
continue
|
continue
|
||||||
# If they already have a server nick OR already claimed/verified, skip nudging
|
|
||||||
dm = self.bot.data_manager
|
dm = self.bot.data_manager
|
||||||
if (member.nick and member.nick.strip()):
|
if (member.nick and member.nick.strip()):
|
||||||
continue
|
continue
|
||||||
@ -218,10 +208,52 @@ class NickNudgeCog(commands.Cog):
|
|||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
await asyncio.sleep(1800) # every 30 minutes
|
await asyncio.sleep(1800)
|
||||||
|
|
||||||
# ---------- listeners ----------
|
# ---------- listeners ----------
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||||
|
"""Revoke verification if a verified user changes their nick to anything."""
|
||||||
|
if before.bot or before.guild != after.guild:
|
||||||
|
return
|
||||||
|
if before.nick == after.nick:
|
||||||
|
return
|
||||||
|
|
||||||
|
dm = self.bot.data_manager
|
||||||
|
# Only act if the user is currently verified
|
||||||
|
if before.id in dm.get('nick_verified'):
|
||||||
|
dm.remove('nick_verified', lambda x: x == before.id)
|
||||||
|
dm.remove('nick_claim_pending', lambda x: x == before.id)
|
||||||
|
try:
|
||||||
|
dm.remove('nick_verified_name', lambda r: r.get('guild_id') == before.guild.id and r.get('user_id') == before.id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
dm.add('nick_verified_name', {
|
||||||
|
'guild_id': int(before.guild.id),
|
||||||
|
'user_id': int(before.id),
|
||||||
|
'nick': after.nick if after.nick else None,
|
||||||
|
'ts': int(time.time())
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self._modlog(after.guild, f"⚠️ {after.mention} changed nickname; **verification revoked**. They must re-claim for a new review.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
rr = self.bot.get_cog('ReactionRoleCog')
|
||||||
|
if rr:
|
||||||
|
try:
|
||||||
|
await rr.maybe_apply_full_access(after)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
cards = self.bot.get_cog('UserCardsCog')
|
||||||
|
if cards:
|
||||||
|
try:
|
||||||
|
await cards.refresh_card(after)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
|
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
|
||||||
# 1) Handle DM nudge confirmations (user reacts with an accept in DM)
|
# 1) Handle DM nudge confirmations (user reacts with an accept in DM)
|
||||||
@ -234,16 +266,13 @@ class NickNudgeCog(commands.Cog):
|
|||||||
if not member:
|
if not member:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Atomic claim path
|
|
||||||
try:
|
try:
|
||||||
await self.ensure_pending_and_maybe_open(guild, member, source="nick_same")
|
await self.ensure_pending_and_maybe_open(guild, member, source="nick_same")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Clean map entry
|
|
||||||
self.bot.data_manager.remove('nick_dm_map', lambda m: m['message_id'] == payload.message_id)
|
self.bot.data_manager.remove('nick_dm_map', lambda m: m['message_id'] == payload.message_id)
|
||||||
|
|
||||||
# Refresh card and maybe full access (pending does NOT block full access)
|
|
||||||
rr = self.bot.get_cog('ReactionRoleCog')
|
rr = self.bot.get_cog('ReactionRoleCog')
|
||||||
if rr:
|
if rr:
|
||||||
try:
|
try:
|
||||||
@ -265,11 +294,9 @@ class NickNudgeCog(commands.Cog):
|
|||||||
guild = self.bot.get_guild(payload.guild_id)
|
guild = self.bot.get_guild(payload.guild_id)
|
||||||
if not guild:
|
if not guild:
|
||||||
return
|
return
|
||||||
# Only moderators can act
|
|
||||||
if not is_moderator_userid(guild, payload.user_id, self.bot):
|
if not is_moderator_userid(guild, payload.user_id, self.bot):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Is this a review message?
|
|
||||||
reviews = self.bot.data_manager.get('nick_reviews')
|
reviews = self.bot.data_manager.get('nick_reviews')
|
||||||
review = next((r for r in reviews if r.get('message_id') == payload.message_id and r.get('guild_id') == guild.id), None)
|
review = next((r for r in reviews if r.get('message_id') == payload.message_id and r.get('guild_id') == guild.id), None)
|
||||||
if not review or review.get('status') != 'pending':
|
if not review or review.get('status') != 'pending':
|
||||||
@ -277,11 +304,9 @@ class NickNudgeCog(commands.Cog):
|
|||||||
|
|
||||||
member = guild.get_member(int(review['user_id']))
|
member = guild.get_member(int(review['user_id']))
|
||||||
if not member:
|
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
|
return
|
||||||
|
|
||||||
# Fetch and edit the review message content (best-effort)
|
|
||||||
try:
|
try:
|
||||||
ch = self.bot.get_channel(payload.channel_id)
|
ch = self.bot.get_channel(payload.channel_id)
|
||||||
msg = await ch.fetch_message(payload.message_id)
|
msg = await ch.fetch_message(payload.message_id)
|
||||||
@ -293,17 +318,25 @@ class NickNudgeCog(commands.Cog):
|
|||||||
approver = f"<@{payload.user_id}>"
|
approver = f"<@{payload.user_id}>"
|
||||||
|
|
||||||
if str(payload.emoji) == CHECK:
|
if str(payload.emoji) == CHECK:
|
||||||
# Approve: mark verified, clear pending, ensure agreed flag set
|
|
||||||
if member.id not in dm.get('agreed_nickname'):
|
if member.id not in dm.get('agreed_nickname'):
|
||||||
dm.add('agreed_nickname', int(member.id))
|
dm.add('agreed_nickname', int(member.id))
|
||||||
dm.remove('nick_claim_pending', lambda x: x == member.id)
|
dm.remove('nick_claim_pending', lambda x: x == member.id)
|
||||||
if member.id not in dm.get('nick_verified'):
|
if member.id not in dm.get('nick_verified'):
|
||||||
dm.add('nick_verified', int(member.id))
|
dm.add('nick_verified', int(member.id))
|
||||||
|
|
||||||
# Update review record
|
try:
|
||||||
|
dm.remove('nick_verified_name', lambda r: r.get('guild_id') == guild.id and r.get('user_id') == member.id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
dm.add('nick_verified_name', {
|
||||||
|
'guild_id': int(guild.id),
|
||||||
|
'user_id': int(member.id),
|
||||||
|
'nick': member.nick if member.nick else None,
|
||||||
|
'ts': now_ts
|
||||||
|
})
|
||||||
|
|
||||||
dm.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'approved', 'decided_ts': now_ts, 'moderator_id': int(payload.user_id)}), r)[1])
|
dm.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'approved', 'decided_ts': now_ts, 'moderator_id': int(payload.user_id)}), r)[1])
|
||||||
|
|
||||||
# Edit the review message
|
|
||||||
if msg:
|
if msg:
|
||||||
try:
|
try:
|
||||||
await msg.clear_reactions()
|
await msg.clear_reactions()
|
||||||
@ -314,10 +347,8 @@ class NickNudgeCog(commands.Cog):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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')
|
rr = self.bot.get_cog('ReactionRoleCog')
|
||||||
if rr:
|
if rr:
|
||||||
try:
|
try:
|
||||||
@ -332,12 +363,10 @@ class NickNudgeCog(commands.Cog):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Reject: clear all nickname flags; Full Access should be revoked by maybe_apply_full_access
|
|
||||||
dm.remove('agreed_nickname', lambda x: x == member.id)
|
dm.remove('agreed_nickname', lambda x: x == member.id)
|
||||||
dm.remove('nick_claim_pending', lambda x: x == member.id)
|
dm.remove('nick_claim_pending', lambda x: x == member.id)
|
||||||
dm.remove('nick_verified', lambda x: x == member.id)
|
dm.remove('nick_verified', lambda x: x == member.id)
|
||||||
|
|
||||||
# Update review record
|
|
||||||
dm.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'rejected', 'decided_ts': now_ts, 'moderator_id': int(payload.user_id)}), r)[1])
|
dm.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'rejected', 'decided_ts': now_ts, 'moderator_id': int(payload.user_id)}), r)[1])
|
||||||
|
|
||||||
if msg:
|
if msg:
|
||||||
@ -352,7 +381,6 @@ class NickNudgeCog(commands.Cog):
|
|||||||
|
|
||||||
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')
|
rr = self.bot.get_cog('ReactionRoleCog')
|
||||||
if rr:
|
if rr:
|
||||||
try:
|
try:
|
||||||
|
@ -138,6 +138,16 @@ class ReactionRoleCog(commands.Cog):
|
|||||||
dm.add('agreed_engagement', int(member.id))
|
dm.add('agreed_engagement', int(member.id))
|
||||||
|
|
||||||
elif payload.message_id == self.nick_msg_id:
|
elif payload.message_id == self.nick_msg_id:
|
||||||
|
# --- STALE-STATE CLEANUP ---
|
||||||
|
# If no pending review exists for this user in this guild, make sure 'nick_claim_pending'
|
||||||
|
# is cleared so the atomic method can transition and open a new review.
|
||||||
|
has_pending_review = any(
|
||||||
|
r.get('guild_id') == guild.id and r.get('user_id') == member.id and r.get('status') == 'pending'
|
||||||
|
for r in dm.get('nick_reviews')
|
||||||
|
)
|
||||||
|
if not has_pending_review:
|
||||||
|
dm.remove('nick_claim_pending', lambda x: x == member.id)
|
||||||
|
|
||||||
# Atomic claim -> ONE review only
|
# Atomic claim -> ONE review only
|
||||||
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'):
|
||||||
|
Loading…
Reference in New Issue
Block a user