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;
}
interface SendImageCommand {
type: 'send_image';
to: string;
imagePath: string;
caption?: string;
}
interface BridgeMessage {
type: 'message' | 'status' | 'qr' | 'error';
[key: string]: unknown;
@@ -72,7 +79,7 @@ export class BridgeServer {
ws.on('message', async (data) => {
try {
const cmd = JSON.parse(data.toString()) as SendCommand;
const cmd = JSON.parse(data.toString()) as SendCommand | SendImageCommand;
await this.handleCommand(cmd);
ws.send(JSON.stringify({ type: 'sent', to: cmd.to }));
} 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) {
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,
fetchLatestBaileysVersion,
makeCacheableSignalKeyStore,
downloadMediaMessage,
extractMessageContent as baileysExtractMessageContent,
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import qrcode from 'qrcode-terminal';
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';
@@ -24,6 +30,7 @@ export interface InboundMessage {
content: string;
timestamp: number;
isGroup: boolean;
media?: string[];
}
export interface WhatsAppClientOptions {
@@ -110,14 +117,21 @@ export class WhatsAppClient {
if (type !== 'notify') return;
for (const msg of messages) {
// Skip own messages
if (msg.key.fromMe) continue;
// Skip status updates
if (msg.key.remoteJid === 'status@broadcast') continue;
const content = this.extractMessageContent(msg);
if (!content) continue;
const unwrapped = baileysExtractMessageContent(msg.message);
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;
@@ -125,18 +139,43 @@ export class WhatsAppClient {
id: msg.key.id || '',
sender: msg.key.remoteJid || '',
pn: msg.key.remoteJidAlt || '',
content,
content: content || '',
timestamp: msg.messageTimestamp as number,
isGroup,
...(mediaPaths.length > 0 ? { media: mediaPaths } : {}),
});
}
});
}
private extractMessageContent(msg: any): string | null {
const message = msg.message;
if (!message) return null;
private async downloadImage(msg: any, mimetype?: string): Promise<string | null> {
try {
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
if (message.conversation) {
return message.conversation;
@@ -147,9 +186,9 @@ export class WhatsAppClient {
return message.extendedTextMessage.text;
}
// Image with caption
if (message.imageMessage?.caption) {
return `[Image] ${message.imageMessage.caption}`;
// Image with optional caption
if (message.imageMessage) {
return message.imageMessage.caption || '';
}
// Video with caption
@@ -178,6 +217,18 @@ export class WhatsAppClient {
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> {
if (this.sock) {
this.sock.end(undefined);

View File

@@ -83,6 +83,20 @@ class WhatsAppChannel(BaseChannel):
return
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 = {
"type": "send",
"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)
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(
sender_id=sender_id,
chat_id=sender, # Use full LID for replies
content=content,
media=media_paths,
metadata={
"message_id": message_id,
"timestamp": data.get("timestamp"),