mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 15:41:23 +00:00
Discord: keep DM component sessions
This commit is contained in:
53
src/discord/send.components.test.ts
Normal file
53
src/discord/send.components.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ChannelType } from "discord-api-types/v10";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { registerDiscordComponentEntries } from "./components-registry.js";
|
||||
import { sendDiscordComponentMessage } from "./send.components.js";
|
||||
import { makeDiscordRest } from "./send.test-harness.js";
|
||||
|
||||
const loadConfigMock = vi.hoisted(() => vi.fn(() => ({ session: { dmScope: "main" } })));
|
||||
|
||||
vi.mock("../config/config.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./components-registry.js", () => ({
|
||||
registerDiscordComponentEntries: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("sendDiscordComponentMessage", () => {
|
||||
const registerMock = vi.mocked(registerDiscordComponentEntries);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("maps DM channel targets to direct-session component entries", async () => {
|
||||
const { rest, postMock, getMock } = makeDiscordRest();
|
||||
getMock.mockResolvedValueOnce({
|
||||
type: ChannelType.DM,
|
||||
recipients: [{ id: "user-1" }],
|
||||
});
|
||||
postMock.mockResolvedValueOnce({ id: "msg1", channel_id: "dm-1" });
|
||||
|
||||
await sendDiscordComponentMessage(
|
||||
"channel:dm-1",
|
||||
{
|
||||
blocks: [{ type: "actions", buttons: [{ label: "Tap" }] }],
|
||||
},
|
||||
{
|
||||
rest,
|
||||
token: "t",
|
||||
sessionKey: "agent:main:discord:channel:dm-1",
|
||||
agentId: "main",
|
||||
},
|
||||
);
|
||||
|
||||
expect(registerMock).toHaveBeenCalledTimes(1);
|
||||
const args = registerMock.mock.calls[0]?.[0];
|
||||
expect(args?.entries[0]?.sessionKey).toBe("agent:main:main");
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import type { APIChannel } from "discord-api-types/v10";
|
||||
import { ChannelType, Routes } from "discord-api-types/v10";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { recordChannelActivity } from "../infra/channel-activity.js";
|
||||
import { buildAgentSessionKey } from "../routing/resolve-route.js";
|
||||
import { loadWebMedia } from "../web/media.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { registerDiscordComponentEntries } from "./components-registry.js";
|
||||
@@ -29,6 +30,50 @@ import type { DiscordSendResult } from "./send.types.js";
|
||||
|
||||
const DISCORD_FORUM_LIKE_TYPES = new Set<number>([ChannelType.GuildForum, ChannelType.GuildMedia]);
|
||||
|
||||
type DiscordRecipient = Awaited<ReturnType<typeof parseAndResolveRecipient>>;
|
||||
|
||||
function resolveDiscordDmRecipientId(channel?: APIChannel): string | undefined {
|
||||
if (!channel || channel.type !== ChannelType.DM) {
|
||||
return undefined;
|
||||
}
|
||||
const recipients = (channel as { recipients?: Array<{ id?: string }> }).recipients;
|
||||
const recipientId = recipients?.[0]?.id;
|
||||
if (typeof recipientId !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = recipientId.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function resolveDiscordComponentSessionKey(params: {
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
accountId: string;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
recipient: DiscordRecipient;
|
||||
channel?: APIChannel;
|
||||
}): string | undefined {
|
||||
if (!params.sessionKey || !params.agentId) {
|
||||
return params.sessionKey;
|
||||
}
|
||||
if (params.recipient.kind !== "channel") {
|
||||
return params.sessionKey;
|
||||
}
|
||||
const recipientId = resolveDiscordDmRecipientId(params.channel);
|
||||
if (!recipientId) {
|
||||
return params.sessionKey;
|
||||
}
|
||||
// DM channel IDs should map back to the user session for component interactions.
|
||||
return buildAgentSessionKey({
|
||||
agentId: params.agentId,
|
||||
channel: "discord",
|
||||
accountId: params.accountId,
|
||||
peer: { kind: "direct", id: recipientId },
|
||||
dmScope: params.cfg.session?.dmScope,
|
||||
identityLinks: params.cfg.session?.identityLinks,
|
||||
});
|
||||
}
|
||||
|
||||
function extractComponentAttachmentNames(spec: DiscordComponentMessageSpec): string[] {
|
||||
const names: string[] = [];
|
||||
for (const block of spec.blocks ?? []) {
|
||||
@@ -63,9 +108,10 @@ export async function sendDiscordComponentMessage(
|
||||
const recipient = await parseAndResolveRecipient(to, opts.accountId);
|
||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||
|
||||
let channel: APIChannel | undefined;
|
||||
let channelType: number | undefined;
|
||||
try {
|
||||
const channel = (await rest.get(Routes.channel(channelId))) as APIChannel | undefined;
|
||||
channel = (await rest.get(Routes.channel(channelId))) as APIChannel | undefined;
|
||||
channelType = channel?.type;
|
||||
} catch {
|
||||
channelType = undefined;
|
||||
@@ -75,9 +121,18 @@ export async function sendDiscordComponentMessage(
|
||||
throw new Error("Discord components are not supported in forum-style channels");
|
||||
}
|
||||
|
||||
const componentSessionKey = resolveDiscordComponentSessionKey({
|
||||
cfg,
|
||||
accountId: accountInfo.accountId,
|
||||
agentId: opts.agentId,
|
||||
sessionKey: opts.sessionKey,
|
||||
recipient,
|
||||
channel,
|
||||
});
|
||||
|
||||
const buildResult = buildDiscordComponentMessage({
|
||||
spec,
|
||||
sessionKey: opts.sessionKey,
|
||||
sessionKey: componentSessionKey,
|
||||
agentId: opts.agentId,
|
||||
accountId: accountInfo.accountId,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user