fix: allow media-only sends
This commit is contained in:
@@ -98,6 +98,18 @@ export const buildTelegramMessageContext = async ({
|
||||
}
|
||||
};
|
||||
|
||||
const sendRecordVoice = async () => {
|
||||
try {
|
||||
await bot.api.sendChatAction(
|
||||
chatId,
|
||||
"record_voice",
|
||||
buildTypingThreadParams(resolvedThreadId),
|
||||
);
|
||||
} catch (err) {
|
||||
logVerbose(`telegram record_voice cue failed for chat ${chatId}: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
// DM access control (secure defaults): "pairing" (default) / "allowlist" / "open" / "disabled"
|
||||
if (!isGroup) {
|
||||
if (dmPolicy === "disabled") return null;
|
||||
@@ -408,6 +420,7 @@ export const buildTelegramMessageContext = async ({
|
||||
route,
|
||||
skillFilter,
|
||||
sendTyping,
|
||||
sendRecordVoice,
|
||||
ackReactionPromise,
|
||||
reactionApi,
|
||||
removeAckAfterReply,
|
||||
|
||||
@@ -37,6 +37,7 @@ export const dispatchTelegramMessage = async ({
|
||||
route,
|
||||
skillFilter,
|
||||
sendTyping,
|
||||
sendRecordVoice,
|
||||
ackReactionPromise,
|
||||
reactionApi,
|
||||
removeAckAfterReply,
|
||||
@@ -144,6 +145,7 @@ export const dispatchTelegramMessage = async ({
|
||||
replyToMode,
|
||||
textLimit,
|
||||
messageThreadId: resolvedThreadId,
|
||||
onVoiceRecording: sendRecordVoice,
|
||||
});
|
||||
didSendReply = true;
|
||||
},
|
||||
|
||||
77
src/telegram/bot/delivery.test.ts
Normal file
77
src/telegram/bot/delivery.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { Bot } from "grammy";
|
||||
|
||||
import { deliverReplies } from "./delivery.js";
|
||||
|
||||
const loadWebMedia = vi.fn();
|
||||
|
||||
vi.mock("../../web/media.js", () => ({
|
||||
loadWebMedia: (...args: unknown[]) => loadWebMedia(...args),
|
||||
}));
|
||||
|
||||
vi.mock("grammy", () => ({
|
||||
InputFile: class {
|
||||
constructor(
|
||||
public buffer: Buffer,
|
||||
public fileName?: string,
|
||||
) {}
|
||||
},
|
||||
}));
|
||||
|
||||
describe("deliverReplies", () => {
|
||||
beforeEach(() => {
|
||||
loadWebMedia.mockReset();
|
||||
});
|
||||
|
||||
it("skips audioAsVoice-only payloads without logging an error", async () => {
|
||||
const runtime = { error: vi.fn() };
|
||||
const bot = { api: {} } as unknown as Bot;
|
||||
|
||||
await deliverReplies({
|
||||
replies: [{ audioAsVoice: true }],
|
||||
chatId: "123",
|
||||
token: "tok",
|
||||
runtime,
|
||||
bot,
|
||||
replyToMode: "off",
|
||||
textLimit: 4000,
|
||||
});
|
||||
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invokes onVoiceRecording before sending a voice note", async () => {
|
||||
const events: string[] = [];
|
||||
const runtime = { error: vi.fn() };
|
||||
const sendVoice = vi.fn(async () => {
|
||||
events.push("sendVoice");
|
||||
return { message_id: 1, chat: { id: "123" } };
|
||||
});
|
||||
const bot = { api: { sendVoice } } as unknown as Bot;
|
||||
const onVoiceRecording = vi.fn(async () => {
|
||||
events.push("recordVoice");
|
||||
});
|
||||
|
||||
loadWebMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("voice"),
|
||||
contentType: "audio/ogg",
|
||||
fileName: "note.ogg",
|
||||
});
|
||||
|
||||
await deliverReplies({
|
||||
replies: [{ mediaUrl: "https://example.com/note.ogg", audioAsVoice: true }],
|
||||
chatId: "123",
|
||||
token: "tok",
|
||||
runtime,
|
||||
bot,
|
||||
replyToMode: "off",
|
||||
textLimit: 4000,
|
||||
onVoiceRecording,
|
||||
});
|
||||
|
||||
expect(onVoiceRecording).toHaveBeenCalledTimes(1);
|
||||
expect(sendVoice).toHaveBeenCalledTimes(1);
|
||||
expect(events).toEqual(["recordVoice", "sendVoice"]);
|
||||
});
|
||||
});
|
||||
@@ -25,12 +25,19 @@ export async function deliverReplies(params: {
|
||||
replyToMode: ReplyToMode;
|
||||
textLimit: number;
|
||||
messageThreadId?: number;
|
||||
/** Callback invoked before sending a voice message to switch typing indicator. */
|
||||
onVoiceRecording?: () => Promise<void> | void;
|
||||
}) {
|
||||
const { replies, chatId, runtime, bot, replyToMode, textLimit, messageThreadId } = params;
|
||||
const threadParams = buildTelegramThreadParams(messageThreadId);
|
||||
let hasReplied = false;
|
||||
for (const reply of replies) {
|
||||
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) {
|
||||
const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
|
||||
if (!reply?.text && !hasMedia) {
|
||||
if (reply?.audioAsVoice) {
|
||||
logVerbose("telegram reply has audioAsVoice without media/text; skipping");
|
||||
continue;
|
||||
}
|
||||
runtime.error?.(danger("reply missing text/media"));
|
||||
continue;
|
||||
}
|
||||
@@ -99,6 +106,8 @@ export async function deliverReplies(params: {
|
||||
});
|
||||
if (useVoice) {
|
||||
// Voice message - displays as round playable bubble (opt-in via [[audio_as_voice]])
|
||||
// Switch typing indicator to record_voice before sending.
|
||||
await params.onVoiceRecording?.();
|
||||
await bot.api.sendVoice(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user