mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:10:52 +00:00
301 lines
10 KiB
TypeScript
301 lines
10 KiB
TypeScript
import {
|
|
Client,
|
|
ReadyListener,
|
|
type BaseCommand,
|
|
type BaseMessageInteractiveComponent,
|
|
type Modal,
|
|
type Plugin,
|
|
} from "@buape/carbon";
|
|
import type { GatewayPlugin } from "@buape/carbon/gateway";
|
|
import { VoicePlugin } from "@buape/carbon/voice";
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
|
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runtime";
|
|
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
|
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
import { createDiscordRequestClient } from "../proxy-request-client.js";
|
|
import type { DiscordGuildEntryResolved } from "./allow-list.js";
|
|
import { createDiscordAutoPresenceController } from "./auto-presence.js";
|
|
import type { DiscordDmPolicy } from "./dm-command-auth.js";
|
|
import type { MutableDiscordGateway } from "./gateway-handle.js";
|
|
import {
|
|
createDiscordGatewayPlugin,
|
|
waitForDiscordGatewayPluginRegistration,
|
|
} from "./gateway-plugin.js";
|
|
import { createDiscordGatewaySupervisor } from "./gateway-supervisor.js";
|
|
import {
|
|
DiscordMessageListener,
|
|
DiscordPresenceListener,
|
|
DiscordReactionListener,
|
|
DiscordReactionRemoveListener,
|
|
DiscordThreadUpdateListener,
|
|
registerDiscordListener,
|
|
} from "./listeners.js";
|
|
import { resolveDiscordPresenceUpdate } from "./presence.js";
|
|
|
|
type DiscordAutoPresenceController = ReturnType<typeof createDiscordAutoPresenceController>;
|
|
type DiscordListenerConfig = {
|
|
dangerouslyAllowNameMatching?: boolean;
|
|
intents?: { presence?: boolean };
|
|
};
|
|
type CreateClientFn = (
|
|
options: ConstructorParameters<typeof Client>[0],
|
|
handlers: ConstructorParameters<typeof Client>[1],
|
|
plugins: ConstructorParameters<typeof Client>[2],
|
|
) => Client;
|
|
type CarbonEventQueueOptions = NonNullable<ConstructorParameters<typeof Client>[0]["eventQueue"]>;
|
|
|
|
type ListenerCompatClient = Client & {
|
|
plugins?: Array<{ id: string; plugin: Plugin }>;
|
|
registerListener?: (listener: object) => object;
|
|
unregisterListener?: (listener: object) => boolean;
|
|
};
|
|
|
|
function withLegacyListenerCompat(client: Client): ListenerCompatClient {
|
|
const compatClient = client as ListenerCompatClient;
|
|
if (!compatClient.registerListener) {
|
|
compatClient.registerListener = (listener: object) => {
|
|
if (!compatClient.listeners.includes(listener as never)) {
|
|
compatClient.listeners.push(listener as never);
|
|
}
|
|
return listener;
|
|
};
|
|
}
|
|
if (!compatClient.unregisterListener) {
|
|
compatClient.unregisterListener = (listener: object) => {
|
|
const index = compatClient.listeners.indexOf(listener as never);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
compatClient.listeners.splice(index, 1);
|
|
return true;
|
|
};
|
|
}
|
|
return compatClient;
|
|
}
|
|
|
|
function registerLatePlugin(client: Client, plugin: Plugin) {
|
|
const compatClient = withLegacyListenerCompat(client);
|
|
void plugin.registerClient?.(compatClient);
|
|
void plugin.registerRoutes?.(compatClient);
|
|
if (!compatClient.plugins?.some((entry) => entry.id === plugin.id)) {
|
|
compatClient.plugins?.push({ id: plugin.id, plugin });
|
|
}
|
|
}
|
|
|
|
export function createDiscordStatusReadyListener(params: {
|
|
discordConfig: Parameters<typeof resolveDiscordPresenceUpdate>[0];
|
|
getAutoPresenceController: () => DiscordAutoPresenceController | null;
|
|
}): ReadyListener {
|
|
return new (class DiscordStatusReadyListener extends ReadyListener {
|
|
async handle(_data: unknown, client: Client) {
|
|
const autoPresenceController = params.getAutoPresenceController();
|
|
if (autoPresenceController?.enabled) {
|
|
autoPresenceController.refresh();
|
|
return;
|
|
}
|
|
|
|
const gateway = client.getPlugin<GatewayPlugin>("gateway");
|
|
if (!gateway) {
|
|
return;
|
|
}
|
|
|
|
const presence = resolveDiscordPresenceUpdate(params.discordConfig);
|
|
if (!presence) {
|
|
return;
|
|
}
|
|
|
|
gateway.updatePresence(presence);
|
|
}
|
|
})();
|
|
}
|
|
|
|
export async function createDiscordMonitorClient(params: {
|
|
accountId: string;
|
|
applicationId: string;
|
|
token: string;
|
|
proxyFetch?: typeof fetch;
|
|
commands: BaseCommand[];
|
|
components: BaseMessageInteractiveComponent[];
|
|
modals: Modal[];
|
|
voiceEnabled: boolean;
|
|
discordConfig: Parameters<typeof resolveDiscordPresenceUpdate>[0] & {
|
|
eventQueue?: Pick<
|
|
CarbonEventQueueOptions,
|
|
"listenerTimeout" | "maxQueueSize" | "maxConcurrency"
|
|
>;
|
|
};
|
|
runtime: RuntimeEnv;
|
|
createClient: CreateClientFn;
|
|
createGatewayPlugin: typeof createDiscordGatewayPlugin;
|
|
createGatewaySupervisor: typeof createDiscordGatewaySupervisor;
|
|
createAutoPresenceController: typeof createDiscordAutoPresenceController;
|
|
isDisallowedIntentsError: (err: unknown) => boolean;
|
|
}) {
|
|
let autoPresenceController: DiscordAutoPresenceController | null = null;
|
|
const clientPlugins: Plugin[] = [
|
|
params.createGatewayPlugin({
|
|
discordConfig: params.discordConfig,
|
|
runtime: params.runtime,
|
|
}),
|
|
];
|
|
if (params.voiceEnabled) {
|
|
clientPlugins.push(new VoicePlugin());
|
|
}
|
|
const voicePlugin = clientPlugins.find((plugin) => plugin.id === "voice");
|
|
const constructorPlugins = voicePlugin
|
|
? clientPlugins.filter((plugin) => plugin !== voicePlugin)
|
|
: clientPlugins;
|
|
|
|
// Pass eventQueue config to Carbon so the gateway listener budget can be tuned.
|
|
// Default listenerTimeout is 120s (Carbon defaults to 30s, which is too short for some
|
|
// Discord normalization/enqueue work).
|
|
const eventQueueOpts = {
|
|
listenerTimeout: 120_000,
|
|
slowListenerThreshold: 30_000,
|
|
...params.discordConfig.eventQueue,
|
|
} satisfies CarbonEventQueueOptions;
|
|
const readyListener = createDiscordStatusReadyListener({
|
|
discordConfig: params.discordConfig,
|
|
getAutoPresenceController: () => autoPresenceController,
|
|
});
|
|
const client = params.createClient(
|
|
{
|
|
baseUrl: "http://localhost",
|
|
deploySecret: "a",
|
|
clientId: params.applicationId,
|
|
publicKey: "a",
|
|
token: params.token,
|
|
autoDeploy: false,
|
|
eventQueue: eventQueueOpts,
|
|
},
|
|
{
|
|
commands: params.commands,
|
|
listeners: [readyListener],
|
|
components: params.components,
|
|
modals: params.modals,
|
|
},
|
|
constructorPlugins,
|
|
);
|
|
if (voicePlugin) {
|
|
registerLatePlugin(client, voicePlugin);
|
|
}
|
|
if (params.proxyFetch) {
|
|
client.rest = createDiscordRequestClient(params.token, {
|
|
fetch: params.proxyFetch,
|
|
});
|
|
}
|
|
const gateway = client.getPlugin<GatewayPlugin>("gateway") as MutableDiscordGateway | undefined;
|
|
await waitForDiscordGatewayPluginRegistration(gateway);
|
|
const gatewaySupervisor = params.createGatewaySupervisor({
|
|
gateway,
|
|
isDisallowedIntentsError: params.isDisallowedIntentsError,
|
|
runtime: params.runtime,
|
|
});
|
|
|
|
if (gateway) {
|
|
autoPresenceController = params.createAutoPresenceController({
|
|
accountId: params.accountId,
|
|
discordConfig: params.discordConfig,
|
|
gateway,
|
|
log: (message) => params.runtime.log?.(message),
|
|
});
|
|
autoPresenceController.start();
|
|
}
|
|
|
|
return {
|
|
client,
|
|
gateway,
|
|
gatewaySupervisor,
|
|
autoPresenceController,
|
|
eventQueueOpts,
|
|
};
|
|
}
|
|
|
|
export async function fetchDiscordBotIdentity(params: {
|
|
client: Pick<Client, "fetchUser">;
|
|
runtime: RuntimeEnv;
|
|
logStartupPhase: (phase: string, details?: string) => void;
|
|
}) {
|
|
params.logStartupPhase("fetch-bot-identity:start");
|
|
try {
|
|
const botUser = await params.client.fetchUser("@me");
|
|
const botUserId = botUser?.id;
|
|
const botUserName =
|
|
normalizeOptionalString(botUser?.username) ?? normalizeOptionalString(botUser?.globalName);
|
|
params.logStartupPhase(
|
|
"fetch-bot-identity:done",
|
|
`botUserId=${botUserId ?? "<missing>"} botUserName=${botUserName ?? "<missing>"}`,
|
|
);
|
|
return { botUserId, botUserName };
|
|
} catch (err) {
|
|
params.runtime.error?.(danger(`discord: failed to fetch bot identity: ${String(err)}`));
|
|
params.logStartupPhase("fetch-bot-identity:error", String(err));
|
|
return { botUserId: undefined, botUserName: undefined };
|
|
}
|
|
}
|
|
|
|
export function registerDiscordMonitorListeners(params: {
|
|
cfg: OpenClawConfig;
|
|
client: Pick<Client, "listeners">;
|
|
accountId: string;
|
|
discordConfig: DiscordListenerConfig;
|
|
runtime: RuntimeEnv;
|
|
botUserId?: string;
|
|
dmEnabled: boolean;
|
|
groupDmEnabled: boolean;
|
|
groupDmChannels?: string[];
|
|
dmPolicy: DiscordDmPolicy;
|
|
allowFrom?: string[];
|
|
groupPolicy: "open" | "allowlist" | "disabled";
|
|
guildEntries?: Record<string, DiscordGuildEntryResolved>;
|
|
logger: NonNullable<ConstructorParameters<typeof DiscordMessageListener>[1]>;
|
|
messageHandler: ConstructorParameters<typeof DiscordMessageListener>[0];
|
|
trackInboundEvent?: () => void;
|
|
eventQueueListenerTimeoutMs?: number;
|
|
}) {
|
|
registerDiscordListener(
|
|
params.client.listeners,
|
|
new DiscordMessageListener(params.messageHandler, params.logger, params.trackInboundEvent, {
|
|
timeoutMs: params.eventQueueListenerTimeoutMs,
|
|
}),
|
|
);
|
|
|
|
const reactionListenerOptions: ConstructorParameters<typeof DiscordReactionListener>[0] = {
|
|
cfg: params.cfg,
|
|
accountId: params.accountId,
|
|
runtime: params.runtime,
|
|
botUserId: params.botUserId,
|
|
dmEnabled: params.dmEnabled,
|
|
groupDmEnabled: params.groupDmEnabled,
|
|
groupDmChannels: params.groupDmChannels ?? [],
|
|
dmPolicy: params.dmPolicy,
|
|
allowFrom: params.allowFrom ?? [],
|
|
groupPolicy: params.groupPolicy,
|
|
allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig),
|
|
guildEntries: params.guildEntries,
|
|
logger: params.logger,
|
|
onEvent: params.trackInboundEvent,
|
|
};
|
|
registerDiscordListener(
|
|
params.client.listeners,
|
|
new DiscordReactionListener(reactionListenerOptions),
|
|
);
|
|
registerDiscordListener(
|
|
params.client.listeners,
|
|
new DiscordReactionRemoveListener(reactionListenerOptions),
|
|
);
|
|
registerDiscordListener(
|
|
params.client.listeners,
|
|
new DiscordThreadUpdateListener(params.cfg, params.accountId, params.logger),
|
|
);
|
|
|
|
if (params.discordConfig.intents?.presence) {
|
|
registerDiscordListener(
|
|
params.client.listeners,
|
|
new DiscordPresenceListener({ logger: params.logger, accountId: params.accountId }),
|
|
);
|
|
params.runtime.log?.("discord: GuildPresences intent enabled — presence listener registered");
|
|
}
|
|
}
|