From 9a9228f7a4855d75e97cf09166c8d6bf6c762dae Mon Sep 17 00:00:00 2001 From: pushiscool Date: Mon, 25 May 2026 21:11:37 -0400 Subject: [PATCH] Resolve legacy warning moderator IDs --- src/cogs/moderation/infraction.py | 161 ++++++++++++++++++++++-------- src/database_handler.py | 54 ++++++++-- tests/test_database_handler.py | 24 +++++ tests/test_moderation_helpers.py | 33 +++++- 4 files changed, 222 insertions(+), 50 deletions(-) diff --git a/src/cogs/moderation/infraction.py b/src/cogs/moderation/infraction.py index a91c9f6..99115eb 100644 --- a/src/cogs/moderation/infraction.py +++ b/src/cogs/moderation/infraction.py @@ -10,18 +10,70 @@ conf = load_optional_config() COMMAND_GUILD_IDS = get_command_guild_ids(conf) +DISCORD_EPOCH_MS = 1420070400000 -def to_snowflake(value): +def is_possible_snowflake(value: int) -> bool: + text = str(value) + if not 17 <= len(text) <= 20: + return False + + created_ms = (value >> 22) + DISCORD_EPOCH_MS + now_ms = int(datetime.now(timezone.utc).timestamp() * 1000) + return DISCORD_EPOCH_MS <= created_ms <= now_ms + 86400000 + + +def add_snowflake_candidate(candidates: list[int], seen: set[int], value: str) -> None: + try: + candidate = int(value) + except (TypeError, ValueError): + return + + if candidate in seen: + return + + if not is_possible_snowflake(candidate): + return + + seen.add(candidate) + candidates.append(candidate) + + +def snowflake_candidates(value) -> list[int]: + candidates: list[int] = [] + seen: set[int] = set() + if value is None: - return None - if isinstance(value, int): - return value - if isinstance(value, str): - m = re.search(r"\d{17,20}", value) - if m: - return int(m.group(0)) - return None + return candidates + + if hasattr(value, "id"): + value = value.id + + text = str(value) + + for match in re.finditer(r"<@!?(\d{17,20})>", text): + add_snowflake_candidate(candidates, seen, match.group(1)) + + for match in re.finditer(r"(? None: self.bot = bot + async def fetch_moderator_from_id(self, guild, moderator_id: int): + if guild is not None: + member = guild.get_member(moderator_id) + + if member is not None: + return member + + try: + member = await guild.fetch_member(moderator_id) + + if member is not None: + return member + except (AttributeError, nextcord.NotFound, nextcord.Forbidden, nextcord.HTTPException): + pass + + try: + cached_user = self.bot.get_user(moderator_id) + + if cached_user is not None: + return cached_user + except AttributeError: + pass + + try: + return await self.bot.fetch_user(moderator_id) + except (AttributeError, nextcord.NotFound, nextcord.Forbidden, nextcord.HTTPException): + return None + + async def resolve_moderator(self, guild, raw_moderator): + candidates = snowflake_candidates(raw_moderator) + + for moderator_id in candidates: + moderator = await self.fetch_moderator_from_id(guild, moderator_id) + + if moderator is not None: + return moderator, moderator_id + + fallback_id = candidates[0] if candidates else None + return None, fallback_id + + def format_moderator(self, raw_moderator, moderator, moderator_id: Optional[int]) -> str: + if moderator is not None: + display = ( + getattr(moderator, "global_name", None) + or getattr(moderator, "display_name", None) + or getattr(moderator, "name", None) + or str(moderator) + ) + mention = getattr(moderator, "mention", f"<@{moderator_id}>") + return f"{mention} ({display})" + + if moderator_id is not None: + return f"<@{moderator_id}> (not found)" + + if raw_moderator: + return f"`{raw_moderator}` (invalid legacy moderator ID)" + + return "Unknown moderator" + def has_mod_role(self, member: Member) -> bool: allowed_role_names = {"Trial Chat Moderator", "Chat Moderator", "Admin"} allowed_role_ids = { @@ -143,37 +254,9 @@ async def warnings(self, inter: Interaction, member: Member): ) reason = inf.reason or "No reason provided" - # ---- Moderator lookup (fixed) ---- raw_mod = getattr(inf, "moderator", None) - moderator_id = to_snowflake(raw_mod) - - mod = None - if moderator_id: - # Try guild cache first (only if in a guild) - if inter.guild is not None: - mod = inter.guild.get_member(moderator_id) - - # Fallback to API fetch - if mod is None: - try: - mod = await self.bot.fetch_user(moderator_id) - except nextcord.HTTPException: - mod = None - - # Build moderator display: - # - If we found the user => mention + display name - # - If not => still mention the ID if we have one, else show whatever was stored - if mod: - display = getattr(mod, "global_name", None) or mod.name - mod_line = f"{mod.mention} ({display})" - else: - if moderator_id: - mod_line = f"<@{moderator_id}> (unknown)" - else: - mod_line = f"{raw_mod} (unknown)" if raw_mod else "Unknown moderator" - # ------------------------------- - - + mod, moderator_id = await self.resolve_moderator(inter.guild, raw_mod) + mod_line = self.format_moderator(raw_mod, mod, moderator_id) time_val = getattr(inf, "actiontime", None) diff --git a/src/database_handler.py b/src/database_handler.py index e1e8bab..9bde678 100644 --- a/src/database_handler.py +++ b/src/database_handler.py @@ -16,6 +16,48 @@ from typing import Optional import re logger = logging.getLogger(__name__) +DISCORD_EPOCH_MS = 1420070400000 + + +def is_possible_snowflake(value: int) -> bool: + text = str(value) + if not 17 <= len(text) <= 20: + return False + + created_ms = (value >> 22) + DISCORD_EPOCH_MS + now_ms = int(datetime.now(timezone.utc).timestamp() * 1000) + return DISCORD_EPOCH_MS <= created_ms <= now_ms + 86400000 + + +def first_snowflake(value): + if value is None: + return None + + if hasattr(value, "id"): + value = value.id + + text = str(value) + + for pattern in (r"<@!?(\d{17,20})>", r"(?", + display_name="Senior Mod", + name="senior-mod", + ) + guild = SimpleNamespace( + get_member=lambda user_id: None, + fetch_member=AsyncMock(return_value=moderator), + ) + bot = SimpleNamespace( + get_user=lambda user_id: None, + fetch_user=AsyncMock(return_value=None), + ) + cog = InfractionCog(bot=bot) + + resolved, moderator_id = asyncio.run(cog.resolve_moderator(guild, "7079852600207606280")) + + assert resolved is moderator + assert moderator_id == 707985260020760628 + guild.fetch_member.assert_awaited_once_with(707985260020760628) def test_format_infraction_updates_shows_note_moderator_and_date():