mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(discord): download attachments at receipt time, not after the run queue (#96183)
* fix(discord): download attachments at receipt time, not after the run queue Discord's CDN attachment URLs carry an expiring `ex` TTL. Media was downloaded in processDiscordMessageInner, after the inbound run queue, so messages delayed behind a busy run lost their attachments silently. Resolve attachments/forwarded media during preflightDiscordMessage instead, before the message is enqueued, and carry the result forward on the context for process to reuse. Fixes #96165 * refactor(discord): carry one prepared media snapshot Co-authored-by: ZacharyYW <zachary.w.yuan123@gmail.com> * fix(discord): guard bot media before download * chore(discord): leave release notes to release flow * docs(changelog): credit Discord attachment fix * chore(changelog): leave attachment fix to release flow --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -72,6 +72,13 @@ describe("buildDiscordInboundJob", () => {
|
||||
},
|
||||
ownerId: "user-1",
|
||||
},
|
||||
preparedMedia: [
|
||||
{
|
||||
path: "/tmp/openclaw-discord-test/photo.png",
|
||||
contentType: "image/png",
|
||||
placeholder: "<media:image>",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const job = buildDiscordInboundJob(ctx);
|
||||
@@ -95,6 +102,7 @@ describe("buildDiscordInboundJob", () => {
|
||||
ownerId: "user-1",
|
||||
});
|
||||
const serializedPayload = jsonRoundTrip(job.payload);
|
||||
expect(serializedPayload.preparedMedia).toEqual(ctx.preparedMedia);
|
||||
expect(serializedPayload.threadChannel).toEqual({
|
||||
id: "thread-1",
|
||||
name: "codex",
|
||||
|
||||
@@ -569,6 +569,46 @@ describe("preflightDiscordMessage", () => {
|
||||
expect(preflight.preflightAudioTranscript).toBe("hello openclaw from dm audio");
|
||||
});
|
||||
|
||||
it("downloads attachments during preflight, before the message reaches the run queue", async () => {
|
||||
// Regression for #96165: Discord CDN attachment URLs expire. Downloading
|
||||
// must happen at receipt time (preflight), not after a possible run-queue
|
||||
// delay, or queued messages lose their media.
|
||||
const result = await runDmPreflight({
|
||||
channelId: "dm-channel-image-1",
|
||||
message: createDiscordMessage({
|
||||
id: "m-dm-image-1",
|
||||
channelId: "dm-channel-image-1",
|
||||
content: "look at this",
|
||||
attachments: [
|
||||
{
|
||||
id: "att-dm-image-1",
|
||||
url: "https://cdn.discordapp.com/attachments/1/photo.png?ex=expired",
|
||||
content_type: "image/png",
|
||||
filename: "photo.png",
|
||||
},
|
||||
],
|
||||
author: {
|
||||
id: "user-1",
|
||||
bot: false,
|
||||
username: "alice",
|
||||
},
|
||||
}),
|
||||
discordConfig: {
|
||||
dmPolicy: "open",
|
||||
} as DiscordConfig,
|
||||
});
|
||||
|
||||
expect(saveRemoteMediaMock).toHaveBeenCalledTimes(1);
|
||||
const preflight = expectPreflightResult(result);
|
||||
expect(preflight.preparedMedia).toEqual([
|
||||
{
|
||||
path: "/tmp/openclaw-discord-test/photo.png",
|
||||
contentType: "image/png",
|
||||
placeholder: "<media:image>",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps no-guild messages direct when channel lookup is unavailable", async () => {
|
||||
const result = await runUnresolvedDmPreflight({
|
||||
cfg: {
|
||||
@@ -654,7 +694,7 @@ describe("preflightDiscordMessage", () => {
|
||||
).toBe("default");
|
||||
});
|
||||
|
||||
it("passes bot-loop protection facts for accepted bot-authored Discord messages (#58789)", async () => {
|
||||
it("suppresses repeated bot messages before downloading attachments (#58789)", async () => {
|
||||
const channelId = "channel-bot-loop";
|
||||
const guildId = "guild-bot-loop";
|
||||
const senderBotId = "relay-bot-1";
|
||||
@@ -675,7 +715,7 @@ describe("preflightDiscordMessage", () => {
|
||||
allowBots: true,
|
||||
botLoopProtection: {
|
||||
enabled: true,
|
||||
maxEventsPerWindow: 3,
|
||||
maxEventsPerWindow: 1,
|
||||
cooldownSeconds: 60,
|
||||
},
|
||||
} as DiscordConfig,
|
||||
@@ -689,58 +729,77 @@ describe("preflightDiscordMessage", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(expectPreflightResult(result).botLoopProtection).toEqual({
|
||||
scopeId: "default",
|
||||
conversationId: channelId,
|
||||
senderId: senderBotId,
|
||||
receiverId: "openclaw-bot",
|
||||
config: {
|
||||
enabled: true,
|
||||
maxEventsPerWindow: 3,
|
||||
cooldownSeconds: 60,
|
||||
},
|
||||
defaultsConfig: undefined,
|
||||
defaultEnabled: true,
|
||||
nowMs: Date.parse(messageTimestamp),
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const repeatedMessage = createDiscordMessage({
|
||||
id: "m-loop-2",
|
||||
channelId,
|
||||
content: "more chatter <@openclaw-bot>",
|
||||
mentionedUsers: [{ id: "openclaw-bot" }],
|
||||
attachments: [
|
||||
{
|
||||
id: "att-loop",
|
||||
url: "https://cdn.discordapp.com/attachments/1/loop.png",
|
||||
content_type: "image/png",
|
||||
filename: "loop.png",
|
||||
},
|
||||
],
|
||||
author: { id: senderBotId, bot: true, username: "Relay" },
|
||||
timestamp: "2026-05-13T05:00:00.001Z",
|
||||
});
|
||||
|
||||
expect(
|
||||
await runGuildPreflight({
|
||||
channelId,
|
||||
guildId,
|
||||
message: repeatedMessage,
|
||||
discordConfig: {
|
||||
allowBots: true,
|
||||
botLoopProtection: {
|
||||
enabled: true,
|
||||
maxEventsPerWindow: 1,
|
||||
cooldownSeconds: 60,
|
||||
},
|
||||
} as DiscordConfig,
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(saveRemoteMediaMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes generic channel defaults for Discord bot loop budgets", async () => {
|
||||
const channelId = "channel-bot-loop-defaults";
|
||||
const guildId = "guild-bot-loop-defaults";
|
||||
const discordConfig = { allowBots: true } as DiscordConfig;
|
||||
const message = createDiscordMessage({
|
||||
id: "m-loop-default-1",
|
||||
channelId,
|
||||
content: "relay <@openclaw-bot>",
|
||||
mentionedUsers: [{ id: "openclaw-bot" }],
|
||||
author: { id: "relay-bot-defaults", bot: true, username: "Relay" },
|
||||
});
|
||||
const result = await runGuildPreflight({
|
||||
channelId,
|
||||
guildId,
|
||||
message,
|
||||
discordConfig,
|
||||
cfg: {
|
||||
...DEFAULT_PREFLIGHT_CFG,
|
||||
channels: {
|
||||
defaults: {
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 1,
|
||||
cooldownSeconds: 60,
|
||||
const runBotMessage = async (id: string) =>
|
||||
await runGuildPreflight({
|
||||
channelId,
|
||||
guildId,
|
||||
message: createDiscordMessage({
|
||||
id,
|
||||
channelId,
|
||||
content: "relay <@openclaw-bot>",
|
||||
mentionedUsers: [{ id: "openclaw-bot" }],
|
||||
author: { id: "relay-bot-defaults", bot: true, username: "Relay" },
|
||||
}),
|
||||
discordConfig,
|
||||
cfg: {
|
||||
...DEFAULT_PREFLIGHT_CFG,
|
||||
channels: {
|
||||
defaults: {
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 1,
|
||||
cooldownSeconds: 60,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(expectPreflightResult(result).botLoopProtection?.defaultsConfig).toEqual({
|
||||
maxEventsPerWindow: 1,
|
||||
cooldownSeconds: 60,
|
||||
});
|
||||
expect(await runBotMessage("m-loop-default-1")).not.toBeNull();
|
||||
expect(await runBotMessage("m-loop-default-2")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not prepare loop-guard facts for bot messages that later preflight gates drop (#58789)", async () => {
|
||||
it("does not count bot messages that earlier preflight gates drop (#58789)", async () => {
|
||||
const channelId = "channel-bot-loop-dropped";
|
||||
const guildId = "guild-bot-loop-dropped";
|
||||
const senderBotId = "relay-bot-dropped";
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
buildMentionRegexes,
|
||||
classifyChannelInboundEvent,
|
||||
logInboundDrop,
|
||||
recordChannelBotPairLoopAndCheckSuppression,
|
||||
resolveInboundMentionDecision,
|
||||
resolveUnmentionedGroupInboundPolicy,
|
||||
recordDroppedChannelInboundHistory,
|
||||
@@ -64,9 +65,14 @@ import {
|
||||
resolveDiscordChannelInfo,
|
||||
resolveDiscordMessageChannelId,
|
||||
resolveDiscordMessageText,
|
||||
resolveForwardedMediaList,
|
||||
resolveMediaList,
|
||||
} from "./message-utils.js";
|
||||
import { resolveDiscordSenderIdentity, resolveDiscordWebhookId } from "./sender-identity.js";
|
||||
import {
|
||||
DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS,
|
||||
DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS,
|
||||
} from "./timeouts.js";
|
||||
|
||||
export type {
|
||||
DiscordMessagePreflightContext,
|
||||
@@ -768,6 +774,38 @@ export async function preflightDiscordMessage(
|
||||
nowMs: resolveTimestampMs(message.timestamp),
|
||||
}
|
||||
: undefined;
|
||||
if (botLoopProtection) {
|
||||
const botLoopResult = recordChannelBotPairLoopAndCheckSuppression(botLoopProtection);
|
||||
if (botLoopResult.suppressed) {
|
||||
logVerbose(
|
||||
`discord: bot-to-bot loop detected before media download, suppressing for ${Math.max(0, Math.ceil((botLoopResult.cooldownUntilMs - Date.now()) / 1000))}s`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Discord CDN attachment URLs expire; download now (receipt time) instead
|
||||
// of after the run queue, which may delay processing past the URL TTL.
|
||||
const mediaResolveOptions = {
|
||||
fetchImpl: params.discordRestFetch,
|
||||
ssrfPolicy: params.cfg.browser?.ssrfPolicy,
|
||||
readIdleTimeoutMs: DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS,
|
||||
totalTimeoutMs: DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS,
|
||||
abortSignal: params.abortSignal,
|
||||
};
|
||||
const preparedMedia = await resolveMediaList(message, params.mediaMaxBytes, mediaResolveOptions);
|
||||
if (isPreflightAborted(params.abortSignal)) {
|
||||
return null;
|
||||
}
|
||||
const forwardedMedia = await resolveForwardedMediaList(
|
||||
message,
|
||||
params.mediaMaxBytes,
|
||||
mediaResolveOptions,
|
||||
);
|
||||
if (isPreflightAborted(params.abortSignal)) {
|
||||
return null;
|
||||
}
|
||||
preparedMedia.push(...forwardedMedia);
|
||||
|
||||
logDebug(
|
||||
`[discord-preflight] success: route=${effectiveRoute.agentId} sessionKey=${effectiveRoute.sessionKey}`,
|
||||
@@ -791,6 +829,7 @@ export async function preflightDiscordMessage(
|
||||
baseText,
|
||||
messageText,
|
||||
...(preflightTranscript !== undefined ? { preflightAudioTranscript: preflightTranscript } : {}),
|
||||
preparedMedia,
|
||||
wasMentioned,
|
||||
route: effectiveRoute,
|
||||
threadBinding,
|
||||
@@ -821,6 +860,5 @@ export async function preflightDiscordMessage(
|
||||
inboundEventKind,
|
||||
canDetectMention,
|
||||
historyEntry,
|
||||
botLoopProtection,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Discord type declarations define plugin contracts.
|
||||
import type { InboundEventKind } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { ChannelBotLoopProtectionFacts } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { SessionBindingRecord } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||
import type { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import type { ChannelType, Client, User } from "../internal/discord.js";
|
||||
import type { DiscordChannelConfigResolved, DiscordGuildEntryResolved } from "./allow-list.js";
|
||||
import type { DiscordChannelInfo } from "./message-utils.js";
|
||||
import type { DiscordChannelInfo, DiscordMediaInfo } from "./message-utils.js";
|
||||
import type { DiscordThreadBindingLookup } from "./reply-delivery.js";
|
||||
import type { DiscordReplyTypingFeedback } from "./reply-typing-feedback.js";
|
||||
import type { DiscordSenderIdentity } from "./sender-identity.js";
|
||||
@@ -60,6 +59,9 @@ export type DiscordMessagePreflightContext = DiscordMessagePreflightSharedFields
|
||||
baseText: string;
|
||||
messageText: string;
|
||||
preflightAudioTranscript?: string;
|
||||
// Keep one required receipt-time snapshot: queued processing must never
|
||||
// fall back to Discord's expiring attachment URLs.
|
||||
preparedMedia: DiscordMediaInfo[];
|
||||
wasMentioned: boolean;
|
||||
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
@@ -100,7 +102,6 @@ export type DiscordMessagePreflightContext = DiscordMessagePreflightSharedFields
|
||||
threadBindings: DiscordThreadBindingLookup;
|
||||
replyTypingFeedback?: DiscordReplyTypingFeedback;
|
||||
discordRestFetch?: typeof fetch;
|
||||
botLoopProtection?: ChannelBotLoopProtectionFacts;
|
||||
};
|
||||
|
||||
export type DiscordMessagePreflightParams = DiscordMessagePreflightSharedFields & {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
// Discord tests cover message handler.process plugin behavior.
|
||||
import { DEFAULT_EMOJIS, DEFAULT_TIMING } from "openclaw/plugin-sdk/channel-feedback";
|
||||
import {
|
||||
recordChannelBotPairLoopAndCheckSuppression,
|
||||
type ChannelBotLoopProtectionFacts,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
||||
import { setReplyPayloadMetadata } from "openclaw/plugin-sdk/reply-payload-testing";
|
||||
import * as runtimeEnvModule from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -754,55 +750,6 @@ function expectFreshFinalText(text: string) {
|
||||
}
|
||||
|
||||
describe("processDiscordMessage ack reactions", () => {
|
||||
it("drops bot-loop-suppressed messages before Discord side effects", async () => {
|
||||
const botLoopProtection: ChannelBotLoopProtectionFacts = {
|
||||
scopeId: "discord-process-side-effect-test",
|
||||
conversationId: "c-loop-side-effects",
|
||||
senderId: "bot-a",
|
||||
receiverId: "bot-b",
|
||||
config: {
|
||||
maxEventsPerWindow: 1,
|
||||
windowSeconds: 60,
|
||||
cooldownSeconds: 60,
|
||||
},
|
||||
defaultEnabled: true,
|
||||
nowMs: 10_000,
|
||||
};
|
||||
expect(recordChannelBotPairLoopAndCheckSuppression(botLoopProtection)).toEqual({
|
||||
suppressed: false,
|
||||
});
|
||||
const observer = { onReplyPlanResolved: vi.fn() };
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
messageChannelId: botLoopProtection.conversationId,
|
||||
message: {
|
||||
id: "m-loop-side-effects",
|
||||
channelId: botLoopProtection.conversationId,
|
||||
timestamp: new Date().toISOString(),
|
||||
attachments: [
|
||||
{
|
||||
id: "att-loop",
|
||||
url: "https://cdn.discordapp.test/loop.png",
|
||||
contentType: "image/png",
|
||||
filename: "loop.png",
|
||||
size: 16,
|
||||
},
|
||||
],
|
||||
},
|
||||
botLoopProtection: {
|
||||
...botLoopProtection,
|
||||
nowMs: 10_001,
|
||||
},
|
||||
});
|
||||
|
||||
await processDiscordMessage(ctx, observer);
|
||||
|
||||
expect(observer.onReplyPlanResolved).not.toHaveBeenCalled();
|
||||
expect(createDiscordRestClientSpy).not.toHaveBeenCalled();
|
||||
expect(sendMocks.reactMessageDiscord).not.toHaveBeenCalled();
|
||||
expect(recordInboundSession).not.toHaveBeenCalled();
|
||||
expect(dispatchInboundMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips ack reactions for group-mentions when mentions are not required", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
shouldRequireMention: false,
|
||||
@@ -1323,12 +1270,6 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
|
||||
describe("processDiscordMessage session routing", () => {
|
||||
it("carries preflight audio transcript into dispatch context and marks media transcribed", async () => {
|
||||
const fetchImpl = vi.fn(
|
||||
async () =>
|
||||
new Response(new Uint8Array([1, 2, 3, 4]), {
|
||||
headers: { "content-type": "audio/ogg" },
|
||||
}),
|
||||
);
|
||||
const ctx = await createBaseContext({
|
||||
message: {
|
||||
id: "m-audio-preflight",
|
||||
@@ -1347,8 +1288,13 @@ describe("processDiscordMessage session routing", () => {
|
||||
baseText: "<media:audio>",
|
||||
messageText: "<media:audio>",
|
||||
preflightAudioTranscript: "hello from discord voice",
|
||||
discordRestFetch: fetchImpl,
|
||||
mediaMaxBytes: 1024 * 1024,
|
||||
preparedMedia: [
|
||||
{
|
||||
path: "/tmp/openclaw-discord-test/voice.ogg",
|
||||
contentType: "audio/ogg",
|
||||
placeholder: "<media:audio>",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
@@ -1361,6 +1307,50 @@ describe("processDiscordMessage session routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses prepared media instead of re-downloading after the run queue", async () => {
|
||||
// Regression for #96165: Discord CDN attachment URLs expire, so process
|
||||
// must not re-fetch attachments preflight already downloaded at receipt
|
||||
// time. A throwing fetchImpl here proves no re-fetch happens.
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
throw new Error("attachment should not be re-fetched after preflight downloaded it");
|
||||
});
|
||||
const ctx = await createBaseContext({
|
||||
message: {
|
||||
id: "m-preflight-media",
|
||||
channelId: "c1",
|
||||
content: "look",
|
||||
timestamp: new Date().toISOString(),
|
||||
attachments: [
|
||||
{
|
||||
id: "att-preflight-media",
|
||||
url: "https://cdn.discordapp.com/attachments/1/photo.png?ex=expired",
|
||||
content_type: "image/png",
|
||||
filename: "photo.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
baseText: "look",
|
||||
messageText: "look",
|
||||
preparedMedia: [
|
||||
{
|
||||
path: "/tmp/openclaw-discord-test/photo.png",
|
||||
contentType: "image/png",
|
||||
placeholder: "<media:image>",
|
||||
},
|
||||
],
|
||||
discordRestFetch: fetchImpl,
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
|
||||
MediaPath: "/tmp/openclaw-discord-test/photo.png",
|
||||
MediaType: "image/png",
|
||||
MediaPaths: ["/tmp/openclaw-discord-test/photo.png"],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not attach referenced reply media when reply context is hidden", async () => {
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
throw new Error("hidden reply media should not be fetched");
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import {
|
||||
dispatchChannelInboundReply,
|
||||
hasFinalInboundReplyDispatch,
|
||||
recordChannelBotPairLoopAndCheckSuppression,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import {
|
||||
createChannelMessageReplyPipeline,
|
||||
@@ -59,14 +58,9 @@ import {
|
||||
import { buildDiscordMessageProcessContext } from "./message-handler.context.js";
|
||||
import { createDiscordDraftPreviewController } from "./message-handler.draft-preview.js";
|
||||
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
|
||||
import { resolveForwardedMediaList, resolveMediaList } from "./message-utils.js";
|
||||
import { deliverDiscordReply } from "./reply-delivery.js";
|
||||
import { sanitizeDiscordFrontChannelReplyPayloads } from "./reply-safety.js";
|
||||
import { createDiscordReplyTypingFeedback } from "./reply-typing-feedback.js";
|
||||
import {
|
||||
DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS,
|
||||
DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS,
|
||||
} from "./timeouts.js";
|
||||
|
||||
const loadReplyRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/reply-runtime"));
|
||||
const TARGETED_ONLY_ALLOWED_MENTIONS = {
|
||||
@@ -162,7 +156,6 @@ async function processDiscordMessageInner(
|
||||
runtime,
|
||||
guildHistories,
|
||||
historyLimit,
|
||||
mediaMaxBytes,
|
||||
textLimit,
|
||||
replyToMode,
|
||||
ackReactionScope,
|
||||
@@ -179,45 +172,13 @@ async function processDiscordMessageInner(
|
||||
channelConfig,
|
||||
threadBindings,
|
||||
route,
|
||||
discordRestFetch,
|
||||
abortSignal,
|
||||
botLoopProtection,
|
||||
replyTypingFeedback,
|
||||
preparedMedia: mediaList,
|
||||
} = ctx;
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
if (botLoopProtection) {
|
||||
const botLoopResult = recordChannelBotPairLoopAndCheckSuppression(botLoopProtection);
|
||||
if (botLoopResult.suppressed) {
|
||||
logVerbose(
|
||||
`discord: bot-to-bot loop detected before dispatch setup, suppressing for ${Math.max(0, Math.ceil((botLoopResult.cooldownUntilMs - Date.now()) / 1000))}s`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const ssrfPolicy = cfg.browser?.ssrfPolicy;
|
||||
const mediaResolveOptions = {
|
||||
fetchImpl: discordRestFetch,
|
||||
ssrfPolicy,
|
||||
readIdleTimeoutMs: DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS,
|
||||
totalTimeoutMs: DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS,
|
||||
abortSignal,
|
||||
};
|
||||
const mediaList = await resolveMediaList(message, mediaMaxBytes, mediaResolveOptions);
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
const forwardedMediaList = await resolveForwardedMediaList(
|
||||
message,
|
||||
mediaMaxBytes,
|
||||
mediaResolveOptions,
|
||||
);
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
mediaList.push(...forwardedMediaList);
|
||||
const text = messageText;
|
||||
if (!text) {
|
||||
logVerbose("discord: drop message " + message.id + " (empty content)");
|
||||
|
||||
@@ -47,6 +47,7 @@ export async function createBaseDiscordMessageContext(
|
||||
commandAuthorized: true,
|
||||
baseText: "hi",
|
||||
messageText: "hi",
|
||||
preparedMedia: [],
|
||||
wasMentioned: false,
|
||||
shouldRequireMention: true,
|
||||
canDetectMention: true,
|
||||
|
||||
Reference in New Issue
Block a user