@ezychat/wa 0.1.68 → 0.1.70
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.
- package/dist/baileys/adapter.d.ts +16 -1
- package/dist/baileys/adapter.js +113 -15
- package/dist/baileys/protocol.d.ts +16 -0
- package/dist/baileys/protocol.js +67 -0
- package/dist/baileys/wa-version.d.ts +33 -0
- package/dist/baileys/wa-version.js +44 -0
- package/dist/fake/fake-adapter.d.ts +39 -1
- package/dist/fake/fake-adapter.js +65 -3
- package/dist/types.d.ts +76 -1
- package/dist/types.js +3 -0
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { type BaileysEventMap, type MessageUpsertType, type MessageUserReceiptUpdate, type WAMessageUpdate, type WAMessage } from 'baileys';
|
|
3
3
|
import type { WaStatus } from '@ezychat/shared';
|
|
4
|
-
import { type SendResult, type WaAdapter, type WaAdapterEvents, type WaAdapterOptions, type WaContactAlias, type WaGroupInfo, type WaMessageStatusUpdate, type WaReactionTarget, type WaSendFile, type WaSendOptions } from '../types.js';
|
|
4
|
+
import { type SendResult, type WaAdapter, type WaAdapterEvents, type WaAdapterOptions, type WaContactAlias, type WaGroupInfo, type WaMessageStatusUpdate, type WaMessageTarget, type WaReactionTarget, type WaSendFile, type WaSendOptions } from '../types.js';
|
|
5
5
|
/** True when a send error means "the connection went away" (retry later) rather than a real rejection. */
|
|
6
6
|
export declare function isConnectionError(err: unknown): boolean;
|
|
7
7
|
/** Per-participant group receipt → our status (read wins over delivered). */
|
|
@@ -25,6 +25,11 @@ export declare class BaileysAdapter extends EventEmitter implements WaAdapter {
|
|
|
25
25
|
private readonly logger;
|
|
26
26
|
private readonly historyDaysOpt;
|
|
27
27
|
private raw;
|
|
28
|
+
/**
|
|
29
|
+
* Target ids of edits and deletes this adapter sent → when: WhatsApp echoes them back like the
|
|
30
|
+
* phone's own, and the core already recorded them with the teammate who made them.
|
|
31
|
+
*/
|
|
32
|
+
private readonly ownChanges;
|
|
28
33
|
private attempt;
|
|
29
34
|
private reconnectTimer;
|
|
30
35
|
/** true when the socket was closed on purpose (disconnect/logout/replaced/blocked). */
|
|
@@ -87,7 +92,17 @@ export declare class BaileysAdapter extends EventEmitter implements WaAdapter {
|
|
|
87
92
|
private send;
|
|
88
93
|
sendText(chatJid: string, text: string, opts?: WaSendOptions): Promise<SendResult>;
|
|
89
94
|
sendMedia(chatJid: string, file: WaSendFile, opts?: WaSendOptions): Promise<SendResult>;
|
|
95
|
+
/** The key WhatsApp matches a message by; the cached key carries the group author as delivered. */
|
|
96
|
+
private targetKey;
|
|
90
97
|
sendReaction(chatJid: string, target: WaReactionTarget, emoji: string): Promise<SendResult>;
|
|
98
|
+
/** Remember an edit/delete we send, so its echo is not reported as the phone's. */
|
|
99
|
+
private markOwnChange;
|
|
100
|
+
/** True (and forgotten) when a fromMe change of `targetId` is the echo of our own. */
|
|
101
|
+
private takeOwnChange;
|
|
102
|
+
private sendChange;
|
|
103
|
+
editText(chatJid: string, target: WaMessageTarget, text: string): Promise<SendResult>;
|
|
104
|
+
deleteForEveryone(chatJid: string, target: WaMessageTarget): Promise<SendResult>;
|
|
105
|
+
deleteForMe(chatJid: string, target: WaMessageTarget, timestamp: number): Promise<void>;
|
|
91
106
|
markRead(chatJid: string, messageIds: string[]): Promise<void>;
|
|
92
107
|
sendPresence(chatJid: string, presence: 'composing' | 'paused'): Promise<void>;
|
|
93
108
|
downloadMedia(messageId: string): Promise<Buffer | null>;
|
package/dist/baileys/adapter.js
CHANGED
|
@@ -5,14 +5,22 @@ import { WaUnavailableError, } from '../types.js';
|
|
|
5
5
|
import { createAuthStore } from './auth-store.js';
|
|
6
6
|
import { backoffMs, classifyDisconnect } from './disconnect.js';
|
|
7
7
|
import { Lru } from './lru.js';
|
|
8
|
+
import { createWaVersionSource } from './wa-version.js';
|
|
8
9
|
import { jidType, mapStoredReactions, mapWAMessage, mapWAReaction, toMs } from './mapping.js';
|
|
9
10
|
import { quotedForChat, quotedFromRef } from './quoted.js';
|
|
11
|
+
import { mapWAProtocol } from './protocol.js';
|
|
10
12
|
import { contactAliasPair, normalizeContactJid } from './contact-aliases.js';
|
|
11
13
|
import { checkedNumberJid } from './number-check.js';
|
|
12
14
|
import { profilePhoto } from './photos.js';
|
|
13
15
|
import { aboutFromStatusResult, mapGroupMetadata } from './groups.js';
|
|
14
16
|
const RAW_CACHE_MAX = 2000;
|
|
17
|
+
/** How long an edit or delete this adapter sent waits for WhatsApp's echo of it. */
|
|
18
|
+
const OWN_CHANGE_TTL_MS = 60_000;
|
|
15
19
|
const DAY_MS = 86_400_000;
|
|
20
|
+
/** One per process: every inbox and reconnect shares the looked-up WhatsApp Web version. */
|
|
21
|
+
const waVersion = createWaVersionSource({
|
|
22
|
+
fetchLatest: () => fetchLatestBaileysVersion(),
|
|
23
|
+
});
|
|
16
24
|
/** Boom status codes Baileys uses when the socket is closed / lost / replaced / unavailable. */
|
|
17
25
|
const CONNECTION_STATUS_CODES = new Set([408, 428, 440, 503]);
|
|
18
26
|
/** True when a send error means "the connection went away" (retry later) rather than a real rejection. */
|
|
@@ -75,6 +83,11 @@ export class BaileysAdapter extends EventEmitter {
|
|
|
75
83
|
logger;
|
|
76
84
|
historyDaysOpt;
|
|
77
85
|
raw = new Lru(RAW_CACHE_MAX);
|
|
86
|
+
/**
|
|
87
|
+
* Target ids of edits and deletes this adapter sent → when: WhatsApp echoes them back like the
|
|
88
|
+
* phone's own, and the core already recorded them with the teammate who made them.
|
|
89
|
+
*/
|
|
90
|
+
ownChanges = new Map();
|
|
78
91
|
attempt = 0;
|
|
79
92
|
reconnectTimer = null;
|
|
80
93
|
/** true when the socket was closed on purpose (disconnect/logout/replaced/blocked). */
|
|
@@ -231,14 +244,12 @@ export class BaileysAdapter extends EventEmitter {
|
|
|
231
244
|
async open() {
|
|
232
245
|
this.setStatus({ state: 'connecting', qr: null });
|
|
233
246
|
const { state, saveCreds } = await this.auth.load();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.logger.warn({ err }, 'could not fetch latest WA version; using bundled default');
|
|
241
|
-
}
|
|
247
|
+
// Never wait for the GitHub lookup: connect with the version we have, refresh it for next time.
|
|
248
|
+
const version = waVersion.current();
|
|
249
|
+
void waVersion.refresh().then(({ error }) => {
|
|
250
|
+
if (error)
|
|
251
|
+
this.logger.warn({ err: error }, 'could not fetch latest WA version');
|
|
252
|
+
});
|
|
242
253
|
if (this.stopped)
|
|
243
254
|
return; // disconnect() was called while loading
|
|
244
255
|
const blog = this.logger.child({ module: 'baileys' });
|
|
@@ -407,8 +418,15 @@ export class BaileysAdapter extends EventEmitter {
|
|
|
407
418
|
if (!key.id || !key.remoteJid || !key.fromMe)
|
|
408
419
|
continue;
|
|
409
420
|
const status = mapAck(update.status);
|
|
410
|
-
if (status)
|
|
411
|
-
|
|
421
|
+
if (!status)
|
|
422
|
+
continue;
|
|
423
|
+
const code = status === 'failed' ? update.messageStubParameters?.[0] : undefined;
|
|
424
|
+
this.emitTyped('messageStatus', {
|
|
425
|
+
id: key.id,
|
|
426
|
+
chatJid: key.remoteJid,
|
|
427
|
+
status,
|
|
428
|
+
...(typeof code === 'string' && code ? { error: code } : {}),
|
|
429
|
+
});
|
|
412
430
|
}
|
|
413
431
|
}
|
|
414
432
|
/** @internal exposed for tests */
|
|
@@ -439,6 +457,21 @@ export class BaileysAdapter extends EventEmitter {
|
|
|
439
457
|
this.emitTyped('reaction', reaction, { source });
|
|
440
458
|
return;
|
|
441
459
|
}
|
|
460
|
+
// Edits and deletes arrive as protocol messages naming the changed message. Baileys also
|
|
461
|
+
// reports them as messages.update; only this path is read, so each change counts once.
|
|
462
|
+
const change = mapWAProtocol(m);
|
|
463
|
+
if (change) {
|
|
464
|
+
const { targetId, fromMe, timestamp } = change.kind === 'edit' ? change.edit : change.revoke;
|
|
465
|
+
if (source === 'history' && timestamp < this.historyCutoff())
|
|
466
|
+
return;
|
|
467
|
+
if (fromMe && this.takeOwnChange(targetId))
|
|
468
|
+
return;
|
|
469
|
+
if (change.kind === 'edit')
|
|
470
|
+
this.emitTyped('messageEdited', change.edit, { source });
|
|
471
|
+
else
|
|
472
|
+
this.emitTyped('messageDeleted', change.revoke, { source });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
442
475
|
if (m.message)
|
|
443
476
|
this.raw.set(id, m);
|
|
444
477
|
// History sync keeps earlier reactions on the reacted message itself.
|
|
@@ -613,7 +646,11 @@ export class BaileysAdapter extends EventEmitter {
|
|
|
613
646
|
: undefined;
|
|
614
647
|
let res;
|
|
615
648
|
try {
|
|
616
|
-
|
|
649
|
+
const options = {
|
|
650
|
+
...(quoted ? { quoted } : {}),
|
|
651
|
+
...(opts?.messageId ? { messageId: opts.messageId } : {}),
|
|
652
|
+
};
|
|
653
|
+
res = await sock.sendMessage(chatJid, content, Object.keys(options).length ? options : undefined);
|
|
617
654
|
}
|
|
618
655
|
catch (err) {
|
|
619
656
|
// connection dropped mid-send: report as unavailable so the queue keeps the job pending
|
|
@@ -666,16 +703,77 @@ export class BaileysAdapter extends EventEmitter {
|
|
|
666
703
|
}
|
|
667
704
|
return this.send(chatJid, content, opts);
|
|
668
705
|
}
|
|
669
|
-
|
|
670
|
-
|
|
706
|
+
/** The key WhatsApp matches a message by; the cached key carries the group author as delivered. */
|
|
707
|
+
targetKey(chatJid, target) {
|
|
671
708
|
const participant = this.raw.get(target.id)?.key?.participant ?? target.participant;
|
|
672
|
-
|
|
709
|
+
return {
|
|
673
710
|
remoteJid: chatJid,
|
|
674
711
|
id: target.id,
|
|
675
712
|
fromMe: target.fromMe,
|
|
676
713
|
...(participant && chatJid.endsWith('@g.us') ? { participant } : {}),
|
|
677
714
|
};
|
|
678
|
-
|
|
715
|
+
}
|
|
716
|
+
sendReaction(chatJid, target, emoji) {
|
|
717
|
+
return this.send(chatJid, { react: { text: emoji, key: this.targetKey(chatJid, target) } }, undefined, false);
|
|
718
|
+
}
|
|
719
|
+
/** Remember an edit/delete we send, so its echo is not reported as the phone's. */
|
|
720
|
+
markOwnChange(targetId) {
|
|
721
|
+
const t = Date.now();
|
|
722
|
+
for (const [id, at] of this.ownChanges)
|
|
723
|
+
if (t - at > OWN_CHANGE_TTL_MS)
|
|
724
|
+
this.ownChanges.delete(id);
|
|
725
|
+
this.ownChanges.set(targetId, t);
|
|
726
|
+
}
|
|
727
|
+
/** True (and forgotten) when a fromMe change of `targetId` is the echo of our own. */
|
|
728
|
+
takeOwnChange(targetId) {
|
|
729
|
+
const at = this.ownChanges.get(targetId);
|
|
730
|
+
if (at === undefined)
|
|
731
|
+
return false;
|
|
732
|
+
this.ownChanges.delete(targetId);
|
|
733
|
+
return Date.now() - at <= OWN_CHANGE_TTL_MS;
|
|
734
|
+
}
|
|
735
|
+
async sendChange(chatJid, target, content) {
|
|
736
|
+
this.markOwnChange(target.id);
|
|
737
|
+
try {
|
|
738
|
+
return await this.send(chatJid, content, undefined, false);
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
this.ownChanges.delete(target.id);
|
|
742
|
+
throw err;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
editText(chatJid, target, text) {
|
|
746
|
+
const key = this.targetKey(chatJid, target);
|
|
747
|
+
const cached = this.raw.get(target.id);
|
|
748
|
+
return this.sendChange(chatJid, target, { text, edit: key }).then((r) => {
|
|
749
|
+
// Later quotes of the message show the new text.
|
|
750
|
+
const m = cached?.message;
|
|
751
|
+
if (m?.conversation != null)
|
|
752
|
+
m.conversation = text;
|
|
753
|
+
else if (m?.extendedTextMessage)
|
|
754
|
+
m.extendedTextMessage.text = text;
|
|
755
|
+
return r;
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
deleteForEveryone(chatJid, target) {
|
|
759
|
+
return this.sendChange(chatJid, target, { delete: this.targetKey(chatJid, target) });
|
|
760
|
+
}
|
|
761
|
+
async deleteForMe(chatJid, target, timestamp) {
|
|
762
|
+
const sock = this.requireOpen();
|
|
763
|
+
try {
|
|
764
|
+
await sock.chatModify({
|
|
765
|
+
deleteForMe: {
|
|
766
|
+
deleteMedia: true,
|
|
767
|
+
key: this.targetKey(chatJid, target),
|
|
768
|
+
timestamp: Math.floor(timestamp / 1000),
|
|
769
|
+
},
|
|
770
|
+
}, chatJid);
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
if (isConnectionError(err) || this.sock !== sock || this._status.state !== 'open')
|
|
774
|
+
throw new WaUnavailableError(`WhatsApp connection lost: ${String(err?.message ?? err)}`);
|
|
775
|
+
throw err;
|
|
776
|
+
}
|
|
679
777
|
}
|
|
680
778
|
async markRead(chatJid, messageIds) {
|
|
681
779
|
const sock = this.requireOpen();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { proto } from 'baileys';
|
|
2
|
+
import type { WaMessageEdit, WaMessageRevoke } from '../types.js';
|
|
3
|
+
export type WaProtocolChange = {
|
|
4
|
+
kind: 'edit';
|
|
5
|
+
edit: WaMessageEdit;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'revoke';
|
|
8
|
+
revoke: WaMessageRevoke;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* An edit (`MESSAGE_EDIT`) or a delete for everyone (`REVOKE`) as WhatsApp delivers it: a
|
|
12
|
+
* `protocolMessage` whose `key` names the changed message, sent by whoever changed it (the outer
|
|
13
|
+
* key: the DM partner, a group participant or the linked number). Null for every other message,
|
|
14
|
+
* for changes without a target or author, and for status/broadcast/newsletter chats.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mapWAProtocol(msg: proto.IWebMessageInfo): WaProtocolChange | null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { normalizeMessageContent, proto } from 'baileys';
|
|
2
|
+
import { normalizeContactJid } from './contact-aliases.js';
|
|
3
|
+
import { jidType, toMs } from './mapping.js';
|
|
4
|
+
/** The text or caption of an edited message (what `MESSAGE_EDIT` carries), or null. */
|
|
5
|
+
function editedText(message) {
|
|
6
|
+
const m = normalizeMessageContent(message);
|
|
7
|
+
if (!m)
|
|
8
|
+
return null;
|
|
9
|
+
if (m.conversation != null)
|
|
10
|
+
return m.conversation;
|
|
11
|
+
return (m.extendedTextMessage?.text ??
|
|
12
|
+
m.imageMessage?.caption ??
|
|
13
|
+
m.videoMessage?.caption ??
|
|
14
|
+
m.documentMessage?.caption ??
|
|
15
|
+
null);
|
|
16
|
+
}
|
|
17
|
+
function millis(ts) {
|
|
18
|
+
if (ts == null)
|
|
19
|
+
return null;
|
|
20
|
+
if (typeof ts === 'number')
|
|
21
|
+
return ts;
|
|
22
|
+
if ('toNumber' in ts && typeof ts.toNumber === 'function')
|
|
23
|
+
return ts.toNumber();
|
|
24
|
+
if ('low' in ts)
|
|
25
|
+
return (ts.high >>> 0) * 2 ** 32 + (ts.low >>> 0);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An edit (`MESSAGE_EDIT`) or a delete for everyone (`REVOKE`) as WhatsApp delivers it: a
|
|
30
|
+
* `protocolMessage` whose `key` names the changed message, sent by whoever changed it (the outer
|
|
31
|
+
* key: the DM partner, a group participant or the linked number). Null for every other message,
|
|
32
|
+
* for changes without a target or author, and for status/broadcast/newsletter chats.
|
|
33
|
+
*/
|
|
34
|
+
export function mapWAProtocol(msg) {
|
|
35
|
+
const key = msg.key;
|
|
36
|
+
const chatJid = key?.remoteJid;
|
|
37
|
+
if (!key || !chatJid || jidType(chatJid) === 'other')
|
|
38
|
+
return null;
|
|
39
|
+
const protocol = normalizeMessageContent(msg.message)?.protocolMessage;
|
|
40
|
+
const targetId = protocol?.key?.id;
|
|
41
|
+
if (!protocol || !targetId)
|
|
42
|
+
return null;
|
|
43
|
+
const isGroup = jidType(chatJid) === 'group';
|
|
44
|
+
const fromMe = !!key.fromMe;
|
|
45
|
+
const senderJid = fromMe
|
|
46
|
+
? null
|
|
47
|
+
: isGroup
|
|
48
|
+
? normalizeContactJid(key.participant)
|
|
49
|
+
: (normalizeContactJid(chatJid) ?? chatJid);
|
|
50
|
+
if (!fromMe && !senderJid)
|
|
51
|
+
return null;
|
|
52
|
+
const senderJidAlt = !fromMe && isGroup ? normalizeContactJid(key.participantAlt) : null;
|
|
53
|
+
const alt = isGroup ? null : normalizeContactJid(key.remoteJidAlt);
|
|
54
|
+
const chatJidAlt = alt && alt.endsWith('@lid') !== chatJid.endsWith('@lid') ? alt : null;
|
|
55
|
+
const sent = toMs(msg.messageTimestamp);
|
|
56
|
+
const base = { targetId, chatJid, chatJidAlt, senderJid, senderJidAlt, fromMe };
|
|
57
|
+
if (protocol.type === proto.Message.ProtocolMessage.Type.REVOKE)
|
|
58
|
+
return { kind: 'revoke', revoke: { ...base, timestamp: sent } };
|
|
59
|
+
if (protocol.type === proto.Message.ProtocolMessage.Type.MESSAGE_EDIT) {
|
|
60
|
+
const body = editedText(protocol.editedMessage);
|
|
61
|
+
if (body === null)
|
|
62
|
+
return null;
|
|
63
|
+
const at = millis(protocol.timestampMs) ?? sent;
|
|
64
|
+
return { kind: 'edit', edit: { ...base, body, timestamp: at } };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WhatsApp Web version to announce. WhatsApp refuses clients whose version is too old, so Baileys
|
|
3
|
+
* suggests looking up the newest one from its GitHub copy on every connect. That lookup has no
|
|
4
|
+
* timeout, and awaiting it before the socket held every connect (and every new QR) at "connecting"
|
|
5
|
+
* while GitHub was slow or blocked. A connect never waits for it now: it uses the last version
|
|
6
|
+
* looked up (or Baileys' bundled one) at once, and the lookup refreshes in the background, bounded
|
|
7
|
+
* by `timeoutMs` and cached, so a connect WhatsApp refused for an old version retries with the new one.
|
|
8
|
+
*/
|
|
9
|
+
export type WaVersion = [number, number, number];
|
|
10
|
+
export interface WaVersionSourceOptions {
|
|
11
|
+
fetchLatest: () => Promise<{
|
|
12
|
+
version: WaVersion;
|
|
13
|
+
}>;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
/** How long a fetched version is reused before the next lookup. */
|
|
16
|
+
ttlMs?: number;
|
|
17
|
+
/** How long to skip the lookup after a failure or timeout. */
|
|
18
|
+
failureTtlMs?: number;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
}
|
|
21
|
+
export declare const WA_VERSION_TIMEOUT_MS = 10000;
|
|
22
|
+
export interface WaVersionRefresh {
|
|
23
|
+
version: WaVersion | undefined;
|
|
24
|
+
/** Set when this refresh tried the lookup and it failed or timed out. */
|
|
25
|
+
error?: unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface WaVersionSource {
|
|
28
|
+
/** The version to connect with now: the last one looked up, else undefined (Baileys' bundled one). */
|
|
29
|
+
current(): WaVersion | undefined;
|
|
30
|
+
/** Looks the version up unless a recent result is cached; never rejects, one lookup at a time. */
|
|
31
|
+
refresh(): Promise<WaVersionRefresh>;
|
|
32
|
+
}
|
|
33
|
+
export declare function createWaVersionSource(opts: WaVersionSourceOptions): WaVersionSource;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const WA_VERSION_TIMEOUT_MS = 10_000;
|
|
2
|
+
const WA_VERSION_TTL_MS = 6 * 60 * 60_000;
|
|
3
|
+
const WA_VERSION_FAILURE_TTL_MS = 10 * 60_000;
|
|
4
|
+
export function createWaVersionSource(opts) {
|
|
5
|
+
const timeoutMs = opts.timeoutMs ?? WA_VERSION_TIMEOUT_MS;
|
|
6
|
+
const ttlMs = opts.ttlMs ?? WA_VERSION_TTL_MS;
|
|
7
|
+
const failureTtlMs = opts.failureTtlMs ?? WA_VERSION_FAILURE_TTL_MS;
|
|
8
|
+
const now = opts.now ?? Date.now;
|
|
9
|
+
let version;
|
|
10
|
+
let nextLookupAt = 0;
|
|
11
|
+
let inflight = null;
|
|
12
|
+
const lookup = async () => {
|
|
13
|
+
let timer;
|
|
14
|
+
try {
|
|
15
|
+
const timeout = new Promise((_, reject) => {
|
|
16
|
+
timer = setTimeout(() => reject(new Error(`WA version lookup timed out after ${timeoutMs} ms`)), timeoutMs);
|
|
17
|
+
timer.unref?.();
|
|
18
|
+
});
|
|
19
|
+
({ version } = await Promise.race([opts.fetchLatest(), timeout]));
|
|
20
|
+
nextLookupAt = now() + ttlMs;
|
|
21
|
+
return { version };
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
nextLookupAt = now() + failureTtlMs;
|
|
25
|
+
return { version, error };
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
current: () => version,
|
|
33
|
+
refresh() {
|
|
34
|
+
if (inflight)
|
|
35
|
+
return inflight;
|
|
36
|
+
if (now() < nextLookupAt)
|
|
37
|
+
return Promise.resolve({ version });
|
|
38
|
+
inflight = lookup().finally(() => {
|
|
39
|
+
inflight = null;
|
|
40
|
+
});
|
|
41
|
+
return inflight;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import type { WaStatus } from '@ezychat/shared';
|
|
3
|
-
import { type SendResult, type WaAdapter, type WaAdapterEvents, type WaCapabilities, type WaContactAlias, type WaGroupInfo, type WaIncomingMessage, type WaReaction, type WaReactionTarget, type WaSendFile, type WaSendOptions, type WaTemplate, type WaTemplateSend } from '../types.js';
|
|
3
|
+
import { type SendResult, type WaAdapter, type WaAdapterEvents, type WaCapabilities, type WaContactAlias, type WaGroupInfo, type WaIncomingMessage, type WaMessageEdit, type WaMessageRevoke, type WaMessageTarget, type WaReaction, type WaReactionTarget, type WaSendFile, type WaSendOptions, type WaTemplate, type WaTemplateSend } from '../types.js';
|
|
4
4
|
export interface FakeSentRecord {
|
|
5
5
|
chatJid: string;
|
|
6
6
|
text?: string;
|
|
@@ -25,6 +25,7 @@ export declare class FakeWaAdapter extends EventEmitter implements WaAdapter {
|
|
|
25
25
|
private inCounter;
|
|
26
26
|
private outCounter;
|
|
27
27
|
private nextSendError;
|
|
28
|
+
private skipDelivery;
|
|
28
29
|
private readonly media;
|
|
29
30
|
private readonly groups;
|
|
30
31
|
private readonly abouts;
|
|
@@ -38,6 +39,22 @@ export declare class FakeWaAdapter extends EventEmitter implements WaAdapter {
|
|
|
38
39
|
target: WaReactionTarget;
|
|
39
40
|
emoji: string;
|
|
40
41
|
}>;
|
|
42
|
+
/** Every edit, delete for everyone and delete for me sent (`editText`, `deleteFor…`). */
|
|
43
|
+
readonly changes: Array<{
|
|
44
|
+
kind: 'edit';
|
|
45
|
+
chatJid: string;
|
|
46
|
+
target: WaMessageTarget;
|
|
47
|
+
text: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'deleteForEveryone';
|
|
50
|
+
chatJid: string;
|
|
51
|
+
target: WaMessageTarget;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'deleteForMe';
|
|
54
|
+
chatJid: string;
|
|
55
|
+
target: WaMessageTarget;
|
|
56
|
+
timestamp: number;
|
|
57
|
+
}>;
|
|
41
58
|
readonly reads: Array<{
|
|
42
59
|
chatJid: string;
|
|
43
60
|
messageIds: string[];
|
|
@@ -101,6 +118,10 @@ export declare class FakeWaAdapter extends EventEmitter implements WaAdapter {
|
|
|
101
118
|
sendMedia(chatJid: string, file: WaSendFile, opts?: WaSendOptions): Promise<SendResult>;
|
|
102
119
|
/** Like WhatsApp, the linked number's own reaction comes back as a `reaction` event first. */
|
|
103
120
|
sendReaction(chatJid: string, target: WaReactionTarget, emoji: string): Promise<SendResult>;
|
|
121
|
+
/** Unlike reactions, WhatsApp's echo of our own edit or delete is never reported (as Baileys). */
|
|
122
|
+
editText(chatJid: string, target: WaMessageTarget, text: string): Promise<SendResult>;
|
|
123
|
+
deleteForEveryone(chatJid: string, target: WaMessageTarget): Promise<SendResult>;
|
|
124
|
+
deleteForMe(chatJid: string, target: WaMessageTarget, timestamp: number): Promise<void>;
|
|
104
125
|
markRead(chatJid: string, messageIds: string[]): Promise<void>;
|
|
105
126
|
sendPresence(chatJid: string, presence: 'composing' | 'paused'): Promise<void>;
|
|
106
127
|
downloadMedia(messageId: string): Promise<Buffer | null>;
|
|
@@ -140,7 +161,24 @@ export declare class FakeWaAdapter extends EventEmitter implements WaAdapter {
|
|
|
140
161
|
targetId: string;
|
|
141
162
|
emoji: string;
|
|
142
163
|
}, source?: 'live' | 'history'): WaReaction;
|
|
164
|
+
/**
|
|
165
|
+
* Emit an edit (live unless `source` says history). In a DM the editor defaults to the chat
|
|
166
|
+
* partner; `fromMe` means the linked phone edited.
|
|
167
|
+
*/
|
|
168
|
+
simulateEdit(p: Partial<WaMessageEdit> & {
|
|
169
|
+
chatJid: string;
|
|
170
|
+
targetId: string;
|
|
171
|
+
body: string;
|
|
172
|
+
}, source?: 'live' | 'history'): WaMessageEdit;
|
|
173
|
+
/** Emit a delete for everyone (live unless `source` says history), as `simulateEdit`. */
|
|
174
|
+
simulateRevoke(p: Partial<WaMessageRevoke> & {
|
|
175
|
+
chatJid: string;
|
|
176
|
+
targetId: string;
|
|
177
|
+
}, source?: 'live' | 'history'): WaMessageRevoke;
|
|
178
|
+
private changeAuthor;
|
|
143
179
|
simulateStatus(s: Partial<WaStatus>): void;
|
|
180
|
+
/** The next send gets its server ack but no delivered receipt (no phone took it). */
|
|
181
|
+
skipNextDelivery(): void;
|
|
144
182
|
failNextSend(err?: Error): void;
|
|
145
183
|
setConnected(connected: boolean): void;
|
|
146
184
|
}
|
|
@@ -15,6 +15,7 @@ export class FakeWaAdapter extends EventEmitter {
|
|
|
15
15
|
inCounter = 0;
|
|
16
16
|
outCounter = 0;
|
|
17
17
|
nextSendError = null;
|
|
18
|
+
skipDelivery = false;
|
|
18
19
|
media = new Map();
|
|
19
20
|
groups = new Map();
|
|
20
21
|
abouts = new Map();
|
|
@@ -24,6 +25,8 @@ export class FakeWaAdapter extends EventEmitter {
|
|
|
24
25
|
aboutCalls = [];
|
|
25
26
|
sent = [];
|
|
26
27
|
reactions = [];
|
|
28
|
+
/** Every edit, delete for everyone and delete for me sent (`editText`, `deleteFor…`). */
|
|
29
|
+
changes = [];
|
|
27
30
|
reads = [];
|
|
28
31
|
presences = [];
|
|
29
32
|
/** Every template sent (adapters given `templates`). */
|
|
@@ -132,12 +135,15 @@ export class FakeWaAdapter extends EventEmitter {
|
|
|
132
135
|
afterSend(chatJid, id) {
|
|
133
136
|
const timestamp = Date.now();
|
|
134
137
|
this.emitTyped('messageStatus', { id, chatJid, status: 'sent' });
|
|
135
|
-
|
|
138
|
+
if (this.skipDelivery)
|
|
139
|
+
this.skipDelivery = false;
|
|
140
|
+
else
|
|
141
|
+
setImmediate(() => this.emitTyped('messageStatus', { id, chatJid, status: 'delivered' }));
|
|
136
142
|
return { id, timestamp };
|
|
137
143
|
}
|
|
138
144
|
async sendText(chatJid, text, opts) {
|
|
139
145
|
this.beforeSend();
|
|
140
|
-
const id = `${this.idPrefix}-OUT-${FAKE_RUN}-${++this.outCounter}`;
|
|
146
|
+
const id = opts?.messageId ?? `${this.idPrefix}-OUT-${FAKE_RUN}-${++this.outCounter}`;
|
|
141
147
|
this.sent.push({
|
|
142
148
|
chatJid,
|
|
143
149
|
text,
|
|
@@ -149,7 +155,7 @@ export class FakeWaAdapter extends EventEmitter {
|
|
|
149
155
|
}
|
|
150
156
|
async sendMedia(chatJid, file, opts) {
|
|
151
157
|
this.beforeSend();
|
|
152
|
-
const id = `${this.idPrefix}-OUT-${FAKE_RUN}-${++this.outCounter}`;
|
|
158
|
+
const id = opts?.messageId ?? `${this.idPrefix}-OUT-${FAKE_RUN}-${++this.outCounter}`;
|
|
153
159
|
this.sent.push({ chatJid, file, id, ...(opts?.quotedId ? { reply: opts } : {}) });
|
|
154
160
|
this.media.set(id, file.buffer);
|
|
155
161
|
return this.afterSend(chatJid, id);
|
|
@@ -163,6 +169,24 @@ export class FakeWaAdapter extends EventEmitter {
|
|
|
163
169
|
this.simulateReaction({ chatJid, targetId: target.id, emoji, fromMe: true, timestamp });
|
|
164
170
|
return { id, timestamp };
|
|
165
171
|
}
|
|
172
|
+
/** Unlike reactions, WhatsApp's echo of our own edit or delete is never reported (as Baileys). */
|
|
173
|
+
async editText(chatJid, target, text) {
|
|
174
|
+
this.beforeSend();
|
|
175
|
+
this.changes.push({ kind: 'edit', chatJid, target: { ...target }, text });
|
|
176
|
+
return { id: `${this.idPrefix}-EDIT-${FAKE_RUN}-${++this.outCounter}`, timestamp: Date.now() };
|
|
177
|
+
}
|
|
178
|
+
async deleteForEveryone(chatJid, target) {
|
|
179
|
+
this.beforeSend();
|
|
180
|
+
this.changes.push({ kind: 'deleteForEveryone', chatJid, target: { ...target } });
|
|
181
|
+
return {
|
|
182
|
+
id: `${this.idPrefix}-REVOKE-${FAKE_RUN}-${++this.outCounter}`,
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async deleteForMe(chatJid, target, timestamp) {
|
|
187
|
+
this.beforeSend();
|
|
188
|
+
this.changes.push({ kind: 'deleteForMe', chatJid, target: { ...target }, timestamp });
|
|
189
|
+
}
|
|
166
190
|
async markRead(chatJid, messageIds) {
|
|
167
191
|
this.reads.push({ chatJid, messageIds: [...messageIds] });
|
|
168
192
|
}
|
|
@@ -298,9 +322,47 @@ export class FakeWaAdapter extends EventEmitter {
|
|
|
298
322
|
this.emitTyped('reaction', reaction, { source });
|
|
299
323
|
return reaction;
|
|
300
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Emit an edit (live unless `source` says history). In a DM the editor defaults to the chat
|
|
327
|
+
* partner; `fromMe` means the linked phone edited.
|
|
328
|
+
*/
|
|
329
|
+
simulateEdit(p, source = 'live') {
|
|
330
|
+
const edit = { ...this.changeAuthor(p), body: p.body };
|
|
331
|
+
this.emitTyped('messageEdited', edit, { source });
|
|
332
|
+
return edit;
|
|
333
|
+
}
|
|
334
|
+
/** Emit a delete for everyone (live unless `source` says history), as `simulateEdit`. */
|
|
335
|
+
simulateRevoke(p, source = 'live') {
|
|
336
|
+
const revoke = this.changeAuthor(p);
|
|
337
|
+
this.emitTyped('messageDeleted', revoke, { source });
|
|
338
|
+
return revoke;
|
|
339
|
+
}
|
|
340
|
+
changeAuthor(p) {
|
|
341
|
+
const isGroup = p.chatJid.endsWith('@g.us');
|
|
342
|
+
const fromMe = p.fromMe ?? false;
|
|
343
|
+
return {
|
|
344
|
+
targetId: p.targetId,
|
|
345
|
+
chatJid: p.chatJid,
|
|
346
|
+
chatJidAlt: p.chatJidAlt ?? null,
|
|
347
|
+
senderJid: fromMe
|
|
348
|
+
? null
|
|
349
|
+
: p.senderJid !== undefined
|
|
350
|
+
? p.senderJid
|
|
351
|
+
: isGroup
|
|
352
|
+
? null
|
|
353
|
+
: p.chatJid,
|
|
354
|
+
senderJidAlt: p.senderJidAlt ?? null,
|
|
355
|
+
fromMe,
|
|
356
|
+
timestamp: p.timestamp ?? Date.now(),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
301
359
|
simulateStatus(s) {
|
|
302
360
|
this.setStatus(s);
|
|
303
361
|
}
|
|
362
|
+
/** The next send gets its server ack but no delivered receipt (no phone took it). */
|
|
363
|
+
skipNextDelivery() {
|
|
364
|
+
this.skipDelivery = true;
|
|
365
|
+
}
|
|
304
366
|
failNextSend(err = new Error('Fake send failure')) {
|
|
305
367
|
this.nextSendError = err;
|
|
306
368
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -50,6 +50,43 @@ export interface WaReaction {
|
|
|
50
50
|
/** epoch ms */
|
|
51
51
|
timestamp: number;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Someone edited a message they sent (WhatsApp `MESSAGE_EDIT`): the customer, a group member or the
|
|
55
|
+
* linked number itself (its phone or another linked device). Only text and captions are edited.
|
|
56
|
+
*/
|
|
57
|
+
export interface WaMessageEdit {
|
|
58
|
+
/** WhatsApp id of the edited message. */
|
|
59
|
+
targetId: string;
|
|
60
|
+
/** The chat of the message (as WhatsApp delivered the edit). */
|
|
61
|
+
chatJid: string;
|
|
62
|
+
/** DM only: the same person's other address (`key.remoteJidAlt`), as for messages. */
|
|
63
|
+
chatJidAlt?: string | null;
|
|
64
|
+
/** Who edited: the group participant or the DM partner; null when `fromMe`. */
|
|
65
|
+
senderJid: string | null;
|
|
66
|
+
senderJidAlt?: string | null;
|
|
67
|
+
/** Edited from the linked number (its phone or another linked device). */
|
|
68
|
+
fromMe: boolean;
|
|
69
|
+
/** The new text or caption. */
|
|
70
|
+
body: string;
|
|
71
|
+
/** epoch ms of the edit */
|
|
72
|
+
timestamp: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Someone deleted a message for everyone (WhatsApp `REVOKE`): its sender, the linked number, or in
|
|
76
|
+
* a group an admin deleting a member's message.
|
|
77
|
+
*/
|
|
78
|
+
export interface WaMessageRevoke {
|
|
79
|
+
/** WhatsApp id of the deleted message. */
|
|
80
|
+
targetId: string;
|
|
81
|
+
chatJid: string;
|
|
82
|
+
chatJidAlt?: string | null;
|
|
83
|
+
/** Who deleted it: the group participant or the DM partner; null when `fromMe`. */
|
|
84
|
+
senderJid: string | null;
|
|
85
|
+
senderJidAlt?: string | null;
|
|
86
|
+
fromMe: boolean;
|
|
87
|
+
/** epoch ms of the deletion */
|
|
88
|
+
timestamp: number;
|
|
89
|
+
}
|
|
53
90
|
export interface WaChatInfo {
|
|
54
91
|
jid: string;
|
|
55
92
|
type: 'dm' | 'group';
|
|
@@ -97,6 +134,8 @@ export interface WaMessageStatusUpdate {
|
|
|
97
134
|
id: string;
|
|
98
135
|
chatJid: string;
|
|
99
136
|
status: 'sent' | 'delivered' | 'read' | 'failed';
|
|
137
|
+
/** `failed` only: the error code WhatsApp gave, when it gave one (for the log). */
|
|
138
|
+
error?: string;
|
|
100
139
|
}
|
|
101
140
|
export interface WaAdapterEvents {
|
|
102
141
|
status: [WaStatus];
|
|
@@ -107,6 +146,14 @@ export interface WaAdapterEvents {
|
|
|
107
146
|
reaction: [WaReaction, {
|
|
108
147
|
source: 'live' | 'history';
|
|
109
148
|
}];
|
|
149
|
+
/** A message was edited (live or history); never an echo of an edit this adapter sent. */
|
|
150
|
+
messageEdited: [WaMessageEdit, {
|
|
151
|
+
source: 'live' | 'history';
|
|
152
|
+
}];
|
|
153
|
+
/** A message was deleted for everyone (live or history); never an echo of this adapter's own. */
|
|
154
|
+
messageDeleted: [WaMessageRevoke, {
|
|
155
|
+
source: 'live' | 'history';
|
|
156
|
+
}];
|
|
110
157
|
messageStatus: [WaMessageStatusUpdate];
|
|
111
158
|
chats: [WaChatInfo[]];
|
|
112
159
|
contacts: [WaContactInfo[]];
|
|
@@ -139,8 +186,17 @@ export interface WaSendOptions {
|
|
|
139
186
|
* Only for adapters with `capabilities.mentions`.
|
|
140
187
|
*/
|
|
141
188
|
mentions?: string[];
|
|
189
|
+
/**
|
|
190
|
+
* Send under this WhatsApp message id instead of a new one (Retry of a message WhatsApp already
|
|
191
|
+
* took): phones that already have that id ignore the copy, so no one sees it twice. An adapter
|
|
192
|
+
* that cannot choose the id ignores it.
|
|
193
|
+
*/
|
|
194
|
+
messageId?: string;
|
|
142
195
|
}
|
|
143
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* The message a reaction, an edit or a delete points at: WhatsApp matches it by chat, id, side and
|
|
198
|
+
* group author.
|
|
199
|
+
*/
|
|
144
200
|
export interface WaReactionTarget {
|
|
145
201
|
/** WhatsApp id of the message. */
|
|
146
202
|
id: string;
|
|
@@ -149,6 +205,7 @@ export interface WaReactionTarget {
|
|
|
149
205
|
/** Group messages from someone else: who wrote it. */
|
|
150
206
|
participant?: string | null;
|
|
151
207
|
}
|
|
208
|
+
export type WaMessageTarget = WaReactionTarget;
|
|
152
209
|
export interface SendResult {
|
|
153
210
|
id: string;
|
|
154
211
|
timestamp: number;
|
|
@@ -196,6 +253,12 @@ export interface WaCapabilities {
|
|
|
196
253
|
blockWindowHours?: number | null;
|
|
197
254
|
/** Sends WhatsApp mentions (`WaSendOptions.mentions`); absent: no. */
|
|
198
255
|
mentions?: boolean;
|
|
256
|
+
/** Edits a sent text or caption (`editText`). Absent: no. */
|
|
257
|
+
editMessage?: boolean;
|
|
258
|
+
/** Deletes a sent message for everyone (`deleteForEveryone`). Absent: no. */
|
|
259
|
+
deleteForEveryone?: boolean;
|
|
260
|
+
/** Deletes a message on the linked device itself (`deleteForMe`). Absent: no. */
|
|
261
|
+
deleteForMe?: boolean;
|
|
199
262
|
}
|
|
200
263
|
/** The capabilities of an adapter that declares none: a phone linked by QR (Baileys, the fake). */
|
|
201
264
|
export declare const LINKED_PHONE_CAPABILITIES: WaCapabilities;
|
|
@@ -246,6 +309,18 @@ export interface WaAdapter {
|
|
|
246
309
|
* reaction per number, so this replaces one made on the phone. Throws WaUnavailableError offline.
|
|
247
310
|
*/
|
|
248
311
|
sendReaction(chatJid: string, target: WaReactionTarget, emoji: string): Promise<SendResult>;
|
|
312
|
+
/**
|
|
313
|
+
* Replaces the text (or caption) of a message the linked number sent (`capabilities.editMessage`).
|
|
314
|
+
* WhatsApp shows the customer "Edited". Throws WaUnavailableError offline.
|
|
315
|
+
*/
|
|
316
|
+
editText?(chatJid: string, target: WaMessageTarget, text: string): Promise<SendResult>;
|
|
317
|
+
/** Deletes a message for everyone (`capabilities.deleteForEveryone`). Throws WaUnavailableError offline. */
|
|
318
|
+
deleteForEveryone?(chatJid: string, target: WaMessageTarget): Promise<SendResult>;
|
|
319
|
+
/**
|
|
320
|
+
* Deletes a message on the linked phone only ("Delete for me", `capabilities.deleteForMe`);
|
|
321
|
+
* `timestamp` is the message's (epoch ms). Throws WaUnavailableError offline.
|
|
322
|
+
*/
|
|
323
|
+
deleteForMe?(chatJid: string, target: WaMessageTarget, timestamp: number): Promise<void>;
|
|
249
324
|
markRead(chatJid: string, messageIds: string[]): Promise<void>;
|
|
250
325
|
sendPresence(chatJid: string, presence: 'composing' | 'paused'): Promise<void>;
|
|
251
326
|
/** re-download by id if still cached */
|
package/dist/types.js
CHANGED
|
@@ -9,6 +9,9 @@ export const LINKED_PHONE_CAPABILITIES = Object.freeze({
|
|
|
9
9
|
block: true,
|
|
10
10
|
blockWindowHours: null,
|
|
11
11
|
mentions: true,
|
|
12
|
+
editMessage: true,
|
|
13
|
+
deleteForEveryone: true,
|
|
14
|
+
deleteForMe: true,
|
|
12
15
|
});
|
|
13
16
|
export class WaUnavailableError extends Error {
|
|
14
17
|
code = 'wa_unavailable';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ezychat/wa",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "EzyChat core: WhatsApp adapter interface, Baileys adapter and fake adapter.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"typecheck": "tsc --noEmit"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@ezychat/shared": "0.1.
|
|
40
|
+
"@ezychat/shared": "0.1.70",
|
|
41
41
|
"baileys": "^7.0.0-rc14",
|
|
42
42
|
"pino": "^10.4.0",
|
|
43
43
|
"qrcode": "^1.5.4"
|