Spaces:
Runtime error
Runtime error
| 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; | |
| } | |