| 
						 
							
							
							
						 
					 | 
				
			
			 | 
			 | 
			
				@ -1,216 +1,72 @@
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				# modules/reaction_role/reaction_role.py
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				from __future__ import annotations
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				from datetime import datetime, timezone
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				from typing import Any, Dict, Optional, Iterable, Tuple
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import time
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import discord
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				from discord.ext import commands
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				from discord import app_commands
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				from modules.common.emoji_accept import is_accept
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				from modules.common.settings import cfg  # dynamic settings helper
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				from modules.common.settings import cfg  # ENV-first helper
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				CHECKMARK = '✅'  # kept for consistency with other flows
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				# ------------------ small helpers ------------------
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				def _fmt_dt(ts: float) -> str:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return discord.utils.format_dt(datetime.fromtimestamp(ts, tz=timezone.utc), style="f")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				def _as_list(x: Optional[Iterable]) -> list:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return list(x) if isinstance(x, Iterable) else []
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				# ------------------ Fedaykin approval buttons ------------------
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				class _FedaykinApprovalView(discord.ui.View):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    """Approval buttons for a single Fedaykin request (persistent)."""
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def __init__(self, cog: "ReactionRoleCog", req: Dict[str, Any], *, timeout: Optional[float] = None):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        super().__init__(timeout=timeout)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self.cog = cog
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self.req = req  # dict persisted via data_manager
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def interaction_check(self, interaction: discord.Interaction) -> bool:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Only Field Mod or Admin may act
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        member = interaction.user
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not isinstance(member, discord.Member):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            await interaction.response.send_message("Server context required.", ephemeral=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return False
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        allow_roles = {self.cog.field_mod_role_id, self.cog.admin_role_id}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not any(r.id in allow_roles for r in getattr(member, "roles", [])):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            await interaction.response.send_message("You are not allowed to decide this request.", ephemeral=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return False
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        return True
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _finish(self, interaction: discord.Interaction, status: str):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self.cog._fedaykin_decide(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            guild=interaction.guild,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            actor=interaction.user if isinstance(interaction.user, discord.Member) else None,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            req=self.req,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            status=status,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            edit_view=True,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        )
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            await interaction.response.send_message(f"{status.capitalize()}.", ephemeral=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @discord.ui.button(label="Approve", style=discord.ButtonStyle.success, custom_id="fdk.approve")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _approve(self, interaction: discord.Interaction, button: discord.ui.Button):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self._finish(interaction, "approved")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @discord.ui.button(label="Reject", style=discord.ButtonStyle.danger, custom_id="fdk.reject")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _reject(self, interaction: discord.Interaction, button: discord.ui.Button):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self._finish(interaction, "rejected")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				# ------------------ Main cog ------------------
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				CHECKMARK = '✅'
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				class ReactionRoleCog(commands.Cog):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    """
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    Original: Records agreements and manages Full Access.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    Added: Crew reaction roles (Harvester/Escort toggles) + Fedaykin approval flow via buttons.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    - No debug slash commands.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    - Debounced nickname review to avoid duplicates when users add multiple accept emojis.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    - Fedaykin role is removed when the user unreacts the Fedaykin emoji.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    - Settings are reloaded dynamically on each event (hot-apply without restart).
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    Records agreements and manages Full Access.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    Nickname flow:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      • Add accept on nickname message -> atomically mark agreed + pending and open ONE review
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      • Remove accept on nickname message -> clear only if user has no accept reactions left
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    Full Access: granted when Rules ✅ + RoE ✅ + Nickname *claimed* (pending or verified).
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    Revoked when any of the three is missing.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    """
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def __init__(self, bot):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self.bot = bot
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        r = cfg(bot)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Snapshot (will be refreshed dynamically on each event)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._settings = {}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._hub_channel_id: Optional[int] = None  # cache for crew hub channel
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # 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)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Debounce / locks to prevent duplicate nickname review on bursty reactions
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._nick_debounce: Dict[tuple[int, int], float] = {}  # (guild_id, user_id) -> last_ts
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._nick_lock: set[tuple[int, int]] = set()           # in-flight review creations
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        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)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Apply initial snapshot and schedule restore
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._refresh_settings(force=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self.bot.loop.create_task(self._boot_restore())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    # ---- helpers ----
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def _has_rules(self, member_id: int) -> bool:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        return member_id in self.bot.data_manager.get('agreed_rules')
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    # ------------------ settings ------------------
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def _has_engage(self, member_id: int) -> bool:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        return member_id in self.bot.data_manager.get('agreed_engagement')
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def _refresh_settings(self, *, force: bool = False):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        """Hot-read settings on demand; cheap and avoids restart requirement."""
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        r = cfg(self.bot)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        s = {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            # Message IDs
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "rules_msg_id": r.int('rules_message_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "engage_msg_id": r.int('engagement_message_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "nick_msg_id": r.int('nickname_message_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "crew_msg_id": r.int('crew_roles_message_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    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')
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            # Role IDs
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "rules_role": r.int('rules_role_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "engage_role": r.int('engagement_role_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "full_access_role": r.int('full_access_role_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "role_harvest_id": r.int('role_harvest_crew_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "role_escort_id": r.int('role_escort_crew_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "role_fedaykin_id": r.int('role_fedaykin_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    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
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            # Approver roles
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "field_mod_role_id": r.int('field_mod_role_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "admin_role_id": r.int('admin_role_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        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
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            # Emojis
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "emoji_harvest_id": r.int('emoji_harvester_crew', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "emoji_escort_id": r.int('emoji_escort_crew', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "emoji_fedaykin_id": r.int('emoji_fedaykin', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            # Channels
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "report_channel_id": r.int('report_channel_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "mod_channel_id": r.int('mod_channel_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "modlog_channel_id": r.int('modlog_channel_id', 0),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._settings = s
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    # handy properties (avoid tons of self._settings["x"] everywhere)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def rules_msg_id(self) -> int: return self._settings.get("rules_msg_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def engage_msg_id(self) -> int: return self._settings.get("engage_msg_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def nick_msg_id(self) -> int: return self._settings.get("nick_msg_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def crew_msg_id(self) -> int: return self._settings.get("crew_msg_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def rules_role(self) -> int: return self._settings.get("rules_role", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def engage_role(self) -> int: return self._settings.get("engage_role", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def full_access_role(self) -> int: return self._settings.get("full_access_role", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def role_harvest_id(self) -> int: return self._settings.get("role_harvest_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def role_escort_id(self) -> int: return self._settings.get("role_escort_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def role_fedaykin_id(self) -> int: return self._settings.get("role_fedaykin_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def field_mod_role_id(self) -> int: return self._settings.get("field_mod_role_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def admin_role_id(self) -> int: return self._settings.get("admin_role_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def emoji_harvest_id(self) -> int: return self._settings.get("emoji_harvest_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def emoji_escort_id(self) -> int: return self._settings.get("emoji_escort_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def emoji_fedaykin_id(self) -> int: return self._settings.get("emoji_fedaykin_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def report_channel_id(self) -> int: return self._settings.get("report_channel_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def mod_channel_id(self) -> int: return self._settings.get("mod_channel_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @property
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    def modlog_channel_id(self) -> int: return self._settings.get("modlog_channel_id", 0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    # ------------------ boot & persistence ------------------
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _boot_restore(self):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self.bot.wait_until_ready()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._refresh_settings()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        dm = self.bot.data_manager
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Ensure list key exists to be safe
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not isinstance(dm.get('fedaykin_requests'), list):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Best-effort: refresh user card
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        cards = self.bot.get_cog('UserCardsCog')
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if cards:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                dm.add('fedaykin_requests', {"_init": True})
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                dm.remove('fedaykin_requests', lambda x: isinstance(x, dict) and x.get("_init") is True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                await cards.refresh_card(member)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Re-register views for any pending requests
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            for req in _as_list(dm.get('fedaykin_requests')):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if req.get("status") == "pending" and req.get("review_message_id") and req.get("review_channel_id"):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    self.bot.add_view(_FedaykinApprovalView(self, req), message_id=int(req["review_message_id"]))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _save_fedaykin_request(self, req: Dict[str, Any]):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        """Upsert by (guild_id, user_id)."""
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        dm = self.bot.data_manager
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        gid, uid = int(req["guild_id"]), int(req["user_id"])
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            for _ in list(_as_list(dm.get('fedaykin_requests'))):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                dm.remove('fedaykin_requests', lambda x, gid=gid, uid=uid:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                          int(x.get("guild_id", 0)) == gid and int(x.get("user_id", 0)) == uid)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            dm.add('fedaykin_requests', dict(req))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    # ------------------ shared helpers ------------------
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _get_member(self, guild: discord.Guild, user_id: int):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not guild:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return None
 | 
			
		
		
	
	
		
			
				
					
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -238,453 +94,114 @@ class ReactionRoleCog(commands.Cog):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return False
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    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
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    # ---- commands (hybrid = prefix + slash) ----
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @app_commands.command(name="nick_same", description="Claim that your global display name matches your in-game name (triggers mod review)")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def nick_same(self, interaction: discord.Interaction):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not interaction.guild or not isinstance(interaction.user, discord.Member):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        dm = self.bot.data_manager
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        has_rules = member.id in _as_list(dm.get('agreed_rules'))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        has_engage = member.id in _as_list(dm.get('agreed_engagement'))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        has_nick_claim = member.id in _as_list(dm.get('agreed_nickname'))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        has_all = has_rules and has_engage and has_nick_claim
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        member: discord.Member = interaction.user
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        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:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        nn = self.bot.get_cog('NickNudgeCog')
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if nn and hasattr(nn, 'ensure_pending_and_maybe_open'):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                await cards.refresh_card(member)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                await nn.ensure_pending_and_maybe_open(interaction.guild, member, source="nick_same")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _find_hub_message(self, guild: discord.Guild) -> Tuple[Optional[discord.Message], Optional[int]]:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        """Return (message, channel_id) for the hub message, scanning text channels if needed."""
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not self.crew_msg_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return None, None
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Try cached channel
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if self._hub_channel_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ch = guild.get_channel(self._hub_channel_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if isinstance(ch, discord.TextChannel):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    msg = await ch.fetch_message(self.crew_msg_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    return msg, ch.id
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Scan
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        for ch in guild.text_channels:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                msg = await ch.fetch_message(self.crew_msg_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                self._hub_channel_id = ch.id
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                return msg, ch.id
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            except (discord.NotFound, discord.Forbidden):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                continue
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                continue
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        return None, None
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _edit_review_message_footer(self, req: Dict[str, Any], note: str, *, clear_view: bool = False):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ch = self.bot.get_channel(int(req["review_channel_id"]))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if isinstance(ch, (discord.TextChannel, discord.Thread)):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                msg = await ch.fetch_message(int(req["review_message_id"]))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                emb = msg.embeds[0] if msg.embeds else discord.Embed(title="Fedaykin Request")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if emb.footer and emb.footer.text:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    emb.set_footer(text=emb.footer.text + "\n" + note)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                else:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    emb.set_footer(text=note)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                await msg.edit(embed=emb, view=None if clear_view else msg.components)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _modlog_decision(self, guild: discord.Guild, status: str, target: discord.Member, actor: discord.Member, req: Dict[str, Any]):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        """Send an approval/rejection/revoke line to modlog channel."""
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        chan = self.bot.get_channel(self.modlog_channel_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not isinstance(chan, (discord.TextChannel, discord.Thread)):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        color_map = {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "approved": discord.Color.green(),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "rejected": discord.Color.red(),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "withdrawn": discord.Color.orange(),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "revoked": discord.Color.orange()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        colour = color_map.get(status, discord.Color.blurple())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        jump = f"https://discord.com/channels/{guild.id}/{req.get('review_channel_id')}/{req.get('review_message_id')}" \
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				               if req.get('review_channel_id') and req.get('review_message_id') else ""
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        created_ts = float(req.get("created_ts", datetime.now(timezone.utc).timestamp()))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        decided_ts = float(req.get("decision_ts", datetime.now(timezone.utc).timestamp()))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb = discord.Embed(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            title=f"Fedaykin {status.capitalize()}",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            description=(f"[View review card]({jump})" if jump else "No card link available."),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            color=colour,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            timestamp=datetime.fromtimestamp(decided_ts, tz=timezone.utc),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        )
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb.add_field(name="User", value=f"{target.mention} (`{target.id}`)", inline=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb.add_field(name="By", value=f"{actor.mention} (`{actor.id}`)", inline=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb.add_field(name="Requested", value=_fmt_dt(created_ts), inline=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb.add_field(name="Decision", value=_fmt_dt(decided_ts), inline=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            await chan.send(embed=emb)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _fedaykin_decide(self, guild: Optional[discord.Guild], actor: Optional[discord.Member], req: Dict[str, Any], status: str, *, edit_view: bool = False):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        """Common decision path for approvals via buttons."""
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if guild is None or actor is None:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Idempotency
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if req.get("status") != "pending" and status in ("approved", "rejected"):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        target = await self._get_member(guild, int(req["user_id"]))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if target is None:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        role = guild.get_role(self.role_fedaykin_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if status == "approved":
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if role is None:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                await self._edit_review_message_footer(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    req, f"**Decision attempt:** Approve by {actor.mention} failed — Fedaykin role not configured."
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                )
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                await target.add_roles(role, reason=f"Fedaykin approved by {actor} ({actor.id})")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        elif status == "rejected":
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if role and role in target.roles:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    await target.remove_roles(role, reason=f"Fedaykin rejected by {actor} ({actor.id})")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        elif status in ("withdrawn", "revoked"):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            # Both withdrawn (user cancels before approval) and revoked (user unreacts after approval)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if role and role in target.roles:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    await target.remove_roles(role, reason=f"Fedaykin {status} by user")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if status in ("approved", "rejected"):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            req["status"] = status
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            req["approver_id"] = int(actor.id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        else:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            req["status"] = status  # withdrawn / revoked don't set approver
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        req["decision_ts"] = datetime.now(timezone.utc).timestamp()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self._save_fedaykin_request(req)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        note = f"**Decision:** {status.capitalize()} by {actor.mention if actor else 'system'} at {discord.utils.format_dt(datetime.now(timezone.utc), style='f')}"
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self._edit_review_message_footer(req, note, clear_view=edit_view)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # modlog
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            await self._modlog_decision(guild, status, target, actor or guild.me, req)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def _post_fedaykin_card(self, guild: discord.Guild, member: discord.Member, hub_id: int) -> bool:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        """Post the Fedaykin approval card; return True if posted somewhere."""
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        created_ts = datetime.now(timezone.utc).timestamp()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb = discord.Embed(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            title="Fedaykin Request",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            description=f"{member.mention} requested **Fedaykin** via reaction.",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            color=discord.Color.orange(),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            timestamp=datetime.fromtimestamp(created_ts, tz=timezone.utc),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        )
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb.add_field(name="User", value=f"{member} (`{member.id}`)", inline=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb.add_field(name="Requested", value=_fmt_dt(created_ts), inline=True)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        emb.set_footer(text=f"Hub message ID: {hub_id}")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        req = {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "guild_id": guild.id,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "user_id": member.id,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "status": "pending",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "created_ts": created_ts,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "review_message_id": 0,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "review_channel_id": 0,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        view = _FedaykinApprovalView(self, req, timeout=None)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Primary: report_channel_id; fallback: mod_channel_id
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        targets = []
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        chan = self.bot.get_channel(self.report_channel_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if isinstance(chan, (discord.TextChannel, discord.Thread)):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            targets.append(chan)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        mchan = self.bot.get_channel(self.mod_channel_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if isinstance(mchan, (discord.TextChannel, discord.Thread)):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            targets.append(mchan)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        for ch in targets:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                msg = await ch.send(embed=emb, view=view)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                req["review_message_id"] = int(msg.id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                req["review_channel_id"] = int(msg.channel.id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                await self._save_fedaykin_request(req)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    self.bot.add_view(_FedaykinApprovalView(self, req), message_id=msg.id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                return True
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                continue
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        return False
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    # ------------------ listeners ------------------
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self.maybe_apply_full_access(member)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await interaction.response.send_message("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):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # hot-reload settings
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._refresh_settings()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not payload.guild_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not payload.guild_id or not is_accept(payload.emoji):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            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
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # ----- Original accept-based flows -----
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if is_accept(payload.emoji):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            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))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # RULES
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if self.rules_msg_id and payload.message_id == self.rules_msg_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    role = guild.get_role(self.rules_role)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if role:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.add_roles(role, reason="Agreed to rules")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if member.id not in _as_list(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))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # ENGAGEMENT
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                elif self.engage_msg_id and payload.message_id == self.engage_msg_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    role = guild.get_role(self.engage_role)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if role:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.add_roles(role, reason="Agreed to engagement")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if member.id not in _as_list(dm.get('agreed_engagement')):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        dm.add('agreed_engagement', int(member.id))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # NICKNAME (debounced + idempotent)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                elif self.nick_msg_id and payload.message_id == self.nick_msg_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    now = time.monotonic()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    key = (guild.id, member.id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    last = self._nick_debounce.get(key, 0.0)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if now - last < 10.0:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        return  # debounce bursty multi-emoji reacts
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    self._nick_debounce[key] = now
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    # If any pending review already exists for this user, do nothing
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    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 _as_list(dm.get('nick_reviews'))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    )
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if has_pending_review:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    # In-flight lock to avoid concurrent duplicate openings
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if key in self._nick_lock:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    self._nick_lock.add(key)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            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
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                nn = self.bot.get_cog('NickNudgeCog')
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if nn and hasattr(nn, 'ensure_pending_and_maybe_open'):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        # Clear stale marker if no active review, then open exactly one
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        dm.remove('nick_claim_pending', lambda x: x == member.id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        nn = self.bot.get_cog('NickNudgeCog')
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        if nn and hasattr(nn, 'ensure_pending_and_maybe_open'):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                                await nn.ensure_pending_and_maybe_open(guild, member, source="claim")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                                pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    finally:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        self._nick_lock.discard(key)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        await nn.ensure_pending_and_maybe_open(guild, member, source="claim")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                else:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            else:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # ----- Crew roles hub (custom emoji toggles + Fedaykin request) -----
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if self.crew_msg_id and payload.message_id == self.crew_msg_id and payload.emoji.id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # Harvester / Escort
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if payload.emoji.id == self.emoji_harvest_id and self.role_harvest_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    role = guild.get_role(self.role_harvest_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if role:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.add_roles(role, reason=f"Reaction role via hub {self.crew_msg_id}")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if payload.emoji.id == self.emoji_escort_id and self.role_escort_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    role = guild.get_role(self.role_escort_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if role:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.add_roles(role, reason=f"Reaction role via hub {self.crew_msg_id}")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # Fedaykin -> open approval card (only one pending allowed)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if payload.emoji.id == self.emoji_fedaykin_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    fed_role = guild.get_role(self.role_fedaykin_id) if self.role_fedaykin_id else None
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if fed_role and fed_role in member.roles:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        return  # already has Fedaykin
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    pending = next((x for x in _as_list(dm.get('fedaykin_requests'))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                                    if x.get("guild_id") == guild.id and x.get("user_id") == member.id and x.get("status") == "pending"), None)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if pending:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    hub_msg, _ = await self._find_hub_message(guild)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    hub_id = hub_msg.id if hub_msg else self.crew_msg_id
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    await self._post_fedaykin_card(guild, member, hub_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Only the original (agreements) flow affects Full Access
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self.maybe_apply_full_access(member)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    @commands.Cog.listener()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # hot-reload settings
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        self._refresh_settings()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not payload.guild_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        if not payload.guild_id or not is_accept(payload.emoji):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            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
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # ----- Original accept-based flows -----
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if is_accept(payload.emoji):
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # RULES
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if self.rules_msg_id and 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 and role in member.roles:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.remove_roles(role, reason="Rules un-ticked")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            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")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # ENGAGEMENT
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                elif self.engage_msg_id and 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 and role in member.roles:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.remove_roles(role, reason="Engagement un-ticked")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            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")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # NICKNAME: clear only if no other accept left
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                elif self.nick_msg_id and payload.message_id == self.nick_msg_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    still_has_accept = await self._user_has_any_accept(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        guild, payload.channel_id, payload.message_id, member.id
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    )
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if not still_has_accept:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        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)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            elif payload.message_id == self.nick_msg_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # Clear only if the user has NO accept reactions left on the message
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                still_has_accept = await self._user_has_any_accept(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    guild, payload.channel_id, payload.message_id, member.id
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                )
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if not still_has_accept:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    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
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # ----- Crew roles hub (custom emoji) -----
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if self.crew_msg_id and payload.message_id == self.crew_msg_id and payload.emoji.id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # Harvester
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if payload.emoji.id == self.emoji_harvest_id and self.role_harvest_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    role = guild.get_role(self.role_harvest_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if role and role in member.roles:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.remove_roles(role, reason=f"Reaction role via hub {self.crew_msg_id} (unreact)")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # Escort
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if payload.emoji.id == self.emoji_escort_id and self.role_escort_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    role = guild.get_role(self.role_escort_id)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if role and role in member.roles:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.remove_roles(role, reason=f"Reaction role via hub {self.crew_msg_id} (unreact)")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                # Fedaykin unreact -> remove role + mark request accordingly
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                if payload.emoji.id == self.emoji_fedaykin_id:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    fed_role = guild.get_role(self.role_fedaykin_id) if self.role_fedaykin_id else None
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    # Find a related request if any (prefer pending; else last approved)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    req = None
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    pend = [x for x in _as_list(dm.get('fedaykin_requests'))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            if x.get("guild_id") == guild.id and x.get("user_id") == member.id and x.get("status") == "pending"]
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if pend:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        req = pend[-1]
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        req["status"] = "withdrawn"
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    else:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        appr = [x for x in _as_list(dm.get('fedaykin_requests'))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                                if x.get("guild_id") == guild.id and x.get("user_id") == member.id and x.get("status") == "approved"]
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        if appr:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            req = appr[-1]
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            req["status"] = "revoked"
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    # Always remove the role on unreact (per requirement)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if fed_role and fed_role in member.roles:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await member.remove_roles(fed_role, reason="Fedaykin reaction removed")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    if req:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        req["decision_ts"] = datetime.now(timezone.utc).timestamp()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        await self._save_fedaykin_request(req)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        actor = guild.me  # system action
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        status = req["status"]
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        # Update card footer if we still have it
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        await self._edit_review_message_footer(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            req,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            f"**Decision:** {status.capitalize()} (user unreact) at {discord.utils.format_dt(datetime.now(timezone.utc), style='f')}",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            clear_view=True,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        )
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        # Modlog
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        try:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            await self._modlog_decision(guild, status, member, actor, req)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    return
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        except Exception:
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            pass
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        # Only the original (agreements) flow affects Full Access
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        await self.maybe_apply_full_access(member)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
	
		
			
				
					
					| 
						
					 | 
				
			
			 | 
			 | 
			
				
 
 |