openclaw/src/web/inbound/send-api.ts
ANDRES FELIPE CARDONA SANTA 876641e11b 2214 feat: support native WhatsApp location messages
Add location parameters to the message tool and wire them through WhatsApp web outbound delivery to send native location pins.
2026-01-26 12:10:08 -05:00

135 lines
4.1 KiB
TypeScript

import type { AnyMessageContent, WAPresence } from '@whiskeysockets/baileys';
import { recordChannelActivity } from '../../infra/channel-activity.js';
import { toWhatsappJid } from '../../utils.js';
import type { ActiveWebSendOptions } from '../active-listener.js';
export function createWebSendApi(params: {
sock: {
sendMessage: (jid: string, content: AnyMessageContent) => Promise<unknown>;
sendPresenceUpdate: (
presence: WAPresence,
jid?: string
) => Promise<unknown>;
};
defaultAccountId: string;
}) {
return {
sendMessage: async (
to: string,
text: string,
mediaBuffer?: Buffer,
mediaType?: string,
sendOptions?: ActiveWebSendOptions,
location?: {
latitude: number;
longitude: number;
name?: string;
address?: string;
accuracy?: number;
}
): Promise<{ messageId: string }> => {
const jid = toWhatsappJid(to);
let payload: AnyMessageContent;
// Location messages take precedence
if (location) {
payload = {
location: {
degreesLatitude: location.latitude,
degreesLongitude: location.longitude,
name: location.name,
address: location.address,
accuracyInMeters: location.accuracy,
},
};
} else if (mediaBuffer && mediaType) {
if (mediaType.startsWith('image/')) {
payload = {
image: mediaBuffer,
caption: text || undefined,
mimetype: mediaType,
};
} else if (mediaType.startsWith('audio/')) {
payload = { audio: mediaBuffer, ptt: true, mimetype: mediaType };
} else if (mediaType.startsWith('video/')) {
const gifPlayback = sendOptions?.gifPlayback;
payload = {
video: mediaBuffer,
caption: text || undefined,
mimetype: mediaType,
...(gifPlayback ? { gifPlayback: true } : {}),
};
} else {
payload = {
document: mediaBuffer,
fileName: 'file',
caption: text || undefined,
mimetype: mediaType,
};
}
} else {
payload = { text };
}
const result = await params.sock.sendMessage(jid, payload);
const accountId = sendOptions?.accountId ?? params.defaultAccountId;
recordChannelActivity({
channel: 'whatsapp',
accountId,
direction: 'outbound',
});
const messageId =
typeof result === 'object' && result && 'key' in result
? String((result as { key?: { id?: string } }).key?.id ?? 'unknown')
: 'unknown';
return { messageId };
},
sendPoll: async (
to: string,
poll: { question: string; options: string[]; maxSelections?: number }
): Promise<{ messageId: string }> => {
const jid = toWhatsappJid(to);
const result = await params.sock.sendMessage(jid, {
poll: {
name: poll.question,
values: poll.options,
selectableCount: poll.maxSelections ?? 1,
},
} as AnyMessageContent);
recordChannelActivity({
channel: 'whatsapp',
accountId: params.defaultAccountId,
direction: 'outbound',
});
const messageId =
typeof result === 'object' && result && 'key' in result
? String((result as { key?: { id?: string } }).key?.id ?? 'unknown')
: 'unknown';
return { messageId };
},
sendReaction: async (
chatJid: string,
messageId: string,
emoji: string,
fromMe: boolean,
participant?: string
): Promise<void> => {
const jid = toWhatsappJid(chatJid);
await params.sock.sendMessage(jid, {
react: {
text: emoji,
key: {
remoteJid: jid,
id: messageId,
fromMe,
participant: participant ? toWhatsappJid(participant) : undefined,
},
},
} as AnyMessageContent);
},
sendComposingTo: async (to: string): Promise<void> => {
const jid = toWhatsappJid(to);
await params.sock.sendPresenceUpdate('composing', jid);
},
} as const;
}