qwen_2.5_model / src /engine /adapters /message-mapper.ts
Muhammad Noman
Deploy OpenWA to Hugging Face Spaces
46252cd
Raw
History Blame Contribute Delete
7.99 kB
import { EditedMessage, IncomingMessage, MessageContact, MessageType } from '../interfaces/whatsapp-engine.interface';
import type { SerializedWid } from '../types/whatsapp-web-js.types';
/**
* Map a whatsapp-web.js `MessageTypes` token to the engine-neutral {@link MessageType}, so no
* consumer outside the adapter sees wwebjs-specific type strings. Notably `chat` -> `text` (aligning
* incoming with the neutral types outgoing sends already use) and `ptt` -> `voice`. Anything not
* mapped becomes `unknown`.
*/
export function mapWwebjsMessageType(raw: string): MessageType {
switch (raw) {
case 'chat':
return 'text';
case 'image':
return 'image';
case 'video':
return 'video';
case 'audio':
return 'audio';
case 'ptt':
return 'voice';
case 'document':
return 'document';
case 'sticker':
return 'sticker';
case 'location':
return 'location';
case 'vcard':
case 'multi_vcard':
return 'contact';
case 'call_log':
return 'call';
case 'poll_creation':
return 'poll';
case 'revoked':
return 'revoked';
default:
return 'unknown';
}
}
/**
* The subset of whatsapp-web.js `Message` fields we read synchronously to build
* the base of an {@link IncomingMessage}. Declared explicitly so the mapping is
* unit-testable without constructing a full wwebjs `Message`.
*/
export interface RawMessageFields {
/**
* Typed as the raw wid rather than `{ _serialized: string }`: a WA Web build that renamed the field
* to `$1` (#747) leaves `_serialized` undefined, and the old type made that state unrepresentable —
* so `$1` could not even be read without a cast, and the unsound `id: string` it fed was `undefined`
* at runtime on every inbound message.
*/
id: SerializedWid;
from: string;
to: string;
body: string;
type: string;
timestamp: number;
fromMe: boolean;
/** Set on group messages: the participant WID that actually sent the message. */
author?: string;
/** WIDs @mentioned in the message; whatsapp-web.js attaches this to every Message. */
mentionedIds?: string[];
/** Raw wwebjs payload; `notifyName` carries the sender's push name without an extra lookup. */
_data?: { notifyName?: string; ephemeralDuration?: number };
}
/**
* Build the synchronous base of an IncomingMessage from a raw wwebjs message.
* Async enrichment (media, quoted message, saved-contact name) is layered on by
* the adapter; this covers the fields available without an await.
*/
export function buildIncomingMessageBase(msg: RawMessageFields): IncomingMessage {
// For an outgoing (fromMe) message `from` is the account's own JID and `to` is the conversation;
// for an incoming message it's the reverse. So the chat is `to` when fromMe, else `from`.
const chatId = msg.fromMe ? msg.to : msg.from;
const incoming: IncomingMessage = {
// Read `$1` before giving up, as the send/ack/status paths do (#762/#765/#773). This runs on the
// LIVE inbound path (`onMessage`/`onMessageCreate`), so on a renamed build without the build-time
// backport every arriving message otherwise carries `id: undefined`. The empty sentinel means
// "received, id unreadable" and is normalized to NULL where it is persisted — never to `''`, which
// the non-partial (sessionId, waMessageId) unique index would collide the second such message on.
id: msg.id._serialized ?? msg.id.$1 ?? '',
from: msg.from,
to: msg.to,
chatId,
body: msg.body,
type: mapWwebjsMessageType(msg.type),
timestamp: msg.timestamp,
fromMe: msg.fromMe,
isGroup: chatId.endsWith('@g.us'),
// Flag status/story broadcasts here (the engine-specific `status@broadcast` pseudo-JID stays in
// the adapter) so engine-neutral code can skip them without matching the literal.
isStatusBroadcast: msg.to === 'status@broadcast' || chatId === 'status@broadcast',
};
// In a group, `from` is the group JID, so `author` is the only way to know the real sender.
if (msg.author) {
incoming.author = msg.author;
}
// @mentioned WIDs, when present — used for command targeting (e.g. `/tr grant @user`).
if (msg.mentionedIds && msg.mentionedIds.length > 0) {
incoming.mentionedIds = msg.mentionedIds;
}
// Flag senders identified by a WhatsApp privacy id (`@lid`) so engine-neutral code can opt to
// resolve a phone number without matching the engine-specific JID scheme itself (#263).
const senderJid = msg.author ?? msg.from;
if (senderJid.endsWith('@lid')) {
incoming.isLidSender = true;
}
// Push name is available synchronously on the raw payload — no contact lookup needed.
const pushName = msg._data?.notifyName;
if (pushName) {
incoming.contact = { pushName };
}
// Ephemeral/disappearing-messages timer, when the chat has one set.
if (msg._data?.ephemeralDuration && msg._data.ephemeralDuration > 0) {
incoming.ephemeralDuration = msg._data.ephemeralDuration;
}
return incoming;
}
/**
* Project an engine-neutral message base into the public edit-event contract. Keeping this projection
* shared prevents the two adapters from drifting on identity, direction, group, type, or filter fields.
*/
export function buildEditedMessage(message: IncomingMessage, hasMedia: boolean): EditedMessage {
return {
messageId: message.id,
chatId: message.chatId,
body: message.body,
senderId: message.author ?? message.from,
from: message.from,
to: message.to,
fromMe: message.fromMe,
isGroup: message.isGroup,
type: message.type,
hasMedia,
...(message.author ? { author: message.author } : {}),
...(message.mentionedIds ? { mentionedIds: message.mentionedIds } : {}),
timestamp: message.timestamp,
};
}
/**
* The subset of whatsapp-web.js `Contact` properties we read synchronously (already on the resolved
* contact, no network call). Declared explicitly so {@link mapContactFields} is unit-testable without a
* full wwebjs `Contact`, and so the async getters stay out by construction.
*/
export interface RawContactFields {
id?: { _serialized?: string };
number?: string;
name?: string;
pushname?: string;
shortName?: string;
type?: string;
isMyContact?: boolean;
isWAContact?: boolean;
isBusiness?: boolean;
isEnterprise?: boolean;
verifiedName?: string;
verifiedLevel?: number;
isBlocked?: boolean;
labels?: string[];
}
/**
* Map the synchronous fields of a wwebjs `Contact` to a {@link MessageContact}, copying only the values
* that are set. No network calls, which on a per-message path would risk rate-limiting.
*
* With `full` false (the default) it returns just `name`/`pushName`, the long-standing payload. With
* `full` true (operator opt-in via `WEBHOOK_CONTACT_DETAILS`) it returns the complete field set.
*/
export function mapContactFields(contact: RawContactFields, full = false): MessageContact {
const out: MessageContact = {};
if (contact.name) out.name = contact.name;
if (contact.pushname) out.pushName = contact.pushname;
if (!full) return out;
const id = contact.id?._serialized;
if (id) out.id = id;
if (contact.number) out.number = contact.number;
if (contact.shortName) out.shortName = contact.shortName;
if (contact.type) out.type = contact.type;
if (contact.isMyContact !== undefined) out.isMyContact = contact.isMyContact;
if (contact.isWAContact !== undefined) out.isWAContact = contact.isWAContact;
if (contact.isBusiness !== undefined) out.isBusiness = contact.isBusiness;
if (contact.isEnterprise !== undefined) out.isEnterprise = contact.isEnterprise;
if (contact.verifiedName) out.verifiedName = contact.verifiedName;
if (contact.verifiedLevel !== undefined) out.verifiedLevel = contact.verifiedLevel;
if (contact.isBlocked !== undefined) out.isBlocked = contact.isBlocked;
if (contact.labels && contact.labels.length > 0) out.labels = contact.labels;
return out;
}