mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix(discord): restore DM reactions and guild activation
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discord: let `message` tool reactions resolve `user:<id>` DM targets and preserve `channels.discord.guilds.<guild>.channels.<channel>.requireMention: false` during reply-stage activation fallback. Fixes #70165 and #69441.
|
||||
- Telegram/webhooks: lower the grammY webhook callback timeout to 5s so Telegram gets an early 200 response instead of retrying long-running updates as read timeouts. (#70146) Thanks @friday-james.
|
||||
- Telegram/polling: rebuild the polling HTTP transport after `getUpdates` 409 conflicts, so retries use a fresh TCP connection instead of looping on a Telegram-terminated keep-alive socket. (#69873) Thanks @hclsys.
|
||||
- Slack/files: resolve `downloadFile` bot tokens from the runtime config when callers provide `cfg` without an explicit token or prebuilt client, preserving cfg-only file downloads outside the action runtime path. (#70160) Thanks @martingarramon.
|
||||
|
||||
@@ -71,6 +71,55 @@ describe("handleDiscordMessageAction", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to Discord toolContext.currentChannelId for reaction targets", async () => {
|
||||
await handleDiscordMessageAction({
|
||||
action: "react",
|
||||
params: {
|
||||
emoji: "ok",
|
||||
},
|
||||
cfg: {
|
||||
channels: { discord: { token: "tok" } },
|
||||
} as OpenClawConfig,
|
||||
toolContext: {
|
||||
currentChannelProvider: "discord",
|
||||
currentChannelId: "user:U1",
|
||||
currentMessageId: "9001",
|
||||
},
|
||||
});
|
||||
|
||||
expect(handleDiscordActionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "react",
|
||||
channelId: "user:U1",
|
||||
messageId: "9001",
|
||||
emoji: "ok",
|
||||
}),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not use another provider's current target for Discord reactions", async () => {
|
||||
await expect(
|
||||
handleDiscordMessageAction({
|
||||
action: "react",
|
||||
params: {
|
||||
emoji: "ok",
|
||||
},
|
||||
cfg: {
|
||||
channels: { discord: { token: "tok" } },
|
||||
} as OpenClawConfig,
|
||||
toolContext: {
|
||||
currentChannelProvider: "telegram",
|
||||
currentChannelId: "user:U1",
|
||||
currentMessageId: "9001",
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/channel target is required/i);
|
||||
|
||||
expect(handleDiscordActionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects reactions when no message id source is available", async () => {
|
||||
await expect(
|
||||
handleDiscordMessageAction({
|
||||
|
||||
@@ -22,6 +22,17 @@ import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-a
|
||||
|
||||
const providerId = "discord";
|
||||
|
||||
function readCurrentDiscordTarget(
|
||||
toolContext: Pick<ChannelMessageActionContext, "toolContext">["toolContext"],
|
||||
): string | undefined {
|
||||
const provider = toolContext?.currentChannelProvider?.trim().toLowerCase();
|
||||
if (provider && provider !== providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const target = toolContext?.currentChannelId?.trim();
|
||||
return target || undefined;
|
||||
}
|
||||
|
||||
export async function handleDiscordMessageAction(
|
||||
ctx: Pick<
|
||||
ChannelMessageActionContext,
|
||||
@@ -44,10 +55,17 @@ export async function handleDiscordMessageAction(
|
||||
mediaReadFile: ctx.mediaReadFile,
|
||||
} as const;
|
||||
|
||||
const resolveChannelId = () =>
|
||||
resolveDiscordChannelId(
|
||||
readStringParam(params, "channelId") ?? readStringParam(params, "to", { required: true }),
|
||||
);
|
||||
const readTarget = () => {
|
||||
const target =
|
||||
readStringParam(params, "channelId") ??
|
||||
readStringParam(params, "to") ??
|
||||
readCurrentDiscordTarget(ctx.toolContext);
|
||||
if (!target) {
|
||||
throw new Error("Discord channel target is required (use channel:<id>).");
|
||||
}
|
||||
return target;
|
||||
};
|
||||
const resolveChannelId = () => resolveDiscordChannelId(readTarget());
|
||||
|
||||
if (action === "send") {
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
@@ -137,7 +155,7 @@ export async function handleDiscordMessageAction(
|
||||
{
|
||||
action: "react",
|
||||
accountId: accountId ?? undefined,
|
||||
channelId: resolveChannelId(),
|
||||
channelId: readTarget(),
|
||||
messageId,
|
||||
emoji,
|
||||
remove,
|
||||
@@ -154,7 +172,7 @@ export async function handleDiscordMessageAction(
|
||||
{
|
||||
action: "reactions",
|
||||
accountId: accountId ?? undefined,
|
||||
channelId: resolveChannelId(),
|
||||
channelId: readTarget(),
|
||||
messageId,
|
||||
limit,
|
||||
},
|
||||
|
||||
@@ -38,7 +38,11 @@ import {
|
||||
sendVoiceMessageDiscord,
|
||||
unpinMessageDiscord,
|
||||
} from "../send.js";
|
||||
import type { DiscordSendComponents, DiscordSendEmbeds } from "../send.shared.js";
|
||||
import {
|
||||
resolveDiscordTargetChannelId,
|
||||
type DiscordSendComponents,
|
||||
type DiscordSendEmbeds,
|
||||
} from "../send.shared.js";
|
||||
import { resolveDiscordChannelId } from "../targets.js";
|
||||
|
||||
export const discordMessagingActionRuntime = {
|
||||
@@ -56,6 +60,7 @@ export const discordMessagingActionRuntime = {
|
||||
readMessagesDiscord,
|
||||
removeOwnReactionsDiscord,
|
||||
removeReactionDiscord,
|
||||
resolveDiscordReactionTargetChannelId,
|
||||
resolveDiscordChannelId,
|
||||
searchMessagesDiscord,
|
||||
sendDiscordComponentMessage,
|
||||
@@ -66,6 +71,23 @@ export const discordMessagingActionRuntime = {
|
||||
unpinMessageDiscord,
|
||||
};
|
||||
|
||||
export async function resolveDiscordReactionTargetChannelId(params: {
|
||||
target: string;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string;
|
||||
}): Promise<string> {
|
||||
try {
|
||||
return resolveDiscordChannelId(params.target);
|
||||
} catch {
|
||||
return (
|
||||
await resolveDiscordTargetChannelId(params.target, {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})
|
||||
).channelId;
|
||||
}
|
||||
}
|
||||
|
||||
function hasDiscordComponentObjectKeys(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(
|
||||
value &&
|
||||
@@ -114,6 +136,15 @@ export async function handleDiscordMessagingAction(
|
||||
}
|
||||
const cfgOptions = { cfg };
|
||||
const resolvedReactionAccountId = accountId ?? resolveDefaultDiscordAccountId(cfg);
|
||||
const resolveReactionChannelId = async () => {
|
||||
const target =
|
||||
readStringParam(params, "channelId") ?? readStringParam(params, "to", { required: true });
|
||||
return await discordMessagingActionRuntime.resolveDiscordReactionTargetChannelId({
|
||||
target,
|
||||
cfg,
|
||||
accountId: resolvedReactionAccountId,
|
||||
});
|
||||
};
|
||||
const reactionRuntimeOptions = resolvedReactionAccountId
|
||||
? createDiscordRuntimeAccountContext({
|
||||
cfg,
|
||||
@@ -138,7 +169,7 @@ export async function handleDiscordMessagingAction(
|
||||
if (!isActionEnabled("reactions")) {
|
||||
throw new Error("Discord reactions are disabled.");
|
||||
}
|
||||
const channelId = resolveChannelId();
|
||||
const channelId = await resolveReactionChannelId();
|
||||
const messageId = readStringParam(params, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
@@ -174,7 +205,7 @@ export async function handleDiscordMessagingAction(
|
||||
if (!isActionEnabled("reactions")) {
|
||||
throw new Error("Discord reactions are disabled.");
|
||||
}
|
||||
const channelId = resolveChannelId();
|
||||
const channelId = await resolveReactionChannelId();
|
||||
const messageId = readStringParam(params, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ const {
|
||||
createThreadDiscord,
|
||||
deleteChannelDiscord,
|
||||
editChannelDiscord,
|
||||
fetchReactionsDiscord,
|
||||
fetchMessageDiscord,
|
||||
kickMemberDiscord,
|
||||
listGuildChannelsDiscord,
|
||||
@@ -198,6 +199,56 @@ describe("handleDiscordMessagingAction", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves Discord DM targets for reaction adds", async () => {
|
||||
const resolveReactionTarget = vi.fn(async () => "DM1");
|
||||
discordMessagingActionRuntime.resolveDiscordReactionTargetChannelId = resolveReactionTarget;
|
||||
|
||||
await handleMessagingAction(
|
||||
"react",
|
||||
{
|
||||
to: "user:U1",
|
||||
messageId: "M1",
|
||||
emoji: "✅",
|
||||
},
|
||||
enableAllActions,
|
||||
);
|
||||
|
||||
expect(resolveReactionTarget).toHaveBeenCalledWith({
|
||||
target: "user:U1",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
accountId: "default",
|
||||
});
|
||||
expect(reactMessageDiscord).toHaveBeenCalledWith("DM1", "M1", "✅", {
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
accountId: "default",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves Discord DM targets for reaction listing", async () => {
|
||||
const resolveReactionTarget = vi.fn(async () => "DM1");
|
||||
discordMessagingActionRuntime.resolveDiscordReactionTargetChannelId = resolveReactionTarget;
|
||||
|
||||
await handleMessagingAction(
|
||||
"reactions",
|
||||
{
|
||||
to: "user:U1",
|
||||
messageId: "M1",
|
||||
},
|
||||
enableAllActions,
|
||||
);
|
||||
|
||||
expect(resolveReactionTarget).toHaveBeenCalledWith({
|
||||
target: "user:U1",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
accountId: "default",
|
||||
});
|
||||
expect(fetchReactionsDiscord).toHaveBeenCalledWith("DM1", "M1", {
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
accountId: "default",
|
||||
limit: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("removes reactions on empty emoji", async () => {
|
||||
await handleMessagingAction(
|
||||
"react",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { requireRuntimeConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { parseAndResolveDiscordTarget } from "./target-resolver.js";
|
||||
import type { DiscordTargetParseOptions } from "./targets.js";
|
||||
|
||||
type DiscordRecipient =
|
||||
| {
|
||||
@@ -16,6 +17,7 @@ export async function parseAndResolveRecipient(
|
||||
raw: string,
|
||||
accountId?: string,
|
||||
cfg?: OpenClawConfig,
|
||||
parseOptions: DiscordTargetParseOptions = {},
|
||||
): Promise<DiscordRecipient> {
|
||||
if (!cfg) {
|
||||
throw new Error(
|
||||
@@ -25,7 +27,8 @@ export async function parseAndResolveRecipient(
|
||||
const resolvedCfg = requireRuntimeConfig(cfg, "Discord recipient resolution");
|
||||
const accountInfo = resolveDiscordAccount({ cfg: resolvedCfg, accountId });
|
||||
const trimmed = raw.trim();
|
||||
const parseOptions = {
|
||||
const resolvedParseOptions = {
|
||||
...parseOptions,
|
||||
ambiguousMessage: `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
|
||||
};
|
||||
const resolved = await parseAndResolveDiscordTarget(
|
||||
@@ -34,7 +37,7 @@ export async function parseAndResolveRecipient(
|
||||
cfg: resolvedCfg,
|
||||
accountId: accountInfo.accountId,
|
||||
},
|
||||
parseOptions,
|
||||
resolvedParseOptions,
|
||||
);
|
||||
return { kind: resolved.kind, id: resolved.id };
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ let removeReactionDiscord: typeof import("./send.js").removeReactionDiscord;
|
||||
let searchMessagesDiscord: typeof import("./send.js").searchMessagesDiscord;
|
||||
let sendMessageDiscord: typeof import("./send.js").sendMessageDiscord;
|
||||
let unpinMessageDiscord: typeof import("./send.js").unpinMessageDiscord;
|
||||
let resolveDiscordTargetChannelId: typeof import("./send.shared.js").resolveDiscordTargetChannelId;
|
||||
let loadWebMedia: typeof import("openclaw/plugin-sdk/web-media").loadWebMedia;
|
||||
let __resetDiscordDirectoryCacheForTest: typeof import("./directory-cache.js").__resetDiscordDirectoryCacheForTest;
|
||||
let rememberDiscordDirectoryUser: typeof import("./directory-cache.js").rememberDiscordDirectoryUser;
|
||||
@@ -39,6 +40,7 @@ beforeAll(async () => {
|
||||
sendMessageDiscord,
|
||||
unpinMessageDiscord,
|
||||
} = await import("./send.js"));
|
||||
({ resolveDiscordTargetChannelId } = await import("./send.shared.js"));
|
||||
({ loadWebMedia } = await import("openclaw/plugin-sdk/web-media"));
|
||||
({ __resetDiscordDirectoryCacheForTest, rememberDiscordDirectoryUser } =
|
||||
await import("./directory-cache.js"));
|
||||
@@ -49,6 +51,39 @@ beforeEach(() => {
|
||||
__resetDiscordDirectoryCacheForTest();
|
||||
});
|
||||
|
||||
describe("resolveDiscordTargetChannelId", () => {
|
||||
it("creates a DM channel for user targets", async () => {
|
||||
const { rest, postMock } = makeDiscordRest();
|
||||
postMock.mockResolvedValueOnce({ id: "dm-1" });
|
||||
|
||||
await expect(
|
||||
resolveDiscordTargetChannelId("user:U1", {
|
||||
rest,
|
||||
token: "t",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
}),
|
||||
).resolves.toEqual({ channelId: "dm-1", dm: true });
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith(Routes.userChannels(), {
|
||||
body: { recipient_id: "U1" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps channel targets on the channel path", async () => {
|
||||
const { rest, postMock } = makeDiscordRest();
|
||||
|
||||
await expect(
|
||||
resolveDiscordTargetChannelId("channel:C1", {
|
||||
rest,
|
||||
token: "t",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
}),
|
||||
).resolves.toEqual({ channelId: "C1" });
|
||||
|
||||
expect(postMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessageDiscord", () => {
|
||||
function expectReplyReference(
|
||||
body: { message_reference?: unknown } | undefined,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { PollLayoutType } from "discord-api-types/payloads/v10";
|
||||
import type { RESTAPIPoll } from "discord-api-types/rest/v10";
|
||||
import { Routes, type APIChannel, type APIEmbed } from "discord-api-types/v10";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { requireRuntimeConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { buildOutboundMediaLoadOptions } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { extensionForMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
@@ -22,7 +22,8 @@ import { resolveTextChunksWithFallback } from "openclaw/plugin-sdk/reply-payload
|
||||
import type { RetryRunner } from "openclaw/plugin-sdk/retry-runtime";
|
||||
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
||||
import { chunkDiscordTextWithMode } from "./chunk.js";
|
||||
import { createDiscordClient, resolveDiscordRest } from "./client.js";
|
||||
import { createDiscordClient, resolveDiscordRest, type DiscordClientOpts } from "./client.js";
|
||||
import { parseAndResolveRecipient } from "./recipient-resolution.js";
|
||||
import { fetchChannelPermissionsDiscord, isThreadChannelType } from "./send.permissions.js";
|
||||
import { DiscordSendError } from "./send.types.js";
|
||||
|
||||
@@ -193,6 +194,18 @@ async function resolveChannelId(
|
||||
return { channelId: dmChannel.id, dm: true };
|
||||
}
|
||||
|
||||
async function resolveDiscordTargetChannelId(
|
||||
raw: string,
|
||||
opts: DiscordClientOpts & { cfg: OpenClawConfig },
|
||||
): Promise<{ channelId: string; dm?: boolean }> {
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Discord target channel resolution");
|
||||
const recipient = await parseAndResolveRecipient(raw, opts.accountId, cfg, {
|
||||
defaultKind: "channel",
|
||||
});
|
||||
const { rest, request } = createDiscordClient(opts, cfg);
|
||||
return await resolveChannelId(rest, recipient, request);
|
||||
}
|
||||
|
||||
export async function resolveDiscordChannelType(
|
||||
rest: RequestClient,
|
||||
channelId: string,
|
||||
@@ -455,6 +468,7 @@ export {
|
||||
normalizeReactionEmoji,
|
||||
normalizeStickerIds,
|
||||
resolveChannelId,
|
||||
resolveDiscordTargetChannelId,
|
||||
resolveDiscordRest,
|
||||
sendDiscordMedia,
|
||||
sendDiscordText,
|
||||
|
||||
@@ -84,4 +84,87 @@ describe("group runtime loading", () => {
|
||||
expect(groupsRuntimeLoads).toHaveBeenCalled();
|
||||
vi.doUnmock("./groups.runtime.js");
|
||||
});
|
||||
|
||||
it("honors Discord guild channel requireMention fallback when runtime plugin is unavailable", async () => {
|
||||
vi.doMock("./groups.runtime.js", () => ({
|
||||
getChannelPlugin: () => undefined,
|
||||
normalizeChannelId: (channelId?: string) => channelId?.trim().toLowerCase(),
|
||||
}));
|
||||
const groups = await import("./groups.js");
|
||||
|
||||
await expect(
|
||||
groups.resolveGroupRequireMention({
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
guilds: {
|
||||
G1: {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
C1: { requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
ctx: {
|
||||
Provider: "discord",
|
||||
From: "discord:channel:C1",
|
||||
GroupSpace: "G1",
|
||||
GroupChannel: "general",
|
||||
},
|
||||
groupResolution: {
|
||||
key: "discord:channel:C1",
|
||||
channel: "discord",
|
||||
id: "C1",
|
||||
chatType: "group",
|
||||
},
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
vi.doUnmock("./groups.runtime.js");
|
||||
});
|
||||
|
||||
it("honors account-scoped Discord guild requireMention fallback", async () => {
|
||||
vi.doMock("./groups.runtime.js", () => ({
|
||||
getChannelPlugin: () => undefined,
|
||||
normalizeChannelId: (channelId?: string) => channelId?.trim().toLowerCase(),
|
||||
}));
|
||||
const groups = await import("./groups.js");
|
||||
|
||||
await expect(
|
||||
groups.resolveGroupRequireMention({
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
guilds: {
|
||||
G1: { requireMention: true },
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
guilds: {
|
||||
G1: { requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
ctx: {
|
||||
Provider: "discord",
|
||||
From: "discord:channel:C1",
|
||||
GroupSpace: "G1",
|
||||
GroupChannel: "general",
|
||||
AccountId: "work",
|
||||
},
|
||||
groupResolution: {
|
||||
key: "discord:channel:C1",
|
||||
channel: "discord",
|
||||
id: "C1",
|
||||
chatType: "group",
|
||||
},
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
vi.doUnmock("./groups.runtime.js");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,17 @@ import { extractExplicitGroupId } from "./group-id.js";
|
||||
|
||||
let groupsRuntimePromise: Promise<typeof import("./groups.runtime.js")> | null = null;
|
||||
|
||||
type DiscordGroupConfig = {
|
||||
requireMention?: boolean;
|
||||
slug?: string;
|
||||
channels?: Record<string, DiscordGroupConfig>;
|
||||
};
|
||||
|
||||
type DiscordConfigWithGuilds = {
|
||||
accounts?: Record<string, { guilds?: Record<string, DiscordGroupConfig> }>;
|
||||
guilds?: Record<string, DiscordGroupConfig>;
|
||||
};
|
||||
|
||||
function loadGroupsRuntime() {
|
||||
groupsRuntimePromise ??= import("./groups.runtime.js");
|
||||
return groupsRuntimePromise;
|
||||
@@ -37,6 +48,99 @@ async function resolveRuntimeChannelId(raw?: string | null): Promise<string | nu
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDiscordSlug(value?: string | null) {
|
||||
const normalized = normalizeOptionalLowercaseString(value);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
return normalized
|
||||
.replace(/^#/, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
function resolveDiscordGuilds(
|
||||
cfg: OpenClawConfig,
|
||||
accountId?: string | null,
|
||||
): Record<string, DiscordGroupConfig> | undefined {
|
||||
const discord = cfg.channels?.discord as DiscordConfigWithGuilds | undefined;
|
||||
if (!discord) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedAccountId = normalizeOptionalString(accountId);
|
||||
const accountGuilds = normalizedAccountId
|
||||
? discord.accounts?.[normalizedAccountId]?.guilds
|
||||
: undefined;
|
||||
return accountGuilds ?? discord.guilds;
|
||||
}
|
||||
|
||||
function resolveDiscordGuildEntry(
|
||||
guilds: Record<string, DiscordGroupConfig> | undefined,
|
||||
groupSpace?: string | null,
|
||||
): DiscordGroupConfig | undefined {
|
||||
if (!guilds || Object.keys(guilds).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const space = normalizeOptionalString(groupSpace) ?? "";
|
||||
if (space && guilds[space]) {
|
||||
return guilds[space];
|
||||
}
|
||||
const slug = normalizeDiscordSlug(space);
|
||||
if (slug && guilds[slug]) {
|
||||
return guilds[slug];
|
||||
}
|
||||
if (slug) {
|
||||
const match = Object.values(guilds).find((entry) => normalizeDiscordSlug(entry?.slug) === slug);
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return guilds["*"];
|
||||
}
|
||||
|
||||
function resolveDiscordChannelEntry(
|
||||
channels: Record<string, DiscordGroupConfig> | undefined,
|
||||
params: { groupId?: string | null; groupChannel?: string | null },
|
||||
): DiscordGroupConfig | undefined {
|
||||
if (!channels || Object.keys(channels).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const groupId = normalizeOptionalString(params.groupId);
|
||||
const groupChannel = normalizeOptionalString(params.groupChannel);
|
||||
const channelSlug = normalizeDiscordSlug(groupChannel);
|
||||
return (
|
||||
(groupId ? channels[groupId] : undefined) ??
|
||||
(channelSlug ? (channels[channelSlug] ?? channels[`#${channelSlug}`]) : undefined) ??
|
||||
(groupChannel ? channels[groupChannel] : undefined) ??
|
||||
channels["*"]
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDiscordRequireMentionFallback(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
groupId?: string | null;
|
||||
groupChannel?: string | null;
|
||||
groupSpace?: string | null;
|
||||
accountId?: string | null;
|
||||
}): boolean | undefined {
|
||||
if (params.channel !== "discord") {
|
||||
return undefined;
|
||||
}
|
||||
const guildEntry = resolveDiscordGuildEntry(
|
||||
resolveDiscordGuilds(params.cfg, params.accountId),
|
||||
params.groupSpace,
|
||||
);
|
||||
const channelEntry = resolveDiscordChannelEntry(guildEntry?.channels, params);
|
||||
if (typeof channelEntry?.requireMention === "boolean") {
|
||||
return channelEntry.requireMention;
|
||||
}
|
||||
if (typeof guildEntry?.requireMention === "boolean") {
|
||||
return guildEntry.requireMention;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function resolveGroupRequireMention(params: {
|
||||
cfg: OpenClawConfig;
|
||||
ctx: TemplateContext;
|
||||
@@ -70,6 +174,17 @@ export async function resolveGroupRequireMention(params: {
|
||||
if (typeof requireMention === "boolean") {
|
||||
return requireMention;
|
||||
}
|
||||
const discordRequireMention = resolveDiscordRequireMentionFallback({
|
||||
cfg,
|
||||
channel,
|
||||
groupId,
|
||||
groupChannel,
|
||||
groupSpace,
|
||||
accountId: ctx.AccountId,
|
||||
});
|
||||
if (typeof discordRequireMention === "boolean") {
|
||||
return discordRequireMention;
|
||||
}
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg,
|
||||
channel,
|
||||
|
||||
Reference in New Issue
Block a user