mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 17:18:25 +00:00
feat(discord): add set-presence action for bot activity and status
Bridge the agent tools layer to the Discord gateway WebSocket via a new gateway registry, allowing agents to set the bot's activity and online status. Supports playing, streaming, listening, watching, custom, and competing activity types. Custom type uses activityState as the sidebar text; other types show activityName in the sidebar and activityState in the flyout. Opt-in via channels.discord.actions.presence (default false). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
clawdinator[bot]
parent
b64c1a56a1
commit
5af322f710
55
src/discord/monitor/gateway-registry.test.ts
Normal file
55
src/discord/monitor/gateway-registry.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { GatewayPlugin } from "@buape/carbon/gateway";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearGateways,
|
||||
getGateway,
|
||||
registerGateway,
|
||||
unregisterGateway,
|
||||
} from "./gateway-registry.js";
|
||||
|
||||
function fakeGateway(props: Partial<GatewayPlugin> = {}): GatewayPlugin {
|
||||
return { isConnected: true, ...props } as unknown as GatewayPlugin;
|
||||
}
|
||||
|
||||
describe("gateway-registry", () => {
|
||||
beforeEach(() => {
|
||||
clearGateways();
|
||||
});
|
||||
|
||||
it("stores and retrieves a gateway by account", () => {
|
||||
const gateway = fakeGateway();
|
||||
registerGateway("account-a", gateway);
|
||||
expect(getGateway("account-a")).toBe(gateway);
|
||||
expect(getGateway("account-b")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses 'default' key when accountId is undefined", () => {
|
||||
const gateway = fakeGateway();
|
||||
registerGateway(undefined, gateway);
|
||||
expect(getGateway(undefined)).toBe(gateway);
|
||||
expect(getGateway("default")).toBe(gateway);
|
||||
});
|
||||
|
||||
it("unregisters a gateway", () => {
|
||||
const gateway = fakeGateway();
|
||||
registerGateway("account-a", gateway);
|
||||
unregisterGateway("account-a");
|
||||
expect(getGateway("account-a")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears all gateways", () => {
|
||||
registerGateway("a", fakeGateway());
|
||||
registerGateway("b", fakeGateway());
|
||||
clearGateways();
|
||||
expect(getGateway("a")).toBeUndefined();
|
||||
expect(getGateway("b")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("overwrites existing entry for same account", () => {
|
||||
const gateway1 = fakeGateway({ isConnected: true });
|
||||
const gateway2 = fakeGateway({ isConnected: false });
|
||||
registerGateway("account-a", gateway1);
|
||||
registerGateway("account-a", gateway2);
|
||||
expect(getGateway("account-a")).toBe(gateway2);
|
||||
});
|
||||
});
|
||||
33
src/discord/monitor/gateway-registry.ts
Normal file
33
src/discord/monitor/gateway-registry.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { GatewayPlugin } from "@buape/carbon/gateway";
|
||||
|
||||
/**
|
||||
* Module-level registry of active Discord GatewayPlugin instances.
|
||||
* Bridges the gap between agent tool handlers (which only have REST access)
|
||||
* and the gateway WebSocket (needed for operations like updatePresence).
|
||||
* Follows the same pattern as presence-cache.ts.
|
||||
*/
|
||||
const gatewayRegistry = new Map<string, GatewayPlugin>();
|
||||
|
||||
function resolveAccountKey(accountId?: string): string {
|
||||
return accountId ?? "default";
|
||||
}
|
||||
|
||||
/** Register a GatewayPlugin instance for an account. */
|
||||
export function registerGateway(accountId: string | undefined, gateway: GatewayPlugin): void {
|
||||
gatewayRegistry.set(resolveAccountKey(accountId), gateway);
|
||||
}
|
||||
|
||||
/** Unregister a GatewayPlugin instance for an account. */
|
||||
export function unregisterGateway(accountId?: string): void {
|
||||
gatewayRegistry.delete(resolveAccountKey(accountId));
|
||||
}
|
||||
|
||||
/** Get the GatewayPlugin for an account. Returns undefined if not registered. */
|
||||
export function getGateway(accountId?: string): GatewayPlugin | undefined {
|
||||
return gatewayRegistry.get(resolveAccountKey(accountId));
|
||||
}
|
||||
|
||||
/** Clear all registered gateways (for testing). */
|
||||
export function clearGateways(): void {
|
||||
gatewayRegistry.clear();
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { resolveDiscordChannelAllowlist } from "../resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "../resolve-users.js";
|
||||
import { normalizeDiscordToken } from "../token.js";
|
||||
import { createExecApprovalButton, DiscordExecApprovalHandler } from "./exec-approvals.js";
|
||||
import { registerGateway, unregisterGateway } from "./gateway-registry.js";
|
||||
import {
|
||||
DiscordMessageListener,
|
||||
DiscordPresenceListener,
|
||||
@@ -591,6 +592,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
}
|
||||
|
||||
const gateway = client.getPlugin<GatewayPlugin>("gateway");
|
||||
if (gateway) {
|
||||
registerGateway(account.accountId, gateway);
|
||||
}
|
||||
const gatewayEmitter = getDiscordGatewayEmitter(gateway);
|
||||
const stopGatewayLogging = attachDiscordGatewayLogging({
|
||||
emitter: gatewayEmitter,
|
||||
@@ -657,6 +661,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
unregisterGateway(account.accountId);
|
||||
stopGatewayLogging();
|
||||
if (helloTimeoutId) {
|
||||
clearTimeout(helloTimeoutId);
|
||||
|
||||
Reference in New Issue
Block a user