mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
feat(channels): add outbound media types and sendMedia to channel adapter
Add OutboundMedia interface and OutboundMediaType to the channel type system. Implement sendMedia in the Telegram plugin using grammy's InputFile API with HTML caption formatting and plain-text fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,8 @@
|
||||
import { writeFile, mkdir } from "node:fs/promises";
|
||||
import { join, extname } from "node:path";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { Bot, GrammyError } from "grammy";
|
||||
import type { ChannelPlugin, ChannelMessage, ChannelConfigAdapter, ChannelsConfig, DeliveryContext } from "../types.js";
|
||||
import { Bot, GrammyError, InputFile } from "grammy";
|
||||
import type { ChannelPlugin, ChannelMessage, ChannelConfigAdapter, ChannelsConfig, DeliveryContext, OutboundMedia } from "../types.js";
|
||||
import { markdownToTelegramHtml } from "./telegram-format.js";
|
||||
import { MEDIA_CACHE_DIR } from "@multica/utils";
|
||||
|
||||
@@ -321,6 +321,67 @@ export const telegramChannel: ChannelPlugin = {
|
||||
// Best-effort
|
||||
}
|
||||
},
|
||||
|
||||
async sendMedia(ctx: DeliveryContext, media: OutboundMedia): Promise<void> {
|
||||
const bot = bots.get(ctx.accountId);
|
||||
if (!bot) throw new Error(`No Telegram bot for account ${ctx.accountId}`);
|
||||
|
||||
const chatId = Number(ctx.conversationId);
|
||||
const inputFile = new InputFile(media.source);
|
||||
// Telegram caption limit: 1024 chars. Truncate if needed.
|
||||
const caption = media.caption?.slice(0, 1024);
|
||||
const captionHtml = caption ? markdownToTelegramHtml(caption) : undefined;
|
||||
const extra = captionHtml ? { caption: captionHtml, parse_mode: "HTML" as const } : {};
|
||||
|
||||
console.log(`[Telegram] Sending ${media.type} to chatId=${chatId}`);
|
||||
|
||||
try {
|
||||
switch (media.type) {
|
||||
case "photo":
|
||||
await bot.api.sendPhoto(chatId, inputFile, extra);
|
||||
break;
|
||||
case "video":
|
||||
await bot.api.sendVideo(chatId, inputFile, extra);
|
||||
break;
|
||||
case "audio":
|
||||
await bot.api.sendAudio(chatId, inputFile, extra);
|
||||
break;
|
||||
case "voice":
|
||||
await bot.api.sendVoice(chatId, inputFile, extra);
|
||||
break;
|
||||
case "document":
|
||||
default:
|
||||
await bot.api.sendDocument(chatId, inputFile, extra);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// If HTML caption fails, retry without formatting
|
||||
if (isParseError(err) && caption) {
|
||||
console.warn("[Telegram] Media caption HTML parse failed, retrying as plain text");
|
||||
const plainExtra = { caption };
|
||||
switch (media.type) {
|
||||
case "photo":
|
||||
await bot.api.sendPhoto(chatId, inputFile, plainExtra);
|
||||
break;
|
||||
case "video":
|
||||
await bot.api.sendVideo(chatId, inputFile, plainExtra);
|
||||
break;
|
||||
case "audio":
|
||||
await bot.api.sendAudio(chatId, inputFile, plainExtra);
|
||||
break;
|
||||
case "voice":
|
||||
await bot.api.sendVoice(chatId, inputFile, plainExtra);
|
||||
break;
|
||||
case "document":
|
||||
default:
|
||||
await bot.api.sendDocument(chatId, inputFile, plainExtra);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async downloadMedia(fileId: string, accountId: string): Promise<string> {
|
||||
|
||||
@@ -88,6 +88,23 @@ export interface ChannelGatewayAdapter {
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
// ─── Outbound Media ───
|
||||
|
||||
/** Media type for outbound messages */
|
||||
export type OutboundMediaType = "photo" | "document" | "video" | "audio" | "voice";
|
||||
|
||||
/** Media payload for sending files back to the platform */
|
||||
export interface OutboundMedia {
|
||||
/** Media type (determines which API method to use) */
|
||||
type: OutboundMediaType;
|
||||
/** Local file path */
|
||||
source: string;
|
||||
/** Caption text (optional, may be truncated per platform limits) */
|
||||
caption?: string | undefined;
|
||||
/** Filename hint (optional, used for documents) */
|
||||
filename?: string | undefined;
|
||||
}
|
||||
|
||||
// ─── Outbound Adapter ───
|
||||
|
||||
/** Sends messages back to the platform */
|
||||
@@ -96,6 +113,8 @@ export interface ChannelOutboundAdapter {
|
||||
sendText(ctx: DeliveryContext, text: string): Promise<void>;
|
||||
/** Reply to a specific message */
|
||||
replyText(ctx: DeliveryContext, text: string): Promise<void>;
|
||||
/** Send a media file (photo, document, video, audio, voice) to a conversation (optional) */
|
||||
sendMedia?(ctx: DeliveryContext, media: OutboundMedia): Promise<void>;
|
||||
/** Send "typing" indicator (optional, not all platforms support it) */
|
||||
sendTyping?(ctx: DeliveryContext): Promise<void>;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user