feat(discord): add configurable privileged Gateway Intents (GuildPresences, GuildMembers) (#2266)

* feat(discord): add configurable privileged Gateway Intents (GuildPresences, GuildMembers)

Add support for optionally enabling Discord privileged Gateway Intents
via config, starting with GuildPresences and GuildMembers.

When `channels.discord.intents.presence` is set to true:
- GatewayIntents.GuildPresences is added to the gateway connection
- A PresenceUpdateListener caches user presence data in memory
- The member-info action includes user status and activities
  (e.g. Spotify listening activity) from the cache

This enables use cases like:
- Seeing what music a user is currently listening to
- Checking user online/offline/idle/dnd status
- Tracking user activities through the bot API

Both intents require Portal opt-in (Discord Developer Portal →
Privileged Gateway Intents) before they can be used.

Changes:
- config: add `channels.discord.intents.{presence,guildMembers}`
- provider: compute intents dynamically from config
- listeners: add DiscordPresenceListener (extends PresenceUpdateListener)
- presence-cache: simple in-memory Map<userId, GatewayPresenceUpdate>
- discord-actions-guild: include cached presence in member-info response
- schema: add labels and descriptions for new config fields

* fix(test): add PresenceUpdateListener to @buape/carbon mock

* Discord: scope presence cache by account

---------

Co-authored-by: kugutsushi <kugutsushi@clawd>
Co-authored-by: Shadow <hi@shadowing.dev>
This commit is contained in:
Kentaro Kuribayashi
2026-01-27 01:39:54 +09:00
committed by GitHub
parent 97200984f8
commit 3e07bd8b48
8 changed files with 142 additions and 8 deletions

View File

@@ -28,6 +28,7 @@ import { resolveDiscordUserAllowlist } from "../resolve-users.js";
import { normalizeDiscordToken } from "../token.js";
import {
DiscordMessageListener,
DiscordPresenceListener,
DiscordReactionListener,
DiscordReactionRemoveListener,
registerDiscordListener,
@@ -109,6 +110,25 @@ function formatDiscordDeployErrorDetails(err: unknown): string {
return details.length > 0 ? ` (${details.join(", ")})` : "";
}
function resolveDiscordGatewayIntents(
intentsConfig?: import("../../config/types.discord.js").DiscordIntentsConfig,
): number {
let intents =
GatewayIntents.Guilds |
GatewayIntents.GuildMessages |
GatewayIntents.MessageContent |
GatewayIntents.DirectMessages |
GatewayIntents.GuildMessageReactions |
GatewayIntents.DirectMessageReactions;
if (intentsConfig?.presence) {
intents |= GatewayIntents.GuildPresences;
}
if (intentsConfig?.guildMembers) {
intents |= GatewayIntents.GuildMembers;
}
return intents;
}
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const cfg = opts.config ?? loadConfig();
const account = resolveDiscordAccount({
@@ -451,13 +471,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
reconnect: {
maxAttempts: Number.POSITIVE_INFINITY,
},
intents:
GatewayIntents.Guilds |
GatewayIntents.GuildMessages |
GatewayIntents.MessageContent |
GatewayIntents.DirectMessages |
GatewayIntents.GuildMessageReactions |
GatewayIntents.DirectMessageReactions,
intents: resolveDiscordGatewayIntents(discordCfg.intents),
autoInteractions: true,
}),
],
@@ -527,6 +541,14 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
}),
);
if (discordCfg.intents?.presence) {
registerDiscordListener(
client.listeners,
new DiscordPresenceListener({ logger, accountId: account.accountId }),
);
runtime.log?.("discord: GuildPresences intent enabled — presence listener registered");
}
runtime.log?.(`logged in to discord${botUserId ? ` as ${botUserId}` : ""}`);
// Start exec approvals handler after client is ready