From fe5e7799f248fabd1ebecf5a0d5e5de8831163b7 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:59:17 -0700 Subject: [PATCH] refactor: fold tailored intents guidance into the connect classifier The cherry-picked #79448 predated #85049's _classify_connect_exception, so it added a parallel PrivilegedIntentsRequired branch ahead of the classifier (plus its own _is_privileged_intents_required detector). Fold the tailored guidance into the classifier's existing intents arm instead: one classification path, one error code (discord_intents_required), and the message now names exactly the intents Hermes requested (Message Content always; Server Members only when username/role allowlists need it). Wizard callout, docs corrections, and tests from #79448 kept as-is. --- plugins/platforms/discord/adapter.py | 50 ++++--------------- .../test_adapter_connect_classification.py | 7 ++- tests/gateway/test_discord_connect.py | 2 +- 3 files changed, 18 insertions(+), 41 deletions(-) diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 1a497b607ab06..c54224895f914 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -282,17 +282,6 @@ def _needs_server_members_intent( return bool(allowed_role_ids) -def _is_privileged_intents_required(exc: BaseException) -> bool: - """True when ``exc`` is discord.py's PrivilegedIntentsRequired error.""" - if type(exc).__name__ == "PrivilegedIntentsRequired": - return True - if discord is None: - return False - err_mod = getattr(discord, "errors", None) - cls = getattr(err_mod, "PrivilegedIntentsRequired", None) - return cls is not None and isinstance(exc, cls) - - def _format_privileged_intents_guidance(*, needs_members: bool) -> str: """Actionable fix text when Discord rejects privileged Gateway Intents.""" lines = [ @@ -1493,25 +1482,7 @@ class DiscordAdapter(BasePlatformAdapter): ) return False except Exception as e: # pragma: no cover - defensive logging - # PrivilegedIntentsRequired is a Developer Portal config error, not a - # transient network blip — name the exact intents Hermes requested and - # mark non-retryable so the gateway does not spin reconnect forever - # (#79430). - if _is_privileged_intents_required(e): - guidance = _format_privileged_intents_guidance( - needs_members=_needs_server_members_intent( - getattr(self, "_allowed_user_ids", None), - getattr(self, "_allowed_role_ids", None), - ) - ) - logger.error("[%s] %s", self.name, guidance) - self._set_fatal_error( - "privileged_intents_required", - guidance, - retryable=False, - ) - else: - logger.error("[%s] Failed to connect to Discord: %s", self.name, e, exc_info=True) + logger.error("[%s] Failed to connect to Discord: %s", self.name, e, exc_info=True) # Same zombie-client hazard as the timeout branch: the background # client.start() task may already be running when a later setup # step raises. Cancel it so the discarded adapter cannot connect. @@ -1528,8 +1499,7 @@ class DiscordAdapter(BasePlatformAdapter): self._set_fatal_error(code, message, retryable=retryable) return False - @staticmethod - def _classify_connect_exception(error: Exception) -> tuple: + def _classify_connect_exception(self, error: Exception) -> tuple: """Map a Discord startup exception to ``(code, message, retryable)``. Type-based only — never match on message text. Unknown exception @@ -1559,14 +1529,16 @@ class DiscordAdapter(BasePlatformAdapter): False, ) if _is("PrivilegedIntentsRequired"): - return ( - "discord_intents_required", - "Discord privileged intents are not enabled for this bot: " - f"{error}. Enable 'Message Content Intent' (and any other " - "required privileged intents) for this application in the " - "Discord Developer Portal → Bot → Privileged Gateway Intents.", - False, + # Name the exact intents Hermes requested (#79430): Message + # Content always; Server Members only when username/role + # allowlists actually need member lookups. + guidance = _format_privileged_intents_guidance( + needs_members=_needs_server_members_intent( + getattr(self, "_allowed_user_ids", None), + getattr(self, "_allowed_role_ids", None), + ) ) + return ("discord_intents_required", guidance, False) return ("discord_connect_error", f"Discord startup failed: {error}", True) def _discord_message_admission( diff --git a/tests/gateway/test_adapter_connect_classification.py b/tests/gateway/test_adapter_connect_classification.py index 724c8391b86eb..ec9c7f9aa93e6 100644 --- a/tests/gateway/test_adapter_connect_classification.py +++ b/tests/gateway/test_adapter_connect_classification.py @@ -97,7 +97,12 @@ class PrivilegedIntentsRequired(Exception): def _discord_classifier(): from plugins.platforms.discord.adapter import DiscordAdapter - return DiscordAdapter._classify_connect_exception + # Instance-bound (the intents branch reads the adapter's allowlists to + # tailor its guidance); a bare instance with empty allowlists suffices. + adapter = object.__new__(DiscordAdapter) + adapter._allowed_user_ids = set() + adapter._allowed_role_ids = set() + return adapter._classify_connect_exception class TestDiscordConnectClassification: diff --git a/tests/gateway/test_discord_connect.py b/tests/gateway/test_discord_connect.py index 8fe9e7881b02c..5a116dabb17f4 100644 --- a/tests/gateway/test_discord_connect.py +++ b/tests/gateway/test_discord_connect.py @@ -702,7 +702,7 @@ class TestPrivilegedIntentsRequiredFatal: assert ok is False assert adapter.has_fatal_error is True assert adapter.fatal_error_retryable is False - assert adapter.fatal_error_code == "privileged_intents_required" + assert adapter.fatal_error_code == "discord_intents_required" assert "Message Content Intent" in (adapter.fatal_error_message or "") assert "discord.com/developers/applications" in (adapter.fatal_error_message or "") assert adapter._bot_task is None