@violetix/baileys 0.3.18 → 0.4.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.
- package/README.md +174 -0
- package/lib/Socket/index.d.ts +7 -0
- package/lib/Socket/messages-send.js +136 -6
- package/lib/Socket/socket.d.ts +6 -0
- package/lib/Socket/socket.js +18 -0
- package/lib/Utils/a2ui.d.ts +50 -0
- package/lib/Utils/a2ui.js +168 -0
- package/lib/Utils/anchor-guard.d.ts +71 -0
- package/lib/Utils/anchor-guard.js +404 -0
- package/lib/Utils/business.js +25 -11
- package/lib/Utils/ignore-key.d.ts +51 -0
- package/lib/Utils/ignore-key.js +174 -0
- package/lib/Utils/index.d.ts +3 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/messages-media.js +22 -0
- package/lib/Utils/messages.d.ts +1 -0
- package/lib/Utils/messages.js +72 -0
- package/lib/Utils/use-multi-file-auth-state.d.ts +2 -1
- package/lib/Utils/use-multi-file-auth-state.js +27 -6
- package/lib/Utils/use-single-file-auth-state.d.ts +1 -1
- package/lib/Utils/use-single-file-auth-state.js +12 -3
- package/package.json +2 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BinaryNode } from '../WABinary/index.js';
|
|
2
|
+
export declare const IGNORE_KEY_KINDS: readonly ['message', 'receipt', 'notification', 'presence', 'chatstate', 'call'];
|
|
3
|
+
export type WaIgnoreKeyKind = (typeof IGNORE_KEY_KINDS)[number];
|
|
4
|
+
export type WaIgnoreKeyContext = {
|
|
5
|
+
/** Stanza tag. */
|
|
6
|
+
kind: WaIgnoreKeyKind;
|
|
7
|
+
/** Deviceless chat JID; userless `from` values are kept verbatim. */
|
|
8
|
+
remoteJid: string | null;
|
|
9
|
+
/** Resolved against the account's own PN and LID. */
|
|
10
|
+
fromMe: boolean;
|
|
11
|
+
/** Stanza id. */
|
|
12
|
+
id: string | undefined;
|
|
13
|
+
/** Author in groups / broadcasts; `null` otherwise. */
|
|
14
|
+
participant: string | null;
|
|
15
|
+
};
|
|
16
|
+
export type WaIgnoreKey = {
|
|
17
|
+
/** Chat JID; array entries OR. Alt `sender_pn` / `sender_lid` attrs are matched too. */
|
|
18
|
+
remoteJid?: string | readonly string[];
|
|
19
|
+
/** Whether the stanza was sent by this account. */
|
|
20
|
+
fromMe?: boolean;
|
|
21
|
+
/** Stanza id. */
|
|
22
|
+
id?: string;
|
|
23
|
+
/** Author in groups / broadcasts. Alt forms are matched too. */
|
|
24
|
+
participant?: string;
|
|
25
|
+
/** Restrict to specific tags. Default: all six. */
|
|
26
|
+
only?: readonly WaIgnoreKeyKind[];
|
|
27
|
+
};
|
|
28
|
+
export type WaIgnoreKeyPredicate = (ctx: WaIgnoreKeyContext) => boolean;
|
|
29
|
+
export type WaIgnoreKeyInput = WaIgnoreKey | WaIgnoreKeyPredicate;
|
|
30
|
+
export type WaIgnoreKeyEntry = {
|
|
31
|
+
remoteJid?: string[];
|
|
32
|
+
participant?: string;
|
|
33
|
+
fromMe?: boolean;
|
|
34
|
+
id?: string;
|
|
35
|
+
only?: readonly WaIgnoreKeyKind[];
|
|
36
|
+
predicate?: WaIgnoreKeyPredicate;
|
|
37
|
+
};
|
|
38
|
+
export declare const isIgnorableKind: (tag: string) => boolean;
|
|
39
|
+
export declare const normalizeIgnoreKey: (input: WaIgnoreKeyInput) => WaIgnoreKeyEntry;
|
|
40
|
+
export declare const buildIgnoreContext: (node: BinaryNode, meId?: string, meLid?: string) => WaIgnoreKeyContext;
|
|
41
|
+
export declare const matchesIgnoreKey: (entry: WaIgnoreKeyEntry, ctx: WaIgnoreKeyContext, node: BinaryNode) => boolean;
|
|
42
|
+
export declare const ignoredStanzaNeedsAck: (tag: string) => boolean;
|
|
43
|
+
export declare const makeIgnoreKeyRegistry: (deps: {
|
|
44
|
+
getMeId: () => string | undefined;
|
|
45
|
+
getMeLid: () => string | undefined;
|
|
46
|
+
logger?: any;
|
|
47
|
+
}) => {
|
|
48
|
+
register: (input: WaIgnoreKeyInput) => () => void;
|
|
49
|
+
shouldDrop: (node: BinaryNode) => boolean;
|
|
50
|
+
readonly size: number;
|
|
51
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { areJidsSameUser, jidDecode, jidEncode } from '../WABinary/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stanza tags a filter may drop. Stream control, `iq` replies and the auth-critical
|
|
4
|
+
* `success` / `failure` tags are deliberately absent so the connection flow stays intact.
|
|
5
|
+
*/
|
|
6
|
+
export const IGNORE_KEY_KINDS = ['message', 'receipt', 'notification', 'presence', 'chatstate', 'call'];
|
|
7
|
+
const IGNORE_KEY_KIND_SET = new Set(IGNORE_KEY_KINDS);
|
|
8
|
+
/**
|
|
9
|
+
* Kinds the server re-delivers until acked. Dropping one without an ack makes the server
|
|
10
|
+
* retry it forever, so the filter acks on the handler's behalf. Presence and chat-state are
|
|
11
|
+
* fire-and-forget and must not be acked.
|
|
12
|
+
*/
|
|
13
|
+
const ACKED_KINDS = new Set(['message', 'receipt', 'notification', 'call']);
|
|
14
|
+
/**
|
|
15
|
+
* A JID arrives in whichever namespace the sender used, and the alt attributes carry the other
|
|
16
|
+
* form. Matching every one of these lets a filter written with a phone number also catch the
|
|
17
|
+
* LID-addressed copy of the same stanza (and vice versa).
|
|
18
|
+
*/
|
|
19
|
+
const REMOTE_JID_ATTRS = ['from', 'sender_pn', 'sender_lid'];
|
|
20
|
+
const PARTICIPANT_ATTRS = ['participant', 'participant_pn', 'participant_lid'];
|
|
21
|
+
const DESCRIPTOR_FIELDS = ['remoteJid', 'fromMe', 'id', 'participant'];
|
|
22
|
+
/** Deviceless user-form JID. Userless values (e.g. `s.whatsapp.net`) are kept verbatim. */
|
|
23
|
+
const stripDevice = (jid) => {
|
|
24
|
+
if (!jid) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const decoded = jidDecode(jid);
|
|
28
|
+
if (!decoded?.user) {
|
|
29
|
+
return jid;
|
|
30
|
+
}
|
|
31
|
+
return jidEncode(decoded.user, decoded.server);
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Compares deviceless JIDs as whole strings rather than by user part. `areJidsSameUser` ignores
|
|
35
|
+
* the server, so it reports `5551234@s.whatsapp.net` and `5551234@lid` as the same user — two
|
|
36
|
+
* unrelated accounts that would then share a filter. Server equality is required here; the
|
|
37
|
+
* PN/LID crossover is handled by the alt attributes instead.
|
|
38
|
+
*/
|
|
39
|
+
const sameJid = (a, b) => {
|
|
40
|
+
if (!a || !b) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return stripDevice(a) === stripDevice(b);
|
|
44
|
+
};
|
|
45
|
+
export const isIgnorableKind = (tag) => IGNORE_KEY_KIND_SET.has(tag);
|
|
46
|
+
/** Validates a descriptor or predicate and freezes it into an internal entry. */
|
|
47
|
+
export const normalizeIgnoreKey = (input) => {
|
|
48
|
+
if (typeof input === 'function') {
|
|
49
|
+
return { predicate: input };
|
|
50
|
+
}
|
|
51
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
52
|
+
throw new Error('ignoreKey expects a descriptor object or a predicate function');
|
|
53
|
+
}
|
|
54
|
+
if (!DESCRIPTOR_FIELDS.some(field => input[field] !== undefined)) {
|
|
55
|
+
throw new Error('ignoreKey descriptor needs at least one of remoteJid, fromMe, id, participant');
|
|
56
|
+
}
|
|
57
|
+
const entry = {};
|
|
58
|
+
if (input.remoteJid !== undefined) {
|
|
59
|
+
const jids = (Array.isArray(input.remoteJid) ? input.remoteJid : [input.remoteJid]).filter(Boolean);
|
|
60
|
+
if (!jids.length) {
|
|
61
|
+
throw new Error('ignoreKey remoteJid cannot be empty');
|
|
62
|
+
}
|
|
63
|
+
entry.remoteJid = jids;
|
|
64
|
+
}
|
|
65
|
+
if (input.participant !== undefined) {
|
|
66
|
+
if (typeof input.participant !== 'string' || !input.participant) {
|
|
67
|
+
throw new Error('ignoreKey participant must be a non-empty jid');
|
|
68
|
+
}
|
|
69
|
+
entry.participant = input.participant;
|
|
70
|
+
}
|
|
71
|
+
if (input.fromMe !== undefined) {
|
|
72
|
+
entry.fromMe = !!input.fromMe;
|
|
73
|
+
}
|
|
74
|
+
if (input.id !== undefined) {
|
|
75
|
+
if (typeof input.id !== 'string' || !input.id) {
|
|
76
|
+
throw new Error('ignoreKey id must be a non-empty string');
|
|
77
|
+
}
|
|
78
|
+
entry.id = input.id;
|
|
79
|
+
}
|
|
80
|
+
if (input.only !== undefined) {
|
|
81
|
+
const only = Array.isArray(input.only) ? input.only : [input.only];
|
|
82
|
+
if (!only.length) {
|
|
83
|
+
throw new Error('ignoreKey only cannot be empty');
|
|
84
|
+
}
|
|
85
|
+
const unknown = only.filter(kind => !IGNORE_KEY_KIND_SET.has(kind));
|
|
86
|
+
if (unknown.length) {
|
|
87
|
+
throw new Error(`ignoreKey only has unknown kinds: ${unknown.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
entry.only = only;
|
|
90
|
+
}
|
|
91
|
+
return entry;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Parses the wire node into the shape filters match on, so a predicate never has to walk a
|
|
95
|
+
* BinaryNode. `remoteJid` and `participant` are device-stripped to line up with the
|
|
96
|
+
* `event.key.remoteJid` a handler would see.
|
|
97
|
+
*/
|
|
98
|
+
export const buildIgnoreContext = (node, meId, meLid) => {
|
|
99
|
+
const attrs = node.attrs || {};
|
|
100
|
+
const author = attrs.participant || attrs.participant_pn || attrs.participant_lid || attrs.from;
|
|
101
|
+
const fromMe = !!author && ((!!meId && areJidsSameUser(author, meId)) || (!!meLid && areJidsSameUser(author, meLid)));
|
|
102
|
+
return {
|
|
103
|
+
kind: node.tag,
|
|
104
|
+
remoteJid: stripDevice(attrs.from),
|
|
105
|
+
fromMe,
|
|
106
|
+
id: attrs.id,
|
|
107
|
+
participant: stripDevice(attrs.participant || attrs.participant_pn || attrs.participant_lid)
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
export const matchesIgnoreKey = (entry, ctx, node) => {
|
|
111
|
+
if (entry.predicate) {
|
|
112
|
+
return !!entry.predicate(ctx);
|
|
113
|
+
}
|
|
114
|
+
if (entry.only && !entry.only.includes(ctx.kind)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (entry.id !== undefined && entry.id !== ctx.id) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (entry.fromMe !== undefined && entry.fromMe !== ctx.fromMe) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const attrs = node.attrs || {};
|
|
124
|
+
if (entry.remoteJid && !entry.remoteJid.some(wanted => REMOTE_JID_ATTRS.some(attr => sameJid(attrs[attr], wanted)))) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (entry.participant && !PARTICIPANT_ATTRS.some(attr => sameJid(attrs[attr], entry.participant))) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
};
|
|
132
|
+
export const ignoredStanzaNeedsAck = (tag) => ACKED_KINDS.has(tag);
|
|
133
|
+
/**
|
|
134
|
+
* Holds the registered filters and answers the one question the socket asks per stanza.
|
|
135
|
+
*
|
|
136
|
+
* `category='peer'` traffic from this account's own devices is never dropped: those stanzas
|
|
137
|
+
* carry app-state key shares, history sync and PDO responses between the account's devices, so
|
|
138
|
+
* the common `{ fromMe: true }` filter would otherwise swallow the app-state key share and leave
|
|
139
|
+
* the collections blocked on a key they re-request on every sync. The exemption only applies
|
|
140
|
+
* when the sender really is this account — resolved against both `meJid` and `meLid` — so a
|
|
141
|
+
* foreign stanza cannot dodge a filter by stamping the attribute.
|
|
142
|
+
*/
|
|
143
|
+
export const makeIgnoreKeyRegistry = ({ getMeId, getMeLid, logger }) => {
|
|
144
|
+
const entries = new Set();
|
|
145
|
+
const register = (input) => {
|
|
146
|
+
const entry = normalizeIgnoreKey(input);
|
|
147
|
+
entries.add(entry);
|
|
148
|
+
return () => {
|
|
149
|
+
entries.delete(entry);
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
const shouldDrop = (node) => {
|
|
153
|
+
if (!entries.size || !isIgnorableKind(node.tag)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const ctx = buildIgnoreContext(node, getMeId(), getMeLid());
|
|
157
|
+
if (ctx.fromMe && node.attrs?.category === 'peer') {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
try {
|
|
162
|
+
if (matchesIgnoreKey(entry, ctx, node)) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
// A throwing predicate must not take the stanza with it; the stanza stays.
|
|
168
|
+
logger?.error({ err, kind: ctx.kind, id: ctx.id }, 'ignoreKey filter threw, keeping stanza');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
};
|
|
173
|
+
return { register, shouldDrop, get size() { return entries.size; } };
|
|
174
|
+
};
|
package/lib/Utils/index.d.ts
CHANGED
|
@@ -22,5 +22,8 @@ export * from "./browser-utils.js";
|
|
|
22
22
|
export * from "./companion-reg-client-utils.js";
|
|
23
23
|
export * from "./identity-change-handler.js";
|
|
24
24
|
export * from "./stanza-ack.js";
|
|
25
|
+
export * from "./ignore-key.js";
|
|
26
|
+
export * from "./anchor-guard.js";
|
|
27
|
+
export * from "./a2ui.js";
|
|
25
28
|
export * from "./meta-ai-msmsg.js";
|
|
26
29
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/Utils/index.js
CHANGED
|
@@ -22,5 +22,8 @@ export * from './browser-utils.js';
|
|
|
22
22
|
export * from './companion-reg-client-utils.js';
|
|
23
23
|
export * from './identity-change-handler.js';
|
|
24
24
|
export * from './stanza-ack.js';
|
|
25
|
+
export * from './ignore-key.js';
|
|
26
|
+
export * from './anchor-guard.js';
|
|
27
|
+
export * from './a2ui.js';
|
|
25
28
|
export * from './meta-ai-msmsg.js';
|
|
26
29
|
//# sourceMappingURL=index.js.map
|
|
@@ -303,9 +303,31 @@ export const getStream = async (item, opts) => {
|
|
|
303
303
|
if (Buffer.isBuffer(item)) {
|
|
304
304
|
return { stream: toReadable(item), type: 'buffer' };
|
|
305
305
|
}
|
|
306
|
+
// A bare path or URL string is accepted, and the reason is the error it used to produce. The
|
|
307
|
+
// contract below is `Buffer | { stream } | { url }`, so `'stream' in item` throws on a string —
|
|
308
|
+
// and it throws with the path interpolated:
|
|
309
|
+
//
|
|
310
|
+
// TypeError: Cannot use 'in' operator to search for 'stream' in /tmp/8cktb7o7x.webp
|
|
311
|
+
//
|
|
312
|
+
// Measured on a production fleet, 2026-10-10: five of those under [SEND_STICKER]. The message
|
|
313
|
+
// names a file, so it reads as a missing or unreadable one, while the real fault is the SHAPE of
|
|
314
|
+
// the argument — and the file was perfectly readable: `createReadStream(item.url)` at the end of
|
|
315
|
+
// this function handles exactly that string. Widening here costs nothing and removes the whole
|
|
316
|
+
// class, for every caller rather than the one that was caught.
|
|
317
|
+
if (typeof item === 'string') {
|
|
318
|
+
item = { url: item };
|
|
319
|
+
}
|
|
320
|
+
if (!item || typeof item !== 'object') {
|
|
321
|
+
throw new Boom(`media source must be a Buffer, a string, { stream } or { url }, got ${item === null ? 'null' : typeof item}`);
|
|
322
|
+
}
|
|
306
323
|
if ('stream' in item) {
|
|
307
324
|
return { stream: item.stream, type: 'readable' };
|
|
308
325
|
}
|
|
326
|
+
// Named rather than left to throw "Cannot read properties of undefined (reading 'toString')",
|
|
327
|
+
// which says nothing about which field is missing.
|
|
328
|
+
if (item.url === undefined || item.url === null) {
|
|
329
|
+
throw new Boom('media source object has neither `stream` nor `url`');
|
|
330
|
+
}
|
|
309
331
|
const urlStr = item.url.toString();
|
|
310
332
|
if (urlStr.startsWith('data:')) {
|
|
311
333
|
const buffer = Buffer.from(urlStr.split(',')[1], 'base64');
|
package/lib/Utils/messages.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export function getAggregateResponsesInEventMessage({ eventResponses }: {
|
|
|
19
19
|
}, meId: any): any[];
|
|
20
20
|
export function extractUrlFromText(text: any): any;
|
|
21
21
|
export function generateLinkPreviewIfRequired(text: any, getUrlInfo: any, logger: any): Promise<any>;
|
|
22
|
+
export function hashImagePollOption(optionName: any, fileSha256?: Uint8Array | null): string;
|
|
22
23
|
export function prepareWAMessageMedia(message: any, options: any): Promise<proto.Message>;
|
|
23
24
|
export function prepareDisappearingMessageSettingContent(ephemeralExpiration: any): proto.Message;
|
|
24
25
|
export function generateForwardMessageContent(message: any, forceForward: any): any;
|
package/lib/Utils/messages.js
CHANGED
|
@@ -6,6 +6,7 @@ import { proto } from '../../WAProto/index.js';
|
|
|
6
6
|
import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, DONATE_URL, LIBRARY_NAME, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
|
|
7
7
|
import { AssociationType, ButtonHeaderType, ButtonType, CarouselCardType, ListType, ProtocolType, WAMessageStatus, WAProto } from '../Types/index.js';
|
|
8
8
|
import { isLidUser, isPnUser, isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
|
|
9
|
+
import { buildA2UIWidget } from './a2ui.js';
|
|
9
10
|
import { sha256 } from './crypto.js';
|
|
10
11
|
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
|
|
11
12
|
import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getImageProcessingLibrary, getRawMediaUploadData, getStream, toBuffer } from './messages-media.js';
|
|
@@ -60,6 +61,16 @@ const assertColor = async (color) => {
|
|
|
60
61
|
}
|
|
61
62
|
return assertedColor;
|
|
62
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Option identifier for an image poll: WhatsApp hashes the option name, appends the base64
|
|
66
|
+
* media hash, and hashes that — `sha256(hex(sha256(name)) + base64(fileSha256))`. A vote carries
|
|
67
|
+
* this hash, not the name, so it has to match byte for byte.
|
|
68
|
+
*/
|
|
69
|
+
export const hashImagePollOption = (optionName, fileSha256) => {
|
|
70
|
+
const nameHash = sha256(Buffer.from(String(optionName), 'utf-8')).toString('hex');
|
|
71
|
+
const mediaHashB64 = fileSha256 ? Buffer.from(fileSha256).toString('base64') : '';
|
|
72
|
+
return sha256(Buffer.from(nameHash + mediaHashB64, 'utf-8')).toString('hex');
|
|
73
|
+
};
|
|
63
74
|
export const prepareWAMessageMedia = async (message, options) => {
|
|
64
75
|
const logger = options.logger;
|
|
65
76
|
let mediaType;
|
|
@@ -891,6 +902,36 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
891
902
|
messageSecret: message.poll.messageSecret || randomBytes(32)
|
|
892
903
|
};
|
|
893
904
|
}
|
|
905
|
+
// Image poll: the parent carries only the option names and their hashes. The images
|
|
906
|
+
// themselves travel as separate pollCreationOptionImageMessage children, associated back
|
|
907
|
+
// to this message by MEDIA_POLL — sendMessage sends those after the parent.
|
|
908
|
+
else if (hasNonNullishProperty(message, 'imagePoll')) {
|
|
909
|
+
if (!Array.isArray(message.imagePoll.options) || !message.imagePoll.options.length) {
|
|
910
|
+
throw new Boom('Invalid imagePoll options', { statusCode: 400 });
|
|
911
|
+
}
|
|
912
|
+
const selectableOptionsCount = message.imagePoll.selectableCount ?? 1;
|
|
913
|
+
if (selectableOptionsCount < 0 || selectableOptionsCount > message.imagePoll.options.length) {
|
|
914
|
+
throw new Boom(`imagePoll.selectableCount should be >= 0 and <= ${message.imagePoll.options.length}`, {
|
|
915
|
+
statusCode: 400
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
const missingHash = message.imagePoll.options.find(option => !option.optionHash);
|
|
919
|
+
if (missingHash) {
|
|
920
|
+
throw new Boom('imagePoll options must be prepared with hashImagePollOption; send via sendMessage instead of building the content by hand', { statusCode: 400 });
|
|
921
|
+
}
|
|
922
|
+
m.pollCreationMessageV3 = {
|
|
923
|
+
name: message.imagePoll.name,
|
|
924
|
+
selectableOptionsCount,
|
|
925
|
+
options: message.imagePoll.options.map(option => ({
|
|
926
|
+
optionName: option.optionName,
|
|
927
|
+
optionHash: option.optionHash
|
|
928
|
+
})),
|
|
929
|
+
pollContentType: proto.Message.PollContentType.IMAGE
|
|
930
|
+
};
|
|
931
|
+
m.messageContextInfo = {
|
|
932
|
+
messageSecret: message.imagePoll.messageSecret || randomBytes(32)
|
|
933
|
+
};
|
|
934
|
+
}
|
|
894
935
|
// Lia@Changes 08-02-26 --- Add poll result snapshot message
|
|
895
936
|
else if (hasNonNullishProperty(message, 'pollResult')) {
|
|
896
937
|
const pollResultSnapshotMessage = {
|
|
@@ -994,6 +1035,12 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
994
1035
|
}
|
|
995
1036
|
};
|
|
996
1037
|
}
|
|
1038
|
+
// An A2UI widget has no base content of its own — the interactive chain below replaces `m`
|
|
1039
|
+
// wholesale. Without this it falls through to the media branch and is rejected as a media
|
|
1040
|
+
// message, so a widget would need a dummy `text` to send at all.
|
|
1041
|
+
else if (hasNonNullishProperty(message, 'a2ui')) {
|
|
1042
|
+
m = {};
|
|
1043
|
+
}
|
|
997
1044
|
else {
|
|
998
1045
|
m = await prepareWAMessageMedia(message, options);
|
|
999
1046
|
}
|
|
@@ -1117,6 +1164,31 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
1117
1164
|
}
|
|
1118
1165
|
};
|
|
1119
1166
|
}
|
|
1167
|
+
// A2UI widget. `nativeFlowMessage` is always set, even empty: it is what the client keys the
|
|
1168
|
+
// interactive render off, and it is also what makes the socket attach the <biz> node.
|
|
1169
|
+
else if (hasNonNullishProperty(message, 'a2ui')) {
|
|
1170
|
+
const interactiveMessage = {
|
|
1171
|
+
bloksWidget: buildA2UIWidget(message.a2ui, {
|
|
1172
|
+
type: message.a2uiType,
|
|
1173
|
+
wrapped: message.a2uiWrapped !== false
|
|
1174
|
+
}),
|
|
1175
|
+
nativeFlowMessage: hasNonNullishProperty(message, 'nativeFlow')
|
|
1176
|
+
? prepareNativeFlowButtons(message)
|
|
1177
|
+
: { messageParamsJson: '' }
|
|
1178
|
+
};
|
|
1179
|
+
// singleScreen drops the surrounding bubble so the widget is the whole message
|
|
1180
|
+
if (!message.singleScreen) {
|
|
1181
|
+
interactiveMessage.header = { hasMediaAttachment: false };
|
|
1182
|
+
interactiveMessage.body = { text: message.text || '' };
|
|
1183
|
+
interactiveMessage.footer = { text: message.footer || '' };
|
|
1184
|
+
}
|
|
1185
|
+
m = {
|
|
1186
|
+
interactiveMessage,
|
|
1187
|
+
messageContextInfo: {
|
|
1188
|
+
messageSecret: message.messageSecret || randomBytes(32)
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1120
1192
|
else if (hasNonNullishProperty(message, 'nativeFlow')) {
|
|
1121
1193
|
const interactiveMessage = {
|
|
1122
1194
|
nativeFlowMessage: prepareNativeFlowButtons(message)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function useMultiFileAuthState(folder: any): Promise<{
|
|
1
|
+
export function useMultiFileAuthState(folder: any, logger?: any): Promise<{
|
|
2
2
|
state: {
|
|
3
3
|
creds: any;
|
|
4
4
|
keys: {
|
|
@@ -8,5 +8,6 @@ export function useMultiFileAuthState(folder: any): Promise<{
|
|
|
8
8
|
};
|
|
9
9
|
saveCreds: () => void;
|
|
10
10
|
_flushCreds: () => Promise<void>;
|
|
11
|
+
_destroy: () => void;
|
|
11
12
|
}>;
|
|
12
13
|
//# sourceMappingURL=use-multi-file-auth-state.d.ts.map
|
|
@@ -51,7 +51,11 @@ const makeDebouncedSaver = (filePath, getData, logger, delayMs = 500) => {
|
|
|
51
51
|
clearTimeout(timer);
|
|
52
52
|
try {
|
|
53
53
|
writeFileSync(filePath, JSON.stringify(getData(), BufferJSON.replacer));
|
|
54
|
-
} catch {
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Last-chance save on the way out. Nothing left to retry with, but a silent
|
|
56
|
+
// failure here loses the session and looks like a random logout next boot.
|
|
57
|
+
logger?.error?.({ err, filePath }, 'Failed to save credentials on exit');
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
};
|
|
57
61
|
|
|
@@ -82,7 +86,7 @@ const makeDebouncedSaver = (filePath, getData, logger, delayMs = 500) => {
|
|
|
82
86
|
return { save, flush: flushNow, destroy };
|
|
83
87
|
};
|
|
84
88
|
|
|
85
|
-
export const useMultiFileAuthState = async (folder) => {
|
|
89
|
+
export const useMultiFileAuthState = async (folder, logger) => {
|
|
86
90
|
const writeData = async (data, file) => {
|
|
87
91
|
const filePath = join(folder, fixFileName(file));
|
|
88
92
|
const mutex = getFileLock(filePath);
|
|
@@ -107,7 +111,13 @@ export const useMultiFileAuthState = async (folder) => {
|
|
|
107
111
|
release();
|
|
108
112
|
}
|
|
109
113
|
});
|
|
110
|
-
} catch {
|
|
114
|
+
} catch (err) {
|
|
115
|
+
// A missing file is the normal "no such key yet" answer. Anything else — corrupt
|
|
116
|
+
// JSON, bad permissions — also yields null, which silently starts a fresh pairing,
|
|
117
|
+
// so it is logged rather than swallowed.
|
|
118
|
+
if (err?.code !== 'ENOENT') {
|
|
119
|
+
logger?.warn?.({ err, file }, 'Failed to read auth state file');
|
|
120
|
+
}
|
|
111
121
|
return null;
|
|
112
122
|
}
|
|
113
123
|
};
|
|
@@ -119,7 +129,12 @@ export const useMultiFileAuthState = async (folder) => {
|
|
|
119
129
|
return mutex.acquire().then(async (release) => {
|
|
120
130
|
try {
|
|
121
131
|
await unlink(filePath);
|
|
122
|
-
} catch {
|
|
132
|
+
} catch (err) {
|
|
133
|
+
// Deleting a key that was never written is normal.
|
|
134
|
+
if (err?.code !== 'ENOENT') {
|
|
135
|
+
logger?.warn?.({ err, file }, 'Failed to remove auth state file');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
123
138
|
finally {
|
|
124
139
|
release();
|
|
125
140
|
}
|
|
@@ -139,7 +154,7 @@ export const useMultiFileAuthState = async (folder) => {
|
|
|
139
154
|
const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-');
|
|
140
155
|
const creds = (await readData('creds.json')) || initAuthCreds();
|
|
141
156
|
const credsPath = join(folder, 'creds.json');
|
|
142
|
-
const debouncer = makeDebouncedSaver(credsPath, () => creds,
|
|
157
|
+
const debouncer = makeDebouncedSaver(credsPath, () => creds, logger, 500);
|
|
143
158
|
|
|
144
159
|
return {
|
|
145
160
|
state: {
|
|
@@ -170,6 +185,12 @@ export const useMultiFileAuthState = async (folder) => {
|
|
|
170
185
|
}
|
|
171
186
|
},
|
|
172
187
|
saveCreds: debouncer.save,
|
|
173
|
-
_flushCreds: debouncer.flush
|
|
188
|
+
_flushCreds: debouncer.flush,
|
|
189
|
+
/**
|
|
190
|
+
* Drops the process exit/SIGINT/SIGTERM listeners this auth state installed. A long-lived
|
|
191
|
+
* host that opens and closes many sessions leaks one set per call without it, and Node
|
|
192
|
+
* starts warning about listener counts after eleven.
|
|
193
|
+
*/
|
|
194
|
+
_destroy: debouncer.destroy
|
|
174
195
|
};
|
|
175
196
|
};
|
|
@@ -8,7 +8,7 @@ import { Mutex } from 'async-mutex';
|
|
|
8
8
|
// Lia@Changes 25-03-26 --- Add useSingleFileAuthState with integrated cache
|
|
9
9
|
const FLUSH_TIMEOUT_MS = 3000;
|
|
10
10
|
// Lia@Changes 22-04-26 --- Enhanced useSingleFileAuthState with LRUCache
|
|
11
|
-
export const useSingleFileAuthState = async (fileName) => {
|
|
11
|
+
export const useSingleFileAuthState = async (fileName, logger) => {
|
|
12
12
|
const cache = new LRUCache({
|
|
13
13
|
max: 20000,
|
|
14
14
|
ttl: 1000 * DEFAULT_CACHE_TTLS.SIGNAL_STORE,
|
|
@@ -32,7 +32,12 @@ export const useSingleFileAuthState = async (fileName) => {
|
|
|
32
32
|
cache.set(keyName, value);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
catch {
|
|
35
|
+
catch (err) {
|
|
36
|
+
// A missing file is a fresh session. Corrupt JSON or bad permissions also land
|
|
37
|
+
// here and silently discard a working session, so they are logged.
|
|
38
|
+
if (err?.code !== 'ENOENT') {
|
|
39
|
+
logger?.warn?.({ err, fileName }, 'Failed to load auth state file, starting empty');
|
|
40
|
+
}
|
|
36
41
|
fileData = {};
|
|
37
42
|
}
|
|
38
43
|
isLoaded = true;
|
|
@@ -49,7 +54,11 @@ export const useSingleFileAuthState = async (fileName) => {
|
|
|
49
54
|
await writeFile(tempFile, JSON.stringify(fileData, BufferJSON.replacer));
|
|
50
55
|
await rename(tempFile, fileName);
|
|
51
56
|
}
|
|
52
|
-
catch {
|
|
57
|
+
catch (err) {
|
|
58
|
+
// Swallowing this lost every key written since the last successful flush with
|
|
59
|
+
// no trace, which surfaces later as a session that cannot decrypt.
|
|
60
|
+
logger?.error?.({ err, fileName }, 'Failed to flush auth state file');
|
|
61
|
+
}
|
|
53
62
|
});
|
|
54
63
|
}, FLUSH_TIMEOUT_MS);
|
|
55
64
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@violetix/baileys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Enhanced Baileys v7 with fixes for newsletter media uploads, plus support for interactive messages, albums, and additional message types.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"better-sqlite3": "^11.0.0",
|
|
68
68
|
"jimp": "^1.6.1",
|
|
69
69
|
"link-preview-js": "^3.0.0",
|
|
70
|
-
"sharp": "
|
|
70
|
+
"sharp": ">=0.33.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependenciesMeta": {
|
|
73
73
|
"@napi-rs/image": {
|