Discord: avoid reply spam on chunked sends

This commit is contained in:
Shadow
2026-02-20 16:37:06 -06:00
parent df002ef840
commit 64c29c3755
5 changed files with 49 additions and 3 deletions

View File

@@ -887,6 +887,7 @@ async function dispatchDiscordComponentEvent(params: {
rest: interaction.client.rest,
runtime,
replyToId,
replyToMode,
textLimit,
maxLinesPerMessage: ctx.discordConfig?.maxLinesPerMessage,
tableMode,

View File

@@ -628,6 +628,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
rest: client.rest,
runtime,
replyToId,
replyToMode,
textLimit,
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
tableMode,

View File

@@ -84,4 +84,24 @@ describe("deliverDiscordReply", () => {
expect(sendVoiceMessageDiscordMock).toHaveBeenCalledTimes(1);
expect(sendMessageDiscordMock).not.toHaveBeenCalled();
});
it("uses replyToId only for the first chunk when replyToMode is first", async () => {
await deliverDiscordReply({
replies: [
{
text: "1234567890",
},
],
target: "channel:789",
token: "token",
runtime,
textLimit: 5,
replyToId: "reply-1",
replyToMode: "first",
});
expect(sendMessageDiscordMock).toHaveBeenCalledTimes(2);
expect(sendMessageDiscordMock.mock.calls[0]?.[2]?.replyTo).toBe("reply-1");
expect(sendMessageDiscordMock.mock.calls[1]?.[2]?.replyTo).toBeUndefined();
});
});

View File

@@ -1,7 +1,7 @@
import type { RequestClient } from "@buape/carbon";
import type { ChunkMode } from "../../auto-reply/chunk.js";
import type { ReplyPayload } from "../../auto-reply/types.js";
import type { MarkdownTableMode } from "../../config/types.base.js";
import type { MarkdownTableMode, ReplyToMode } from "../../config/types.base.js";
import { convertMarkdownTables } from "../../markdown/tables.js";
import type { RuntimeEnv } from "../../runtime.js";
import { chunkDiscordTextWithMode } from "../chunk.js";
@@ -17,10 +17,29 @@ export async function deliverDiscordReply(params: {
textLimit: number;
maxLinesPerMessage?: number;
replyToId?: string;
replyToMode?: ReplyToMode;
tableMode?: MarkdownTableMode;
chunkMode?: ChunkMode;
}) {
const chunkLimit = Math.min(params.textLimit, 2000);
const replyTo = params.replyToId?.trim() || undefined;
const replyToMode = params.replyToMode ?? "all";
// replyToMode=first should only apply to the first physical send.
const replyOnce = replyToMode === "first";
let replyUsed = false;
const resolveReplyTo = () => {
if (!replyTo) {
return undefined;
}
if (!replyOnce) {
return replyTo;
}
if (replyUsed) {
return undefined;
}
replyUsed = true;
return replyTo;
};
for (const payload of params.replies) {
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
const rawText = payload.text ?? "";
@@ -29,8 +48,6 @@ export async function deliverDiscordReply(params: {
if (!text && mediaList.length === 0) {
continue;
}
const replyTo = params.replyToId?.trim() || undefined;
if (mediaList.length === 0) {
const mode = params.chunkMode ?? "length";
const chunks = chunkDiscordTextWithMode(text, {
@@ -46,6 +63,7 @@ export async function deliverDiscordReply(params: {
if (!trimmed) {
continue;
}
const replyTo = resolveReplyTo();
await sendMessageDiscord(params.target, trimmed, {
token: params.token,
rest: params.rest,
@@ -63,6 +81,7 @@ export async function deliverDiscordReply(params: {
// Voice message path: audioAsVoice flag routes through sendVoiceMessageDiscord
if (payload.audioAsVoice) {
const replyTo = resolveReplyTo();
await sendVoiceMessageDiscord(params.target, firstMedia, {
token: params.token,
rest: params.rest,
@@ -71,6 +90,7 @@ export async function deliverDiscordReply(params: {
});
// Voice messages cannot include text; send remaining text separately if present
if (text.trim()) {
const replyTo = resolveReplyTo();
await sendMessageDiscord(params.target, text, {
token: params.token,
rest: params.rest,
@@ -80,6 +100,7 @@ export async function deliverDiscordReply(params: {
}
// Additional media items are sent as regular attachments (voice is single-file only)
for (const extra of mediaList.slice(1)) {
const replyTo = resolveReplyTo();
await sendMessageDiscord(params.target, "", {
token: params.token,
rest: params.rest,
@@ -91,6 +112,7 @@ export async function deliverDiscordReply(params: {
continue;
}
const replyTo = resolveReplyTo();
await sendMessageDiscord(params.target, text, {
token: params.token,
rest: params.rest,
@@ -99,6 +121,7 @@ export async function deliverDiscordReply(params: {
replyTo,
});
for (const extra of mediaList.slice(1)) {
const replyTo = resolveReplyTo();
await sendMessageDiscord(params.target, "", {
token: params.token,
rest: params.rest,