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.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user