@arcaelas/whatsapp 4.5.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +282 -239
  2. package/build/cjs/decorators.js +0 -1
  3. package/build/cjs/index.d.ts +6 -4
  4. package/build/cjs/index.js +22 -3
  5. package/build/cjs/lib/bot/decorator.d.ts +2 -0
  6. package/build/cjs/lib/bot/decorator.js +6 -1
  7. package/build/cjs/lib/bot/decorators.d.ts +2 -2
  8. package/build/cjs/lib/bot/decorators.js +4 -4
  9. package/build/cjs/lib/bot/index.js +0 -1
  10. package/build/cjs/lib/chat/index.d.ts +381 -204
  11. package/build/cjs/lib/chat/index.js +243 -212
  12. package/build/cjs/lib/contact/index.d.ts +162 -179
  13. package/build/cjs/lib/contact/index.js +96 -191
  14. package/build/cjs/lib/internal.d.ts +40 -0
  15. package/build/cjs/lib/internal.js +38 -0
  16. package/build/cjs/lib/message/index.d.ts +468 -289
  17. package/build/cjs/lib/message/index.js +871 -743
  18. package/build/cjs/lib/status/index.d.ts +60 -70
  19. package/build/cjs/lib/status/index.js +106 -112
  20. package/build/cjs/lib/store/engine/index.d.ts +35 -7
  21. package/build/cjs/lib/store/engine/index.js +6 -4
  22. package/build/cjs/lib/store/engine/lib/file_system/index.d.ts +48 -8
  23. package/build/cjs/lib/store/engine/lib/file_system/index.js +117 -66
  24. package/build/cjs/lib/store/engine/lib/index.d.ts +112 -0
  25. package/build/cjs/lib/store/engine/lib/index.js +212 -0
  26. package/build/cjs/lib/store/engine/lib/redis/index.d.ts +44 -15
  27. package/build/cjs/lib/store/engine/lib/redis/index.js +75 -43
  28. package/build/cjs/lib/store/engine/lib/s3/index.d.ts +44 -1
  29. package/build/cjs/lib/store/engine/lib/s3/index.js +112 -65
  30. package/build/cjs/lib/store/engine/lib/sqlite/index.d.ts +110 -0
  31. package/build/cjs/lib/store/engine/lib/sqlite/index.js +132 -0
  32. package/build/cjs/lib/store/index.d.ts +7 -3
  33. package/build/cjs/lib/store/index.js +14 -5
  34. package/build/cjs/lib/whatsapp/index.d.ts +106 -68
  35. package/build/cjs/lib/whatsapp/index.js +393 -167
  36. package/build/esm/decorators.js +0 -1
  37. package/build/esm/index.d.ts +6 -4
  38. package/build/esm/index.js +5 -3
  39. package/build/esm/lib/bot/decorator.d.ts +2 -0
  40. package/build/esm/lib/bot/decorator.js +6 -1
  41. package/build/esm/lib/bot/decorators.d.ts +2 -2
  42. package/build/esm/lib/bot/decorators.js +4 -4
  43. package/build/esm/lib/bot/index.js +0 -1
  44. package/build/esm/lib/chat/index.d.ts +381 -204
  45. package/build/esm/lib/chat/index.js +243 -212
  46. package/build/esm/lib/contact/index.d.ts +162 -179
  47. package/build/esm/lib/contact/index.js +96 -190
  48. package/build/esm/lib/internal.d.ts +40 -0
  49. package/build/esm/lib/internal.js +34 -0
  50. package/build/esm/lib/message/index.d.ts +468 -289
  51. package/build/esm/lib/message/index.js +866 -738
  52. package/build/esm/lib/status/index.d.ts +60 -70
  53. package/build/esm/lib/status/index.js +105 -111
  54. package/build/esm/lib/store/engine/index.d.ts +35 -7
  55. package/build/esm/lib/store/engine/index.js +4 -3
  56. package/build/esm/lib/store/engine/lib/file_system/index.d.ts +48 -8
  57. package/build/esm/lib/store/engine/lib/file_system/index.js +117 -66
  58. package/build/esm/lib/store/engine/lib/index.d.ts +112 -0
  59. package/build/esm/lib/store/engine/lib/index.js +205 -0
  60. package/build/esm/lib/store/engine/lib/redis/index.d.ts +44 -15
  61. package/build/esm/lib/store/engine/lib/redis/index.js +73 -41
  62. package/build/esm/lib/store/engine/lib/s3/index.d.ts +44 -1
  63. package/build/esm/lib/store/engine/lib/s3/index.js +111 -64
  64. package/build/esm/lib/store/engine/lib/sqlite/index.d.ts +110 -0
  65. package/build/esm/lib/store/engine/lib/sqlite/index.js +128 -0
  66. package/build/esm/lib/store/index.d.ts +7 -3
  67. package/build/esm/lib/store/index.js +13 -5
  68. package/build/esm/lib/whatsapp/index.d.ts +106 -68
  69. package/build/esm/lib/whatsapp/index.js +395 -169
  70. package/package.json +25 -6
  71. package/build/cjs/decorators.js.map +0 -1
  72. package/build/cjs/index.js.map +0 -1
  73. package/build/cjs/lib/bot/decorator.js.map +0 -1
  74. package/build/cjs/lib/bot/decorators.js.map +0 -1
  75. package/build/cjs/lib/bot/index.js.map +0 -1
  76. package/build/cjs/lib/chat/index.js.map +0 -1
  77. package/build/cjs/lib/contact/index.js.map +0 -1
  78. package/build/cjs/lib/message/index.js.map +0 -1
  79. package/build/cjs/lib/status/index.js.map +0 -1
  80. package/build/cjs/lib/store/engine/index.js.map +0 -1
  81. package/build/cjs/lib/store/engine/lib/file_system/index.js.map +0 -1
  82. package/build/cjs/lib/store/engine/lib/redis/index.js.map +0 -1
  83. package/build/cjs/lib/store/engine/lib/s3/index.js.map +0 -1
  84. package/build/cjs/lib/store/index.js.map +0 -1
  85. package/build/cjs/lib/whatsapp/index.js.map +0 -1
  86. package/build/cjs/test.d.ts +0 -1
  87. package/build/cjs/test.js +0 -71
  88. package/build/cjs/test.js.map +0 -1
  89. package/build/esm/decorators.js.map +0 -1
  90. package/build/esm/index.js.map +0 -1
  91. package/build/esm/lib/bot/decorator.js.map +0 -1
  92. package/build/esm/lib/bot/decorators.js.map +0 -1
  93. package/build/esm/lib/bot/index.js.map +0 -1
  94. package/build/esm/lib/chat/index.js.map +0 -1
  95. package/build/esm/lib/contact/index.js.map +0 -1
  96. package/build/esm/lib/message/index.js.map +0 -1
  97. package/build/esm/lib/status/index.js.map +0 -1
  98. package/build/esm/lib/store/engine/index.js.map +0 -1
  99. package/build/esm/lib/store/engine/lib/file_system/index.js.map +0 -1
  100. package/build/esm/lib/store/engine/lib/redis/index.js.map +0 -1
  101. package/build/esm/lib/store/engine/lib/s3/index.js.map +0 -1
  102. package/build/esm/lib/store/index.js.map +0 -1
  103. package/build/esm/lib/whatsapp/index.js.map +0 -1
  104. package/build/esm/test.d.ts +0 -1
  105. package/build/esm/test.js +0 -66
  106. package/build/esm/test.js.map +0 -1
@@ -1,28 +1,26 @@
1
1
  /**
2
2
  * @file message/index.ts
3
- * @description Entidad Message — una sola clase base que recibe `{wa, doc}` en el constructor.
4
- * Subclases especializadas (Text, Image, Video, Audio, Gps, Poll) override `content()` según su tipo.
3
+ * @description Entidad Message — clase base con getters puros y subclases por tipo
4
+ * (Text, Image, Video, Audio, Sticker, Document, Location, Poll, VCard, Event);
5
+ * `message(wa, raw)` evalúa el tipo y retorna la instancia correcta.
6
+ * Message entity — pure-getter base class with per-type subclasses;
7
+ * `message(wa, raw)` evaluates the type and returns the right instance.
5
8
  */
6
9
  import { aesEncryptGCM, downloadMediaMessage, generateForwardMessageContent, generateMessageID, generateWAMessageFromContent, getAggregateVotesInPollMessage, getContentType, getKeyAuthor, hmacSign, jidNormalizedUser, proto, sha256, updateMessageWithPollUpdate, } from 'baileys';
7
10
  import { randomBytes } from 'node:crypto';
8
11
  import { Readable } from 'node:stream';
12
+ import { internals } from '../../lib/internal.js';
9
13
  import { Chat } from '../../lib/chat/index.js';
10
14
  import { deserialize, serialize } from '../../lib/store/index.js';
