import discord from discord.ext import commands CHECKMARK = '✅' ACCEPT = {CHECKMARK, '🫡'} class ReactionRoleCog(commands.Cog): """ Records agreements and manages Full Access. Now integrates nickname *pending/verified* flow: • Nickname reaction add -> mark agreed + pending, open review via NickNudgeCog • Nickname reaction remove -> clear agreed/pending/verified and re-check access • /nick_same -> same as claim (no reaction required) Full Access: granted when Rules ✅ + RoE ✅ + Nickname **claimed (pending or verified)**. Revoked only when nickname becomes unclaimed (rejected or unreacted) or when Rules/RoE are missing. """ def __init__(self, bot): self.bot = bot cfg = bot.config['DEFAULT'] def _i(key): try: v = cfg.get(key) return int(v) if v else 0 except Exception: return 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') # ---- helpers ---- def _has_rules(self, member_id: int) -> bool: return member_id in self.bot.data_manager.get('agreed_rules') def _has_engage(self, member_id: int) -> bool: return member_id in self.bot.data_manager.get('agreed_engagement') def _has_nick_claim(self, member_id: int) -> bool: """Claimed = agreed_nickname; pending/verified tracked separately.""" return member_id in self.bot.data_manager.get('agreed_nickname') async def maybe_apply_full_access(self, member: discord.Member): """Grant when Rules+RoE+Nickname *claimed*; revoke when any missing.""" guild = member.guild role = guild.get_role(self.full_access_role) if self.full_access_role else None if not role: return has_all = self._has_rules(member.id) and self._has_engage(member.id) and self._has_nick_claim(member.id) try: if has_all and role not in member.roles: await member.add_roles(role, reason="All agreements completed (nickname may be pending)") elif not has_all and role in member.roles: await member.remove_roles(role, reason="Agreements incomplete or nickname unclaimed") except discord.Forbidden: pass except Exception: pass # Best-effort: refresh user card cards = self.bot.get_cog('UserCardsCog') if cards: try: await cards.refresh_card(member) except Exception: pass async def _get_member(self, guild: discord.Guild, user_id: int): if not guild: return None m = guild.get_member(user_id) if m is None: try: m = await guild.fetch_member(user_id) except Exception: return None return m # ---- commands (hybrid = prefix + slash) ---- @commands.hybrid_command(name='nick_same', description='Claim that your global display name matches your in-game name (triggers mod review)') async def nick_same(self, ctx: commands.Context): member = ctx.author if isinstance(ctx.author, discord.Member) else None if not member or not ctx.guild: return await ctx.reply("Use this in a server.", ephemeral=True) dm = self.bot.data_manager if member.id not in dm.get('agreed_nickname'): dm.add('agreed_nickname', int(member.id)) # Mark pending (clear verified if present) dm.remove('nick_verified', lambda x: x == member.id) if member.id not in dm.get('nick_claim_pending'): dm.add('nick_claim_pending', int(member.id)) # Open/refresh a review with NickNudge nn = self.bot.get_cog('NickNudgeCog') if nn and hasattr(nn, 'start_nick_review'): try: await nn.start_nick_review(ctx.guild, member, source="nick_same") except Exception: pass await self.maybe_apply_full_access(member) await ctx.reply("Thanks — your nickname claim was sent for moderator review.", ephemeral=True) # ---- listeners ---- @commands.Cog.listener() async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): if str(payload.emoji) not in ACCEPT or not payload.guild_id: return guild = self.bot.get_guild(payload.guild_id) member = await self._get_member(guild, payload.user_id) if not member or member.bot: return dm = self.bot.data_manager try: if payload.message_id == self.rules_msg_id: role = guild.get_role(self.rules_role) if role: await member.add_roles(role, reason="Agreed to rules") if member.id not in dm.get('agreed_rules'): dm.add('agreed_rules', int(member.id)) elif payload.message_id == self.engage_msg_id: role = guild.get_role(self.engage_role) if role: await member.add_roles(role, reason="Agreed to engagement") if member.id not in dm.get('agreed_engagement'): dm.add('agreed_engagement', int(member.id)) elif payload.message_id == self.nick_msg_id: # Claim nickname via reaction -> mark agreed + pending, clear verified if member.id not in dm.get('agreed_nickname'): dm.add('agreed_nickname', int(member.id)) dm.remove('nick_verified', lambda x: x == member.id) if member.id not in dm.get('nick_claim_pending'): dm.add('nick_claim_pending', int(member.id)) # Kick off a review in mod channel nn = self.bot.get_cog('NickNudgeCog') if nn and hasattr(nn, 'start_nick_review'): try: await nn.start_nick_review(guild, member, source="claim") except Exception: pass else: return except Exception: pass await self.maybe_apply_full_access(member) @commands.Cog.listener() async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent): if str(payload.emoji) not in ACCEPT or not payload.guild_id: return guild = self.bot.get_guild(payload.guild_id) member = await self._get_member(guild, payload.user_id) if not member or member.bot: return dm = self.bot.data_manager try: if payload.message_id == self.rules_msg_id: dm.remove('agreed_rules', lambda x: x == member.id) role = guild.get_role(self.rules_role) if role: await member.remove_roles(role, reason="Rules un-ticked") elif payload.message_id == self.engage_msg_id: dm.remove('agreed_engagement', lambda x: x == member.id) role = guild.get_role(self.engage_role) if role: await member.remove_roles(role, reason="Engagement un-ticked") elif payload.message_id == self.nick_msg_id: # Un-claim nickname -> clear everything related dm.remove('agreed_nickname', lambda x: x == member.id) dm.remove('nick_claim_pending', lambda x: x == member.id) dm.remove('nick_verified', lambda x: x == member.id) else: return except Exception: pass await self.maybe_apply_full_access(member) async def setup(bot): await bot.add_cog(ReactionRoleCog(bot))