mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 11:02:12 +03:00
Discord: refine presence config defaults (#10855) (thanks @h0tp-ftw)
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
- Skills: remove duplicate `local-places` Google Places skill/proxy and keep `goplaces` as the single supported Google Places path.
|
- Skills: remove duplicate `local-places` Google Places skill/proxy and keep `goplaces` as the single supported Google Places path.
|
||||||
- Discord: send voice messages with waveform previews from local audio files (including silent delivery). (#7253) Thanks @nyanjou.
|
- Discord: send voice messages with waveform previews from local audio files (including silent delivery). (#7253) Thanks @nyanjou.
|
||||||
|
- Discord: add configurable presence status/activity/type/url (custom status defaults to activity text). (#10855) Thanks @h0tp-ftw.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
@@ -252,6 +253,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- CLI: sort commands alphabetically in help output. (#8068) Thanks @deepsoumya617.
|
- CLI: sort commands alphabetically in help output. (#8068) Thanks @deepsoumya617.
|
||||||
- CI: optimize pipeline throughput (macOS consolidation, Windows perf, workflow concurrency). (#10784) Thanks @mcaxtr.
|
- CI: optimize pipeline throughput (macOS consolidation, Windows perf, workflow concurrency). (#10784) Thanks @mcaxtr.
|
||||||
- Agents: bump pi-mono to 0.52.7; add embedded forward-compat fallback for Opus 4.6 model ids.
|
- Agents: bump pi-mono to 0.52.7; add embedded forward-compat fallback for Opus 4.6 model ids.
|
||||||
|
- Discord: add configurable presence status/activity/type/url (custom status defaults to activity text). (#10855) Thanks @h0tp-ftw.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { validateConfigObject } from "./config.js";
|
||||||
|
|
||||||
|
describe("config discord presence", () => {
|
||||||
|
it("accepts status-only presence", () => {
|
||||||
|
const res = validateConfigObject({
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
status: "idle",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts custom activity when type is omitted", () => {
|
||||||
|
const res = validateConfigObject({
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
activity: "Focus time",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts custom activity type", () => {
|
||||||
|
const res = validateConfigObject({
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
activity: "Chilling",
|
||||||
|
activityType: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects streaming activity without url", () => {
|
||||||
|
const res = validateConfigObject({
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
activity: "Live",
|
||||||
|
activityType: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects activityUrl without streaming type", () => {
|
||||||
|
const res = validateConfigObject({
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
activity: "Live",
|
||||||
|
activityUrl: "https://twitch.tv/openclaw",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -369,6 +369,11 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
"Resolve PluralKit proxied messages and treat system members as distinct senders.",
|
"Resolve PluralKit proxied messages and treat system members as distinct senders.",
|
||||||
"channels.discord.pluralkit.token":
|
"channels.discord.pluralkit.token":
|
||||||
"Optional PluralKit token for resolving private systems or members.",
|
"Optional PluralKit token for resolving private systems or members.",
|
||||||
|
"channels.discord.activity": "Discord presence activity text (defaults to custom status).",
|
||||||
|
"channels.discord.status": "Discord presence status (online, dnd, idle, invisible).",
|
||||||
|
"channels.discord.activityType":
|
||||||
|
"Discord presence activity type (0=Playing,1=Streaming,2=Listening,3=Watching,4=Custom,5=Competing).",
|
||||||
|
"channels.discord.activityUrl": "Discord presence streaming URL (required for activityType=1).",
|
||||||
"channels.slack.dm.policy":
|
"channels.slack.dm.policy":
|
||||||
'Direct message access control ("pairing" recommended). "open" requires channels.slack.dm.allowFrom=["*"].',
|
'Direct message access control ("pairing" recommended). "open" requires channels.slack.dm.allowFrom=["*"].',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ export const FIELD_LABELS: Record<string, string> = {
|
|||||||
"channels.discord.intents.guildMembers": "Discord Guild Members Intent",
|
"channels.discord.intents.guildMembers": "Discord Guild Members Intent",
|
||||||
"channels.discord.pluralkit.enabled": "Discord PluralKit Enabled",
|
"channels.discord.pluralkit.enabled": "Discord PluralKit Enabled",
|
||||||
"channels.discord.pluralkit.token": "Discord PluralKit Token",
|
"channels.discord.pluralkit.token": "Discord PluralKit Token",
|
||||||
|
"channels.discord.activity": "Discord Presence Activity",
|
||||||
|
"channels.discord.status": "Discord Presence Status",
|
||||||
|
"channels.discord.activityType": "Discord Presence Activity Type",
|
||||||
|
"channels.discord.activityUrl": "Discord Presence Activity URL",
|
||||||
"channels.slack.dm.policy": "Slack DM Policy",
|
"channels.slack.dm.policy": "Slack DM Policy",
|
||||||
"channels.slack.allowBots": "Slack Allow Bot Messages",
|
"channels.slack.allowBots": "Slack Allow Bot Messages",
|
||||||
"channels.discord.token": "Discord Bot Token",
|
"channels.discord.token": "Discord Bot Token",
|
||||||
|
|||||||
@@ -177,11 +177,11 @@ export type DiscordAccountConfig = {
|
|||||||
responsePrefix?: string;
|
responsePrefix?: string;
|
||||||
/** Bot activity status text (e.g. "Watching X"). */
|
/** Bot activity status text (e.g. "Watching X"). */
|
||||||
activity?: string;
|
activity?: string;
|
||||||
/** Bot status (online|dnd|idle|invisible). Default: online. */
|
/** Bot status (online|dnd|idle|invisible). Defaults to online when presence is configured. */
|
||||||
status?: "online" | "dnd" | "idle" | "invisible";
|
status?: "online" | "dnd" | "idle" | "invisible";
|
||||||
/** Activity type (0=Game, 1=Streaming, 2=Listening, 3=Watching, 5=Competing). Default: 3 (Watching). */
|
/** Activity type (0=Game, 1=Streaming, 2=Listening, 3=Watching, 4=Custom, 5=Competing). Defaults to 4 (Custom) when activity is set. */
|
||||||
activityType?: 0 | 1 | 2 | 3 | 5;
|
activityType?: 0 | 1 | 2 | 3 | 4 | 5;
|
||||||
/** Streaming URL (Twitch/YouTube). Required if activityType=1. */
|
/** Streaming URL (Twitch/YouTube). Required when activityType=1. */
|
||||||
activityUrl?: string;
|
activityUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -335,11 +335,42 @@ export const DiscordAccountSchema = z
|
|||||||
activity: z.string().optional(),
|
activity: z.string().optional(),
|
||||||
status: z.enum(["online", "dnd", "idle", "invisible"]).optional(),
|
status: z.enum(["online", "dnd", "idle", "invisible"]).optional(),
|
||||||
activityType: z
|
activityType: z
|
||||||
.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(5)])
|
.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)])
|
||||||
.optional(),
|
.optional(),
|
||||||
activityUrl: z.string().optional(),
|
activityUrl: z.string().url().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict()
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
const activityText = typeof value.activity === "string" ? value.activity.trim() : "";
|
||||||
|
const hasActivity = Boolean(activityText);
|
||||||
|
const hasActivityType = value.activityType !== undefined;
|
||||||
|
const activityUrl = typeof value.activityUrl === "string" ? value.activityUrl.trim() : "";
|
||||||
|
const hasActivityUrl = Boolean(activityUrl);
|
||||||
|
|
||||||
|
if ((hasActivityType || hasActivityUrl) && !hasActivity) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "channels.discord.activity is required when activityType or activityUrl is set",
|
||||||
|
path: ["activity"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.activityType === 1 && !hasActivityUrl) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "channels.discord.activityUrl is required when activityType is 1 (Streaming)",
|
||||||
|
path: ["activityUrl"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasActivityUrl && value.activityType !== 1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "channels.discord.activityType must be 1 (Streaming) when activityUrl is set",
|
||||||
|
path: ["activityType"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const DiscordConfigSchema = DiscordAccountSchema.extend({
|
export const DiscordConfigSchema = DiscordAccountSchema.extend({
|
||||||
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
|
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { resolveDiscordPresenceUpdate } from "./presence.js";
|
||||||
|
|
||||||
|
describe("resolveDiscordPresenceUpdate", () => {
|
||||||
|
it("returns null when no presence config provided", () => {
|
||||||
|
expect(resolveDiscordPresenceUpdate({})).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns status-only presence when activity is omitted", () => {
|
||||||
|
const presence = resolveDiscordPresenceUpdate({ status: "dnd" });
|
||||||
|
expect(presence).not.toBeNull();
|
||||||
|
expect(presence?.status).toBe("dnd");
|
||||||
|
expect(presence?.activities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to custom activity type when activity is set without type", () => {
|
||||||
|
const presence = resolveDiscordPresenceUpdate({ activity: "Focus time" });
|
||||||
|
expect(presence).not.toBeNull();
|
||||||
|
expect(presence?.status).toBe("online");
|
||||||
|
expect(presence?.activities).toHaveLength(1);
|
||||||
|
expect(presence?.activities[0]).toMatchObject({
|
||||||
|
type: 4,
|
||||||
|
name: "Custom Status",
|
||||||
|
state: "Focus time",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes streaming url when activityType is streaming", () => {
|
||||||
|
const presence = resolveDiscordPresenceUpdate({
|
||||||
|
activity: "Live",
|
||||||
|
activityType: 1,
|
||||||
|
activityUrl: "https://twitch.tv/openclaw",
|
||||||
|
});
|
||||||
|
expect(presence).not.toBeNull();
|
||||||
|
expect(presence?.activities).toHaveLength(1);
|
||||||
|
expect(presence?.activities[0]).toMatchObject({
|
||||||
|
type: 1,
|
||||||
|
name: "Live",
|
||||||
|
url: "https://twitch.tv/openclaw",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { Activity, UpdatePresenceData } from "@buape/carbon/gateway";
|
||||||
|
import type { DiscordAccountConfig } from "../../config/config.js";
|
||||||
|
|
||||||
|
const DEFAULT_CUSTOM_ACTIVITY_TYPE = 4;
|
||||||
|
const CUSTOM_STATUS_NAME = "Custom Status";
|
||||||
|
|
||||||
|
type DiscordPresenceConfig = Pick<
|
||||||
|
DiscordAccountConfig,
|
||||||
|
"activity" | "status" | "activityType" | "activityUrl"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function resolveDiscordPresenceUpdate(
|
||||||
|
config: DiscordPresenceConfig,
|
||||||
|
): UpdatePresenceData | null {
|
||||||
|
const activityText = typeof config.activity === "string" ? config.activity.trim() : "";
|
||||||
|
const status = typeof config.status === "string" ? config.status.trim() : "";
|
||||||
|
const activityType = config.activityType;
|
||||||
|
const activityUrl = typeof config.activityUrl === "string" ? config.activityUrl.trim() : "";
|
||||||
|
|
||||||
|
const hasActivity = Boolean(activityText);
|
||||||
|
const hasStatus = Boolean(status);
|
||||||
|
|
||||||
|
if (!hasActivity && !hasStatus) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activities: Activity[] = [];
|
||||||
|
|
||||||
|
if (hasActivity) {
|
||||||
|
const resolvedType = activityType ?? DEFAULT_CUSTOM_ACTIVITY_TYPE;
|
||||||
|
const activity: Activity =
|
||||||
|
resolvedType === DEFAULT_CUSTOM_ACTIVITY_TYPE
|
||||||
|
? { name: CUSTOM_STATUS_NAME, type: resolvedType, state: activityText }
|
||||||
|
: { name: activityText, type: resolvedType };
|
||||||
|
|
||||||
|
if (resolvedType === 1 && activityUrl) {
|
||||||
|
activity.url = activityUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
activities.push(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
since: null,
|
||||||
|
activities,
|
||||||
|
status: (status || "online") as UpdatePresenceData["status"],
|
||||||
|
afk: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
createDiscordCommandArgFallbackButton,
|
createDiscordCommandArgFallbackButton,
|
||||||
createDiscordNativeCommand,
|
createDiscordNativeCommand,
|
||||||
} from "./native-command.js";
|
} from "./native-command.js";
|
||||||
|
import { resolveDiscordPresenceUpdate } from "./presence.js";
|
||||||
|
|
||||||
export type MonitorDiscordOpts = {
|
export type MonitorDiscordOpts = {
|
||||||
token?: string;
|
token?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user