11
- export var MessageStatus;
12
- (function (MessageStatus) {
13
- MessageStatus[MessageStatus["ERROR"] = 0] = "ERROR";
14
- MessageStatus[MessageStatus["PENDING"] = 1] = "PENDING";
15
- MessageStatus[MessageStatus["SERVER_ACK"] = 2] = "SERVER_ACK";
16
- MessageStatus[MessageStatus["DELIVERED"] = 3] = "DELIVERED";
17
- MessageStatus[MessageStatus["READ"] = 4] = "READ";
18
- MessageStatus[MessageStatus["PLAYED"] = 5] = "PLAYED";
19
- })(MessageStatus || (MessageStatus = {}));
15
+ /** Estados legibles indexados por el status numérico de baileys. / Readable states indexed by the baileys numeric status. */
16
+ const STATUS = ['error', 'pending', 'sent', 'delivered', 'read', 'played'];
20
17
  const MESSAGE_TYPE_MAP = {
21
18
  conversation: 'text',
22
19
  extendedTextMessage: 'text',
23
20
  imageMessage: 'image',
24
21
  videoMessage: 'video',
25
22
  audioMessage: 'audio',
23
+ stickerMessage: 'sticker',
26
24
  locationMessage: 'location',
27
25
  liveLocationMessage: 'location',
28
26
  pollCreationMessage: 'poll',
@@ -35,292 +33,441 @@ const MESSAGE_TYPE_MAP = {
35
33
  eventMessage: 'event',
36
34
  };
37
35
  /**
38
- * Construye el documento IMessage desde el raw de baileys.
36
+ * Desenvuelve los wrappers de contenido (view-once, documento con caption).
37
+ * Unwraps content wrappers (view-once, captioned document).
39
38
  */
40
- export function build_message_doc(raw, self_id, edited = false) {
41
- const key = raw.key ?? {};
42
- const msg = raw.message ?? {};
43
- const unwrapped = msg.viewOnceMessage?.message ??
39
+ function unwrap(msg) {
40
+ return (msg.viewOnceMessage?.message ??
44
41
  msg.viewOnceMessageV2?.message ??
45
42
  msg.viewOnceMessageV2Extension?.message ??
46
43
  msg.documentWithCaptionMessage?.message ??
47
- msg;
48
- const content_type = getContentType(unwrapped);
49
- const msg_type = MESSAGE_TYPE_MAP[content_type ?? ''] ?? 'text';
50
- const msg_content = unwrapped[content_type];
51
- const context_info = msg_content
52
- ?.contextInfo;
53
- const ephemeral_duration = raw.ephemeralDuration ?? context_info?.expiration;
54
- const deleted_at = raw.ephemeralStartTimestamp != null && ephemeral_duration != null
55
- ? (Number(raw.ephemeralStartTimestamp) + ephemeral_duration) * 1000
56
- : null;
57
- let mime = 'text/plain';
58
- if (msg_type === 'location' || msg_type === 'poll' || msg_type === 'event') {
59
- mime = 'application/json';
60
- }
61
- else if (msg_type === 'vcard') {
62
- mime = 'text/vcard';
63
- }
64
- else if (msg_type !== 'text' && typeof msg_content === 'object') {
65
- mime = msg_content?.mimetype ?? 'application/octet-stream';
66
- }
67
- let caption = '';
68
- if (typeof msg_content === 'string') {
69
- caption = msg_content;
70
- }
71
- else if (msg_type === 'event') {
72
- caption = msg_content?.description ?? '';
73
- }
74
- else if (msg_content) {
75
- caption =
76
- msg_content.caption ??
77
- msg_content.text ??
78
- msg_content.name ??
79
- msg_content.displayName ??
80
- '';
81
- }
82
- const author = key.fromMe && self_id
83
- ? jidNormalizedUser(self_id)
84
- : key.participant || key.remoteJidAlt || key.remoteJid || '';
85
- return {
86
- id: key.id ?? '',
87
- cid: key.remoteJidAlt ?? key.remoteJid ?? '',
88
- mid: context_info?.stanzaId ?? null,
89
- me: key.fromMe ?? false,
90
- type: msg_type,
91
- author,
92
- status: raw.status ?? MessageStatus.PENDING,
93
- starred: raw.starred ?? false,
94
- forwarded: context_info?.isForwarded ?? false,
95
- created_at: (Number(raw.messageTimestamp) || Math.floor(Date.now() / 1000)) * 1000,
96
- deleted_at,
97
- mime,
98
- caption,
99
- edited,
100
- raw,
101
- };
44
+ msg);
45
+ }
46
+ /**
47
+ * Convierte un número o Long del proto a number plano.
48
+ * Converts a proto number or Long into a plain number.
49
+ */
50
+ function to_number(value) {
51
+ return Number(value?.toString() ?? 0);
52
+ }
53
+ /**
54
+ * Convierte los bytes del proto (Uint8Array runtime o base64 post-engine) a Buffer.
55
+ * Converts proto bytes (runtime Uint8Array or post-engine base64) into a Buffer.
56
+ */
57
+ function to_buffer(value) {
58
+ if (value instanceof Uint8Array && value.length > 0)
59
+ return Buffer.from(value);
60
+ if (typeof value === 'string' && value.length > 0)
61
+ return Buffer.from(value, 'base64');
62
+ return null;
63
+ }
64
+ /**
65
+ * Envío base: resuelve el destino, cita si hay `mid`, marca view-once, persiste el doc
66
+ * (con su binario cuando aplica) y retorna la instancia del mensaje enviado.
67
+ * Base send: resolves the target, quotes when `mid` is given, flags view-once, persists
68
+ * the doc (with its binary when it applies) and returns the sent message instance.
69
+ */
70
+ async function send(wa, cid, content, binary, extra = {}, doc_overrides) {
71
+ let result = null;
72
+ const jid = await internals(wa).resolve_jid(cid);
73
+ const socket = internals(wa).socket;
74
+ if (jid && socket) {
75
+ const quoted = extra.mid
76
+ ? (deserialize(await wa.engine.get(`/chat/${jid}/message/${extra.mid}`))?.raw ?? undefined)
77
+ : undefined;
78
+ const raw = await socket.sendMessage(jid, {
79
+ ...content,
80
+ ...(extra.once && { viewOnce: true }),
81
+ ...(quoted && { quoted }),
82
+ });
83
+ const sent_id = raw?.key?.id;
84
+ if (raw && sent_id) {
85
+ const instance = message(wa, raw);
86
+ if (doc_overrides) {
87
+ Object.assign(instance._raw, doc_overrides);
88
+ }
89
+ await wa.engine.set(`/chat/${jid}/message/${sent_id}`, serialize(instance._raw), instance._raw.created_at);
90
+ if (binary) {
91
+ await write_content(wa, `/chat/${jid}/message/${sent_id}/content`, binary);
92
+ }
93
+ result = instance;
94
+ }
95
+ }
96
+ return result;
97
+ }
98
+ /**
99
+ * Binario persistido de un documento: primero el crudo del engine y, si el driver no soporta
100
+ * binarios o el dato viene de una sesión anterior, el documento JSON con base64.
101
+ * A document's persisted binary: the engine's raw one first and, when the driver lacks binary
102
+ * support or the data comes from an older session, the base64 JSON document.
103
+ *
104
+ * @param wa - Cliente dueño / Owner client
105
+ * @param path - Ruta del contenido / Content path
106
+ * @returns Binario o null / Binary or null
107
+ */
108
+ async function read_content(wa, path) {
109
+ const raw = await wa.engine.get_buffer?.(path);
110
+ if (raw?.length) {
111
+ return raw;
112
+ }
113
+ const legacy = deserialize(await wa.engine.get(path));
114
+ return legacy?.data ? Buffer.from(legacy.data, 'base64') : null;
115
+ }
116
+ /**
117
+ * Persiste el binario de un documento: crudo cuando el driver lo soporta, JSON con base64
118
+ * en caso contrario.
119
+ * Persists a document's binary: raw when the driver supports it, base64 JSON otherwise.
120
+ *
121
+ * @param wa - Cliente dueño / Owner client
122
+ * @param path - Ruta del contenido / Content path
123
+ * @param data - Binario a guardar / Binary to store
124
+ */
125
+ async function write_content(wa, path, data) {
126
+ if (wa.engine.set_buffer) {
127
+ await wa.engine.set_buffer(path, data);
128
+ }
129
+ else {
130
+ await wa.engine.set(path, serialize({ data: data.toString('base64') }));
131
+ }
102
132
  }
103
133
  /**
104
- * Clase base del mensaje. Una sola clase toda la API de instancia vive aquí.
105
- * Base message class. Single class all instance API lives here.
134
+ * Stream del binario del mensaje: cache del engine o descarga de baileys.
135
+ * Message binary stream: engine cache or baileys download.
136
+ */
137
+ async function media_stream(wa, doc) {
138
+ const cached = await read_content(wa, `/chat/${doc.cid}/message/${doc.id}/content`);
139
+ if (cached) {
140
+ return Readable.from(cached);
141
+ }
142
+ const socket = internals(wa).socket;
143
+ if (socket) {
144
+ try {
145
+ return (await downloadMediaMessage(doc.raw, 'stream', {}));
146
+ }
147
+ catch {
148
+ /* media may be expired */
149
+ }
150
+ }
151
+ return Readable.from(Buffer.alloc(0));
152
+ }
153
+ /**
154
+ * Acumula un Readable en Buffer.
155
+ * Drains a Readable into a Buffer.
156
+ */
157
+ async function drain(readable) {
158
+ const chunks = [];
159
+ for await (const chunk of readable) {
160
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
161
+ }
162
+ return Buffer.concat(chunks);
163
+ }
164
+ /**
165
+ * Clase base del mensaje: recibe el cliente y el raw, deriva el estado con getters y
166
+ * opera contra WhatsApp con sus métodos. Los estáticos reciben el cliente como primer
167
+ * argumento (`Message.text(wa, cid, …)`).
168
+ * Base message class: receives the client and the raw, derives state via getters and
169
+ * operates against WhatsApp with its methods. Statics take the client as their first
170
+ * argument (`Message.text(wa, cid, …)`).
106
171
  */
107
172
  export class Message {
108
- constructor(options) {
109
- /** @internal Cache de la promesa de `author()` para evitar engine round-trips repetidos. */
110
- this._author_cache = null;
111
- /** @internal Cache de la promesa de `chat()` para evitar engine round-trips repetidos. */
112
- this._chat_cache = null;
113
- this._wa = options.wa;
114
- this._doc = options.doc;
173
+ /**
174
+ * @internal Cliente dueño y documento persistido. `status`, `created_at` y
175
+ * `deleted_at` guardan los valores numéricos del protocolo; los getters los
176
+ * presentan legibles. `viewed` existe únicamente en los status broadcast (Feed).
177
+ * Owner client and persisted document. `status`, `created_at` and `deleted_at`
178
+ * keep the protocol numeric values; getters expose them readable. `viewed` only
179
+ * exists on status broadcasts (Feed).
180
+ */
181
+ constructor(_wa, _raw) {
182
+ this._wa = _wa;
183
+ this._raw = _raw;
115
184
  }
185
+ /** Identificador del mensaje. / Message identifier. */
116
186
  get id() {
117
- return this._doc.id;
187
+ return this._raw.id;
118
188
  }
189
+ /** Identificador del chat del mensaje. / Message chat identifier. */
119
190
  get cid() {
120
- return this._doc.cid;
191
+ return this._raw.cid;
121
192
  }
122
- get type() {
123
- return this._doc.type;
193
+ /** Identificador del mensaje citado, o null. / Quoted message identifier, or null. */
194
+ get mid() {
195
+ return this._raw.mid;
124
196
  }
125
- /** JID del autor del mensaje. Para acceso síncrono sin hidratar el Contact. */
197
+ /** JID del autor, para acceso síncrono. / Author JID, for sync access. */
126
198
  get from() {
127
- return this._doc.author;
128
- }
129
- /** MIME type del contenido. `text/plain` para texto, `application/json` para location/poll, mimetype real para media. */
130
- get mime() {
131
- return this._doc.mime;
132
- }
133
- get mid() {
134
- return this._doc.mid;
199
+ return this._raw.author;
135
200
  }
201
+ /** true si soy el autor. / true when I am the author. */
136
202
  get me() {
137
- return this._doc.me;
203
+ return this._raw.me;
138
204
  }
205
+ /** Tipo del mensaje. / Message type. */
206
+ get type() {
207
+ return this._raw.type;
208
+ }
209
+ /** MIME: text/plain en texto, text/json en poll/location/vcard/event, real en media. / MIME: text/plain for text, text/json for poll/location/vcard/event, actual for media. */
210
+ get mime() {
211
+ const { type } = this._raw;
212
+ if (type === 'text')
213
+ return 'text/plain';
214
+ if (type === 'poll' || type === 'location' || type === 'vcard' || type === 'event')
215
+ return 'text/json';
216
+ return this._raw.mime;
217
+ }
218
+ /** Texto del mensaje o caption del media. / Message text or media caption. */
139
219
  get caption() {
140
- return this._doc.caption;
220
+ return this._raw.caption;
141
221
  }
222
+ /** Estado legible del mensaje. / Readable message state. */
223
+ get status() {
224
+ return STATUS[this._raw.status] ?? 'pending';
225
+ }
226
+ /** true si el mensaje fue visto. / true when the message was seen. */
227
+ get read() {
228
+ return this._raw.status >= 4;
229
+ }
230
+ /** true si está destacado. / true when starred. */
142
231
  get starred() {
143
- return this._doc.starred;
232
+ return this._raw.starred;
144
233
  }
234
+ /** true si fue reenviado. / true when forwarded. */
145
235
  get forwarded() {
146
- return this._doc.forwarded;
236
+ return this._raw.forwarded;
147
237
  }
238
+ /** true si fue editado. / true when edited. */
239
+ get edited() {
240
+ return this._raw.edited;
241
+ }
242
+ /** true si es de una sola lectura (view-once). / true when view-once. */
148
243
  get once() {
149
- return this._doc.deleted_at !== null;
244
+ const msg = this._raw.raw.message;
245
+ return Boolean(msg?.viewOnceMessage ?? msg?.viewOnceMessageV2 ?? msg?.viewOnceMessageV2Extension);
150
246
  }
247
+ /** Fecha de creación en ISO UTC. / Creation date as ISO UTC. */
151
248
  get created_at() {
152
- return this._doc.created_at;
249
+ return new Date(this._raw.created_at).toISOString();
153
250
  }
154
- get deleted_at() {
155
- return this._doc.deleted_at;
156
- }
157
- get status() {
158
- return this._doc.status;
159
- }
160
- get edited() {
161
- return this._doc.edited;
251
+ /** Vencimiento del mensaje temporal en ISO UTC, o null. / Ephemeral expiration as ISO UTC, or null. */
252
+ get expires_at() {
253
+ return this._raw.deleted_at != null ? new Date(this._raw.deleted_at).toISOString() : null;
162
254
  }
163
255
  /**
164
- * Contacto del autor (resuelve vía `wa.Contact.get`; fallback a instancia mínima).
165
- * Memoizado por instancia: la primera llamada va al engine; las siguientes
166
- * reutilizan la misma promesa sin nuevos round-trips.
167
- * Author contact (resolves via `wa.Contact.get`; falls back to a minimal instance).
168
- * Instance-memoized: first call hits the engine; subsequent calls reuse the
169
- * same promise without further round-trips.
256
+ * Contacto autor del mensaje, desde el engine (ficha mínima si no está persistido).
257
+ * Message author contact, from the engine (minimal card when not persisted).
258
+ *
259
+ * @returns Contacto / Contact
170
260
  */
171
261
  async author() {
172
- return (this._author_cache ??= (async () => {
173
- const { _wa, _doc } = this;
174
- const fetched = _doc.author ? await _wa.Contact.get(_doc.author) : null;
175
- return (fetched ??
176
- new _wa.Contact({
177
- id: _doc.author,
178
- lid: null,
179
- name: null,
180
- notify: null,
181
- verified_name: null,
182
- img_url: null,
183
- status: null,
184
- }, new _wa.Chat({ id: _doc.author, name: _doc.author.split('@')[0] })));
185
- })());
262
+ const cached = deserialize(await this._wa.engine.get(`/contact/${this._raw.author}`));
263
+ return new this._wa.Contact(cached ?? { id: this._raw.author });
186
264
  }
187
265
  /**
188
- * Chat al que pertenece el mensaje. Memoizado por instancia: la primera llamada
189
- * va al engine; las siguientes reutilizan la misma promesa.
190
- * Chat the message belongs to. Instance-memoized: first call hits the engine;
191
- * subsequent calls reuse the same promise.
266
+ * Chat al que pertenece el mensaje.
267
+ * Chat the message belongs to.
268
+ *
269
+ * @returns Chat / Chat
192
270
  */
193
271
  async chat() {
194
- return (this._chat_cache ??= (async () => {
195
- const cached = deserialize(await this._wa.engine.get(`/chat/${this._doc.cid}`));
196
- return new this._wa.Chat(cached ?? { id: this._doc.cid, name: this._doc.cid.split('@')[0] });
197
- })());
272
+ const cached = deserialize(await this._wa.engine.get(`/chat/${this._raw.cid}`));
273
+ return new this._wa.Chat(cached ?? { id: this._raw.cid });
274
+ }
275
+ /**
276
+ * Mensaje citado cuando hay `mid`, o null.
277
+ * Quoted message when `mid` exists, or null.
278
+ *
279
+ * @returns Mensaje citado o null / Quoted message or null
280
+ */
281
+ async message() {
282
+ return this._raw.mid ? Message.get(this._wa, this._raw.cid, this._raw.mid) : null;
198
283
  }
199
- /** Contenido sin parsear, directo del engine. Las subclases override según su tipo. */
284
+ /**
285
+ * Contenido del mensaje: texto como su caption, media como binario, y
286
+ * poll/location/vcard/event como JSON.
287
+ * Message content: text as its caption, media as binary, and
288
+ * poll/location/vcard/event as JSON.
289
+ *
290
+ * @returns Buffer del contenido / Content buffer
291
+ */
200
292
  async content() {
201
- const cached = deserialize(await this._wa.engine.get(`/chat/${this._doc.cid}/message/${this._doc.id}/content`));
202
- return cached?.data ? Buffer.from(cached.data, 'base64') : Buffer.alloc(0);
293
+ return (await read_content(this._wa, `/chat/${this._raw.cid}/message/${this._raw.id}/content`)) ?? Buffer.alloc(0);
203
294
  }
204
295
  /**
205
- * Devuelve un `Readable` con los bytes del mensaje. Para media (image/video/audio)
206
- * lee desde el cache del engine o, si falta, descarga desde baileys. Para text/location/poll
207
- * retorna el mismo binario que `content()` envuelto en un Readable.
208
- * Returns a `Readable` of the message bytes. For media (image/video/audio) it reads
209
- * from the engine cache or, if missing, downloads from baileys. For text/location/poll
210
- * it wraps the `content()` buffer in a Readable.
296
+ * Stream del contenido; para media descarga de baileys si el cache falta.
297
+ * Content stream; media falls back to a baileys download when the cache is missing.
298
+ *
299
+ * @returns Readable del contenido / Content readable
211
300
  */
212
301
  async stream() {
213
- return _media_stream(this);
302
+ return media_stream(this._wa, this._raw);
214
303
  }
215
- /** Reacciona al mensaje (emoji vacío para retirar). */
304
+ /**
305
+ * Reacciones del mensaje agrupadas por emoji con su conteo.
306
+ * Message reactions grouped by emoji with their count.
307
+ *
308
+ * @returns Conteo por emoji / Per-emoji count
309
+ */
310
+ async reactions() {
311
+ const counts = new Map();
312
+ for (const { emoji } of this._raw.reactions ?? []) {
313
+ counts.set(emoji, (counts.get(emoji) ?? 0) + 1);
314
+ }
315
+ return [...counts.entries()].map(([emoji, count]) => ({ emoji, count }));
316
+ }
317
+ /**
318
+ * Reacciona al mensaje; el emoji vacío retira la reacción.
319
+ * Reacts to the message; an empty emoji removes the reaction.
320
+ *
321
+ * @param emoji - Emoji o '' / Emoji or ''
322
+ * @returns true si se envió / true when sent
323
+ */
216
324
  async react(emoji) {
217
- const { _wa, _doc } = this;
218
325
  let ok = false;
219
- if (_wa._socket) {
220
- await _wa._socket.sendMessage(_doc.cid, {
221
- react: { text: emoji, key: { remoteJid: _doc.cid, id: _doc.id, fromMe: _doc.me } },
326
+ const socket = internals(this._wa).socket;
327
+ if (socket) {
328
+ const { cid, id, me, author } = this._raw;
329
+ await socket.sendMessage(cid, {
330
+ react: {
331
+ text: emoji,
332
+ key: { remoteJid: cid, id, fromMe: me, ...(cid.endsWith('@g.us') && !me && { participant: author }) },
333
+ },
222
334
  });
223
335
  ok = true;
224
336
  }
225
337
  return ok;
226
338
  }
227
- /** Reenvía el mensaje a un destino (CID, Chat o Contact). */
228
- async forward(target) {
229
- const { _wa, _doc } = this;
230
- const to_cid = typeof target === 'string'
231
- ? target
232
- : target instanceof Chat
233
- ? target.id
234
- : (await target.chat()).id;
339
+ /**
340
+ * Destaca o quita el destacado del mensaje.
341
+ * Stars or unstars the message.
342
+ *
343
+ * @param value - true destaca / true stars
344
+ * @returns true si se envió / true when sent
345
+ */
346
+ async star(value) {
235
347
  let ok = false;
236
- if (_wa._socket) {
237
- const jid = await _wa._resolve_jid(to_cid);
238
- if (jid) {
239
- if (_doc.raw.message) {
240
- try {
241
- const wa_msg = generateWAMessageFromContent(jid, generateForwardMessageContent(_doc.raw, false), { userJid: _wa._socket.user?.id ?? '' });
242
- if (wa_msg.message && wa_msg.key?.id) {
243
- await _wa._socket.relayMessage(jid, wa_msg.message, { messageId: wa_msg.key.id });
244
- ok = true;
245
- }
246
- }
247
- catch {
248
- /* fallback below */
249
- }
250
- }
251
- if (!ok) {
252
- const buf = await this.content();
253
- if (buf.length > 0) {
254
- if (_doc.type === 'text') {
255
- ok = (await _wa.Message.text(jid, buf.toString())) !== null;
256
- }
257
- else if (_doc.type === 'image') {
258
- ok =
259
- (await _wa.Message.image(jid, buf, { caption: _doc.caption || undefined })) !==
260
- null;
261
- }
262
- else if (_doc.type === 'video') {
263
- ok =
264
- (await _wa.Message.video(jid, buf, { caption: _doc.caption || undefined })) !==
265
- null;
266
- }
267
- else if (_doc.type === 'audio') {
268
- ok = (await _wa.Message.audio(jid, buf)) !== null;
269
- }
270
- else if (_doc.type === 'location') {
271
- try {
272
- ok =
273
- (await _wa.Message.location(jid, JSON.parse(buf.toString()))) !== null;
274
- }
275
- catch {
276
- /* invalid payload */
277
- }
278
- }
279
- }
280
- }
281
- }
348
+ const socket = internals(this._wa).socket;
349
+ if (socket) {
350
+ const doc = this._raw;
351
+ await socket.chatModify({ star: { messages: [{ id: doc.id, fromMe: doc.me }], star: value } }, doc.cid);
352
+ doc.starred = value;
353
+ await this._wa.engine.set(`/chat/${doc.cid}/message/${doc.id}`, serialize(doc), doc.created_at);
354
+ ok = true;
282
355
  }
283
356
  return ok;
284
357
  }
285
- /** Marca el mensaje como leído. */
358
+ /**
359
+ * Marca el mensaje como leído.
360
+ * Marks the message as read.
361
+ *
362
+ * @returns true si se envió / true when sent
363
+ */
286
364
  async seen() {
287
- const { _wa, _doc } = this;
288
365
  let ok = false;
289
- if (_wa._socket) {
290
- const is_group = _doc.cid.endsWith('@g.us');
291
- await _wa._socket.readMessages([
292
- { remoteJid: _doc.cid, id: _doc.id, participant: is_group ? _doc.author : undefined },
366
+ const socket = internals(this._wa).socket;
367
+ if (socket) {
368
+ const { cid, id, author } = this._raw;
369
+ await socket.readMessages([
370
+ { remoteJid: cid, id, participant: cid.endsWith('@g.us') ? author : undefined },
293
371
  ]);
294
372
  ok = true;
295
373
  }
296
374
  return ok;
297
375
  }
298
- /** Destaca/retira destacado del mensaje. */
299
- async star(value) {
300
- const { _wa: wa, _doc: doc } = this;
376
+ /**
377
+ * Edita el caption del mensaje propio (texto, imagen o video).
378
+ * Edits the own message caption (text, image or video).
379
+ *
380
+ * @param caption - Texto nuevo / New text
381
+ * @returns true si se editó / true when edited
382
+ */
383
+ async edit(caption) {
384
+ const doc = this._raw;
301
385
  let ok = false;
302
- if (wa._socket) {
303
- await wa._socket.chatModify({
304
- star: { messages: [{ id: doc.id, fromMe: doc.me }], star: value },
305
- }, doc.cid);
306
- doc.starred = value;
307
- await wa.engine.set(`/chat/${doc.cid}/message/${doc.id}`, serialize(doc));
308
- ok = true;
386
+ const socket = internals(this._wa).socket;
387
+ if (socket && doc.me) {
388
+ if (doc.type === 'text') {
389
+ await socket.sendMessage(doc.cid, {
390
+ text: caption,
391
+ edit: { remoteJid: doc.cid, id: doc.id, fromMe: true },
392
+ });
393
+ const content_type = getContentType(doc.raw.message ?? {});
394
+ doc.raw.message =
395
+ content_type === 'conversation' ? { conversation: caption } : { extendedTextMessage: { text: caption } };
396
+ ok = true;
397
+ }
398
+ else if (doc.type === 'image' || doc.type === 'video') {
399
+ const media_key = doc.type === 'image' ? 'imageMessage' : 'videoMessage';
400
+ const media = doc.raw.message?.[media_key];
401
+ if (media) {
402
+ const edited = { [media_key]: { ...media, caption } };
403
+ await socket.relayMessage(doc.cid, {
404
+ protocolMessage: {
405
+ key: { remoteJid: doc.cid, id: doc.id, fromMe: true },
406
+ type: proto.Message.ProtocolMessage.Type.MESSAGE_EDIT,
407
+ editedMessage: edited,
408
+ timestampMs: Date.now(),
409
+ },
410
+ }, { messageId: generateMessageID() });
411
+ doc.raw.message = { ...doc.raw.message, ...edited };
412
+ ok = true;
413
+ }
414
+ }
415
+ if (ok) {
416
+ doc.caption = caption;
417
+ doc.edited = true;
418
+ await this._wa.engine.set(`/chat/${doc.cid}/message/${doc.id}`, serialize(doc), doc.created_at);
419
+ }
309
420
  }
310
421
  return ok;
311
422
  }
312
- /** Elimina el mensaje. `all=true` (default) borra para todos; `all=false` borra sólo en mi dispositivo. */
313
- async delete(all = true) {
314
- const { _wa: wa, _doc: doc } = this;
423
+ /**
424
+ * Reenvía el mensaje a otro chat.
425
+ * Forwards the message to another chat.
426
+ *
427
+ * @param target - CID, Chat o Contact destino / Target CID, Chat or Contact
428
+ * @returns true si se reenvió / true when forwarded
429
+ */
430
+ async forward(target) {
431
+ const doc = this._raw;
432
+ const to_cid = typeof target === 'string' ? target : target instanceof Chat ? target._raw.id : (target.jid ?? target.lid ?? '');
315
433
  let ok = false;
316
- if (wa._socket) {
434
+ const socket = internals(this._wa).socket;
435
+ if (socket && to_cid) {
436
+ const jid = await internals(this._wa).resolve_jid(to_cid);
437
+ if (jid && doc.raw.message) {
438
+ try {
439
+ const wa_msg = generateWAMessageFromContent(jid, generateForwardMessageContent(doc.raw, false), {
440
+ userJid: socket.user?.id ?? '',
441
+ });
442
+ if (wa_msg.message && wa_msg.key?.id) {
443
+ await socket.relayMessage(jid, wa_msg.message, { messageId: wa_msg.key.id });
444
+ ok = true;
445
+ }
446
+ }
447
+ catch {
448
+ /* forward content may be unsupported */
449
+ }
450
+ }
451
+ }
452
+ return ok;
453
+ }
454
+ /**
455
+ * Elimina el mensaje: solo en mi dispositivo por defecto, para todos con `true`.
456
+ * Deletes the message: this device only by default, for everyone with `true`.
457
+ *
458
+ * @param all - true elimina para todos / true deletes for everyone
459
+ * @returns true si se eliminó / true when deleted
460
+ */
461
+ async delete(all = false) {
462
+ const doc = this._raw;
463
+ let ok = false;
464
+ const socket = internals(this._wa).socket;
465
+ if (socket) {
317
466
  if (all) {
318
- await wa._socket.sendMessage(doc.cid, {
319
- delete: { remoteJid: doc.cid, id: doc.id, fromMe: doc.me },
320
- });
467
+ await socket.sendMessage(doc.cid, { delete: { remoteJid: doc.cid, id: doc.id, fromMe: doc.me } });
321
468
  }
322
469
  else {
323
- await wa._socket.chatModify({
470
+ await socket.chatModify({
324
471
  deleteForMe: {
325
472
  deleteMedia: false,
326
473
  key: { remoteJid: doc.cid, id: doc.id, fromMe: doc.me },
@@ -328,161 +475,372 @@ export class Message {
328
475
  },
329
476
  }, doc.cid);
330
477
  }
331
- await wa.engine.unset(`/chat/${doc.cid}/message/${doc.id}`);
478
+ await this._wa.engine.unset(`/chat/${doc.cid}/message/${doc.id}`);
332
479
  ok = true;
333
480
  }
334
481
  return ok;
335
482
  }
336
- /** Edita el texto/caption del mensaje (solo propios editables). */
337
- async edit(text) {
338
- const { _wa: wa, _doc: doc } = this;
339
- let ok = false;
340
- if (wa._socket && doc.me) {
341
- try {
342
- await wa._socket.sendMessage(doc.cid, {
343
- text,
344
- edit: { remoteJid: doc.cid, id: doc.id, fromMe: true },
345
- });
346
- const content_type = getContentType(doc.raw.message ?? {});
347
- if (content_type === 'conversation') {
348
- doc.raw.message = { conversation: text };
349
- }
350
- else if (content_type === 'extendedTextMessage') {
351
- doc.raw.message = { extendedTextMessage: { text } };
352
- }
353
- doc.caption = text;
354
- doc.edited = true;
355
- await wa.engine.set(`/chat/${doc.cid}/message/${doc.id}`, serialize(doc));
356
- ok = true;
357
- }
358
- catch {
359
- ok = false;
360
- }
361
- }
362
- return ok;
483
+ /** Responde con texto. / Replies with text. */
484
+ async text(caption, extra = {}) {
485
+ return Message.text(this._wa, this._raw.cid, caption, { ...extra, mid: this._raw.id });
363
486
  }
364
- /** Observa cambios sobre este mensaje. Retorna unsubscribe. / Watches this message. Returns unsubscribe. */
365
- watch(handler) {
366
- const cid = this.cid;
367
- const id = this.id;
368
- return this._wa.on('message:updated', (updated) => {
369
- if (updated.cid === cid && updated.id === id) {
370
- handler(updated);
371
- }
372
- });
487
+ /** Responde con imagen. / Replies with an image. */
488
+ async image(buf, extra = {}) {
489
+ return Message.image(this._wa, this._raw.cid, buf, { ...extra, mid: this._raw.id });
373
490
  }
374
- /** Responde con texto. */
375
- async text(caption, opts = {}) {
376
- return this._wa.Message.text(this.cid, caption, { ...opts, mid: this.id });
491
+ /** Responde con video. / Replies with a video. */
492
+ async video(buf, extra = {}) {
493
+ return Message.video(this._wa, this._raw.cid, buf, { ...extra, mid: this._raw.id });
377
494
  }
378
- /** Responde con imagen. */
379
- async image(buf, opts = {}) {
380
- return this._wa.Message.image(this.cid, buf, { ...opts, mid: this.id });
495
+ /** Responde con audio. / Replies with audio. */
496
+ async audio(buf, extra = {}) {
497
+ return Message.audio(this._wa, this._raw.cid, buf, { ...extra, mid: this._raw.id });
381
498
  }
382
- /** Responde con video. */
383
- async video(buf, opts = {}) {
384
- return this._wa.Message.video(this.cid, buf, { ...opts, mid: this.id });
499
+ /** Responde con ubicación. / Replies with a location. */
500
+ async location(loc, extra = {}) {
501
+ return Message.location(this._wa, this._raw.cid, loc, { ...extra, mid: this._raw.id });
385
502
  }
386
- /** Responde con audio. */
387
- async audio(buf, opts = {}) {
388
- return this._wa.Message.audio(this.cid, buf, { ...opts, mid: this.id });
503
+ /** Responde con encuesta. / Replies with a poll. */
504
+ async poll(input, extra = {}) {
505
+ return Message.poll(this._wa, this._raw.cid, input, { ...extra, mid: this._raw.id });
389
506
  }
390
- /** Responde con ubicación. */
391
- async location(loc, opts = {}) {
392
- return this._wa.Message.location(this.cid, loc, { ...opts, mid: this.id });
507
+ /** Responde con documento. / Replies with a document. */
508
+ async document(buf, extra) {
509
+ return Message.document(this._wa, this._raw.cid, buf, { ...extra, mid: this._raw.id });
393
510
  }
394
- /** Responde con encuesta. */
395
- async poll(poll, opts = {}) {
396
- return this._wa.Message.poll(this.cid, poll, { ...opts, mid: this.id });
511
+ /** Responde con tarjeta(s) de contacto. / Replies with contact card(s). */
512
+ async vcard(contacts, extra = {}) {
513
+ return Message.vcard(this._wa, this._raw.cid, contacts, { ...extra, mid: this._raw.id });
397
514
  }
398
- /** Responde con documento. */
399
- async document(buf, opts) {
400
- return this._wa.Message.document(this.cid, buf, { ...opts, mid: this.id });
515
+ /** Responde con evento. / Replies with an event. */
516
+ async event(data, extra = {}) {
517
+ return Message.event(this._wa, this._raw.cid, data, { ...extra, mid: this._raw.id });
518
+ }
519
+ /**
520
+ * Obtiene un mensaje por chat y id.
521
+ * Retrieves a message by chat and id.
522
+ *
523
+ * @param wa - Cliente dueño / Owner client
524
+ * @param cid - Identificador del chat / Chat identifier
525
+ * @param mid - Identificador del mensaje / Message identifier
526
+ * @returns Mensaje o null / Message or null
527
+ */
528
+ static async get(wa, cid, mid) {
529
+ const jid = await internals(wa).resolve_jid(cid);
530
+ if (jid) {
531
+ const doc = deserialize(await wa.engine.get(`/chat/${jid}/message/${mid}`));
532
+ if (doc)
533
+ return message(wa, doc);
534
+ }
535
+ return null;
401
536
  }
402
- /** Responde con tarjeta(s) de contacto. */
403
- async vcard(contacts, opts = {}) {
404
- return this._wa.Message.vcard(this.cid, contacts, { ...opts, mid: this.id });
537
+ /**
538
+ * Pagina los mensajes de un chat desde el más reciente.
539
+ * Paginates chat messages from the most recent one.
540
+ *
541
+ * @param wa - Cliente dueño / Owner client
542
+ * @param cid - Identificador del chat / Chat identifier
543
+ * @param offset - Desplazamiento / Offset
544
+ * @param limit - Tamaño de página / Page size
545
+ * @returns Página de mensajes / Message page
546
+ */
547
+ static async list(wa, cid, offset = 0, limit = 50) {
548
+ const jid = await internals(wa).resolve_jid(cid);
549
+ if (jid) {
550
+ return (await wa.engine.list(`/chat/${jid}/message`, offset, limit))
551
+ .map((raw) => deserialize(raw))
552
+ .filter((doc) => doc !== null)
553
+ .map((doc) => message(wa, doc));
554
+ }
555
+ return [];
556
+ }
557
+ /** Envía texto. / Sends text. */
558
+ static async text(wa, cid, caption, extra = {}) {
559
+ return send(wa, cid, { text: caption }, undefined, extra);
560
+ }
561
+ /** Envía imagen. / Sends an image. */
562
+ static async image(wa, cid, buf, extra = {}) {
563
+ return send(wa, cid, { image: buf, caption: extra.caption }, buf, extra);
564
+ }
565
+ /** Envía video. / Sends a video. */
566
+ static async video(wa, cid, buf, extra = {}) {
567
+ return send(wa, cid, { video: buf, caption: extra.caption }, buf, extra);
568
+ }
569
+ /** Envía audio. / Sends audio. */
570
+ static async audio(wa, cid, buf, extra = {}) {
571
+ return send(wa, cid, { audio: buf, ptt: extra.ptt ?? true }, buf, extra);
572
+ }
573
+ /** Envía ubicación. / Sends a location. */
574
+ static async location(wa, cid, loc, extra = {}) {
575
+ return send(wa, cid, { location: { degreesLatitude: loc.lat, degreesLongitude: loc.lng } }, undefined, extra);
576
+ }
577
+ /** Envía encuesta. / Sends a poll. */
578
+ static async poll(wa, cid, input, extra = {}) {
579
+ const multiple = extra.multiple ?? false;
580
+ return send(wa, cid, {
581
+ poll: {
582
+ name: input.content,
583
+ values: input.options.map((option) => option.content),
584
+ selectableCount: multiple ? 0 : 1,
585
+ },
586
+ }, undefined, extra, { multiple });
587
+ }
588
+ /** Envía documento. / Sends a document. */
589
+ static async document(wa, cid, buf, extra) {
590
+ return send(wa, cid, { document: buf, fileName: extra.file_name, mimetype: extra.mimetype ?? 'application/octet-stream', caption: extra.caption }, buf, extra);
591
+ }
592
+ /** Envía tarjeta(s) de contacto. / Sends contact card(s). */
593
+ static async vcard(wa, cid, contacts, extra = {}) {
594
+ const built = contacts.map((entry) => ({
595
+ displayName: entry.name,
596
+ vcard: [
597
+ 'BEGIN:VCARD',
598
+ 'VERSION:3.0',
599
+ `FN:${entry.name}`,
600
+ `TEL;type=CELL;waid=${entry.phone.replace(/\D+/g, '')}:${entry.phone}`,
601
+ 'END:VCARD',
602
+ ].join('\n'),
603
+ }));
604
+ return send(wa, cid, { contacts: { contacts: built } }, Buffer.from(built.map((entry) => entry.vcard).join('\n'), 'utf-8'), extra);
405
605
  }
406
- /** Responde con evento. */
407
- async event(data, opts = {}) {
408
- return this._wa.Message.event(this.cid, data, { ...opts, mid: this.id });
606
+ /** Envía evento. / Sends an event. */
607
+ static async event(wa, cid, data, extra = {}) {
608
+ const binary = Buffer.from(JSON.stringify({ name: data.name, caption: data.caption, start: data.start.getTime(), end: data.end?.getTime() ?? null }), 'utf-8');
609
+ return send(wa, cid, {
610
+ event: {
611
+ name: data.name,
612
+ description: data.caption,
613
+ startDate: data.start,
614
+ endDate: data.end,
615
+ location: data.place ? { degreesLatitude: data.place.lat, degreesLongitude: data.place.lng } : undefined,
616
+ },
617
+ }, binary, extra);
618
+ }
619
+ /** Reacciona a un mensaje por id. / Reacts to a message by id. */
620
+ static async react(wa, cid, mid, emoji) {
621
+ const msg = await Message.get(wa, cid, mid);
622
+ return msg ? msg.react(emoji) : false;
623
+ }
624
+ /** Destaca un mensaje por id. / Stars a message by id. */
625
+ static async star(wa, cid, mid, value) {
626
+ const msg = await Message.get(wa, cid, mid);
627
+ return msg ? msg.star(value) : false;
628
+ }
629
+ /** Marca un mensaje como leído por id. / Marks a message as read by id. */
630
+ static async seen(wa, cid, mid) {
631
+ const msg = await Message.get(wa, cid, mid);
632
+ return msg ? msg.seen() : false;
633
+ }
634
+ /** Edita un mensaje por id. / Edits a message by id. */
635
+ static async edit(wa, cid, mid, caption) {
636
+ const msg = await Message.get(wa, cid, mid);
637
+ return msg ? msg.edit(caption) : false;
638
+ }
639
+ /** Reenvía un mensaje por id. / Forwards a message by id. */
640
+ static async forward(wa, cid, mid, target) {
641
+ const msg = await Message.get(wa, cid, mid);
642
+ return msg ? msg.forward(target) : false;
643
+ }
644
+ /** Elimina un mensaje por id. / Deletes a message by id. */
645
+ static async delete(wa, cid, mid, all = false) {
646
+ const msg = await Message.get(wa, cid, mid);
647
+ return msg ? msg.delete(all) : false;
648
+ }
649
+ /** Reacciones de un mensaje por id. / Reactions of a message by id. */
650
+ static async reactions(wa, cid, mid) {
651
+ const msg = await Message.get(wa, cid, mid);
652
+ return msg ? msg.reactions() : [];
409
653
  }
410
654
  }
411
655
  /**
412
- * Mensaje de texto. Override `content()` para retornar el caption como Buffer sin pasar por engine.
413
- * Text message. Overrides `content()` returning caption as Buffer directly.
656
+ * Mensaje de texto; `preview()` expone la tarjeta del enlace embebido.
657
+ * Text message; `preview()` exposes the embedded link card.
414
658
  */
415
659
  export class Text extends Message {
416
- async content() {
417
- return Buffer.from(this.caption, 'utf-8');
418
- }
419
- }
420
- /** @internal Helper compartido por media classes: stream del binario (cache download). */
421
- async function _media_stream(msg) {
422
- const wa = msg._wa;
423
- const doc = msg._doc;
424
- const cached = deserialize(await wa.engine.get(`/chat/${doc.cid}/message/${doc.id}/content`));
425
- if (cached?.data) {
426
- return Readable.from(Buffer.from(cached.data, 'base64'));
427
- }
428
- if (wa._socket) {
429
- try {
430
- return (await downloadMediaMessage(doc.raw, 'stream', {}));
431
- }
432
- catch {
433
- /* fallback empty */
660
+ /**
661
+ * Preview del enlace del texto, o null cuando no trae metadata.
662
+ * Text link preview, or null when it carries no metadata.
663
+ *
664
+ * @returns { link, name, content, thumb } o null / { link, name, content, thumb } or null
665
+ */
666
+ async preview() {
667
+ const ext = this._raw.raw.message?.extendedTextMessage;
668
+ if (ext?.matchedText && (ext.title || ext.description)) {
669
+ return {
670
+ link: ext.matchedText,
671
+ name: ext.title ?? null,
672
+ content: ext.description ?? null,
673
+ thumb: to_buffer(ext.jpegThumbnail),
674
+ };
434
675
  }
676
+ return null;
435
677
  }
436
- return Readable.from(Buffer.alloc(0));
437
- }
438
- /** @internal Acumula un Readable en Buffer. */
439
- async function _drain(readable) {
440
- const chunks = [];
441
- for await (const chunk of readable) {
442
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
443
- }
444
- return Buffer.concat(chunks);
445
678
  }
446
679
  /**
447
- * Mensaje de imagen. Hereda `stream()` de la clase base; `content()` drena el stream.
448
- * Image message. Inherits `stream()` from the base class; `content()` drains the stream.
680
+ * Mensaje de imagen: dimensiones, peso y miniatura.
681
+ * Image message: dimensions, size and thumbnail.
449
682
  */
450
683
  export class Image extends Message {
684
+ /** @internal */
685
+ get _media() {
686
+ return unwrap(this._raw.raw.message ?? {}).imageMessage;
687
+ }
688
+ /** Ancho en píxeles. / Width in pixels. */
689
+ get width() {
690
+ return this._media?.width ?? 0;
691
+ }
692
+ /** Alto en píxeles. / Height in pixels. */
693
+ get height() {
694
+ return this._media?.height ?? 0;
695
+ }
696
+ /** Peso en bytes. / Size in bytes. */
697
+ get size() {
698
+ return to_number(this._media?.fileLength);
699
+ }
700
+ /**
701
+ * Miniatura JPEG embebida, o null.
702
+ * Embedded JPEG thumbnail, or null.
703
+ *
704
+ * @returns Buffer de la miniatura o null / Thumbnail buffer or null
705
+ */
706
+ async thumb() {
707
+ return to_buffer(this._media?.jpegThumbnail);
708
+ }
451
709
  async content() {
452
- return _drain(await this.stream());
710
+ return drain(await this.stream());
453
711
  }
454
712
  }
455
713
  /**
456
- * Mensaje de video. Hereda `stream()` de la clase base; `content()` drena el stream.
457
- * Video message. Inherits `stream()` from the base class; `content()` drains the stream.
714
+ * Mensaje de video: dimensiones, duración, peso y miniatura.
715
+ * Video message: dimensions, duration, size and thumbnail.
458
716
  */
459
717
  export class Video extends Message {
718
+ /** @internal */
719
+ get _media() {
720
+ return unwrap(this._raw.raw.message ?? {}).videoMessage;
721
+ }
722
+ /** Ancho en píxeles. / Width in pixels. */
723
+ get width() {
724
+ return this._media?.width ?? 0;
725
+ }
726
+ /** Alto en píxeles. / Height in pixels. */
727
+ get height() {
728
+ return this._media?.height ?? 0;
729
+ }
730
+ /** Duración en segundos. / Duration in seconds. */
731
+ get duration() {
732
+ return this._media?.seconds ?? 0;
733
+ }
734
+ /** Peso en bytes. / Size in bytes. */
735
+ get size() {
736
+ return to_number(this._media?.fileLength);
737
+ }
738
+ /**
739
+ * Miniatura JPEG embebida, o null.
740
+ * Embedded JPEG thumbnail, or null.
741
+ *
742
+ * @returns Buffer de la miniatura o null / Thumbnail buffer or null
743
+ */
744
+ async thumb() {
745
+ return to_buffer(this._media?.jpegThumbnail);
746
+ }
460
747
  async content() {
461
- return _drain(await this.stream());
748
+ return drain(await this.stream());
462
749
  }
463
750
  }
464
751
  /**
465
- * Mensaje de audio. Hereda `stream()` de la clase base; `content()` drena el stream.
466
- * `ptt` indica si es nota de voz (push-to-talk).
467
- * Audio message. Inherits `stream()` from the base class; `content()` drains the stream.
468
- * `ptt` flags voice notes.
752
+ * Mensaje de audio: nota de voz o archivo, con forma de onda del protocolo.
753
+ * Audio message: voice note or file, with the protocol waveform.
469
754
  */
470
755
  export class Audio extends Message {
471
- /** true si es nota de voz (push-to-talk). / true if push-to-talk voice note. */
756
+ /** @internal */
757
+ get _media() {
758
+ return unwrap(this._raw.raw.message ?? {}).audioMessage;
759
+ }
760
+ /** true si es nota de voz. / true when push-to-talk. */
472
761
  get ptt() {
473
- return this._doc.raw.message?.audioMessage?.ptt === true;
762
+ return this._media?.ptt === true;
763
+ }
764
+ /** Duración en segundos. / Duration in seconds. */
765
+ get duration() {
766
+ return this._media?.seconds ?? 0;
767
+ }
768
+ /** Peso en bytes. / Size in bytes. */
769
+ get size() {
770
+ return to_number(this._media?.fileLength);
771
+ }
772
+ /** Forma de onda 0-100 lista para pintar. / Paint-ready 0-100 waveform. */
773
+ get waveform() {
774
+ const wave = this._media?.waveform;
775
+ const bytes = wave instanceof Uint8Array ? wave : typeof wave === 'string' ? Buffer.from(wave, 'base64') : Buffer.alloc(0);
776
+ return Array.from(bytes);
474
777
  }
475
778
  async content() {
476
- return _drain(await this.stream());
779
+ return drain(await this.stream());
477
780
  }
478
781
  }
479
782
  /**
480
- * Mensaje de ubicación. Agrega getters sync `lat`, `lng`, `link` derivados del raw.
481
- * Location message. Adds sync getters `lat`, `lng`, `link` derived from raw.
783
+ * Mensaje de sticker: dimensiones, peso y animación.
784
+ * Sticker message: dimensions, size and animation.
482
785
  */
483
- export class Gps extends Message {
786
+ export class Sticker extends Message {
787
+ /** @internal */
788
+ get _media() {
789
+ return unwrap(this._raw.raw.message ?? {}).stickerMessage;
790
+ }
791
+ /** Ancho en píxeles. / Width in pixels. */
792
+ get width() {
793
+ return this._media?.width ?? 0;
794
+ }
795
+ /** Alto en píxeles. / Height in pixels. */
796
+ get height() {
797
+ return this._media?.height ?? 0;
798
+ }
799
+ /** true si el sticker es animado. / true when animated. */
800
+ get animated() {
801
+ return this._media?.isAnimated === true;
802
+ }
803
+ /** Peso en bytes. / Size in bytes. */
804
+ get size() {
805
+ return to_number(this._media?.fileLength);
806
+ }
807
+ async content() {
808
+ return drain(await this.stream());
809
+ }
810
+ }
811
+ /**
812
+ * Mensaje de documento; el MIME real vive en `mime` de la base.
813
+ * Document message; the actual MIME lives in the base `mime`.
814
+ */
815
+ export class Document extends Message {
816
+ /** @internal */
817
+ get _media() {
818
+ return unwrap(this._raw.raw.message ?? {}).documentMessage;
819
+ }
820
+ /** Nombre del archivo. / File name. */
821
+ get name() {
822
+ return this._media?.fileName ?? '';
823
+ }
824
+ /** Número de páginas (PDF); 0 cuando no aplica. / Page count (PDF); 0 when not applicable. */
825
+ get pages() {
826
+ return this._media?.pageCount ?? 0;
827
+ }
828
+ /** Peso en bytes. / Size in bytes. */
829
+ get size() {
830
+ return to_number(this._media?.fileLength);
831
+ }
832
+ async content() {
833
+ return drain(await this.stream());
834
+ }
835
+ }
836
+ /**
837
+ * Mensaje de ubicación, fija o en vivo.
838
+ * Location message, static or live.
839
+ */
840
+ export class Location extends Message {
841
+ /** @internal */
484
842
  get _loc() {
485
- return this._doc.raw.message?.locationMessage ?? this._doc.raw.message?.liveLocationMessage;
843
+ return this._raw.raw.message?.locationMessage ?? this._raw.raw.message?.liveLocationMessage;
486
844
  }
487
845
  /** Latitud en grados. / Latitude in degrees. */
488
846
  get lat() {
@@ -492,283 +850,184 @@ export class Gps extends Message {
492
850
  get lng() {
493
851
  return this._loc?.degreesLongitude ?? 0;
494
852
  }
495
- /** URL de Google Maps apuntando a la coordenada. / Google Maps URL for the coordinate. */
496
- get link() {
497
- return `https://www.google.com/maps/@${this.lat},${this.lng},15z`;
498
- }
499
853
  /** true si es ubicación en tiempo real. / true when live location. */
500
854
  get live() {
501
- return Boolean(this._doc.raw.message?.liveLocationMessage);
855
+ return Boolean(this._raw.raw.message?.liveLocationMessage);
856
+ }
857
+ /** URL de Google Maps de la coordenada. / Google Maps URL for the coordinate. */
858
+ get link() {
859
+ return `https://www.google.com/maps/@${this.lat},${this.lng},15z`;
502
860
  }
503
861
  }
504
862
  /**
505
- * Mensaje de encuesta. `caption` es la pregunta (heredada); agrega `options[]` con conteo y `multiple`.
506
- * Poll message. `caption` is the question (inherited); adds `options[]` with counts and `multiple`.
863
+ * Mensaje de encuesta: opciones con conteo, votos por contacto y voto propio.
864
+ * Poll message: options with counts, per-contact votes and self-voting.
507
865
  */
508
866
  export class Poll extends Message {
867
+ /** @internal */
509
868
  get _poll() {
510
- const m = this._doc.raw.message;
511
- return m?.pollCreationMessage ?? m?.pollCreationMessageV2 ?? m?.pollCreationMessageV3;
869
+ const msg = this._raw.raw.message;
870
+ return msg?.pollCreationMessage ?? msg?.pollCreationMessageV2 ?? msg?.pollCreationMessageV3;
871
+ }
872
+ /** @internal Updates de voto con los bytes normalizados post-engine. / Vote updates with post-engine normalized bytes. */
873
+ get _updates() {
874
+ return (this._raw.raw.pollUpdates ?? []).map((update) => ({
875
+ ...update,
876
+ vote: update.vote
877
+ ? {
878
+ ...update.vote,
879
+ selectedOptions: (update.vote.selectedOptions ?? []).map((option) => typeof option === 'string' ? Buffer.from(option, 'base64') : option),
880
+ }
881
+ : update.vote,
882
+ }));
512
883
  }
513
- /**
514
- * true si admite múltiples respuestas. Lee primero el flag explícito persistido
515
- * al enviar (caso propio, donde WhatsApp pierde el discriminador en el echo);
516
- * fallback al `selectableOptionsCount` del proto (polls foráneos o de UI móvil).
517
- * true if the poll accepts multiple answers. Prefers the persisted send-time
518
- * flag (own polls, since WhatsApp drops the discriminator on echo); falls back
519
- * to the proto `selectableOptionsCount` (foreign or mobile-UI polls).
520
- */
884
+ /** true si admite varias respuestas. / true when multi-select. */
521
885
  get multiple() {
522
- if (typeof this._doc.multiple === 'boolean') {
523
- return this._doc.multiple;
886
+ if (typeof this._raw.multiple === 'boolean') {
887
+ return this._raw.multiple;
524
888
  }
525
889
  return (this._poll?.selectableOptionsCount ?? 1) !== 1;
526
890
  }
527
- /** Opciones con conteo de votos actualizado. / Options with up-to-date vote counts. */
891
+ /** Opciones con su conteo de votos. / Options with their vote counts. */
528
892
  get options() {
529
- const opts = this._poll?.options ?? [];
530
- const raw_updates = this._doc.raw.pollUpdates ?? [];
531
- // Normaliza selectedOptions de base64-string a Buffer (BufferJSON no reconstruye el tipo aquí)
532
- const updates = raw_updates.map((u) => ({
533
- ...u,
534
- vote: u.vote
535
- ? {
536
- ...u.vote,
537
- selectedOptions: (u.vote.selectedOptions ?? []).map((s) => typeof s === 'string' ? Buffer.from(s, 'base64') : s),
538
- }
539
- : u.vote,
540
- }));
541
- const aggregated = getAggregateVotesInPollMessage({ message: this._doc.raw.message ?? undefined, pollUpdates: updates }, this._wa._socket?.user?.id);
542
- const counts = new Map(aggregated.map((a) => [a.name, a.voters.length]));
543
- return opts.map((o) => ({
544
- content: o.optionName ?? '',
545
- count: counts.get(o.optionName ?? '') ?? 0,
893
+ const aggregated = getAggregateVotesInPollMessage({ message: this._raw.raw.message ?? undefined, pollUpdates: this._updates }, internals(this._wa).socket?.user?.id);
894
+ const counts = new Map(aggregated.map((entry) => [entry.name, entry.voters.length]));
895
+ return (this._poll?.options ?? []).map((option) => ({
896
+ name: option.optionName ?? '',
897
+ count: counts.get(option.optionName ?? '') ?? 0,
546
898
  }));
547
899
  }
548
900
  /**
549
- * Vota en la encuesta. Acepta un índice o lista de índices (para `multiple`).
550
- * Cifra el voto (HMAC+AES-GCM, igual que baileys.decryptPollVote) y lo enruta al chat
551
- * @lid del contacto con la identidad LID propia, replicando byte a byte un voto entrante
552
- * que sí descifra. El voto ENTRANTE (contacto → este cliente) funciona; el voto propio
553
- * de este cliente aplicado a una encuesta NO se refleja en el teléfono del destinatario
554
- * cuando este cliente es un dispositivo vinculado (companion): estructura, identidades y
555
- * addressing coinciden con un voto válido, pero WhatsApp no lo propaga. Best-effort.
901
+ * Votos individuales: opción elegida y teléfono del votante.
902
+ * Individual votes: chosen option and voter phone.
556
903
  *
557
- * Votes in the poll. Encrypts the vote (HMAC+AES-GCM, same as baileys.decryptPollVote)
558
- * and routes it to the contact's @lid chat with our own LID identity, mirroring a working
559
- * incoming vote byte-for-byte. INCOMING votes work; a self-vote from this client does NOT
560
- * show on the recipient's phone when this client is a linked (companion) device — the
561
- * structure, identities and addressing match a valid vote but WhatsApp won't propagate it.
562
- * Best-effort.
904
+ * @returns Lista de votos / Vote list
905
+ */
906
+ async votes() {
907
+ const aggregated = getAggregateVotesInPollMessage({ message: this._raw.raw.message ?? undefined, pollUpdates: this._updates }, internals(this._wa).socket?.user?.id);
908
+ const result = [];
909
+ for (const entry of aggregated) {
910
+ for (const voter of entry.voters) {
911
+ const jid = await internals(this._wa).resolve_jid(voter);
912
+ result.push({ name: entry.name, contact: (jid ?? voter).split('@')[0].split(':')[0] });
913
+ }
914
+ }
915
+ return result;
916
+ }
917
+ /**
918
+ * Vota en la encuesta con uno o varios índices. Cifra el voto (HMAC+AES-GCM) y lo
919
+ * enruta al chat @lid con la identidad LID propia. Best-effort: el voto entrante
920
+ * descifra correcto, pero WhatsApp no propaga el voto emitido desde un dispositivo
921
+ * vinculado (companion).
922
+ * Votes in the poll with one or more indexes. Encrypts the vote (HMAC+AES-GCM) and
923
+ * routes it to the @lid chat with the own LID identity. Best-effort: incoming votes
924
+ * decrypt fine, but WhatsApp does not propagate votes emitted from a linked
925
+ * (companion) device.
926
+ *
927
+ * @param index - Índice o índices de la opción / Option index or indexes
928
+ * @returns true si el voto se emitió / true when the vote was relayed
563
929
  */
564
930
  async select(index) {
565
- const { _wa, _doc } = this;
566
- const indices = Array.isArray(index) ? index : [index];
931
+ const doc = this._raw;
567
932
  const opts = this._poll?.options ?? [];
568
- const selected_options = indices
933
+ const selected_options = (Array.isArray(index) ? index : [index])
569
934
  .filter((i) => i >= 0 && i < opts.length)
570
935
  .map((i) => sha256(Buffer.from(opts[i].optionName ?? '')));
571
- // messageSecret puede llegar como Buffer (raw runtime) o string base64 (post-deserialize
572
- // del engine). Normaliza a Buffer antes de derivar el HMAC.
573
- // messageSecret may arrive as Buffer (runtime raw) or base64 string (post engine
574
- // deserialize). Normalize to Buffer before deriving the HMAC.
575
- const secret_raw = _doc.raw.message?.messageContextInfo?.messageSecret;
936
+ const secret_raw = doc.raw.message?.messageContextInfo?.messageSecret;
576
937
  const poll_enc_key = typeof secret_raw === 'string' ? Buffer.from(secret_raw, 'base64') : secret_raw;
577
- if (!poll_enc_key || !_wa._socket || selected_options.length === 0) {
938
+ const socket = internals(this._wa).socket;
939
+ if (!poll_enc_key || !socket || selected_options.length === 0) {
578
940
  return false;
579
941
  }
580
- const poll_key = _doc.raw.key ?? {};
581
- const self_id = _wa._socket.user?.id ?? '';
582
- const self_lid = _wa._socket.user?.lid ?? '';
583
- // El voto se enruta al chat @lid del contacto: los clientes migrados corren el chat
584
- // en @lid y derivan la clave del JID visible del stanza; enviarlo por PN lo deja
585
- // indescifrable en el teléfono. El @lid destino sale del propio poll (si ya es @lid),
586
- // del store de contactos, o del lidMapping de baileys.
587
- // The vote is routed to the contact's @lid chat: migrated clients run the chat on
588
- // @lid and derive the key from the stanza's visible JID; sending via PN leaves it
589
- // undecryptable on the phone. The @lid dest comes from the poll (if already @lid),
590
- // the contact store, or baileys' lidMapping.
591
- let dest = _doc.cid;
942
+ const poll_key = doc.raw.key ?? {};
943
+ const self_id = socket.user?.id ?? '';
944
+ const self_lid = socket.user?.lid ?? '';
945
+ let dest = doc.cid;
592
946
  if (poll_key.remoteJid?.endsWith('@lid')) {
593
947
  dest = poll_key.remoteJid;
594
948
  }
595
- else if (!_doc.cid.endsWith('@lid')) {
596
- const contact_raw = deserialize(await _wa.engine.get(`/contact/${_doc.cid}`));
597
- const mapped = await _wa._socket.signalRepository?.lidMapping?.getLIDForPN(_doc.cid).catch(() => null);
598
- dest = contact_raw?.lid ?? mapped ?? _doc.cid;
949
+ else if (!doc.cid.endsWith('@lid')) {
950
+ const contact_raw = deserialize(await this._wa.engine.get(`/contact/${doc.cid}`));
951
+ const mapped = await socket.signalRepository?.lidMapping?.getLIDForPN(doc.cid).catch(() => null);
952
+ dest = contact_raw?.lid ?? mapped ?? doc.cid;
599
953
  }
600
- // Identidad del HMAC atada al destino real: baileys firma con meLid cuando el envío
601
- // va por @lid, así que voter y (en polls propios) creator usan el LID propio para que
602
- // el receptor derive la misma clave. PN sólo si no se conoce ningún LID.
603
- // HMAC identity tied to the actual destination: baileys signs with meLid for @lid
604
- // sends, so voter and (on own polls) creator use our LID so the receiver derives the
605
- // same key. PN only when no LID is known.
606
954
  const self_for_send = dest.endsWith('@lid') && self_lid ? self_lid : self_id;
607
955
  const voter_jid = jidNormalizedUser(self_for_send);
608
- let poll_creator_raw;
609
- if (poll_key.fromMe) {
610
- poll_creator_raw = self_for_send;
611
- }
612
- else if (poll_key.remoteJid?.endsWith('@lid')) {
613
- poll_creator_raw = poll_key.remoteJid;
614
- }
615
- else {
616
- poll_creator_raw = getKeyAuthor(poll_key, self_id);
617
- }
618
- const poll_creator_jid = jidNormalizedUser(poll_creator_raw);
619
- const poll_msg_id = this.id;
620
- // Derivar clave y encriptar el voto (mismo algoritmo que baileys.decryptPollVote)
956
+ const poll_creator_jid = jidNormalizedUser(poll_key.fromMe
957
+ ? self_for_send
958
+ : poll_key.remoteJid?.endsWith('@lid')
959
+ ? poll_key.remoteJid
960
+ : getKeyAuthor(poll_key, self_id));
621
961
  const sign = Buffer.concat([
622
- Buffer.from(poll_msg_id),
962
+ Buffer.from(doc.id),
623
963
  Buffer.from(poll_creator_jid),
624
964
  Buffer.from(voter_jid),
625
965
  Buffer.from('Poll Vote'),
626
966
  new Uint8Array([1]),
627
967
  ]);
628
- const key0 = hmacSign(poll_enc_key, new Uint8Array(32), 'sha256');
629
- const enc_key = hmacSign(sign, key0, 'sha256');
630
- const payload = proto.Message.PollVoteMessage.encode({
631
- selectedOptions: selected_options,
632
- }).finish();
633
- const aad = Buffer.from(`${poll_msg_id}\x00${voter_jid}`);
968
+ const enc_key = hmacSign(sign, hmacSign(poll_enc_key, new Uint8Array(32), 'sha256'), 'sha256');
634
969
  const enc_iv = randomBytes(12);
635
- const enc_payload = aesEncryptGCM(payload, enc_key, enc_iv, aad);
970
+ const enc_payload = aesEncryptGCM(proto.Message.PollVoteMessage.encode({ selectedOptions: selected_options }).finish(), enc_key, enc_iv, Buffer.from(`${doc.id}\x00${voter_jid}`));
636
971
  const msg_id = generateMessageID();
637
- // La pollCreationMessageKey debe referenciar el poll con el mismo addressing que el
638
- // stanza (dest): un voto entrante que sí funciona trae la key con remoteJid = @lid del
639
- // creador, coincidiendo con el chat @lid del stanza. Enviarla con remoteJid PN mientras
640
- // el stanza va por @lid deja al receptor sin poder casar el voto con el poll.
641
- // The pollCreationMessageKey must reference the poll with the same addressing as the
642
- // stanza (dest): a working incoming vote carries the key with the creator's @lid
643
- // remoteJid, matching the @lid stanza chat. A PN remoteJid on an @lid stanza leaves the
644
- // receiver unable to match the vote to the poll.
645
- const creation_key = dest === _doc.cid ? _doc.raw.key : { ..._doc.raw.key, remoteJid: dest };
646
- await _wa._socket.relayMessage(dest, {
972
+ await socket.relayMessage(dest, {
647
973
  pollUpdateMessage: {
648
- pollCreationMessageKey: creation_key,
974
+ pollCreationMessageKey: dest === doc.cid ? doc.raw.key : { ...doc.raw.key, remoteJid: dest },
649
975
  vote: { encPayload: enc_payload, encIv: enc_iv },
650
976
  senderTimestampMs: Date.now(),
651
977
  },
652
978
  }, { messageId: msg_id });
653
- // baileys.relayMessage no echa upsert local al companion que envió el voto.
654
- // Mergemos el voto propio en el doc para que `options[].count` y el evento
655
- // `message:updated` reflejen el cambio sin esperar (nunca) un echo del servidor.
656
- // baileys.relayMessage doesn't emit a local upsert on the originating companion.
657
- // We merge the own vote into the doc so `options[].count` and the `message:updated`
658
- // event reflect the change without waiting for a server echo that never comes.
659
- const own_update_key = {
660
- remoteJid: _doc.raw.key?.remoteJid ?? _doc.cid,
661
- fromMe: true,
662
- id: msg_id,
663
- };
664
- updateMessageWithPollUpdate(_doc.raw, {
665
- pollUpdateMessageKey: own_update_key,
979
+ updateMessageWithPollUpdate(doc.raw, {
980
+ pollUpdateMessageKey: { remoteJid: doc.raw.key?.remoteJid ?? doc.cid, fromMe: true, id: msg_id },
666
981
  vote: { selectedOptions: selected_options },
667
982
  senderTimestampMs: Date.now(),
668
983
  });
669
- await _wa.engine.set(`/chat/${_doc.cid}/message/${_doc.id}`, serialize(_doc));
670
- _wa._event.emit('message:updated', this, await this.chat(), _wa);
984
+ await this._wa.engine.set(`/chat/${doc.cid}/message/${doc.id}`, serialize(doc), doc.created_at);
985
+ this._wa.emit('message:updated', this, await this.chat(), this._wa);
671
986
  return true;
672
987
  }
673
988
  }
674
989
  /**
675
- * Mensaje de documento. WhatsApp permite enviar cualquier archivo (PDF, imagen,
676
- * audio, video, etc.) como documento; lo que lo distingue de image/video/audio es
677
- * el tipo de mensaje (`documentMessage`), no el mimetype. Hereda `stream()` de la
678
- * clase base; `content()` drena el stream (cache → download).
679
- * Document message. WhatsApp lets you send any file (PDF, image, audio, video…)
680
- * as a document; what sets it apart from image/video/audio is the message type
681
- * (`documentMessage`), not the mimetype. Inherits `stream()` from the base class;
682
- * `content()` drains the stream (cache → download).
683
- */
684
- export class Document extends Message {
685
- get _doc_msg() {
686
- const m = this._doc.raw.message;
687
- return m?.documentMessage ?? m?.documentWithCaptionMessage?.message?.documentMessage;
688
- }
689
- /** Nombre del archivo. / File name. */
690
- get file_name() {
691
- return this._doc_msg?.fileName ?? '';
692
- }
693
- /** Tamaño en bytes (0 si se desconoce). / Size in bytes (0 if unknown). */
694
- get size() {
695
- return Number(this._doc_msg?.fileLength?.toString() ?? '0');
696
- }
697
- /** Número de páginas (PDF); 0 si no aplica. / Page count (PDF); 0 when not applicable. */
698
- get pages() {
699
- return this._doc_msg?.pageCount ?? 0;
700
- }
701
- /** Título del documento. / Document title. */
702
- get title() {
703
- return this._doc_msg?.title ?? '';
704
- }
705
- async content() {
706
- return _drain(await this.stream());
707
- }
708
- }
709
- /**
710
- * Construye un vCard 3.0 mínimo con el teléfono enlazado como contacto de WhatsApp.
711
- * Builds a minimal vCard 3.0 with the phone linked as a WhatsApp contact.
712
- *
713
- * @param name - Nombre mostrado / Display name
714
- * @param phone - Teléfono en formato internacional / Phone in international format
715
- * @returns String vCard (VCF) / vCard (VCF) string
716
- */
717
- function build_vcard(name, phone) {
718
- const waid = phone.replace(/\D+/g, '');
719
- return [
720
- 'BEGIN:VCARD',
721
- 'VERSION:3.0',
722
- `FN:${name}`,
723
- `TEL;type=CELL;waid=${waid}:${phone}`,
724
- 'END:VCARD',
725
- ].join('\n');
726
- }
727
- /**
728
- * Mensaje de tarjeta(s) de contacto (vCard). Unifica `contactMessage` (uno) y
729
- * `contactsArrayMessage` (varios) bajo una sola lista `contacts`.
730
- * Contact card message (vCard). Unifies `contactMessage` (single) and
731
- * `contactsArrayMessage` (multiple) under a single `contacts` list.
990
+ * Mensaje de tarjeta(s) de contacto.
991
+ * Contact card(s) message.
732
992
  */
733
993
  export class VCard extends Message {
734
- /** Contactos de la tarjeta con nombre y teléfono. / Card contacts with name and phone. */
994
+ /** Contactos de la tarjeta. / Card contacts. */
735
995
  get contacts() {
736
- const m = this._doc.raw.message;
737
- const raw = m?.contactsArrayMessage?.contacts ?? (m?.contactMessage ? [m.contactMessage] : []);
738
- return raw.map((c) => {
739
- const tel = (c.vcard ?? '').match(/TEL[^:]*:(.+)/);
740
- return { name: c.displayName ?? '', phone: tel ? tel[1].trim() : '' };
996
+ const msg = this._raw.raw.message;
997
+ const raw = msg?.contactsArrayMessage?.contacts ?? (msg?.contactMessage ? [msg.contactMessage] : []);
998
+ return raw.map((card) => {
999
+ const tel = (card.vcard ?? '').match(/TEL[^:]*:(.+)/);
1000
+ return { name: card.displayName ?? '', phone: tel ? tel[1].trim() : '' };
741
1001
  });
742
1002
  }
743
1003
  }
744
1004
  /**
745
- * Mensaje de evento de calendario (`eventMessage`): nombre, descripción, rango de
746
- * fechas, estado de cancelación, link de unión y ubicación.
747
- * Calendar event message (`eventMessage`): name, description, date range,
748
- * cancellation state, join link and location.
1005
+ * Mensaje de evento de calendario.
1006
+ * Calendar event message.
749
1007
  */
750
1008
  export class Event extends Message {
1009
+ /** @internal */
751
1010
  get _event() {
752
- return this._doc.raw.message?.eventMessage;
1011
+ return this._raw.raw.message?.eventMessage;
753
1012
  }
754
1013
  /** Nombre del evento. / Event name. */
755
1014
  get name() {
756
1015
  return this._event?.name ?? '';
757
1016
  }
758
- /** Epoch ms de inicio. / Start epoch ms. */
1017
+ /** Inicio en ISO UTC. / Start as ISO UTC. */
759
1018
  get start() {
760
- return Number(this._event?.startTime?.toString() ?? '0') * 1000;
1019
+ return new Date(to_number(this._event?.startTime) * 1000).toISOString();
761
1020
  }
762
- /** Epoch ms de fin, o null. / End epoch ms, or null. */
1021
+ /** Fin en ISO UTC, o null. / End as ISO UTC, or null. */
763
1022
  get end() {
764
1023
  const end = this._event?.endTime;
765
- return end == null ? null : Number(end.toString()) * 1000;
1024
+ return end == null ? null : new Date(to_number(end) * 1000).toISOString();
766
1025
  }
767
- /** true si el evento fue cancelado. / true if the event was canceled. */
1026
+ /** true si fue cancelado. / true when canceled. */
768
1027
  get canceled() {
769
1028
  return this._event?.isCanceled ?? false;
770
1029
  }
771
- /** Link de unión (audio/video), si aplica. / Join link (audio/video), if any. */
1030
+ /** Link de unión, si aplica. / Join link, if any. */
772
1031
  get link() {
773
1032
  return this._event?.joinLink ?? '';
774
1033
  }
@@ -779,205 +1038,74 @@ export class Event extends Message {
779
1038
  }
780
1039
  }
781
1040
  /**
782
- * Factoría que enlaza los statics a `wa` y expone las subclases.
783
- * Factory binding statics to `wa` and exposing subclasses.
1041
+ * Factoría de Message: evalúa el tipo y retorna la instancia de la subclase correcta.
1042
+ * Acepta el documento persistido o el mensaje crudo de baileys — en ese caso el
1043
+ * documento se deriva aquí mismo (id, tipo, autor, caption, mime, fechas).
1044
+ * Message factory: evaluates the type and returns the right subclass instance. Accepts
1045
+ * the persisted document or the raw baileys message — in that case the document is
1046
+ * derived right here (id, type, author, caption, mime, dates).
1047
+ *
1048
+ * @param wa - Instancia principal / Main WhatsApp instance
1049
+ * @param raw - Documento persistido o WAMessage crudo / Persisted document or raw WAMessage
1050
+ * @returns Instancia del tipo correcto / Right-type instance
784
1051
  */
785
- export function message(wa) {
786
- async function build_instance(doc) {
787
- const opts = { wa, doc };
788
- if (doc.type === 'text') {
789
- return new Text(opts);
790
- }
791
- if (doc.type === 'image') {
792
- return new Image(opts);
793
- }
794
- if (doc.type === 'video') {
795
- return new Video(opts);
796
- }
797
- if (doc.type === 'audio') {
798
- return new Audio(opts);
799
- }
800
- if (doc.type === 'location') {
801
- return new Gps(opts);
802
- }
803
- if (doc.type === 'poll') {
804
- return new Poll(opts);
805
- }
806
- if (doc.type === 'document') {
807
- return new Document(opts);
808
- }
809
- if (doc.type === 'vcard') {
810
- return new VCard(opts);
811
- }
812
- if (doc.type === 'event') {
813
- return new Event(opts);
814
- }
815
- return new Message(opts);
816
- }
817
- async function send(cid, content, binary, reply_to, doc_overrides) {
818
- let result = null;
819
- const jid = await wa._resolve_jid(cid);
820
- if (jid && wa._socket) {
821
- let quoted;
822
- if (reply_to) {
823
- const ref_doc = deserialize(await wa.engine.get(`/chat/${jid}/message/${reply_to}`));
824
- if (ref_doc) {
825
- quoted = ref_doc.raw;
826
- }
827
- }
828
- const raw = await wa._socket.sendMessage(jid, {
829
- ...content,
830
- ...(quoted ? { quoted } : {}),
831
- });
832
- const sent_id = raw?.key?.id;
833
- if (raw && sent_id) {
834
- const doc = build_message_doc(raw, wa._socket?.user?.id);
835
- if (doc_overrides) {
836
- Object.assign(doc, doc_overrides);
837
- }
838
- await wa.engine.set(`/chat/${jid}/message/${sent_id}`, serialize(doc));
839
- if (binary) {
840
- await wa.engine.set(`/chat/${jid}/message/${sent_id}/content`, serialize({ data: binary.toString('base64') }));
841
- }
842
- result = await build_instance(doc);
843
- }
844
- }
845
- return result;
1052
+ export function message(wa, raw) {
1053
+ let doc;
1054
+ if ('key' in raw) {
1055
+ const key = raw.key ?? {};
1056
+ const unwrapped = unwrap(raw.message ?? {});
1057
+ const content_type = getContentType(unwrapped);
1058
+ const msg_type = MESSAGE_TYPE_MAP[content_type] ?? 'text';
1059
+ const msg_content = unwrapped[content_type];
1060
+ const context_info = msg_content?.contextInfo;
1061
+ const ephemeral_duration = raw.ephemeralDuration ?? context_info?.expiration;
1062
+ const self_id = internals(wa).socket?.user?.id;
1063
+ doc = {
1064
+ id: key.id ?? '',
1065
+ cid: key.remoteJidAlt ?? key.remoteJid ?? '',
1066
+ mid: context_info?.stanzaId ?? null,
1067
+ me: key.fromMe ?? false,
1068
+ type: msg_type,
1069
+ author: key.fromMe && self_id
1070
+ ? jidNormalizedUser(self_id)
1071
+ : key.participant || key.remoteJidAlt || key.remoteJid || '',
1072
+ status: raw.status ?? 1,
1073
+ starred: raw.starred ?? false,
1074
+ forwarded: context_info?.isForwarded ?? false,
1075
+ created_at: (Number(raw.messageTimestamp) || Math.floor(Date.now() / 1000)) * 1000,
1076
+ deleted_at: raw.ephemeralStartTimestamp != null && ephemeral_duration != null
1077
+ ? (Number(raw.ephemeralStartTimestamp) + ephemeral_duration) * 1000
1078
+ : null,
1079
+ mime: typeof msg_content === 'object' && msg_type !== 'text'
1080
+ ? (msg_content?.mimetype ?? 'application/octet-stream')
1081
+ : 'text/plain',
1082
+ caption: typeof msg_content === 'string'
1083
+ ? msg_content
1084
+ : msg_type === 'event'
1085
+ ? (msg_content?.description ?? '')
1086
+ : (msg_content?.caption ??
1087
+ msg_content?.text ??
1088
+ msg_content?.name ??
1089
+ msg_content?.displayName ??
1090
+ ''),
1091
+ edited: false,
1092
+ raw,
1093
+ };
846
1094
  }
847
- const api = {
848
- // Clases expuestas para instanceof
849
- Text,
850
- Image,
851
- Video,
852
- Audio,
853
- Gps,
854
- Poll,
855
- Document,
856
- VCard,
857
- Event,
858
- // Helpers internos (usados por orquestador WhatsApp)
859
- build_message_doc,
860
- build_instance,
861
- /** Obtiene un mensaje por CID/MID. */
862
- async get(cid, mid) {
863
- const jid = await wa._resolve_jid(cid);
864
- if (!jid) {
865
- return null;
866
- }
867
- const doc = deserialize(await wa.engine.get(`/chat/${jid}/message/${mid}`));
868
- return doc ? build_instance(doc) : null;
869
- },
870
- /** Pagina mensajes de un chat. */
871
- async list(cid, offset = 0, limit = 50) {
872
- const jid = await wa._resolve_jid(cid);
873
- const result = [];
874
- if (jid) {
875
- for (const raw of await wa.engine.list(`/chat/${jid}/message`, offset, limit)) {
876
- const doc = deserialize(raw);
877
- if (doc) {
878
- result.push(await build_instance(doc));
879
- }
880
- }
881
- }
882
- return result;
883
- },
884
- /** Cuenta mensajes de un chat. */
885
- async count(cid) {
886
- const jid = await wa._resolve_jid(cid);
887
- return jid ? wa.engine.count(`/chat/${jid}/message`) : 0;
888
- },
889
- async text(cid, caption, opts = {}) {
890
- return send(cid, { text: caption }, undefined, opts.mid);
891
- },
892
- async image(cid, buf, opts = {}) {
893
- return send(cid, { image: buf, caption: opts.caption }, buf, opts.mid);
894
- },
895
- async video(cid, buf, opts = {}) {
896
- return send(cid, { video: buf, caption: opts.caption }, buf, opts.mid);
897
- },
898
- async audio(cid, buf, opts = {}) {
899
- return send(cid, { audio: buf, ptt: opts.ptt ?? true }, buf, opts.mid);
900
- },
901
- async location(cid, loc, opts = {}) {
902
- return send(cid, { location: { degreesLatitude: loc.lat, degreesLongitude: loc.lng } }, undefined, opts.mid);
903
- },
904
- async poll(cid, p, opts = {}) {
905
- const multiple = opts.multiple ?? false;
906
- return send(cid, {
907
- poll: {
908
- name: p.content,
909
- values: p.options.map((o) => o.content),
910
- selectableCount: multiple ? 0 : 1,
911
- },
912
- }, undefined, opts.mid, { multiple });
913
- },
914
- async document(cid, buf, opts) {
915
- return send(cid, {
916
- document: buf,
917
- fileName: opts.file_name,
918
- mimetype: opts.mimetype ?? 'application/octet-stream',
919
- caption: opts.caption,
920
- }, buf, opts.mid);
921
- },
922
- async vcard(cid, contacts, opts = {}) {
923
- const built = contacts.map((c) => ({
924
- displayName: c.name,
925
- vcard: build_vcard(c.name, c.phone),
926
- }));
927
- const binary = Buffer.from(built.map((c) => c.vcard).join('\n'), 'utf-8');
928
- return send(cid, { contacts: { contacts: built } }, binary, opts.mid);
929
- },
930
- async event(cid, data, opts = {}) {
931
- const binary = Buffer.from(JSON.stringify({
932
- name: data.name,
933
- caption: data.caption,
934
- start: data.start.getTime(),
935
- end: data.end?.getTime() ?? null,
936
- }), 'utf-8');
937
- return send(cid, {
938
- event: {
939
- name: data.name,
940
- description: data.caption,
941
- startDate: data.start,
942
- endDate: data.end,
943
- location: data.place
944
- ? { degreesLatitude: data.place.lat, degreesLongitude: data.place.lng }
945
- : undefined,
946
- },
947
- }, binary, opts.mid);
948
- },
949
- async edit(cid, mid, text) {
950
- const m = await api.get(cid, mid);
951
- return m ? m.edit(text) : false;
952
- },
953
- async delete(cid, mid, all = true) {
954
- const m = await api.get(cid, mid);
955
- return m ? m.delete(all) : false;
956
- },
957
- async react(cid, mid, emoji) {
958
- const m = await api.get(cid, mid);
959
- return m ? m.react(emoji) : false;
960
- },
961
- async forward(cid, mid, target) {
962
- const m = await api.get(cid, mid);
963
- return m ? m.forward(target) : false;
964
- },
965
- async seen(cid, mid) {
966
- const m = await api.get(cid, mid);
967
- return m ? m.seen() : false;
968
- },
969
- async star(cid, mid, value) {
970
- const m = await api.get(cid, mid);
971
- return m ? m.star(value) : false;
972
- },
973
- watch(cid, mid, handler) {
974
- return wa.on('message:updated', (updated) => {
975
- if (updated.cid === cid && updated.id === mid) {
976
- handler(updated);
977
- }
978
- });
979
- },
1095
+ else {
1096
+ doc = raw;
1097
+ }
1098
+ const types = {
1099
+ text: Text,
1100
+ image: Image,
1101
+ video: Video,
1102
+ audio: Audio,
1103
+ sticker: Sticker,
1104
+ document: Document,
1105
+ location: Location,
1106
+ poll: Poll,
1107
+ vcard: VCard,
1108
+ event: Event,
980
1109
  };
981
- return api;
1110
+ return new (types[doc.type] ?? Message)(wa, doc);
982
1111
  }
983
- //# sourceMappingURL=index.js.map