fix(telegram): keep rejected voice attachments out of reply history (#130399)

This commit is contained in:
Peter Steinberger
2026-08-26 14:12:51 -07:00
committed by GitHub
parent 771336481e
commit 549360bc42
2 changed files with 57 additions and 17 deletions
+18 -14
View File
@@ -296,12 +296,15 @@ async function deliverMediaReply(params: {
recordMessageId: (messageId: number) => void;
textMode?: "html";
onPlatformSendDispatch?: () => Promise<void>;
}): Promise<{ firstDeliveredMessageId?: number; visibleFallbackText?: string }> {
}): Promise<{
firstDeliveredMessageId?: number;
visibleFallbackText?: string;
mediaUrls: string[];
}> {
let firstDeliveredMessageId: number | undefined;
let visibleFallbackText: string | undefined;
let firstDeliveredCaption: string | undefined;
let first = true;
let pendingFollowUpText: string | undefined;
const mediaUrls: string[] = [];
const recordPromptContextMessage = async (message: Message, text?: string) => {
const promptContextMessage = {
messageId: message.message_id,
@@ -312,6 +315,7 @@ async function deliverMediaReply(params: {
};
const deliverAcceptedMedia = async (options: {
sender: TelegramOutboundMediaSender<Message>;
mediaUrl: string;
requestParams: Record<string, unknown>;
plainCaption?: string;
shouldLog?: (err: unknown) => boolean;
@@ -332,6 +336,7 @@ async function deliverMediaReply(params: {
}),
});
const message = delivery.result;
mediaUrls.push(options.mediaUrl);
// Acceptance precedes topic checks and bookkeeping; losing this id lets
// later failures trigger a duplicate reply for an already visible message.
params.acceptedMessageIds.push(String(message.message_id));
@@ -358,8 +363,8 @@ async function deliverMediaReply(params: {
deliveredCount: 0,
...(params.progress.promptContext ? { promptContext: params.progress.promptContext } : {}),
});
for (const mediaUrl of params.mediaList) {
const isFirstMedia = first;
for (const [index, mediaUrl] of params.mediaList.entries()) {
const isFirstMedia = index === 0;
const media = await params.mediaLoader(
mediaUrl,
buildOutboundMediaLoadOptions({
@@ -382,10 +387,6 @@ async function deliverMediaReply(params: {
asVoice: params.reply.audioAsVoice,
});
const { htmlCaption, plainCaption, followUpText } = mediaPlan;
if (followUpText) {
pendingFollowUpText = followUpText;
}
first = false;
const replyToMessageId = resolveReplyToForSend({
replyToId: params.replyToId,
replyToMode: params.replyToMode,
@@ -417,6 +418,7 @@ async function deliverMediaReply(params: {
const hasCaption = typeof requestParams.caption === "string";
await deliverAcceptedMedia({
sender: mediaSender,
mediaUrl,
requestParams,
plainCaption: hasCaption ? plainCaption : undefined,
shouldLog,
@@ -521,6 +523,7 @@ async function deliverMediaReply(params: {
send: (sender) =>
deliverAcceptedMedia({
sender,
mediaUrl,
requestParams: mediaParams,
plainCaption,
...(sender.label === "photo"
@@ -530,7 +533,7 @@ async function deliverMediaReply(params: {
});
}
markReplyApplied(params.progress, replyToMessageId);
if (pendingFollowUpText && isFirstMedia) {
if (followUpText) {
try {
const followUpMessageId = await deliverTextReply({
bot: params.bot,
@@ -538,7 +541,7 @@ async function deliverMediaReply(params: {
runtime: params.runtime,
thread: params.thread,
chunkText: params.chunkText,
text: pendingFollowUpText,
text: followUpText,
replyMarkup: params.replyMarkup,
richMessages: params.richMessages,
tableMode: params.tableMode,
@@ -567,10 +570,9 @@ async function deliverMediaReply(params: {
});
}
}
pendingFollowUpText = undefined;
}
}
return { firstDeliveredMessageId, visibleFallbackText };
return { firstDeliveredMessageId, visibleFallbackText, mediaUrls };
}
async function maybePinFirstDeliveredMessage(params: {
@@ -881,6 +883,7 @@ export async function deliverReplies(params: {
}),
);
let firstDeliveredMessageId: number | undefined;
let deliveredMediaUrls: string[] = [];
if (reactionEmoji && typeof targetId === "number") {
await params.onPlatformSendDispatch?.();
const reactionResult = await reactMessageTelegram(params.chatId, targetId, reactionEmoji, {
@@ -953,6 +956,7 @@ export async function deliverReplies(params: {
...(params.textMode ? { textMode: params.textMode } : {}),
});
firstDeliveredMessageId = mediaDelivery.firstDeliveredMessageId;
deliveredMediaUrls = mediaDelivery.mediaUrls;
if (mediaDelivery.visibleFallbackText !== undefined) {
contentForSentHook = mediaDelivery.visibleFallbackText;
}
@@ -966,7 +970,7 @@ export async function deliverReplies(params: {
});
if (progress.deliveredCount > deliveredCountBeforeReply && transcriptMirror) {
deliveredContents.push({ text: contentForSentHook, mediaUrls: mediaList });
deliveredContents.push({ text: contentForSentHook, mediaUrls: deliveredMediaUrls });
}
emitMessageSentHooks({
+39 -3
View File
@@ -2891,11 +2891,47 @@ describe("deliverReplies", () => {
expect(sendVoice).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledTimes(1);
expect(firstMockCallArg(sendMessage, 1)).toContain("Hidden voice fallback");
expect(transcriptMirror).toHaveBeenCalledWith(
expect.objectContaining({ text: "Hidden voice fallback" }),
);
expect(transcriptMirror).toHaveBeenCalledWith({
text: "Hidden voice fallback",
mediaUrls: undefined,
});
});
it.each(["before", "after"])(
"mirrors accepted media %s a rejected voice without the rejected attachment",
async (position) => {
const voiceUrl = "https://example.com/note.ogg";
const photoUrl = "https://example.com/photo.jpg";
const mediaUrls = position === "before" ? [photoUrl, voiceUrl] : [voiceUrl, photoUrl];
const sendVoice = vi.fn().mockRejectedValue(createVoiceMessagesForbiddenError());
const sendPhoto = vi.fn().mockResolvedValue({ message_id: 7, chat: { id: "123" } });
const sendMessage = vi.fn().mockResolvedValue({ message_id: 8, chat: { id: "123" } });
const transcriptMirror = vi.fn();
for (const url of mediaUrls) {
mockMediaLoad(
url === voiceUrl ? "note.ogg" : "photo.jpg",
url === voiceUrl ? "audio/ogg" : "image/jpeg",
"media",
);
}
await deliverWith({
replies: [{ mediaUrls, audioAsVoice: true, spokenText: "Voice fallback" }],
runtime: createRuntime(),
bot: createBot({ sendVoice, sendPhoto, sendMessage }),
transcriptMirror,
});
expect(sendVoice).toHaveBeenCalledOnce();
expect(sendPhoto).toHaveBeenCalledOnce();
expect(sendMessage).toHaveBeenCalledOnce();
expect(transcriptMirror).toHaveBeenCalledWith({
text: "Voice fallback",
mediaUrls: [photoUrl],
});
},
);
it("runs message_sending hooks over spokenText voice fallback content", async () => {
messageHookRunner.hasHooks.mockImplementation((name: string) => name === "message_sending");
messageHookRunner.runMessageSending.mockResolvedValue({ content: "Rewritten voice fallback" });