Implemented image support for whatsapp

This commit is contained in:
fat-operator
2026-03-06 23:36:54 +00:00
parent 0409d72579
commit fdd161d7b2
3 changed files with 102 additions and 20 deletions

View File

@@ -12,6 +12,13 @@ interface SendCommand {
text: string; text: string;
} }
interface SendImageCommand {
type: 'send_image';
to: string;
imagePath: string;
caption?: string;
}
interface BridgeMessage { interface BridgeMessage {
type: 'message' | 'status' | 'qr' | 'error'; type: 'message' | 'status' | 'qr' | 'error';
[key: string]: unknown; [key: string]: unknown;
@@ -72,7 +79,7 @@ export class BridgeServer {
ws.on('message', async (data) => { ws.on('message', async (data) => {
try { try {
const cmd = JSON.parse(data.toString()) as SendCommand; const cmd = JSON.parse(data.toString()) as SendCommand | SendImageCommand;
await this.handleCommand(cmd); await this.handleCommand(cmd);
ws.send(JSON.stringify({ type: 'sent', to: cmd.to })); ws.send(JSON.stringify({ type: 'sent', to: cmd.to }));
} catch (error) { } catch (error) {
@@ -92,9 +99,11 @@ export class BridgeServer {
}); });
} }
private async handleCommand(cmd: SendCommand): Promise<void> { private async handleCommand(cmd: SendCommand | SendImageCommand): Promise<void> {
if (cmd.type === 'send' && this.wa) { if (cmd.type === 'send' && this.wa) {
await this.wa.sendMessage(cmd.to, cmd.text); await this.wa.sendMessage(cmd.to, cmd.text);
} else if (cmd.type === 'send_image' && this.wa) {
await this.wa.sendImage(cmd.to, cmd.imagePath, cmd.caption);
} }
} }

View File

@@ -9,11 +9,17 @@ import makeWASocket, {
useMultiFileAuthState, useMultiFileAuthState,
fetchLatestBaileysVersion, fetchLatestBaileysVersion,
makeCacheableSignalKeyStore, makeCacheableSignalKeyStore,
downloadMediaMessage,
extractMessageContent as baileysExtractMessageContent,
} from '@whiskeysockets/baileys'; } from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom'; import { Boom } from '@hapi/boom';
import qrcode from 'qrcode-terminal'; import qrcode from 'qrcode-terminal';
import pino from 'pino'; import pino from 'pino';
import { writeFile, mkdir, readFile } from 'fs/promises';
import { join } from 'path';
import { homedir } from 'os';
import { randomBytes } from 'crypto';
const VERSION = '0.1.0'; const VERSION = '0.1.0';
@@ -24,6 +30,7 @@ export interface InboundMessage {
content: string; content: string;
timestamp: number; timestamp: number;
isGroup: boolean; isGroup: boolean;
media?: string[];
} }
export interface WhatsAppClientOptions { export interface WhatsAppClientOptions {
@@ -110,14 +117,21 @@ export class WhatsAppClient {
if (type !== 'notify') return; if (type !== 'notify') return;
for (const msg of messages) { for (const msg of messages) {
// Skip own messages
if (msg.key.fromMe) continue; if (msg.key.fromMe) continue;
// Skip status updates
if (msg.key.remoteJid === 'status@broadcast') continue; if (msg.key.remoteJid === 'status@broadcast') continue;
const content = this.extractMessageContent(msg); const unwrapped = baileysExtractMessageContent(msg.message);
if (!content) continue; if (!unwrapped) continue;
const content = this.getTextContent(unwrapped);
const mediaPaths: string[] = [];
if (unwrapped.imageMessage) {
const path = await this.downloadImage(msg, unwrapped.imageMessage.mimetype ?? undefined);
if (path) mediaPaths.push(path);
}
if (!content && mediaPaths.length === 0) continue;
const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false; const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false;
@@ -125,18 +139,43 @@ export class WhatsAppClient {
id: msg.key.id || '', id: msg.key.id || '',
sender: msg.key.remoteJid || '', sender: msg.key.remoteJid || '',
pn: msg.key.remoteJidAlt || '', pn: msg.key.remoteJidAlt || '',
content, content: content || '',
timestamp: msg.messageTimestamp as number, timestamp: msg.messageTimestamp as number,
isGroup, isGroup,
...(mediaPaths.length > 0 ? { media: mediaPaths } : {}),
}); });
} }
}); });
} }
private extractMessageContent(msg: any): string | null { private async downloadImage(msg: any, mimetype?: string): Promise<string | null> {
const message = msg.message; try {
if (!message) return null; const mediaDir = join(homedir(), '.nanobot', 'media');
await mkdir(mediaDir, { recursive: true });
const buffer = await downloadMediaMessage(msg, 'buffer', {}) as Buffer;
const mime = mimetype || 'image/jpeg';
const extMap: Record<string, string> = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'image/webp': '.webp',
};
const ext = extMap[mime] || '.jpg';
const filename = `wa_${Date.now()}_${randomBytes(4).toString('hex')}${ext}`;
const filepath = join(mediaDir, filename);
await writeFile(filepath, buffer);
return filepath;
} catch (err) {
console.error('Failed to download image:', err);
return null;
}
}
private getTextContent(message: any): string | null {
// Text message // Text message
if (message.conversation) { if (message.conversation) {
return message.conversation; return message.conversation;
@@ -147,9 +186,9 @@ export class WhatsAppClient {
return message.extendedTextMessage.text; return message.extendedTextMessage.text;
} }
// Image with caption // Image with optional caption
if (message.imageMessage?.caption) { if (message.imageMessage) {
return `[Image] ${message.imageMessage.caption}`; return message.imageMessage.caption || '';
} }
// Video with caption // Video with caption
@@ -178,6 +217,18 @@ export class WhatsAppClient {
await this.sock.sendMessage(to, { text }); await this.sock.sendMessage(to, { text });
} }
async sendImage(to: string, imagePath: string, caption?: string): Promise<void> {
if (!this.sock) {
throw new Error('Not connected');
}
const buffer = await readFile(imagePath);
await this.sock.sendMessage(to, {
image: buffer,
caption: caption || undefined,
});
}
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
if (this.sock) { if (this.sock) {
this.sock.end(undefined); this.sock.end(undefined);

View File

@@ -83,6 +83,20 @@ class WhatsAppChannel(BaseChannel):
return return
try: try:
# Send media files first
for media_path in (msg.media or []):
try:
payload = {
"type": "send_image",
"to": msg.chat_id,
"imagePath": media_path,
}
await self._ws.send(json.dumps(payload, ensure_ascii=False))
except Exception as e:
logger.error("Error sending WhatsApp media {}: {}", media_path, e)
# Send text message if there's content
if msg.content:
payload = { payload = {
"type": "send", "type": "send",
"to": msg.chat_id, "to": msg.chat_id,
@@ -128,10 +142,18 @@ class WhatsAppChannel(BaseChannel):
logger.info("Voice message received from {}, but direct download from bridge is not yet supported.", sender_id) logger.info("Voice message received from {}, but direct download from bridge is not yet supported.", sender_id)
content = "[Voice Message: Transcription not available for WhatsApp yet]" content = "[Voice Message: Transcription not available for WhatsApp yet]"
# Extract media paths (images downloaded by the bridge)
media_paths = data.get("media") or []
# For image messages without caption, provide descriptive content
if not content and media_paths:
content = "[image]"
await self._handle_message( await self._handle_message(
sender_id=sender_id, sender_id=sender_id,
chat_id=sender, # Use full LID for replies chat_id=sender, # Use full LID for replies
content=content, content=content,
media=media_paths,
metadata={ metadata={
"message_id": message_id, "message_id": message_id,
"timestamp": data.get("timestamp"), "timestamp": data.get("timestamp"),