mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(discord): require runtime config in helpers
This commit is contained in:
@@ -98,7 +98,7 @@ export async function handleDiscordGuildAction(
|
||||
action: string,
|
||||
params: Record<string, unknown>,
|
||||
isActionEnabled: ActionGate<DiscordActionConfig>,
|
||||
cfg?: OpenClawConfig,
|
||||
cfg: OpenClawConfig,
|
||||
options?: { mediaLocalRoots?: readonly string[] },
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const accountId = readStringParam(params, "accountId");
|
||||
|
||||
@@ -118,11 +118,11 @@ export async function handleDiscordMessagingAction(
|
||||
action: string,
|
||||
params: Record<string, unknown>,
|
||||
isActionEnabled: ActionGate<DiscordActionConfig>,
|
||||
cfg: OpenClawConfig,
|
||||
options?: {
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
},
|
||||
cfg?: OpenClawConfig,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const resolveChannelId = () =>
|
||||
discordMessagingActionRuntime.resolveDiscordChannelId(
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function handleDiscordModerationAction(
|
||||
action: string,
|
||||
params: Record<string, unknown>,
|
||||
isActionEnabled: ActionGate<DiscordActionConfig>,
|
||||
cfg?: OpenClawConfig,
|
||||
cfg: OpenClawConfig,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
if (!isDiscordModerationAction(action)) {
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
|
||||
@@ -89,13 +89,13 @@ function handleMessagingAction(
|
||||
action: string,
|
||||
params: Record<string, unknown>,
|
||||
isActionEnabled: (key: keyof DiscordActionConfig) => boolean,
|
||||
cfg: OpenClawConfig = DISCORD_TEST_CFG,
|
||||
options?: {
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
},
|
||||
cfg: OpenClawConfig = DISCORD_TEST_CFG,
|
||||
) {
|
||||
return handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg);
|
||||
return handleDiscordMessagingAction(action, params, isActionEnabled, cfg, options);
|
||||
}
|
||||
|
||||
function handleGuildAction(
|
||||
@@ -178,7 +178,6 @@ describe("handleDiscordMessagingAction", () => {
|
||||
emoji: "✅",
|
||||
},
|
||||
enableAllActions,
|
||||
undefined,
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
@@ -363,7 +362,7 @@ describe("handleDiscordMessagingAction", () => {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
await handleMessagingAction("readMessages", { channelId: "C1" }, enableAllActions, {}, cfg);
|
||||
await handleMessagingAction("readMessages", { channelId: "C1" }, enableAllActions, cfg);
|
||||
expect(readMessagesDiscord).toHaveBeenCalledWith("C1", expect.any(Object), { cfg });
|
||||
});
|
||||
|
||||
@@ -397,7 +396,6 @@ describe("handleDiscordMessagingAction", () => {
|
||||
"fetchMessage",
|
||||
{ guildId: "G1", channelId: "C1", messageId: "M1" },
|
||||
enableAllActions,
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
expect(fetchMessageDiscord).toHaveBeenCalledWith("C1", "M1", { cfg });
|
||||
@@ -471,6 +469,7 @@ describe("handleDiscordMessagingAction", () => {
|
||||
mediaUrl: "/tmp/image.png",
|
||||
},
|
||||
enableAllActions,
|
||||
DISCORD_TEST_CFG,
|
||||
{ mediaLocalRoots: ["/tmp/agent-root"] },
|
||||
);
|
||||
expect(sendMessageDiscord).toHaveBeenCalledWith(
|
||||
@@ -496,6 +495,7 @@ describe("handleDiscordMessagingAction", () => {
|
||||
components: {},
|
||||
},
|
||||
enableAllActions,
|
||||
DISCORD_TEST_CFG,
|
||||
{ mediaLocalRoots: ["/tmp/agent-root"] },
|
||||
);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function handleDiscordAction(
|
||||
const isActionEnabled = createDiscordActionGate({ cfg, accountId });
|
||||
|
||||
if (messagingActions.has(action)) {
|
||||
return await handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg);
|
||||
return await handleDiscordMessagingAction(action, params, isActionEnabled, cfg, options);
|
||||
}
|
||||
if (guildActions.has(action)) {
|
||||
return await handleDiscordGuildAction(action, params, isActionEnabled, cfg, options);
|
||||
|
||||
@@ -358,10 +358,11 @@ async function updateMessage(params: {
|
||||
container: DiscordUiContainer;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const { rest, request: discordRequest } = createDiscordClient(
|
||||
{ token: params.token, accountId: params.accountId },
|
||||
params.cfg,
|
||||
);
|
||||
const { rest, request: discordRequest } = createDiscordClient({
|
||||
cfg: params.cfg,
|
||||
token: params.token,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const payload = buildExecApprovalPayload(params.container);
|
||||
await discordRequest(
|
||||
() =>
|
||||
@@ -389,10 +390,11 @@ async function finalizeMessage(params: {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { rest, request: discordRequest } = createDiscordClient(
|
||||
{ token: params.token, accountId: params.accountId },
|
||||
params.cfg,
|
||||
);
|
||||
const { rest, request: discordRequest } = createDiscordClient({
|
||||
cfg: params.cfg,
|
||||
token: params.token,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
await discordRequest(
|
||||
() => rest.delete(Routes.channelMessage(params.channelId, params.messageId)) as Promise<void>,
|
||||
"delete-approval",
|
||||
@@ -517,10 +519,11 @@ export const discordApprovalNativeRuntime = createChannelApprovalNativeRuntimeAd
|
||||
},
|
||||
};
|
||||
}
|
||||
const { rest, request: discordRequest } = createDiscordClient(
|
||||
{ token: resolved.context.token, accountId: resolved.accountId },
|
||||
const { rest, request: discordRequest } = createDiscordClient({
|
||||
cfg,
|
||||
);
|
||||
token: resolved.context.token,
|
||||
accountId: resolved.accountId,
|
||||
});
|
||||
const userId = plannedTarget.target.to;
|
||||
const dmChannel = (await discordRequest(
|
||||
() =>
|
||||
@@ -553,10 +556,11 @@ export const discordApprovalNativeRuntime = createChannelApprovalNativeRuntimeAd
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
const { rest, request: discordRequest } = createDiscordClient(
|
||||
{ token: resolved.context.token, accountId: resolved.accountId },
|
||||
const { rest, request: discordRequest } = createDiscordClient({
|
||||
cfg,
|
||||
);
|
||||
token: resolved.context.token,
|
||||
accountId: resolved.accountId,
|
||||
});
|
||||
const message = (await discordRequest(
|
||||
() =>
|
||||
rest.post(Routes.channelMessages(preparedTarget.discordChannelId), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
DiscordGuildChannelConfig,
|
||||
DiscordGuildEntry,
|
||||
OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
@@ -76,13 +77,14 @@ export function collectDiscordAuditChannelIdsForGuilds(
|
||||
}
|
||||
|
||||
export async function auditDiscordChannelPermissionsWithFetcher(params: {
|
||||
cfg: OpenClawConfig;
|
||||
token: string;
|
||||
accountId?: string | null;
|
||||
channelIds: string[];
|
||||
timeoutMs: number;
|
||||
fetchChannelPermissions: (
|
||||
channelId: string,
|
||||
params: { token: string; accountId?: string },
|
||||
params: { cfg: OpenClawConfig; token: string; accountId?: string },
|
||||
) => Promise<{
|
||||
permissions: string[];
|
||||
}>;
|
||||
@@ -105,6 +107,7 @@ export async function auditDiscordChannelPermissionsWithFetcher(params: {
|
||||
for (const channelId of params.channelIds) {
|
||||
try {
|
||||
const perms = await params.fetchChannelPermissions(channelId, {
|
||||
cfg: params.cfg,
|
||||
token,
|
||||
accountId: params.accountId ?? undefined,
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ describe("discord audit", () => {
|
||||
});
|
||||
|
||||
const audit = await auditDiscordChannelPermissionsWithFetcher({
|
||||
cfg,
|
||||
token: "t",
|
||||
accountId: "default",
|
||||
channelIds: collected.channelIds,
|
||||
|
||||
@@ -19,6 +19,7 @@ export function collectDiscordAuditChannelIds(params: {
|
||||
}
|
||||
|
||||
export async function auditDiscordChannelPermissions(params: {
|
||||
cfg: OpenClawConfig;
|
||||
token: string;
|
||||
accountId?: string | null;
|
||||
channelIds: string[];
|
||||
|
||||
@@ -621,10 +621,23 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
],
|
||||
};
|
||||
}
|
||||
const statusCfg: OpenClawConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
[account.accountId]: {
|
||||
...account.config,
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const perms = await (
|
||||
await loadDiscordSendModule()
|
||||
).fetchChannelPermissionsDiscord(parsedTarget.id, {
|
||||
cfg: statusCfg,
|
||||
token,
|
||||
accountId: account.accountId ?? undefined,
|
||||
});
|
||||
@@ -681,6 +694,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
};
|
||||
}
|
||||
const audit = await auditDiscordChannelPermissions({
|
||||
cfg,
|
||||
token: botToken,
|
||||
accountId: account.accountId,
|
||||
channelIds,
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("createDiscordRestClient proxy support", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { rest } = createDiscordRestClient({}, cfg);
|
||||
const { rest } = createDiscordRestClient({ cfg });
|
||||
const requestClient = rest as unknown as {
|
||||
customFetch?: typeof fetch;
|
||||
options?: { fetch?: typeof fetch };
|
||||
@@ -54,7 +54,7 @@ describe("createDiscordRestClient proxy support", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { rest } = createDiscordRestClient({}, cfg);
|
||||
const { rest } = createDiscordRestClient({ cfg });
|
||||
const requestClient = rest as unknown as {
|
||||
options?: { fetch?: typeof fetch };
|
||||
};
|
||||
@@ -72,7 +72,7 @@ describe("createDiscordRestClient proxy support", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { rest } = createDiscordRestClient({}, cfg);
|
||||
const { rest } = createDiscordRestClient({ cfg });
|
||||
const requestClient = rest as unknown as {
|
||||
options?: { fetch?: typeof fetch };
|
||||
};
|
||||
@@ -91,7 +91,7 @@ describe("createDiscordRestClient proxy support", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { rest } = createDiscordRestClient({}, cfg);
|
||||
const { rest } = createDiscordRestClient({ cfg });
|
||||
const requestClient = rest as unknown as {
|
||||
options?: { fetch?: typeof fetch };
|
||||
};
|
||||
@@ -110,7 +110,7 @@ describe("createDiscordRestClient proxy support", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { rest } = createDiscordRestClient({}, cfg);
|
||||
const { rest } = createDiscordRestClient({ cfg });
|
||||
const requestClient = rest as unknown as {
|
||||
options?: { fetch?: typeof fetch };
|
||||
};
|
||||
|
||||
@@ -19,13 +19,7 @@ describe("createDiscordRestClient", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = createDiscordRestClient(
|
||||
{
|
||||
token: "Bot explicit-token",
|
||||
rest: fakeRest,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
const result = createDiscordRestClient({ cfg, token: "Bot explicit-token", rest: fakeRest });
|
||||
|
||||
expect(result.token).toBe("explicit-token");
|
||||
expect(result.rest).toBe(fakeRest);
|
||||
@@ -52,14 +46,12 @@ describe("createDiscordRestClient", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = createDiscordRestClient(
|
||||
{
|
||||
accountId: "ops",
|
||||
token: "Bot explicit-account-token",
|
||||
rest: fakeRest,
|
||||
},
|
||||
const result = createDiscordRestClient({
|
||||
cfg,
|
||||
);
|
||||
accountId: "ops",
|
||||
token: "Bot explicit-account-token",
|
||||
rest: fakeRest,
|
||||
});
|
||||
|
||||
expect(result.token).toBe("explicit-account-token");
|
||||
expect(result.account.accountId).toBe("ops");
|
||||
@@ -79,13 +71,6 @@ describe("createDiscordRestClient", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(() =>
|
||||
createDiscordRestClient(
|
||||
{
|
||||
rest: fakeRest,
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).toThrow(/unresolved SecretRef/i);
|
||||
expect(() => createDiscordRestClient({ cfg, rest: fakeRest })).toThrow(/unresolved SecretRef/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { DiscordRuntimeAccountContext } from "./send.types.js";
|
||||
import { normalizeDiscordToken } from "./token.js";
|
||||
|
||||
export type DiscordClientOpts = {
|
||||
cfg?: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
token?: string;
|
||||
accountId?: string;
|
||||
rest?: RequestClient;
|
||||
@@ -36,16 +36,9 @@ export function createDiscordRuntimeAccountContext(params: {
|
||||
|
||||
export function resolveDiscordClientAccountContext(
|
||||
opts: Pick<DiscordClientOpts, "cfg" | "accountId">,
|
||||
cfg?: OpenClawConfig,
|
||||
runtime?: Pick<RuntimeEnv, "error">,
|
||||
) {
|
||||
const config = opts.cfg ?? cfg;
|
||||
if (!config) {
|
||||
throw new Error(
|
||||
"Discord client requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path.",
|
||||
);
|
||||
}
|
||||
const resolvedCfg = requireRuntimeConfig(config, "Discord client");
|
||||
const resolvedCfg = requireRuntimeConfig(opts.cfg, "Discord client");
|
||||
const account = resolveAccountWithoutToken({
|
||||
cfg: resolvedCfg,
|
||||
accountId: opts.accountId,
|
||||
@@ -69,10 +62,9 @@ function resolveToken(params: { accountId: string; fallbackToken?: string }) {
|
||||
|
||||
export function resolveDiscordProxyFetch(
|
||||
opts: Pick<DiscordClientOpts, "cfg" | "accountId">,
|
||||
cfg?: OpenClawConfig,
|
||||
runtime?: Pick<RuntimeEnv, "error">,
|
||||
): typeof fetch | undefined {
|
||||
return resolveDiscordClientAccountContext(opts, cfg, runtime).proxyFetch;
|
||||
return resolveDiscordClientAccountContext(opts, runtime).proxyFetch;
|
||||
}
|
||||
|
||||
function resolveRest(
|
||||
@@ -110,9 +102,9 @@ function resolveAccountWithoutToken(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function createDiscordRestClient(opts: DiscordClientOpts, cfg?: OpenClawConfig) {
|
||||
export function createDiscordRestClient(opts: DiscordClientOpts) {
|
||||
const explicitToken = normalizeDiscordToken(opts.token, "channels.discord.token");
|
||||
const proxyContext = resolveDiscordClientAccountContext(opts, cfg);
|
||||
const proxyContext = resolveDiscordClientAccountContext(opts);
|
||||
const resolvedCfg = proxyContext.cfg;
|
||||
const account = explicitToken
|
||||
? proxyContext.account
|
||||
@@ -127,11 +119,12 @@ export function createDiscordRestClient(opts: DiscordClientOpts, cfg?: OpenClawC
|
||||
return { token, rest, account };
|
||||
}
|
||||
|
||||
export function createDiscordClient(
|
||||
opts: DiscordClientOpts,
|
||||
cfg?: OpenClawConfig,
|
||||
): { token: string; rest: RequestClient; request: RetryRunner } {
|
||||
const { token, rest, account } = createDiscordRestClient(opts, opts.cfg ?? cfg);
|
||||
export function createDiscordClient(opts: DiscordClientOpts): {
|
||||
token: string;
|
||||
rest: RequestClient;
|
||||
request: RetryRunner;
|
||||
} {
|
||||
const { token, rest, account } = createDiscordRestClient(opts);
|
||||
const request = createDiscordRetryRunner({
|
||||
retry: opts.retry,
|
||||
configRetry: account.config.retry,
|
||||
@@ -141,5 +134,5 @@ export function createDiscordClient(
|
||||
}
|
||||
|
||||
export function resolveDiscordRest(opts: DiscordClientOpts) {
|
||||
return createDiscordRestClient(opts, opts.cfg).rest;
|
||||
return createDiscordRestClient(opts).rest;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolveDiscordDraftStreamingChunking } from "./draft-chunking.js";
|
||||
|
||||
describe("resolveDiscordDraftStreamingChunking", () => {
|
||||
it("returns sane defaults when discord draft chunking is unset", () => {
|
||||
expect(resolveDiscordDraftStreamingChunking(undefined)).toEqual({
|
||||
expect(resolveDiscordDraftStreamingChunking({} as OpenClawConfig)).toEqual({
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
breakPreference: "paragraph",
|
||||
|
||||
@@ -9,7 +9,7 @@ const DEFAULT_DISCORD_DRAFT_STREAM_MIN = 200;
|
||||
const DEFAULT_DISCORD_DRAFT_STREAM_MAX = 800;
|
||||
|
||||
export function resolveDiscordDraftStreamingChunking(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
cfg: OpenClawConfig,
|
||||
accountId?: string | null,
|
||||
): {
|
||||
minChars: number;
|
||||
|
||||
@@ -1244,6 +1244,7 @@ describe("shouldIgnoreBoundThreadWebhookMessage", () => {
|
||||
|
||||
it("returns true for recently unbound thread webhook echoes", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
cfg: DEFAULT_PREFLIGHT_CFG,
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
|
||||
@@ -683,6 +683,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
it("prefers bound session keys and sets MessageThreadId for bound thread messages", async () => {
|
||||
const threadBindings = createThreadBindingManager({
|
||||
cfg: {} as import("openclaw/plugin-sdk/config-runtime").OpenClawConfig,
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
|
||||
@@ -826,6 +826,8 @@ export async function processDiscordMessage(
|
||||
}
|
||||
notifyFinalReplyStart();
|
||||
await editMessageDiscord(deliverChannelId, previewMessageId, edit, {
|
||||
cfg,
|
||||
accountId,
|
||||
rest: deliveryRest,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Client } from "@buape/carbon";
|
||||
import type { GatewayPresenceUpdate } from "discord-api-types/v10";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { DiscordChannelConfigResolved } from "./allow-list.js";
|
||||
@@ -23,6 +24,8 @@ import {
|
||||
resolveDiscordReplyDeliveryPlan,
|
||||
} from "./threading.js";
|
||||
|
||||
const DEFAULT_CFG = {} as OpenClawConfig;
|
||||
|
||||
describe("resolveDiscordOwnerAllowFrom", () => {
|
||||
it("returns undefined when no allowlist is configured", () => {
|
||||
const result = resolveDiscordOwnerAllowFrom({
|
||||
@@ -432,6 +435,7 @@ describe("maybeCreateDiscordAutoThread", () => {
|
||||
threadChannel: null,
|
||||
baseText: "hello",
|
||||
combinedBody: "hello",
|
||||
cfg: DEFAULT_CFG,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -489,6 +493,7 @@ describe("resolveDiscordAutoThreadReplyPlan", () => {
|
||||
threadChannel: overrides?.threadChannel ?? null,
|
||||
baseText: "hello",
|
||||
combinedBody: "hello",
|
||||
cfg: DEFAULT_CFG,
|
||||
replyToMode: "all" as const,
|
||||
agentId: "agent",
|
||||
channel: "discord" as const,
|
||||
|
||||
@@ -13,6 +13,7 @@ const sendDiscordTextMock = vi.hoisted(() => vi.fn());
|
||||
const buildDiscordSendErrorMock = vi.hoisted(() =>
|
||||
vi.fn<(err: unknown, ctx?: unknown) => Promise<unknown>>(async (err: unknown) => err),
|
||||
);
|
||||
const DEFAULT_CFG = {} as OpenClawConfig;
|
||||
const retryAsyncMock = vi.hoisted(() =>
|
||||
vi.fn(
|
||||
async (
|
||||
@@ -100,6 +101,7 @@ describe("deliverDiscordReply", () => {
|
||||
}> = {},
|
||||
) => {
|
||||
const threadBindings = createThreadBindingManager({
|
||||
cfg: DEFAULT_CFG,
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
|
||||
@@ -9,6 +9,7 @@ const DEFAULT_SEND_RESULT = {
|
||||
messageId: "msg-1",
|
||||
channelId: "thread-1",
|
||||
};
|
||||
const DEFAULT_CFG = {} as OpenClawConfig;
|
||||
|
||||
const restGet = vi.fn<(...args: unknown[]) => Promise<unknown>>();
|
||||
const sendMessageDiscord = vi.fn<typeof discordSendModule.sendMessageDiscord>();
|
||||
@@ -30,6 +31,17 @@ beforeAll(async () => {
|
||||
await import("./thread-bindings.discord-api.js"));
|
||||
});
|
||||
|
||||
function resolveTestChannelIdForBinding(
|
||||
params: Omit<Parameters<typeof resolveChannelIdForBinding>[0], "cfg"> & {
|
||||
cfg?: OpenClawConfig;
|
||||
},
|
||||
) {
|
||||
return resolveChannelIdForBinding({
|
||||
cfg: DEFAULT_CFG,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
describe("resolveChannelIdForBinding", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
@@ -59,7 +71,7 @@ describe("resolveChannelIdForBinding", () => {
|
||||
});
|
||||
|
||||
it("returns explicit channelId without resolving route", async () => {
|
||||
const resolved = await resolveChannelIdForBinding({
|
||||
const resolved = await resolveTestChannelIdForBinding({
|
||||
accountId: "default",
|
||||
threadId: "thread-1",
|
||||
channelId: "channel-explicit",
|
||||
@@ -71,7 +83,7 @@ describe("resolveChannelIdForBinding", () => {
|
||||
});
|
||||
|
||||
it("normalizes prefixed explicit channelId without resolving route", async () => {
|
||||
const resolved = await resolveChannelIdForBinding({
|
||||
const resolved = await resolveTestChannelIdForBinding({
|
||||
accountId: "default",
|
||||
threadId: "thread-1",
|
||||
channelId: "channel:123456789012345678",
|
||||
@@ -88,7 +100,7 @@ describe("resolveChannelIdForBinding", () => {
|
||||
type: ChannelType.GuildText,
|
||||
});
|
||||
|
||||
const resolved = await resolveChannelIdForBinding({
|
||||
const resolved = await resolveTestChannelIdForBinding({
|
||||
accountId: "default",
|
||||
threadId: "channel:123456789012345678",
|
||||
});
|
||||
@@ -106,7 +118,7 @@ describe("resolveChannelIdForBinding", () => {
|
||||
parent_id: "channel-parent",
|
||||
});
|
||||
|
||||
const resolved = await resolveChannelIdForBinding({
|
||||
const resolved = await resolveTestChannelIdForBinding({
|
||||
accountId: "default",
|
||||
threadId: "thread-1",
|
||||
});
|
||||
@@ -124,14 +136,16 @@ describe("resolveChannelIdForBinding", () => {
|
||||
parent_id: "channel-parent",
|
||||
});
|
||||
|
||||
await resolveChannelIdForBinding({
|
||||
await resolveTestChannelIdForBinding({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
threadId: "thread-1",
|
||||
});
|
||||
|
||||
const createDiscordRestClientCalls = createDiscordRestClient.mock.calls as unknown[][];
|
||||
expect(createDiscordRestClientCalls[0]?.[1]).toBe(cfg);
|
||||
expect(
|
||||
(createDiscordRestClientCalls[0]?.[0] as { cfg?: OpenClawConfig } | undefined)?.cfg,
|
||||
).toBe(cfg);
|
||||
});
|
||||
|
||||
it("keeps non-thread channel id even when parent_id exists", async () => {
|
||||
@@ -141,7 +155,7 @@ describe("resolveChannelIdForBinding", () => {
|
||||
parent_id: "category-1",
|
||||
});
|
||||
|
||||
const resolved = await resolveChannelIdForBinding({
|
||||
const resolved = await resolveTestChannelIdForBinding({
|
||||
accountId: "default",
|
||||
threadId: "channel-text",
|
||||
});
|
||||
@@ -156,7 +170,7 @@ describe("resolveChannelIdForBinding", () => {
|
||||
parent_id: "category-1",
|
||||
});
|
||||
|
||||
const resolved = await resolveChannelIdForBinding({
|
||||
const resolved = await resolveTestChannelIdForBinding({
|
||||
accountId: "default",
|
||||
threadId: "forum-1",
|
||||
});
|
||||
|
||||
@@ -173,19 +173,17 @@ export async function maybeSendBindingMessage(params: {
|
||||
}
|
||||
|
||||
export async function createWebhookForChannel(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
token?: string;
|
||||
channelId: string;
|
||||
}): Promise<{ webhookId?: string; webhookToken?: string }> {
|
||||
try {
|
||||
const rest = createDiscordRestClient(
|
||||
{
|
||||
accountId: params.accountId,
|
||||
token: params.token,
|
||||
},
|
||||
params.cfg,
|
||||
).rest;
|
||||
const rest = createDiscordRestClient({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
token: params.token,
|
||||
}).rest;
|
||||
const created = (await rest.post(Routes.channelWebhooks(params.channelId), {
|
||||
body: {
|
||||
name: "OpenClaw Agents",
|
||||
@@ -240,7 +238,7 @@ export function findReusableWebhook(params: { accountId: string; channelId: stri
|
||||
}
|
||||
|
||||
export async function resolveChannelIdForBinding(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
token?: string;
|
||||
threadId: string;
|
||||
@@ -255,13 +253,11 @@ export async function resolveChannelIdForBinding(params: {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const rest = createDiscordRestClient(
|
||||
{
|
||||
accountId: params.accountId,
|
||||
token: params.token,
|
||||
},
|
||||
params.cfg,
|
||||
).rest;
|
||||
const rest = createDiscordRestClient({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
token: params.token,
|
||||
}).rest;
|
||||
const channel = (await rest.get(Routes.channel(lookupThreadId))) as {
|
||||
id?: string;
|
||||
type?: number;
|
||||
@@ -291,7 +287,7 @@ export async function resolveChannelIdForBinding(params: {
|
||||
}
|
||||
|
||||
export async function createThreadForBinding(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
token?: string;
|
||||
channelId: string;
|
||||
|
||||
@@ -70,6 +70,17 @@ const discordClientModule = await import("../client.js");
|
||||
const discordThreadBindingApi = await import("./thread-bindings.discord-api.js");
|
||||
const acpRuntime = await import("openclaw/plugin-sdk/acp-runtime");
|
||||
|
||||
function createTestThreadBindingManager(
|
||||
params: Omit<Parameters<typeof createThreadBindingManager>[0], "cfg"> & {
|
||||
cfg?: OpenClawConfig;
|
||||
},
|
||||
) {
|
||||
return createThreadBindingManager({
|
||||
cfg: {} as OpenClawConfig,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
describe("thread binding lifecycle", () => {
|
||||
beforeEach(() => {
|
||||
__testing.resetThreadBindingsForTests();
|
||||
@@ -200,7 +211,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
const createDefaultSweeperManager = () =>
|
||||
createThreadBindingManager({
|
||||
createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -257,7 +268,7 @@ describe("thread binding lifecycle", () => {
|
||||
it("auto-unfocuses idle-expired bindings and sends inactivity message", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
cfg: {} as OpenClawConfig,
|
||||
persist: false,
|
||||
@@ -297,7 +308,7 @@ describe("thread binding lifecycle", () => {
|
||||
it("auto-unfocuses max-age-expired bindings and sends max-age message", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
cfg: {} as OpenClawConfig,
|
||||
persist: false,
|
||||
@@ -378,7 +389,7 @@ describe("thread binding lifecycle", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2026-02-20T23:00:00.000Z"));
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -423,7 +434,7 @@ describe("thread binding lifecycle", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2026-02-20T10:00:00.000Z"));
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -464,7 +475,7 @@ describe("thread binding lifecycle", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2026-02-20T10:00:00.000Z"));
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -519,7 +530,7 @@ describe("thread binding lifecycle", () => {
|
||||
it("keeps binding when idle timeout is disabled per session key", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -561,7 +572,7 @@ describe("thread binding lifecycle", () => {
|
||||
it("keeps a binding when activity is touched during the same sweep pass", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -626,7 +637,7 @@ describe("thread binding lifecycle", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2026-02-20T00:00:00.000Z"));
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -667,7 +678,7 @@ describe("thread binding lifecycle", () => {
|
||||
try {
|
||||
__testing.resetThreadBindingsForTests();
|
||||
vi.setSystemTime(new Date("2026-02-20T00:00:00.000Z"));
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: true,
|
||||
enableSweeper: false,
|
||||
@@ -690,7 +701,7 @@ describe("thread binding lifecycle", () => {
|
||||
manager.touchThread({ threadId: "thread-1" });
|
||||
|
||||
__testing.resetThreadBindingsForTests();
|
||||
const reloaded = createThreadBindingManager({
|
||||
const reloaded = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: true,
|
||||
enableSweeper: false,
|
||||
@@ -719,7 +730,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("reuses webhook credentials after unbind when rebinding in the same channel", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -756,7 +767,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("creates a new thread when spawning from an already bound thread", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -798,7 +809,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("resolves parent channel when thread target is passed via to without threadId", async () => {
|
||||
createThreadBindingManager({
|
||||
createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -838,7 +849,7 @@ describe("thread binding lifecycle", () => {
|
||||
const cfg = {
|
||||
channels: { discord: { token: "tok" } },
|
||||
} as OpenClawConfig;
|
||||
createThreadBindingManager({
|
||||
createTestThreadBindingManager({
|
||||
accountId: "runtime",
|
||||
token: "runtime-token",
|
||||
cfg,
|
||||
@@ -894,7 +905,7 @@ describe("thread binding lifecycle", () => {
|
||||
const refreshedCfg = {
|
||||
channels: { discord: { token: "refreshed-token" } },
|
||||
} as OpenClawConfig;
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "runtime",
|
||||
token: "runtime-token",
|
||||
cfg: startupCfg,
|
||||
@@ -945,7 +956,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("refreshes manager token when an existing manager is reused", async () => {
|
||||
createThreadBindingManager({
|
||||
createTestThreadBindingManager({
|
||||
accountId: "runtime",
|
||||
token: "token-old",
|
||||
persist: false,
|
||||
@@ -953,7 +964,7 @@ describe("thread binding lifecycle", () => {
|
||||
idleTimeoutMs: 24 * 60 * 60 * 1000,
|
||||
maxAgeMs: 0,
|
||||
});
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "runtime",
|
||||
token: "token-new",
|
||||
persist: false,
|
||||
@@ -987,7 +998,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("normalizes prefixed parentConversationId before creating child thread bindings", async () => {
|
||||
createThreadBindingManager({
|
||||
createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1032,7 +1043,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("preserves prefixed current channel conversation ids as binding keys", async () => {
|
||||
createThreadBindingManager({
|
||||
createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1086,7 +1097,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("binds current Discord DMs as direct conversation bindings", async () => {
|
||||
createThreadBindingManager({
|
||||
createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1137,7 +1148,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("preserves direct-binding metadata when rebinding the same conversation", async () => {
|
||||
createThreadBindingManager({
|
||||
createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1198,14 +1209,14 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("keeps overlapping thread ids isolated per account", async () => {
|
||||
const a = createThreadBindingManager({
|
||||
const a = createTestThreadBindingManager({
|
||||
accountId: "a",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
idleTimeoutMs: 24 * 60 * 60 * 1000,
|
||||
maxAgeMs: 0,
|
||||
});
|
||||
const b = createThreadBindingManager({
|
||||
const b = createTestThreadBindingManager({
|
||||
accountId: "b",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1243,7 +1254,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("removes stale ACP bindings during startup reconciliation", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1326,7 +1337,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("keeps ACP bindings when session store reads fail during startup reconciliation", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1370,7 +1381,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("does not reconcile plugin-owned direct bindings as stale ACP sessions", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1411,7 +1422,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("removes ACP bindings when health probe marks running session as stale", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1455,7 +1466,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("keeps running ACP bindings when health probe is uncertain", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1503,7 +1514,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("keeps ACP bindings in stored error state when no explicit stale probe verdict exists", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1550,7 +1561,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("starts ACP health probes in parallel during startup reconciliation", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1626,7 +1637,7 @@ describe("thread binding lifecycle", () => {
|
||||
});
|
||||
|
||||
it("caps ACP startup health probe concurrency", async () => {
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
@@ -1745,7 +1756,7 @@ describe("thread binding lifecycle", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const manager = createThreadBindingManager({
|
||||
const manager = createTestThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
|
||||
@@ -185,17 +185,15 @@ function toSessionBindingRecord(
|
||||
};
|
||||
}
|
||||
|
||||
export function createThreadBindingManager(
|
||||
params: {
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
cfg?: OpenClawConfig;
|
||||
persist?: boolean;
|
||||
enableSweeper?: boolean;
|
||||
idleTimeoutMs?: number;
|
||||
maxAgeMs?: number;
|
||||
} = {},
|
||||
): ThreadBindingManager {
|
||||
export function createThreadBindingManager(params: {
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
cfg: OpenClawConfig;
|
||||
persist?: boolean;
|
||||
enableSweeper?: boolean;
|
||||
idleTimeoutMs?: number;
|
||||
maxAgeMs?: number;
|
||||
}): ThreadBindingManager {
|
||||
ensureBindingsLoaded();
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const existing = MANAGERS_BY_ACCOUNT_ID.get(accountId);
|
||||
@@ -280,13 +278,11 @@ export function createThreadBindingManager(
|
||||
if (!rest) {
|
||||
try {
|
||||
const cfg = resolveCurrentCfg();
|
||||
rest = createDiscordRestClient(
|
||||
{
|
||||
accountId,
|
||||
token: resolveCurrentToken(),
|
||||
},
|
||||
rest = createDiscordRestClient({
|
||||
cfg,
|
||||
).rest;
|
||||
accountId,
|
||||
token: resolveCurrentToken(),
|
||||
}).rest;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
__testing as threadBindingsTesting,
|
||||
@@ -23,6 +24,7 @@ describe("thread binding manager state", () => {
|
||||
const viaJiti = await loadThreadBindingsViaAlternateLoader();
|
||||
|
||||
createThreadBindingManager({
|
||||
cfg: {} as OpenClawConfig,
|
||||
accountId: "work",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
|
||||
@@ -12,6 +12,7 @@ vi.mock("./thread-title.js", () => ({
|
||||
}));
|
||||
|
||||
let maybeCreateDiscordAutoThread: MaybeCreateDiscordAutoThreadFn;
|
||||
const DEFAULT_CFG = {} as OpenClawConfig;
|
||||
|
||||
const postMock = vi.fn();
|
||||
const getMock = vi.fn();
|
||||
@@ -37,6 +38,7 @@ function createBaseParams(
|
||||
channelType: ChannelType.GuildText,
|
||||
baseText: "test",
|
||||
combinedBody: "test",
|
||||
cfg: DEFAULT_CFG,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ type MaybeCreateDiscordAutoThreadParams = {
|
||||
channelDescription?: string;
|
||||
baseText: string;
|
||||
combinedBody: string;
|
||||
cfg?: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
};
|
||||
|
||||
@@ -459,7 +459,7 @@ export async function resolveDiscordAutoThreadReplyPlan(
|
||||
replyToMode: ReplyToMode;
|
||||
agentId: string;
|
||||
channel: string;
|
||||
cfg?: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
threadParentInheritanceEnabled?: boolean;
|
||||
},
|
||||
): Promise<DiscordAutoThreadReplyPlan> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ResolvedDiscordAccount } from "./accounts.js";
|
||||
|
||||
export function resolveDiscordProxyUrl(
|
||||
account: Pick<ResolvedDiscordAccount, "config">,
|
||||
cfg?: OpenClawConfig,
|
||||
cfg: OpenClawConfig,
|
||||
): string | undefined {
|
||||
const accountProxy = account.config.proxy?.trim();
|
||||
if (accountProxy) {
|
||||
@@ -31,7 +31,7 @@ export function resolveDiscordProxyFetchByUrl(
|
||||
|
||||
export function resolveDiscordProxyFetchForAccount(
|
||||
account: Pick<ResolvedDiscordAccount, "config">,
|
||||
cfg?: OpenClawConfig,
|
||||
cfg: OpenClawConfig,
|
||||
runtime?: Pick<RuntimeEnv, "error">,
|
||||
): typeof fetch | undefined {
|
||||
return resolveDiscordProxyFetchByUrl(resolveDiscordProxyUrl(account, cfg), runtime);
|
||||
|
||||
@@ -15,8 +15,8 @@ type DiscordRecipient =
|
||||
|
||||
export async function parseAndResolveRecipient(
|
||||
raw: string,
|
||||
cfg: OpenClawConfig,
|
||||
accountId?: string,
|
||||
cfg?: OpenClawConfig,
|
||||
parseOptions: DiscordTargetParseOptions = {},
|
||||
): Promise<DiscordRecipient> {
|
||||
if (!cfg) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
|
||||
export async function createChannelDiscord(
|
||||
payload: DiscordChannelCreate,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIChannel> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const body: Record<string, unknown> = {
|
||||
@@ -39,7 +39,7 @@ export async function createChannelDiscord(
|
||||
|
||||
export async function editChannelDiscord(
|
||||
payload: DiscordChannelEdit,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIChannel> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const body: Record<string, unknown> = {};
|
||||
@@ -84,13 +84,13 @@ export async function editChannelDiscord(
|
||||
})) as APIChannel;
|
||||
}
|
||||
|
||||
export async function deleteChannelDiscord(channelId: string, opts: DiscordReactOpts = {}) {
|
||||
export async function deleteChannelDiscord(channelId: string, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.delete(Routes.channel(channelId));
|
||||
return { ok: true, channelId };
|
||||
}
|
||||
|
||||
export async function moveChannelDiscord(payload: DiscordChannelMove, opts: DiscordReactOpts = {}) {
|
||||
export async function moveChannelDiscord(payload: DiscordChannelMove, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const body: Array<Record<string, unknown>> = [
|
||||
{
|
||||
@@ -105,7 +105,7 @@ export async function moveChannelDiscord(payload: DiscordChannelMove, opts: Disc
|
||||
|
||||
export async function setChannelPermissionDiscord(
|
||||
payload: DiscordChannelPermissionSet,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const body: Record<string, unknown> = {
|
||||
@@ -124,7 +124,7 @@ export async function setChannelPermissionDiscord(
|
||||
export async function removeChannelPermissionDiscord(
|
||||
channelId: string,
|
||||
targetId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.delete(`/channels/${channelId}/permissions/${targetId}`);
|
||||
|
||||
@@ -265,8 +265,8 @@ export async function sendDiscordComponentMessage(
|
||||
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Discord component send");
|
||||
const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId });
|
||||
const { token, rest, request } = createDiscordClient(opts, cfg);
|
||||
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
|
||||
const { token, rest, request } = createDiscordClient({ ...opts, cfg });
|
||||
const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId);
|
||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||
|
||||
const channelType = await resolveDiscordChannelType(rest, channelId);
|
||||
@@ -325,8 +325,8 @@ export async function editDiscordComponentMessage(
|
||||
): Promise<DiscordSendResult> {
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Discord component edit");
|
||||
const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId });
|
||||
const { token, rest, request } = createDiscordClient(opts, cfg);
|
||||
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
|
||||
const { token, rest, request } = createDiscordClient({ ...opts, cfg });
|
||||
const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId);
|
||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||
const { body, buildResult } = await buildDiscordComponentPayload({
|
||||
spec,
|
||||
|
||||
@@ -5,12 +5,12 @@ import { normalizeEmojiName, resolveDiscordRest } from "./send.shared.js";
|
||||
import type { DiscordEmojiUpload, DiscordReactOpts, DiscordStickerUpload } from "./send.types.js";
|
||||
import { DISCORD_MAX_EMOJI_BYTES, DISCORD_MAX_STICKER_BYTES } from "./send.types.js";
|
||||
|
||||
export async function listGuildEmojisDiscord(guildId: string, opts: DiscordReactOpts = {}) {
|
||||
export async function listGuildEmojisDiscord(guildId: string, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return await rest.get(Routes.guildEmojis(guildId));
|
||||
}
|
||||
|
||||
export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: DiscordReactOpts = {}) {
|
||||
export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_EMOJI_BYTES);
|
||||
const contentType = normalizeOptionalLowercaseString(media.contentType);
|
||||
@@ -31,10 +31,7 @@ export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: Disc
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadStickerDiscord(
|
||||
payload: DiscordStickerUpload,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
export async function uploadStickerDiscord(payload: DiscordStickerUpload, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_STICKER_BYTES);
|
||||
const contentType = normalizeOptionalLowercaseString(media.contentType);
|
||||
|
||||
@@ -21,7 +21,7 @@ import { DISCORD_MAX_EVENT_COVER_BYTES } from "./send.types.js";
|
||||
export async function fetchMemberInfoDiscord(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIGuildMember> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(Routes.guildMember(guildId, userId))) as APIGuildMember;
|
||||
@@ -29,19 +29,19 @@ export async function fetchMemberInfoDiscord(
|
||||
|
||||
export async function fetchRoleInfoDiscord(
|
||||
guildId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIRole[]> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(Routes.guildRoles(guildId))) as APIRole[];
|
||||
}
|
||||
|
||||
export async function addRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts = {}) {
|
||||
export async function addRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.put(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function removeRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts = {}) {
|
||||
export async function removeRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.delete(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId));
|
||||
return { ok: true };
|
||||
@@ -49,7 +49,7 @@ export async function removeRoleDiscord(payload: DiscordRoleChange, opts: Discor
|
||||
|
||||
export async function fetchChannelInfoDiscord(
|
||||
channelId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIChannel> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(Routes.channel(channelId))) as APIChannel;
|
||||
@@ -57,7 +57,7 @@ export async function fetchChannelInfoDiscord(
|
||||
|
||||
export async function listGuildChannelsDiscord(
|
||||
guildId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIChannel[]> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(Routes.guildChannels(guildId))) as APIChannel[];
|
||||
@@ -66,7 +66,7 @@ export async function listGuildChannelsDiscord(
|
||||
export async function fetchVoiceStatusDiscord(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIVoiceState> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(Routes.guildVoiceState(guildId, userId))) as APIVoiceState;
|
||||
@@ -74,7 +74,7 @@ export async function fetchVoiceStatusDiscord(
|
||||
|
||||
export async function listScheduledEventsDiscord(
|
||||
guildId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIGuildScheduledEvent[]> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(Routes.guildScheduledEvents(guildId))) as APIGuildScheduledEvent[];
|
||||
@@ -102,7 +102,7 @@ export async function resolveEventCoverImage(
|
||||
export async function createScheduledEventDiscord(
|
||||
guildId: string,
|
||||
payload: RESTPostAPIGuildScheduledEventJSONBody,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIGuildScheduledEvent> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.post(Routes.guildScheduledEvents(guildId), {
|
||||
@@ -112,7 +112,7 @@ export async function createScheduledEventDiscord(
|
||||
|
||||
export async function timeoutMemberDiscord(
|
||||
payload: DiscordTimeoutTarget,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIGuildMember> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
let until = payload.until;
|
||||
@@ -128,10 +128,7 @@ export async function timeoutMemberDiscord(
|
||||
})) as APIGuildMember;
|
||||
}
|
||||
|
||||
export async function kickMemberDiscord(
|
||||
payload: DiscordModerationTarget,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
export async function kickMemberDiscord(payload: DiscordModerationTarget, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.delete(Routes.guildMember(payload.guildId, payload.userId), {
|
||||
headers: payload.reason
|
||||
@@ -143,7 +140,7 @@ export async function kickMemberDiscord(
|
||||
|
||||
export async function banMemberDiscord(
|
||||
payload: DiscordModerationTarget & { deleteMessageDays?: number },
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const deleteMessageDays =
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
export async function readMessagesDiscord(
|
||||
channelId: string,
|
||||
query: DiscordMessageQuery = {},
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIMessage[]> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const limit =
|
||||
@@ -39,7 +39,7 @@ export async function readMessagesDiscord(
|
||||
export async function fetchMessageDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIMessage> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(Routes.channelMessage(channelId, messageId))) as APIMessage;
|
||||
@@ -49,7 +49,7 @@ export async function editMessageDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
payload: DiscordMessageEdit,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIMessage> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.patch(Routes.channelMessage(channelId, messageId), {
|
||||
@@ -60,7 +60,7 @@ export async function editMessageDiscord(
|
||||
export async function deleteMessageDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.delete(Routes.channelMessage(channelId, messageId));
|
||||
@@ -70,7 +70,7 @@ export async function deleteMessageDiscord(
|
||||
export async function pinMessageDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.put(Routes.channelPin(channelId, messageId));
|
||||
@@ -80,7 +80,7 @@ export async function pinMessageDiscord(
|
||||
export async function unpinMessageDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.delete(Routes.channelPin(channelId, messageId));
|
||||
@@ -89,7 +89,7 @@ export async function unpinMessageDiscord(
|
||||
|
||||
export async function listPinsDiscord(
|
||||
channelId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<APIMessage[]> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(Routes.channelPins(channelId))) as APIMessage[];
|
||||
@@ -98,7 +98,7 @@ export async function listPinsDiscord(
|
||||
export async function createThreadDiscord(
|
||||
channelId: string,
|
||||
payload: DiscordThreadCreate,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const body: Record<string, unknown> = { name: payload.name };
|
||||
@@ -150,7 +150,7 @@ export async function createThreadDiscord(
|
||||
return thread;
|
||||
}
|
||||
|
||||
export async function listThreadsDiscord(payload: DiscordThreadList, opts: DiscordReactOpts = {}) {
|
||||
export async function listThreadsDiscord(payload: DiscordThreadList, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
if (payload.includeArchived) {
|
||||
if (!payload.channelId) {
|
||||
@@ -168,10 +168,7 @@ export async function listThreadsDiscord(payload: DiscordThreadList, opts: Disco
|
||||
return await rest.get(Routes.guildActiveThreads(payload.guildId));
|
||||
}
|
||||
|
||||
export async function searchMessagesDiscord(
|
||||
query: DiscordSearchQuery,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
export async function searchMessagesDiscord(query: DiscordSearchQuery, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const params = new URLSearchParams();
|
||||
params.set("content", query.content);
|
||||
|
||||
@@ -129,8 +129,8 @@ async function resolveDiscordSendTarget(
|
||||
opts: DiscordSendOpts,
|
||||
): Promise<{ rest: RequestClient; request: DiscordClientRequest; channelId: string }> {
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Discord send target resolution");
|
||||
const { rest, request } = createDiscordClient(opts, cfg);
|
||||
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
|
||||
const { rest, request } = createDiscordClient({ ...opts, cfg });
|
||||
const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId);
|
||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||
return { rest, request, channelId };
|
||||
}
|
||||
@@ -159,8 +159,8 @@ export async function sendMessageDiscord(
|
||||
const textWithMentions = rewriteDiscordKnownMentions(textWithTables, {
|
||||
accountId: accountInfo.accountId,
|
||||
});
|
||||
const { token, rest, request } = createDiscordClient(opts, cfg);
|
||||
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
|
||||
const { token, rest, request } = createDiscordClient({ ...opts, cfg });
|
||||
const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId);
|
||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||
|
||||
// Forum/Media channels reject POST /messages; auto-create a thread post instead.
|
||||
@@ -543,19 +543,18 @@ export async function sendVoiceMessageDiscord(
|
||||
let token: string | undefined;
|
||||
let rest: RequestClient | undefined;
|
||||
let channelId: string | undefined;
|
||||
let cfg: OpenClawConfig | undefined;
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Discord voice send");
|
||||
|
||||
try {
|
||||
cfg = requireRuntimeConfig(opts.cfg, "Discord voice send");
|
||||
const accountInfo = resolveDiscordAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const client = createDiscordClient(opts, cfg);
|
||||
const client = createDiscordClient({ ...opts, cfg });
|
||||
token = client.token;
|
||||
rest = client.rest;
|
||||
const request = client.request;
|
||||
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
|
||||
const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId);
|
||||
channelId = (await resolveChannelId(rest, recipient, request)).channelId;
|
||||
|
||||
// Convert to OGG/Opus if needed
|
||||
@@ -589,7 +588,7 @@ export async function sendVoiceMessageDiscord(
|
||||
|
||||
return toDiscordSendResult(result, channelId);
|
||||
} catch (err) {
|
||||
if (channelId && rest && token && cfg) {
|
||||
if (channelId && rest && token) {
|
||||
throw await buildDiscordSendError(err, {
|
||||
channelId,
|
||||
cfg,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { RequestClient } from "@buape/carbon";
|
||||
import { PermissionFlagsBits, Routes } from "discord-api-types/v10";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockRest = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
}));
|
||||
const DISCORD_TEST_OPTS = { cfg: {} as OpenClawConfig };
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
resolveDiscordRest: () => mockRest as unknown as RequestClient,
|
||||
@@ -59,7 +61,11 @@ describe("discord guild permission authorization", () => {
|
||||
it("returns null when user is not a guild member", async () => {
|
||||
mockRest.get.mockRejectedValueOnce(new Error("404 Member not found"));
|
||||
|
||||
const result = await fetchMemberGuildPermissionsDiscord("guild-1", "user-1");
|
||||
const result = await fetchMemberGuildPermissionsDiscord(
|
||||
"guild-1",
|
||||
"user-1",
|
||||
DISCORD_TEST_OPTS,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -72,7 +78,11 @@ describe("discord guild permission authorization", () => {
|
||||
memberRoles: ["role-mod"],
|
||||
});
|
||||
|
||||
const result = await fetchMemberGuildPermissionsDiscord("guild-1", "user-1");
|
||||
const result = await fetchMemberGuildPermissionsDiscord(
|
||||
"guild-1",
|
||||
"user-1",
|
||||
DISCORD_TEST_OPTS,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
expect((result! & PermissionFlagsBits.ViewChannel) === PermissionFlagsBits.ViewChannel).toBe(
|
||||
true,
|
||||
@@ -93,9 +103,12 @@ describe("discord guild permission authorization", () => {
|
||||
memberRoles: ["role-mod"],
|
||||
});
|
||||
|
||||
const result = await hasAnyGuildPermissionDiscord("guild-1", "user-1", [
|
||||
PermissionFlagsBits.KickMembers,
|
||||
]);
|
||||
const result = await hasAnyGuildPermissionDiscord(
|
||||
"guild-1",
|
||||
"user-1",
|
||||
[PermissionFlagsBits.KickMembers],
|
||||
DISCORD_TEST_OPTS,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
@@ -111,9 +124,12 @@ describe("discord guild permission authorization", () => {
|
||||
memberRoles: ["role-admin"],
|
||||
});
|
||||
|
||||
const result = await hasAnyGuildPermissionDiscord("guild-1", "user-1", [
|
||||
PermissionFlagsBits.KickMembers,
|
||||
]);
|
||||
const result = await hasAnyGuildPermissionDiscord(
|
||||
"guild-1",
|
||||
"user-1",
|
||||
[PermissionFlagsBits.KickMembers],
|
||||
DISCORD_TEST_OPTS,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
@@ -123,10 +139,12 @@ describe("discord guild permission authorization", () => {
|
||||
memberRoles: [],
|
||||
});
|
||||
|
||||
const result = await hasAnyGuildPermissionDiscord("guild-1", "user-1", [
|
||||
PermissionFlagsBits.BanMembers,
|
||||
PermissionFlagsBits.KickMembers,
|
||||
]);
|
||||
const result = await hasAnyGuildPermissionDiscord(
|
||||
"guild-1",
|
||||
"user-1",
|
||||
[PermissionFlagsBits.BanMembers, PermissionFlagsBits.KickMembers],
|
||||
DISCORD_TEST_OPTS,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -141,10 +159,12 @@ describe("discord guild permission authorization", () => {
|
||||
memberRoles: ["role-mod"],
|
||||
});
|
||||
|
||||
const result = await hasAllGuildPermissionsDiscord("guild-1", "user-1", [
|
||||
PermissionFlagsBits.KickMembers,
|
||||
PermissionFlagsBits.BanMembers,
|
||||
]);
|
||||
const result = await hasAllGuildPermissionsDiscord(
|
||||
"guild-1",
|
||||
"user-1",
|
||||
[PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers],
|
||||
DISCORD_TEST_OPTS,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
@@ -157,10 +177,12 @@ describe("discord guild permission authorization", () => {
|
||||
memberRoles: ["role-admin"],
|
||||
});
|
||||
|
||||
const result = await hasAllGuildPermissionsDiscord("guild-1", "user-1", [
|
||||
PermissionFlagsBits.KickMembers,
|
||||
PermissionFlagsBits.BanMembers,
|
||||
]);
|
||||
const result = await hasAllGuildPermissionsDiscord(
|
||||
"guild-1",
|
||||
"user-1",
|
||||
[PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers],
|
||||
DISCORD_TEST_OPTS,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ async function fetchBotUserId(rest: RequestClient) {
|
||||
export async function fetchMemberGuildPermissionsDiscord(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<bigint | null> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
try {
|
||||
@@ -96,7 +96,7 @@ async function hasGuildPermissionsDiscord(
|
||||
userId: string,
|
||||
requiredPermissions: bigint[],
|
||||
check: (permissions: bigint, requiredPermissions: bigint[]) => boolean,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<boolean> {
|
||||
const permissions = await fetchMemberGuildPermissionsDiscord(guildId, userId, opts);
|
||||
if (permissions === null) {
|
||||
@@ -115,7 +115,7 @@ export async function hasAnyGuildPermissionDiscord(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
requiredPermissions: bigint[],
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<boolean> {
|
||||
return await hasGuildPermissionsDiscord(
|
||||
guildId,
|
||||
@@ -134,7 +134,7 @@ export async function hasAllGuildPermissionsDiscord(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
requiredPermissions: bigint[],
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<boolean> {
|
||||
return await hasGuildPermissionsDiscord(
|
||||
guildId,
|
||||
@@ -153,7 +153,7 @@ export const hasGuildPermissionDiscord = hasAnyGuildPermissionDiscord;
|
||||
|
||||
export async function fetchChannelPermissionsDiscord(
|
||||
channelId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<DiscordPermissionsSummary> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const channel = (await rest.get(Routes.channel(channelId))) as APIChannel;
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
} from "./send.types.js";
|
||||
|
||||
function createDiscordReactionRuntimeClient(opts: DiscordReactionRuntimeContext) {
|
||||
return createDiscordClient(opts, opts.cfg);
|
||||
return createDiscordClient(opts);
|
||||
}
|
||||
|
||||
function resolveDiscordReactionClient(opts: DiscordReactOpts) {
|
||||
@@ -23,7 +23,7 @@ function resolveDiscordReactionClient(opts: DiscordReactOpts) {
|
||||
);
|
||||
}
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Discord reactions");
|
||||
return createDiscordClient(opts, cfg);
|
||||
return createDiscordClient({ ...opts, cfg });
|
||||
}
|
||||
|
||||
function isDiscordReactionRuntimeContext(
|
||||
@@ -36,7 +36,7 @@ export async function reactMessageDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const { rest, request } = isDiscordReactionRuntimeContext(opts)
|
||||
? createDiscordReactionRuntimeClient(opts)
|
||||
@@ -53,7 +53,7 @@ export async function removeReactionDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
) {
|
||||
const { rest } = isDiscordReactionRuntimeContext(opts)
|
||||
? createDiscordReactionRuntimeClient(opts)
|
||||
@@ -66,7 +66,7 @@ export async function removeReactionDiscord(
|
||||
export async function removeOwnReactionsDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
opts: DiscordReactOpts,
|
||||
): Promise<{ ok: true; removed: string[] }> {
|
||||
const { rest } = isDiscordReactionRuntimeContext(opts)
|
||||
? createDiscordReactionRuntimeClient(opts)
|
||||
@@ -99,7 +99,7 @@ export async function removeOwnReactionsDiscord(
|
||||
export async function fetchReactionsDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: DiscordReactOpts & { limit?: number } = {},
|
||||
opts: DiscordReactOpts & { limit?: number },
|
||||
): Promise<DiscordReactionSummary[]> {
|
||||
const { rest } = isDiscordReactionRuntimeContext(opts)
|
||||
? createDiscordReactionRuntimeClient(opts)
|
||||
|
||||
@@ -234,10 +234,10 @@ async function resolveDiscordTargetChannelId(
|
||||
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, {
|
||||
const recipient = await parseAndResolveRecipient(raw, cfg, opts.accountId, {
|
||||
defaultKind: "channel",
|
||||
});
|
||||
const { rest, request } = createDiscordClient(opts, cfg);
|
||||
const { rest, request } = createDiscordClient(opts);
|
||||
return await resolveChannelId(rest, recipient, request);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export type DiscordRuntimeAccountContext = {
|
||||
};
|
||||
|
||||
export type DiscordReactOpts = {
|
||||
cfg?: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
rest?: RequestClient;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { RequestClient } from "@buape/carbon";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const resolveDiscordRestMock = vi.hoisted(() => vi.fn());
|
||||
const DEFAULT_CFG = {} as OpenClawConfig;
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
resolveDiscordRest: resolveDiscordRestMock,
|
||||
@@ -25,9 +27,9 @@ describe("sendTypingDiscord", () => {
|
||||
post,
|
||||
} as unknown as RequestClient);
|
||||
|
||||
const result = await sendTypingDiscord("12345", { accountId: "ops" });
|
||||
const result = await sendTypingDiscord("12345", { cfg: DEFAULT_CFG, accountId: "ops" });
|
||||
|
||||
expect(resolveDiscordRestMock).toHaveBeenCalledWith({ accountId: "ops" });
|
||||
expect(resolveDiscordRestMock).toHaveBeenCalledWith({ cfg: DEFAULT_CFG, accountId: "ops" });
|
||||
expect(post).toHaveBeenCalledWith(Routes.channelTyping("12345"));
|
||||
expect(result).toEqual({ ok: true, channelId: "12345" });
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Routes } from "discord-api-types/v10";
|
||||
import { resolveDiscordRest } from "./client.js";
|
||||
import type { DiscordReactOpts } from "./send.types.js";
|
||||
|
||||
export async function sendTypingDiscord(channelId: string, opts: DiscordReactOpts = {}) {
|
||||
export async function sendTypingDiscord(channelId: string, opts: DiscordReactOpts) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.post(Routes.channelTyping(channelId));
|
||||
return { ok: true, channelId };
|
||||
|
||||
@@ -19,7 +19,7 @@ export function normalizeDiscordToken(raw: unknown, path: string): string | unde
|
||||
}
|
||||
|
||||
export function resolveDiscordToken(
|
||||
cfg?: OpenClawConfig,
|
||||
cfg: OpenClawConfig,
|
||||
opts: { accountId?: string | null; envToken?: string | null } = {},
|
||||
): DiscordTokenResolution {
|
||||
const accountId = normalizeAccountId(opts.accountId);
|
||||
|
||||
Reference in New Issue
Block a user