@violetix/baileys 0.5.5 → 0.6.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 +1530 -2009
- package/WAProto/index.d.ts +14030 -14586
- package/WAProto/index.js +58426 -88874
- package/lib/Defaults/index.d.ts +2 -7
- package/lib/Defaults/index.js +5 -20
- package/lib/Signal/Group/sender-key-message.js +19 -1
- package/lib/Signal/libsignal.js +82 -29
- package/lib/Signal/xeddsa-fast.js +114 -0
- package/lib/Socket/Client/types.js +6 -8
- package/lib/Socket/Client/websocket.js +3 -38
- package/lib/Socket/business.d.ts +0 -3
- package/lib/Socket/chats.d.ts +0 -3
- package/lib/Socket/chats.js +18 -45
- package/lib/Socket/communities.d.ts +0 -3
- package/lib/Socket/graphql.js +3 -7
- package/lib/Socket/groups.d.ts +0 -3
- package/lib/Socket/groups.js +3 -8
- package/lib/Socket/index.d.ts +15 -17
- package/lib/Socket/messages-recv.d.ts +0 -3
- package/lib/Socket/messages-recv.js +43 -127
- package/lib/Socket/messages-send.d.ts +0 -4
- package/lib/Socket/messages-send.js +63 -191
- package/lib/Socket/newsletter.d.ts +0 -3
- package/lib/Socket/newsletter.js +1 -2
- package/lib/Socket/newsletter.js.map +1 -1
- package/lib/Socket/socket.d.ts +0 -9
- package/lib/Socket/socket.js +53 -341
- package/lib/Store/make-in-memory-store.js +30 -41
- package/lib/Types/Auth.d.ts +2 -60
- package/lib/Types/Bussines.d.ts +2 -9
- package/lib/Types/Call.d.ts +2 -22
- package/lib/Types/Chat.d.ts +1 -2
- package/lib/Types/Contact.d.ts +2 -11
- package/lib/Types/Events.d.ts +2 -41
- package/lib/Types/GroupMetadata.d.ts +2 -36
- package/lib/Types/Message.d.ts +1 -11
- package/lib/Types/Product.d.ts +2 -5
- package/lib/Types/Signal.d.ts +2 -10
- package/lib/Types/Socket.d.ts +2 -33
- package/lib/Types/USync.d.ts +2 -9
- package/lib/Types/index.d.ts +1 -3
- package/lib/Utils/auth-utils.d.ts +2 -3
- package/lib/Utils/auth-utils.js +53 -37
- package/lib/Utils/business.js +11 -25
- package/lib/Utils/chat-utils.js +4 -6
- package/lib/Utils/decode-wa-message.js +2 -2
- package/lib/Utils/event-buffer.js +12 -21
- package/lib/Utils/generics.d.ts +5 -5
- package/lib/Utils/generics.js +7 -25
- package/lib/Utils/history.d.ts +1 -2
- package/lib/Utils/history.js +83 -137
- package/lib/Utils/index.d.ts +0 -4
- package/lib/Utils/index.js +0 -4
- package/lib/Utils/listener-guard.js +0 -2
- package/lib/Utils/message-retry-manager.js +0 -20
- package/lib/Utils/messages-media.d.ts +8 -39
- package/lib/Utils/messages-media.js +89 -218
- package/lib/Utils/messages.d.ts +0 -1
- package/lib/Utils/messages.js +36 -122
- package/lib/Utils/process-message.d.ts +0 -18
- package/lib/Utils/process-message.js +47 -99
- package/lib/Utils/rich-message-utils.js +4 -3
- package/lib/Utils/use-multi-file-auth-state.d.ts +3 -5
- package/lib/Utils/use-multi-file-auth-state.js +6 -27
- package/lib/Utils/use-single-file-auth-state.d.ts +1 -1
- package/lib/Utils/use-single-file-auth-state.js +3 -12
- package/lib/WABinary/types.d.ts +2 -6
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +3 -6
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +2 -3
- package/lib/WAUSync/USyncQuery.d.ts +1 -1
- package/lib/WAUSync/USyncQuery.js +4 -7
- package/package.json +107 -123
- package/lib/Utils/a2ui.d.ts +0 -50
- package/lib/Utils/a2ui.js +0 -168
- package/lib/Utils/anchor-guard.d.ts +0 -71
- package/lib/Utils/anchor-guard.js +0 -404
- package/lib/Utils/connection-manager.d.ts +0 -44
- package/lib/Utils/connection-manager.js +0 -258
- package/lib/Utils/ignore-key.d.ts +0 -51
- package/lib/Utils/ignore-key.js +0 -176
package/lib/Utils/a2ui.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
// A2UI (Agent-to-UI) widget builder. Declarative components — Text, Image, Video, Button,
|
|
2
|
-
// Card, Column, Row, Divider, CheckBox, TextField, ChoicePicker, plus the `listCard` shortcut
|
|
3
|
-
// — compiled into the JSON payload WhatsApp carries in `interactiveMessage.bloksWidget`.
|
|
4
|
-
//
|
|
5
|
-
// Ported from @rennzsync/baileys (MIT).
|
|
6
|
-
//
|
|
7
|
-
// Every builder method registers a component and returns its id; ids are what the container
|
|
8
|
-
// methods take, so a tree is written bottom-up:
|
|
9
|
-
//
|
|
10
|
-
// const ui = new A2UI()
|
|
11
|
-
// const title = ui.text('Hello', { variant: 'h1' })
|
|
12
|
-
// const button = ui.button(ui.text('Press me'), { action: { name: 'noop' } })
|
|
13
|
-
// ui.root([ui.card(ui.column([title, button]))])
|
|
14
|
-
// await sock.sendMessage(jid, { a2ui: ui, text: 'Widget' })
|
|
15
|
-
//
|
|
16
|
-
// This is an internal WhatsApp format: a send can succeed while the recipient's client renders
|
|
17
|
-
// nothing. Verify against your target client build before relying on it.
|
|
18
|
-
import { randomUUID } from 'crypto';
|
|
19
|
-
const DEFAULT_CATALOG_ID = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
|
|
20
|
-
export class A2UI {
|
|
21
|
-
constructor({ catalogId = DEFAULT_CATALOG_ID, version = 'v0.9' } = {}) {
|
|
22
|
-
this._version = version;
|
|
23
|
-
this._catalogId = catalogId;
|
|
24
|
-
this._components = new Map();
|
|
25
|
-
this._counter = 0;
|
|
26
|
-
this._rootChildren = [];
|
|
27
|
-
this._listCardPayload = undefined;
|
|
28
|
-
}
|
|
29
|
-
#nextId(prefix) {
|
|
30
|
-
return `${prefix}_${(this._counter++).toString(36)}`;
|
|
31
|
-
}
|
|
32
|
-
#reg(id, component, extra = {}) {
|
|
33
|
-
id ??= this.#nextId(component.toLowerCase());
|
|
34
|
-
if (this._components.has(id)) {
|
|
35
|
-
throw new Error(`Component id "${id}" already used`);
|
|
36
|
-
}
|
|
37
|
-
this._components.set(id, { id, component, ...extra });
|
|
38
|
-
return id;
|
|
39
|
-
}
|
|
40
|
-
text(text, { id, variant = 'body' } = {}) {
|
|
41
|
-
return this.#reg(id, 'Text', { text, variant });
|
|
42
|
-
}
|
|
43
|
-
image(url, { id, variant, fit = 'cover' } = {}) {
|
|
44
|
-
return this.#reg(id, 'Image', { url, ...(variant ? { variant } : {}), fit });
|
|
45
|
-
}
|
|
46
|
-
video(url, { id } = {}) {
|
|
47
|
-
return this.#reg(id, 'Video', { url });
|
|
48
|
-
}
|
|
49
|
-
checkbox(label, { id, value = false } = {}) {
|
|
50
|
-
return this.#reg(id, 'CheckBox', { label, value });
|
|
51
|
-
}
|
|
52
|
-
textField(label, { id, variant = 'text' } = {}) {
|
|
53
|
-
return this.#reg(id, 'TextField', { label, variant });
|
|
54
|
-
}
|
|
55
|
-
button(childId, { id, variant = 'primary', action } = {}) {
|
|
56
|
-
if (!childId) {
|
|
57
|
-
throw new TypeError('button(childId) requires the id of a child component (e.g. from .text())');
|
|
58
|
-
}
|
|
59
|
-
return this.#reg(id, 'Button', { child: childId, variant, ...(action ? { action } : {}) });
|
|
60
|
-
}
|
|
61
|
-
card(childId, { id } = {}) {
|
|
62
|
-
if (!childId) {
|
|
63
|
-
throw new TypeError('card(childId) requires the id of a child component');
|
|
64
|
-
}
|
|
65
|
-
return this.#reg(id, 'Card', { child: childId });
|
|
66
|
-
}
|
|
67
|
-
column(children = [], { id, justify, align } = {}) {
|
|
68
|
-
if (!children.length) {
|
|
69
|
-
throw new TypeError('column(children) requires at least one child id');
|
|
70
|
-
}
|
|
71
|
-
return this.#reg(id, 'Column', { children, ...(justify ? { justify } : {}), ...(align ? { align } : {}) });
|
|
72
|
-
}
|
|
73
|
-
row(children = [], { id } = {}) {
|
|
74
|
-
if (!children.length) {
|
|
75
|
-
throw new TypeError('row(children) requires at least one child id');
|
|
76
|
-
}
|
|
77
|
-
return this.#reg(id, 'Row', { children });
|
|
78
|
-
}
|
|
79
|
-
divider({ id } = {}) {
|
|
80
|
-
return this.#reg(id, 'Divider', {});
|
|
81
|
-
}
|
|
82
|
-
choicePicker(label, options, { id, variant = 'mutuallyExclusive', value, displayStyle = 'checkbox', filterable = false } = {}) {
|
|
83
|
-
if (!Array.isArray(options) || !options.length) {
|
|
84
|
-
throw new TypeError('choicePicker(label, options) requires a non-empty options array of {label, value}');
|
|
85
|
-
}
|
|
86
|
-
return this.#reg(id, 'ChoicePicker', {
|
|
87
|
-
label,
|
|
88
|
-
variant,
|
|
89
|
-
...(value !== undefined ? { value } : {}),
|
|
90
|
-
options,
|
|
91
|
-
displayStyle,
|
|
92
|
-
filterable
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
/** Names the top-level components. Required before `build()` unless `listCard` was used. */
|
|
96
|
-
root(children) {
|
|
97
|
-
if (!Array.isArray(children) || !children.length) {
|
|
98
|
-
throw new TypeError('root(children) requires a non-empty array of top-level component ids');
|
|
99
|
-
}
|
|
100
|
-
this._rootChildren = children;
|
|
101
|
-
return this;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Shortcut for the product-list widget, which uses its own payload shape rather than the
|
|
105
|
-
* component catalog. It replaces whatever the component methods built — `build()` returns
|
|
106
|
-
* this payload and ignores `root()`.
|
|
107
|
-
*/
|
|
108
|
-
listCard({ title, items, fallbackText, uuid = randomUUID() } = {}) {
|
|
109
|
-
if (!title) {
|
|
110
|
-
throw new TypeError('listCard requires a title');
|
|
111
|
-
}
|
|
112
|
-
if (!Array.isArray(items) || !items.length) {
|
|
113
|
-
throw new TypeError('listCard requires a non-empty items array');
|
|
114
|
-
}
|
|
115
|
-
this._listCardPayload = {
|
|
116
|
-
uuid,
|
|
117
|
-
data: JSON.stringify({
|
|
118
|
-
type: 'list_card',
|
|
119
|
-
title,
|
|
120
|
-
fallback_text: fallbackText ?? '',
|
|
121
|
-
items: items.map(item => ({
|
|
122
|
-
asset_id: item.assetId ?? randomUUID().replace(/-/g, '').slice(0, 17),
|
|
123
|
-
asset_type: item.assetType ?? 'PRODUCT_ITEM',
|
|
124
|
-
title: item.title,
|
|
125
|
-
trailing_label: item.price ?? item.trailingLabel ?? '',
|
|
126
|
-
trailing_emphasis: item.emphasis ?? 'strong'
|
|
127
|
-
}))
|
|
128
|
-
}),
|
|
129
|
-
type: 'im_a2ui',
|
|
130
|
-
fallback: fallbackText ?? ''
|
|
131
|
-
};
|
|
132
|
-
return this;
|
|
133
|
-
}
|
|
134
|
-
/** Compiles the tree into the `bloksWidget` payload. Called for you by `sendMessage`. */
|
|
135
|
-
build({ uuid = randomUUID(), surfaceId, type = 'im_a2ui', wrapped = true } = {}) {
|
|
136
|
-
if (this._listCardPayload) {
|
|
137
|
-
return this._listCardPayload;
|
|
138
|
-
}
|
|
139
|
-
if (!this._rootChildren.length) {
|
|
140
|
-
throw new Error('Call root([...ids]) before build()');
|
|
141
|
-
}
|
|
142
|
-
const root = { id: 'root', component: 'Column', children: this._rootChildren };
|
|
143
|
-
const components = [root, ...this._components.values()];
|
|
144
|
-
const data = wrapped
|
|
145
|
-
? {
|
|
146
|
-
version: this._version,
|
|
147
|
-
createSurface: {
|
|
148
|
-
surfaceId: surfaceId ?? `starcore-widget=${uuid}`,
|
|
149
|
-
catalogId: this._catalogId,
|
|
150
|
-
components
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
: { components };
|
|
154
|
-
return {
|
|
155
|
-
uuid,
|
|
156
|
-
data: JSON.stringify(data),
|
|
157
|
-
type
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
/** Accepts an `A2UI` instance or an already-built payload, and returns the payload. */
|
|
162
|
-
export const buildA2UIWidget = (a2ui, options = {}) => {
|
|
163
|
-
const widget = typeof a2ui?.build === 'function' ? a2ui.build(options) : a2ui;
|
|
164
|
-
if (!widget || typeof widget.data !== 'string') {
|
|
165
|
-
throw new TypeError('a2ui must be an A2UI instance or a built widget payload with a `data` string');
|
|
166
|
-
}
|
|
167
|
-
return widget;
|
|
168
|
-
};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
export type AnchorGuardLimits = {
|
|
2
|
-
maxText: number;
|
|
3
|
-
maxCaption: number;
|
|
4
|
-
maxMentions: number;
|
|
5
|
-
maxGroupMentions: number;
|
|
6
|
-
maxButtons: number;
|
|
7
|
-
maxSections: number;
|
|
8
|
-
maxRows: number;
|
|
9
|
-
maxCards: number;
|
|
10
|
-
maxDepth: number;
|
|
11
|
-
maxNodes: number;
|
|
12
|
-
maxBytes: number;
|
|
13
|
-
maxParamsJson: number;
|
|
14
|
-
maxInvisibleRun: number;
|
|
15
|
-
maxCombiningRun: number;
|
|
16
|
-
maxNewlines: number;
|
|
17
|
-
};
|
|
18
|
-
export declare const ANCHORGUARD_DEFAULTS: AnchorGuardLimits;
|
|
19
|
-
export type AnchorGuardDetectOptions = Partial<AnchorGuardLimits> & {
|
|
20
|
-
/** Pre-computed encoded size, checked against `maxBytes` without re-encoding. */
|
|
21
|
-
byteLength?: number;
|
|
22
|
-
/** Pass the `proto` namespace to also measure the encoded size. Costs an encode per check. */
|
|
23
|
-
proto?: any;
|
|
24
|
-
};
|
|
25
|
-
export type AnchorGuardDetectResult = {
|
|
26
|
-
flagged: boolean;
|
|
27
|
-
reasons: string[];
|
|
28
|
-
};
|
|
29
|
-
export declare const detectBug: (message: any, options?: AnchorGuardDetectOptions) => AnchorGuardDetectResult;
|
|
30
|
-
export type AnchorGuardDetection = {
|
|
31
|
-
direction: 'incoming' | 'outgoing' | 'group-add';
|
|
32
|
-
jid?: string;
|
|
33
|
-
sender?: string;
|
|
34
|
-
message?: any;
|
|
35
|
-
content?: any;
|
|
36
|
-
reasons: string[];
|
|
37
|
-
burst?: boolean;
|
|
38
|
-
};
|
|
39
|
-
export type AnchorGuardOptions = {
|
|
40
|
-
/** Delete a flagged inbound message. Default `true`. */
|
|
41
|
-
autoDelete?: boolean;
|
|
42
|
-
/** `'auto'` revokes own messages and deletes others locally. Default `'auto'`. */
|
|
43
|
-
deleteMode?: 'auto' | 'everyone' | 'me';
|
|
44
|
-
/** Guard `messages.upsert`. Default `true`. */
|
|
45
|
-
guardIncoming?: boolean;
|
|
46
|
-
/** Wrap `sock.sendMessage` and reject crash payloads. Default `false`. */
|
|
47
|
-
guardOutgoing?: boolean;
|
|
48
|
-
/** Block the sender of a flagged message. Default `false`. */
|
|
49
|
-
blockOnBug?: boolean;
|
|
50
|
-
/** Only guard own chats. Default `false`. */
|
|
51
|
-
selfOnly?: boolean;
|
|
52
|
-
thresholds?: Partial<AnchorGuardLimits>;
|
|
53
|
-
onDetect?: (detection: AnchorGuardDetection) => void | Promise<void>;
|
|
54
|
-
proto?: any;
|
|
55
|
-
/** Messages from one sender inside `burstWindowMs` before it counts as a burst. `0` disables. */
|
|
56
|
-
burstThreshold?: number;
|
|
57
|
-
burstWindowMs?: number;
|
|
58
|
-
/** Remove the sender from the group on burst. Needs admin. Default `false`. */
|
|
59
|
-
kickOnBurst?: boolean;
|
|
60
|
-
/** Flag/kick when a burst-flagged inviter adds this account to a group. Default `false`. */
|
|
61
|
-
guardGroupAdds?: boolean;
|
|
62
|
-
/** Guard Meta AI's numbers instead of exempting them. Default `false`. */
|
|
63
|
-
metaAiNumbers?: boolean;
|
|
64
|
-
ownJid?: string;
|
|
65
|
-
logger?: any;
|
|
66
|
-
};
|
|
67
|
-
export declare const createAnchorGuard: (sock: any, options?: AnchorGuardOptions) => {
|
|
68
|
-
detect: (message: any) => AnchorGuardDetectResult;
|
|
69
|
-
stop: () => void;
|
|
70
|
-
};
|
|
71
|
-
export declare const createAntiBugGuard: typeof createAnchorGuard;
|
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
// Anti force-close guard: detects payloads that crash WhatsApp clients — combining/invisible
|
|
2
|
-
// char floods, mention bombs, oversized native-flow/list/carousel payloads, broken
|
|
3
|
-
// buttonParamsJson, over-deep or circular message structures — then deletes them and optionally
|
|
4
|
-
// blocks the sender.
|
|
5
|
-
//
|
|
6
|
-
// Ported from @rennzsync/baileys' anchor-guard, itself a port of
|
|
7
|
-
// @rexxhayanasi/elaina-anchorguard (MIT).
|
|
8
|
-
import { META_AI_JID } from '../WABinary/index.js';
|
|
9
|
-
export const ANCHORGUARD_DEFAULTS = {
|
|
10
|
-
maxText: 65536,
|
|
11
|
-
maxCaption: 32768,
|
|
12
|
-
maxMentions: 1500,
|
|
13
|
-
maxGroupMentions: 200,
|
|
14
|
-
maxButtons: 100,
|
|
15
|
-
maxSections: 100,
|
|
16
|
-
maxRows: 300,
|
|
17
|
-
maxCards: 60,
|
|
18
|
-
maxDepth: 12,
|
|
19
|
-
maxNodes: 20000,
|
|
20
|
-
maxBytes: 3 * 1024 * 1024,
|
|
21
|
-
maxParamsJson: 262144,
|
|
22
|
-
maxInvisibleRun: 500,
|
|
23
|
-
maxCombiningRun: 100,
|
|
24
|
-
maxNewlines: 20000
|
|
25
|
-
};
|
|
26
|
-
// Escaped on purpose: literal invisible/combining characters make the source unreadable
|
|
27
|
-
// and easy to corrupt on the next edit.
|
|
28
|
-
const INVISIBLE = /[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff\ufff9-\ufffb]/;
|
|
29
|
-
const COMBINING = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\uaa70\uaaf6\uabed\ufe20-\ufe2f]/;
|
|
30
|
-
/** Longest consecutive run of characters matching `matcher`. */
|
|
31
|
-
const longestRun = (text, matcher) => {
|
|
32
|
-
let run = 0;
|
|
33
|
-
let best = 0;
|
|
34
|
-
for (const ch of text) {
|
|
35
|
-
if (matcher.test(ch)) {
|
|
36
|
-
run += 1;
|
|
37
|
-
if (run > best) {
|
|
38
|
-
best = run;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
run = 0;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return best;
|
|
46
|
-
};
|
|
47
|
-
const countChar = (text, ch) => {
|
|
48
|
-
let n = 0;
|
|
49
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
50
|
-
if (text[i] === ch) {
|
|
51
|
-
n += 1;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return n;
|
|
55
|
-
};
|
|
56
|
-
const WRAPPERS = [
|
|
57
|
-
'ephemeralMessage',
|
|
58
|
-
'viewOnceMessage',
|
|
59
|
-
'viewOnceMessageV2',
|
|
60
|
-
'viewOnceMessageV2Extension',
|
|
61
|
-
'documentWithCaptionMessage',
|
|
62
|
-
'editedMessage',
|
|
63
|
-
'deviceSentMessage',
|
|
64
|
-
'botInvokeMessage'
|
|
65
|
-
];
|
|
66
|
-
const unwrapContent = (message) => {
|
|
67
|
-
let current = message;
|
|
68
|
-
let depth = 0;
|
|
69
|
-
while (current && typeof current === 'object' && depth < 64) {
|
|
70
|
-
const wrapper = WRAPPERS.find(w => current[w]?.message);
|
|
71
|
-
if (!wrapper) {
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
current = current[wrapper].message;
|
|
75
|
-
depth += 1;
|
|
76
|
-
}
|
|
77
|
-
return { content: current, wrapDepth: depth };
|
|
78
|
-
};
|
|
79
|
-
/**
|
|
80
|
-
* Inspects a message content object and reports why it looks like a crash payload.
|
|
81
|
-
* Pure — no I/O, no mutation of the input.
|
|
82
|
-
*/
|
|
83
|
-
export const detectBug = (message, options = {}) => {
|
|
84
|
-
const limits = { ...ANCHORGUARD_DEFAULTS, ...options };
|
|
85
|
-
const reasons = [];
|
|
86
|
-
if (!message || typeof message !== 'object') {
|
|
87
|
-
return { flagged: false, reasons };
|
|
88
|
-
}
|
|
89
|
-
const { content, wrapDepth } = unwrapContent(message);
|
|
90
|
-
if (wrapDepth >= 6) {
|
|
91
|
-
reasons.push(`wrapper nesting ${wrapDepth}`);
|
|
92
|
-
}
|
|
93
|
-
let nodes = 0;
|
|
94
|
-
const seen = new WeakSet();
|
|
95
|
-
// One traversal. The mention checks used to be a second full walk of the same tree, which
|
|
96
|
-
// doubled the cost of the guard on the path it is meant to protect: every inbound message.
|
|
97
|
-
const walk = (value, depth) => {
|
|
98
|
-
if (reasons.length > 40) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
nodes += 1;
|
|
102
|
-
if (nodes > limits.maxNodes) {
|
|
103
|
-
reasons.push(`node count > ${limits.maxNodes}`);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (depth > limits.maxDepth) {
|
|
107
|
-
reasons.push(`structure depth > ${limits.maxDepth}`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (typeof value === 'string') {
|
|
111
|
-
if (value.length > limits.maxText) {
|
|
112
|
-
reasons.push(`string length ${value.length} > ${limits.maxText}`);
|
|
113
|
-
}
|
|
114
|
-
if (countChar(value, '\n') > limits.maxNewlines) {
|
|
115
|
-
reasons.push('excessive newlines');
|
|
116
|
-
}
|
|
117
|
-
const invisibleRun = longestRun(value, INVISIBLE);
|
|
118
|
-
if (invisibleRun > limits.maxInvisibleRun) {
|
|
119
|
-
reasons.push(`invisible-char run ${invisibleRun}`);
|
|
120
|
-
}
|
|
121
|
-
const combiningRun = longestRun(value, COMBINING);
|
|
122
|
-
if (combiningRun > limits.maxCombiningRun) {
|
|
123
|
-
reasons.push(`combining-char run ${combiningRun}`);
|
|
124
|
-
}
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (Array.isArray(value)) {
|
|
128
|
-
for (const item of value) {
|
|
129
|
-
walk(item, depth + 1);
|
|
130
|
-
}
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (value && typeof value === 'object') {
|
|
134
|
-
if (seen.has(value)) {
|
|
135
|
-
reasons.push('circular structure');
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
seen.add(value);
|
|
139
|
-
for (const key of Object.keys(value)) {
|
|
140
|
-
const child = value[key];
|
|
141
|
-
if (key === 'contextInfo' && child && typeof child === 'object') {
|
|
142
|
-
if (Array.isArray(child.mentionedJid) && child.mentionedJid.length > limits.maxMentions) {
|
|
143
|
-
reasons.push(`mentionedJid ${child.mentionedJid.length} > ${limits.maxMentions}`);
|
|
144
|
-
}
|
|
145
|
-
if (Array.isArray(child.groupMentions) && child.groupMentions.length > limits.maxGroupMentions) {
|
|
146
|
-
reasons.push(`groupMentions ${child.groupMentions.length} > ${limits.maxGroupMentions}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
walk(child, depth + 1);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
walk(content, 0);
|
|
154
|
-
const inspectInteractive = (node) => {
|
|
155
|
-
if (!node || typeof node !== 'object') {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const nativeFlow = node.nativeFlowMessage || node.interactiveMessage?.nativeFlowMessage;
|
|
159
|
-
if (nativeFlow?.buttons) {
|
|
160
|
-
if (nativeFlow.buttons.length > limits.maxButtons) {
|
|
161
|
-
reasons.push(`nativeFlow buttons ${nativeFlow.buttons.length} > ${limits.maxButtons}`);
|
|
162
|
-
}
|
|
163
|
-
for (const button of nativeFlow.buttons) {
|
|
164
|
-
const params = button?.buttonParamsJson;
|
|
165
|
-
if (typeof params !== 'string') {
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (params.length > limits.maxParamsJson) {
|
|
169
|
-
reasons.push(`buttonParamsJson length ${params.length}`);
|
|
170
|
-
}
|
|
171
|
-
else if (params.length) {
|
|
172
|
-
try {
|
|
173
|
-
JSON.parse(params);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
reasons.push('buttonParamsJson invalid');
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const list = node.listMessage || node.interactiveMessage?.carouselMessage;
|
|
182
|
-
if (list?.sections && list.sections.length > limits.maxSections) {
|
|
183
|
-
reasons.push(`list sections ${list.sections.length} > ${limits.maxSections}`);
|
|
184
|
-
}
|
|
185
|
-
if (Array.isArray(list?.sections)) {
|
|
186
|
-
let rows = 0;
|
|
187
|
-
for (const section of list.sections) {
|
|
188
|
-
rows += Array.isArray(section?.rows) ? section.rows.length : 0;
|
|
189
|
-
}
|
|
190
|
-
if (rows > limits.maxRows) {
|
|
191
|
-
reasons.push(`list rows ${rows} > ${limits.maxRows}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const cards = node.interactiveMessage?.carouselMessage?.cards;
|
|
195
|
-
if (Array.isArray(cards) && cards.length > limits.maxCards) {
|
|
196
|
-
reasons.push(`carousel cards ${cards.length} > ${limits.maxCards}`);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
inspectInteractive(content);
|
|
200
|
-
if (typeof options.byteLength === 'number' && options.byteLength > limits.maxBytes) {
|
|
201
|
-
reasons.push(`encoded size ${options.byteLength} > ${limits.maxBytes}`);
|
|
202
|
-
}
|
|
203
|
-
if (options.proto?.Message) {
|
|
204
|
-
try {
|
|
205
|
-
const encoded = options.proto.Message.encode(options.proto.Message.fromObject(message)).finish();
|
|
206
|
-
if (encoded.length > limits.maxBytes) {
|
|
207
|
-
reasons.push(`encoded size ${encoded.length} > ${limits.maxBytes}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
reasons.push(`proto encode failed: ${error.message}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return { flagged: reasons.length > 0, reasons };
|
|
215
|
-
};
|
|
216
|
-
const META_AI_USER = META_AI_JID.split('@')[0];
|
|
217
|
-
const isMetaAiJid = (jid) => typeof jid === 'string' && jid.startsWith(META_AI_USER);
|
|
218
|
-
/** Bounded so a bot in busy groups cannot grow the burst log without limit. */
|
|
219
|
-
const MAX_BURST_SENDERS = 5000;
|
|
220
|
-
/**
|
|
221
|
-
* Attaches the guard to a socket: flags inbound crash payloads, optionally blocks the sender,
|
|
222
|
-
* and rejects outgoing messages that would crash the recipient.
|
|
223
|
-
*/
|
|
224
|
-
export const createAnchorGuard = (sock, options = {}) => {
|
|
225
|
-
const config = {
|
|
226
|
-
autoDelete: true,
|
|
227
|
-
deleteMode: 'auto',
|
|
228
|
-
guardIncoming: true,
|
|
229
|
-
// Off by default: this one replaces `sock.sendMessage` with a wrapper, which a host that
|
|
230
|
-
// also wraps `sendMessage` needs to know about. Opt in explicitly.
|
|
231
|
-
guardOutgoing: false,
|
|
232
|
-
blockOnBug: false,
|
|
233
|
-
selfOnly: false,
|
|
234
|
-
thresholds: {},
|
|
235
|
-
onDetect: null,
|
|
236
|
-
proto: null,
|
|
237
|
-
/** messages from one sender within burstWindowMs before it counts as a burst; 0 = off */
|
|
238
|
-
burstThreshold: 0,
|
|
239
|
-
burstWindowMs: 4000,
|
|
240
|
-
/** remove the sender from the group on burst (needs admin) */
|
|
241
|
-
kickOnBurst: false,
|
|
242
|
-
/** flag/kick when a burst-flagged inviter adds this account to a group */
|
|
243
|
-
guardGroupAdds: false,
|
|
244
|
-
/** guard Meta AI's own numbers instead of exempting them */
|
|
245
|
-
metaAiNumbers: false,
|
|
246
|
-
logger: sock?.logger,
|
|
247
|
-
...options
|
|
248
|
-
};
|
|
249
|
-
const ownJid = config.ownJid || sock?.user?.id;
|
|
250
|
-
const detectOptions = { ...config.thresholds, proto: config.proto };
|
|
251
|
-
const burstLog = new Map();
|
|
252
|
-
const removeMessage = async (jid, key) => {
|
|
253
|
-
if (!config.autoDelete) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
const revoke = config.deleteMode === 'everyone' || (config.deleteMode === 'auto' && key?.fromMe);
|
|
257
|
-
try {
|
|
258
|
-
if (revoke) {
|
|
259
|
-
await sock.sendMessage(jid, { delete: key });
|
|
260
|
-
}
|
|
261
|
-
else if (typeof sock.chatModify === 'function') {
|
|
262
|
-
await sock.chatModify({ deleteForMe: { key, timestamp: Date.now(), deleteMedia: false } }, jid);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
config.logger?.warn?.({ error: error.message }, 'anchorguard delete failed');
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
const maybeBlock = async (jid, key) => {
|
|
270
|
-
if (!config.blockOnBug || key?.fromMe || typeof sock.updateBlockStatus !== 'function') {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
try {
|
|
274
|
-
await sock.updateBlockStatus(jid, 'block');
|
|
275
|
-
}
|
|
276
|
-
catch (error) {
|
|
277
|
-
config.logger?.warn?.({ error: error.message }, 'anchorguard block failed');
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
const maybeKickBurst = async (groupJid, sender) => {
|
|
281
|
-
if (!config.kickOnBurst || !groupJid?.endsWith('@g.us') || typeof sock.groupParticipantsUpdate !== 'function') {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
await sock.groupParticipantsUpdate(groupJid, [sender], 'remove');
|
|
286
|
-
}
|
|
287
|
-
catch (error) {
|
|
288
|
-
config.logger?.warn?.({ error: error.message }, 'anchorguard kick-on-burst failed');
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
const checkBurst = (sender) => {
|
|
292
|
-
if (!config.burstThreshold || !sender) {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
const now = Date.now();
|
|
296
|
-
const stamps = (burstLog.get(sender) || []).filter(t => now - t < config.burstWindowMs);
|
|
297
|
-
stamps.push(now);
|
|
298
|
-
burstLog.set(sender, stamps);
|
|
299
|
-
// Senders whose window has fully expired are dropped, so the map tracks active senders
|
|
300
|
-
// only. Without this it keeps one entry per sender seen for the lifetime of the socket.
|
|
301
|
-
if (burstLog.size > MAX_BURST_SENDERS) {
|
|
302
|
-
for (const [key, times] of burstLog) {
|
|
303
|
-
if (!times.some(t => now - t < config.burstWindowMs)) {
|
|
304
|
-
burstLog.delete(key);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return stamps.length > config.burstThreshold;
|
|
309
|
-
};
|
|
310
|
-
const handleUpsert = async ({ messages }) => {
|
|
311
|
-
if (!config.guardIncoming || !Array.isArray(messages)) {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
for (const msg of messages) {
|
|
315
|
-
const content = msg?.message;
|
|
316
|
-
if (!content) {
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
const jid = msg.key?.remoteJid;
|
|
320
|
-
const sender = msg.key?.participant || jid;
|
|
321
|
-
if (config.selfOnly && jid !== ownJid && !msg.key?.fromMe) {
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
if (!config.metaAiNumbers && isMetaAiJid(sender)) {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
const result = detectBug(content, detectOptions);
|
|
328
|
-
const burst = checkBurst(sender);
|
|
329
|
-
if (!result.flagged && !burst) {
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
const reasons = burst
|
|
333
|
-
? [...result.reasons, `burst > ${config.burstThreshold}/${config.burstWindowMs}ms`]
|
|
334
|
-
: result.reasons;
|
|
335
|
-
config.logger?.warn?.({ jid, sender, reasons }, 'anchorguard flagged incoming message');
|
|
336
|
-
await config.onDetect?.({ direction: 'incoming', message: msg, jid, sender, reasons, burst });
|
|
337
|
-
if (result.flagged) {
|
|
338
|
-
await removeMessage(jid, msg.key);
|
|
339
|
-
}
|
|
340
|
-
await maybeBlock(sender, msg.key);
|
|
341
|
-
if (burst) {
|
|
342
|
-
await maybeKickBurst(jid, sender);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
const handleGroupParticipantsUpdate = async (update) => {
|
|
347
|
-
if (!config.guardGroupAdds || update?.action !== 'add') {
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const inviter = update?.author;
|
|
351
|
-
if (!inviter || (!config.metaAiNumbers && isMetaAiJid(inviter))) {
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (checkBurst(inviter)) {
|
|
355
|
-
config.logger?.warn?.({ group: update.id, inviter }, 'anchorguard flagged burst group-add');
|
|
356
|
-
await config.onDetect?.({ direction: 'group-add', jid: update.id, sender: inviter, reasons: ['burst group-add'], burst: true });
|
|
357
|
-
await maybeKickBurst(update.id, inviter);
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
if (config.guardIncoming && sock?.ev?.on) {
|
|
361
|
-
sock.ev.on('messages.upsert', handleUpsert);
|
|
362
|
-
}
|
|
363
|
-
if (config.guardGroupAdds && sock?.ev?.on) {
|
|
364
|
-
sock.ev.on('group-participants.update', handleGroupParticipantsUpdate);
|
|
365
|
-
}
|
|
366
|
-
let originalSend = null;
|
|
367
|
-
let patchedSend = null;
|
|
368
|
-
if (config.guardOutgoing && typeof sock?.sendMessage === 'function') {
|
|
369
|
-
// Keep the unbound reference so `stop()` restores the exact function that was there.
|
|
370
|
-
// Restoring a bound copy instead stacks a `bind` layer per guard/stop cycle and makes
|
|
371
|
-
// the socket's own method unrecognisable to a host comparing identities.
|
|
372
|
-
originalSend = sock.sendMessage;
|
|
373
|
-
const callOriginal = originalSend.bind(sock);
|
|
374
|
-
patchedSend = async (jid, content, sendOptions) => {
|
|
375
|
-
const probe = detectBug(content, detectOptions);
|
|
376
|
-
if (probe.flagged) {
|
|
377
|
-
config.logger?.warn?.({ jid, reasons: probe.reasons }, 'anchorguard blocked outgoing message');
|
|
378
|
-
await config.onDetect?.({ direction: 'outgoing', jid, content, reasons: probe.reasons });
|
|
379
|
-
throw new Error(`anchorguard blocked outgoing message: ${probe.reasons.join('; ')}`);
|
|
380
|
-
}
|
|
381
|
-
return callOriginal(jid, content, sendOptions);
|
|
382
|
-
};
|
|
383
|
-
sock.sendMessage = patchedSend;
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
detect: message => detectBug(message, detectOptions),
|
|
387
|
-
stop: () => {
|
|
388
|
-
if (config.guardIncoming && sock?.ev?.off) {
|
|
389
|
-
sock.ev.off('messages.upsert', handleUpsert);
|
|
390
|
-
}
|
|
391
|
-
if (config.guardGroupAdds && sock?.ev?.off) {
|
|
392
|
-
sock.ev.off('group-participants.update', handleGroupParticipantsUpdate);
|
|
393
|
-
}
|
|
394
|
-
// Only unwind our own wrapper. Restoring unconditionally would throw away a wrapper
|
|
395
|
-
// the host installed after this guard did.
|
|
396
|
-
if (patchedSend && sock.sendMessage === patchedSend) {
|
|
397
|
-
sock.sendMessage = originalSend;
|
|
398
|
-
}
|
|
399
|
-
burstLog.clear();
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
};
|
|
403
|
-
// kept for drop-in parity with @rexxhayanasi/elaina-anchorguard's export name
|
|
404
|
-
export const createAntiBugGuard = createAnchorGuard;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "events";
|
|
2
|
-
import { Agent } from "https";
|
|
3
|
-
export type ConnectionManagerSession = {
|
|
4
|
-
id: string;
|
|
5
|
-
[key: string]: any;
|
|
6
|
-
};
|
|
7
|
-
export type ConnectionManagerOptions = {
|
|
8
|
-
/** Build one socket for a session. Called again for every reconnect. */
|
|
9
|
-
create: (session: ConnectionManagerSession) => Promise<any> | any;
|
|
10
|
-
sessions?: ConnectionManagerSession[];
|
|
11
|
-
maxConcurrent?: number;
|
|
12
|
-
reconnect?: boolean;
|
|
13
|
-
baseDelayMs?: number;
|
|
14
|
-
maxDelayMs?: number;
|
|
15
|
-
maxAttempts?: number;
|
|
16
|
-
staggerMs?: number;
|
|
17
|
-
logger?: any;
|
|
18
|
-
};
|
|
19
|
-
export type ConnectionManagerStats = {
|
|
20
|
-
total: number;
|
|
21
|
-
idle: number;
|
|
22
|
-
connecting: number;
|
|
23
|
-
open: number;
|
|
24
|
-
reconnecting: number;
|
|
25
|
-
closed: number;
|
|
26
|
-
failed: number;
|
|
27
|
-
activeConnects: number;
|
|
28
|
-
queuedConnects: number;
|
|
29
|
-
};
|
|
30
|
-
export declare class ConnectionManager extends EventEmitter {
|
|
31
|
-
#private;
|
|
32
|
-
constructor(options: ConnectionManagerOptions);
|
|
33
|
-
start(): ConnectionManagerStats;
|
|
34
|
-
get(id: string): any | null;
|
|
35
|
-
stop(id: string): boolean;
|
|
36
|
-
close(): Promise<void>;
|
|
37
|
-
stats(): ConnectionManagerStats;
|
|
38
|
-
}
|
|
39
|
-
export declare const makeConnectionManager: (options: ConnectionManagerOptions) => ConnectionManager;
|
|
40
|
-
export declare const makeWssAgent: (options?: {
|
|
41
|
-
maxSockets?: number;
|
|
42
|
-
keepAlive?: boolean;
|
|
43
|
-
keepAliveMsecs?: number;
|
|
44
|
-
}) => Agent;
|