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;
}