feat: add outbound attachment support with media.send tool
Introduces OutboundAttachment type on OutboundMessage, an OutboundAttachmentCollector (push/drain pattern), and a media.send tool that queues files for outbound delivery. Each channel adapter (Telegram, Discord, Slack, WhatsApp) sends attachments after the text reply. Includes 15 tests for collector and tool.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { Bot } from 'grammy';
|
||||
import { Bot, InputFile } from 'grammy';
|
||||
|
||||
import type { HookEngine } from '../../hooks/index.js';
|
||||
import type {
|
||||
Attachment,
|
||||
InboundMessage,
|
||||
OutboundMessage,
|
||||
OutboundAttachment,
|
||||
ChannelAdapter,
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
@@ -263,6 +264,80 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Voice message handler ──
|
||||
|
||||
this.bot.on('message:voice', async (ctx) => {
|
||||
if (!this.messageHandler) return;
|
||||
|
||||
const voice = ctx.message.voice;
|
||||
if (!voice) return;
|
||||
|
||||
await ctx.replyWithChatAction('typing');
|
||||
|
||||
const fileData = await this.downloadFileToBase64(voice.file_id);
|
||||
if (!fileData) {
|
||||
console.error(`Failed to download voice message ${voice.file_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const caption = ctx.message.caption ?? '';
|
||||
const mimeType = voice.mime_type ?? 'audio/ogg';
|
||||
|
||||
this.messageHandler({
|
||||
id: String(ctx.message.message_id),
|
||||
channel: 'telegram',
|
||||
senderId: String(ctx.chat.id),
|
||||
senderName: ctx.from?.first_name,
|
||||
text: caption,
|
||||
attachments: [
|
||||
{
|
||||
mimeType,
|
||||
data: fileData,
|
||||
filename: `voice_${voice.file_unique_id}.ogg`,
|
||||
size: voice.file_size,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
// ── Audio message handler ──
|
||||
|
||||
this.bot.on('message:audio', async (ctx) => {
|
||||
if (!this.messageHandler) return;
|
||||
|
||||
const audio = ctx.message.audio;
|
||||
if (!audio) return;
|
||||
|
||||
await ctx.replyWithChatAction('typing');
|
||||
|
||||
const fileData = await this.downloadFileToBase64(audio.file_id);
|
||||
if (!fileData) {
|
||||
console.error(`Failed to download audio message ${audio.file_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const caption = ctx.message.caption ?? '';
|
||||
const mimeType = audio.mime_type ?? 'audio/mpeg';
|
||||
|
||||
this.messageHandler({
|
||||
id: String(ctx.message.message_id),
|
||||
channel: 'telegram',
|
||||
senderId: String(ctx.chat.id),
|
||||
senderName: ctx.from?.first_name,
|
||||
text: caption,
|
||||
attachments: [
|
||||
{
|
||||
mimeType,
|
||||
data: fileData,
|
||||
filename: `audio_${audio.file_unique_id}.${mimeType.split('/')[1]}`,
|
||||
size: audio.file_size,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
// ── Start long polling ──
|
||||
|
||||
this.bot.start({
|
||||
@@ -304,5 +379,34 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
await this.bot.api.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
|
||||
}
|
||||
}
|
||||
|
||||
// Send outbound attachments after text
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
await this.sendAttachment(chatId, attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Send a single outbound attachment via the Telegram API. */
|
||||
private async sendAttachment(chatId: number, attachment: OutboundAttachment): Promise<void> {
|
||||
if (!this.bot) return;
|
||||
|
||||
try {
|
||||
const file = attachment.data
|
||||
? new InputFile(Buffer.from(attachment.data, 'base64'), attachment.filename)
|
||||
: attachment.url ?? '';
|
||||
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
await this.bot.api.sendPhoto(chatId, file);
|
||||
} else {
|
||||
await this.bot.api.sendDocument(chatId, file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to send ${attachment.mimeType} attachment to ${chatId}:`,
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user