mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
Merge branch 'feat/discord-provider-endpoint-runtime' of https://github.com/openclaw/openclaw into HEAD
This commit is contained in:
@@ -1724,9 +1724,11 @@ openclaw gateway
|
||||
```
|
||||
|
||||
This is a process-wide credential-routing contract, not an account setting. A complete group
|
||||
redirects REST, Gateway, media, webhook, voice, OAuth, and Activity traffic for every Discord
|
||||
account in that Gateway. The provider therefore receives Discord credentials and message data;
|
||||
use only endpoints controlled by the operator. Non-loopback endpoints require HTTPS/WSS.
|
||||
redirects REST, Gateway, provider-origin media, webhook, voice, OAuth, and Activity traffic for
|
||||
every Discord account in that Gateway. The provider therefore receives Discord credentials and
|
||||
message data; use only endpoints controlled by the operator. Non-loopback endpoints require
|
||||
HTTPS/WSS. Public Discord CDN assets are never fetched in provider mode: provider-origin
|
||||
attachments remain available, while Discord-generated avatar and sticker assets are skipped.
|
||||
|
||||
The three values are all-or-none and are read once during Discord startup. Put them in the
|
||||
Gateway supervisor environment or trusted global runtime dotenv, not a workspace `.env`. A
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Client, User } from "../internal/discord.js";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
saveRemoteMedia: vi.fn(),
|
||||
logDebug: vi.fn(),
|
||||
resolveProviderGuard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/media-runtime", () => ({
|
||||
@@ -14,6 +15,10 @@ vi.mock("openclaw/plugin-sdk/logging-core", () => ({
|
||||
logDebug: mocks.logDebug,
|
||||
}));
|
||||
|
||||
vi.mock("../provider-endpoint.js", () => ({
|
||||
resolveDiscordProviderMediaDownloadGuard: mocks.resolveProviderGuard,
|
||||
}));
|
||||
|
||||
const { createDiscordAvatarResolver } = await import("./message-avatar.js");
|
||||
|
||||
function deferred<T>() {
|
||||
@@ -35,9 +40,27 @@ const emptyClient = { fetchGuild: vi.fn() } as unknown as Client;
|
||||
beforeEach(() => {
|
||||
mocks.saveRemoteMedia.mockReset();
|
||||
mocks.logDebug.mockReset();
|
||||
mocks.resolveProviderGuard.mockReset();
|
||||
});
|
||||
|
||||
describe("createDiscordAvatarResolver", () => {
|
||||
it("skips public CDN avatars when provider routing is active", () => {
|
||||
mocks.resolveProviderGuard.mockImplementation(() => {
|
||||
throw new Error("Discord provider media URL is outside the configured REST origin");
|
||||
});
|
||||
const resolver = createDiscordAvatarResolver();
|
||||
|
||||
expect(
|
||||
resolver.resolve({
|
||||
client: emptyClient,
|
||||
conversationId: "dm-provider",
|
||||
author: discordUser("user-1", "hash-1"),
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(mocks.saveRemoteMedia).not.toHaveBeenCalled();
|
||||
expect(mocks.logDebug).toHaveBeenCalledWith(expect.stringContaining("download skipped"));
|
||||
});
|
||||
|
||||
it("downloads a DM avatar in the background and reuses it until the hash changes", async () => {
|
||||
const firstDownload = deferred<{ path: string }>();
|
||||
mocks.saveRemoteMedia.mockReturnValueOnce(firstDownload.promise);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { logDebug } from "openclaw/plugin-sdk/logging-core";
|
||||
import { saveRemoteMedia } from "openclaw/plugin-sdk/media-runtime";
|
||||
import type { Client, User } from "../internal/discord.js";
|
||||
import { resolveDiscordProviderMediaDownloadGuard } from "../provider-endpoint.js";
|
||||
import { resolveDiscordCdnPolicy } from "./media-ssrf-policy.js";
|
||||
|
||||
const DISCORD_AVATAR_MAX_BYTES = 256 * 1024;
|
||||
@@ -46,12 +47,22 @@ export function createDiscordAvatarResolver() {
|
||||
if (pending.has(key) || pending.size >= DISCORD_AVATAR_CACHE_MAX_ENTRIES) {
|
||||
return undefined;
|
||||
}
|
||||
let providerGuard: ReturnType<typeof resolveDiscordProviderMediaDownloadGuard>;
|
||||
try {
|
||||
providerGuard = resolveDiscordProviderMediaDownloadGuard(url);
|
||||
} catch (error) {
|
||||
logDebug(`discord conversation avatar download skipped: ${formatErrorMessage(error)}`);
|
||||
return undefined;
|
||||
}
|
||||
pending.add(key);
|
||||
void saveRemoteMedia({
|
||||
url,
|
||||
filePathHint: "conversation-avatar.png",
|
||||
maxBytes: DISCORD_AVATAR_MAX_BYTES,
|
||||
ssrfPolicy: resolveDiscordCdnPolicy(),
|
||||
ssrfPolicy: providerGuard
|
||||
? resolveDiscordCdnPolicy(providerGuard.policy)
|
||||
: resolveDiscordCdnPolicy(),
|
||||
...(providerGuard ? { maxRedirects: providerGuard.maxRedirects } : {}),
|
||||
})
|
||||
.then((media) => {
|
||||
setBoundedEntry(saved, key, media.path);
|
||||
|
||||
Reference in New Issue
Block a user