Files
openclaw/extensions/discord/src/actions/runtime.presence.ts
2026-03-17 19:45:36 -07:00

116 lines
3.1 KiB
TypeScript

import type { Activity, UpdatePresenceData } from "@buape/carbon/gateway";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import {
type ActionGate,
jsonResult,
readStringParam,
type DiscordActionConfig,
} from "openclaw/plugin-sdk/discord-core";
import { getGateway } from "../monitor/gateway-registry.js";
const ACTIVITY_TYPE_MAP: Record<string, number> = {
playing: 0,
streaming: 1,
listening: 2,
watching: 3,
custom: 4,
competing: 5,
};
const VALID_STATUSES = new Set(["online", "dnd", "idle", "invisible"]);
export async function handleDiscordPresenceAction(
action: string,
params: Record<string, unknown>,
isActionEnabled: ActionGate<DiscordActionConfig>,
): Promise<AgentToolResult<unknown>> {
if (action !== "setPresence") {
throw new Error(`Unknown presence action: ${action}`);
}
if (!isActionEnabled("presence", false)) {
throw new Error("Discord presence changes are disabled.");
}
const accountId = readStringParam(params, "accountId");
const gateway = getGateway(accountId);
if (!gateway) {
throw new Error(
`Discord gateway not available${accountId ? ` for account "${accountId}"` : ""}. The bot may not be connected.`,
);
}
if (!gateway.isConnected) {
throw new Error(
`Discord gateway is not connected${accountId ? ` for account "${accountId}"` : ""}.`,
);
}
const statusRaw = readStringParam(params, "status") ?? "online";
if (!VALID_STATUSES.has(statusRaw)) {
throw new Error(
`Invalid status "${statusRaw}". Must be one of: ${[...VALID_STATUSES].join(", ")}`,
);
}
const status = statusRaw as UpdatePresenceData["status"];
const activityTypeRaw = readStringParam(params, "activityType");
const activityName = readStringParam(params, "activityName");
const activities: Activity[] = [];
if (activityTypeRaw || activityName) {
if (!activityTypeRaw) {
throw new Error(
"activityType is required when activityName is provided. " +
`Valid types: ${Object.keys(ACTIVITY_TYPE_MAP).join(", ")}`,
);
}
const typeNum = ACTIVITY_TYPE_MAP[activityTypeRaw.toLowerCase()];
if (typeNum === undefined) {
throw new Error(
`Invalid activityType "${activityTypeRaw}". Must be one of: ${Object.keys(ACTIVITY_TYPE_MAP).join(", ")}`,
);
}
const activity: Activity = {
name: activityName ?? "",
type: typeNum,
};
// Streaming URL (Twitch/YouTube). May not render for bots but is the correct payload shape.
if (typeNum === 1) {
const url = readStringParam(params, "activityUrl");
if (url) {
activity.url = url;
}
}
const state = readStringParam(params, "activityState");
if (state) {
activity.state = state;
}
activities.push(activity);
}
const presenceData: UpdatePresenceData = {
since: null,
activities,
status,
afk: false,
};
gateway.updatePresence(presenceData);
return jsonResult({
ok: true,
status,
activities: activities.map((a) => ({
type: a.type,
name: a.name,
...(a.url ? { url: a.url } : {}),
...(a.state ? { state: a.state } : {}),
})),
});
}