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:
@@ -7,11 +7,12 @@
|
||||
* Messages are chunked at 4096 chars (same as Telegram).
|
||||
*/
|
||||
|
||||
import { Client, LocalAuth } from 'whatsapp-web.js';
|
||||
import { Client, LocalAuth, MessageMedia } from 'whatsapp-web.js';
|
||||
import type {
|
||||
Attachment,
|
||||
InboundMessage,
|
||||
OutboundMessage,
|
||||
OutboundAttachment,
|
||||
ChannelAdapter,
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
@@ -153,6 +154,38 @@ export class WhatsAppAdapter implements ChannelAdapter {
|
||||
await this.client.sendMessage(peerId, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
// Send outbound attachments after text
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
await this.sendAttachment(peerId, attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Send a single outbound attachment via WhatsApp using MessageMedia. */
|
||||
private async sendAttachment(peerId: string, attachment: OutboundAttachment): Promise<void> {
|
||||
if (!this.client) return;
|
||||
|
||||
try {
|
||||
if (attachment.data) {
|
||||
const media = new MessageMedia(
|
||||
attachment.mimeType,
|
||||
attachment.data,
|
||||
attachment.filename,
|
||||
);
|
||||
await this.client.sendMessage(peerId, media);
|
||||
} else if (attachment.url) {
|
||||
// Download from URL and send as MessageMedia
|
||||
const media = await MessageMedia.fromUrl(attachment.url);
|
||||
await this.client.sendMessage(peerId, media);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`WhatsApp: failed to send ${attachment.mimeType} attachment:`,
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal: process an inbound WhatsApp message. */
|
||||
@@ -211,17 +244,24 @@ export class WhatsAppAdapter implements ChannelAdapter {
|
||||
|
||||
const senderName = message._data?.notifyName;
|
||||
|
||||
// Extract image attachments if the message has media
|
||||
// Extract media attachments if the message has media
|
||||
const attachments: Attachment[] = [];
|
||||
if (message.hasMedia) {
|
||||
try {
|
||||
const media = await (message as any).downloadMedia();
|
||||
if (media && typeof media.mimetype === 'string' && media.mimetype.startsWith('image/')) {
|
||||
attachments.push({
|
||||
mimeType: media.mimetype,
|
||||
data: media.data,
|
||||
filename: media.filename,
|
||||
});
|
||||
if (media && typeof media.mimetype === 'string') {
|
||||
const mimeType = media.mimetype;
|
||||
const isAudio = mimeType.startsWith('audio/');
|
||||
const isImage = mimeType.startsWith('image/');
|
||||
const isVoice = message.type === 'ptt';
|
||||
|
||||
if (isAudio || isImage || isVoice) {
|
||||
attachments.push({
|
||||
mimeType: mimeType,
|
||||
data: media.data,
|
||||
filename: media.filename,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
||||
Reference in New Issue
Block a user