Spaces:
Runtime error
Runtime error
File size: 7,988 Bytes
46252cd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | 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;
}
|