whalibmob 5.7.2 → 5.9.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 +532 -143
- package/cli.js +206 -55
- package/index.js +13 -1
- package/lib/Client.js +920 -172
- package/lib/GroupParticipant.js +141 -0
- package/lib/MediaService.js +28 -1
- package/lib/appstate/AppStateStore.js +100 -0
- package/lib/appstate/AppStateSync.js +292 -0
- package/lib/appstate/LTHash.js +119 -0
- package/lib/appstate/Mutations.js +106 -0
- package/lib/appstate/SyncdProto.js +263 -0
- package/lib/messages/MessageSender.js +32 -0
- package/lib/messages/TcTokenStore.js +44 -0
- package/lib/proto/MessageProto.js +55 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Which collection, index and api version each kind of change is filed under.
|
|
4
|
+
//
|
|
5
|
+
// None of this is ours to pick. The server keys a mutation by its index, sorts
|
|
6
|
+
// it by collection, and rejects an api version it does not expect — get any of
|
|
7
|
+
// the three wrong and the patch is either refused or silently lands somewhere
|
|
8
|
+
// the phone will never look. The index is the identity of the setting: writing
|
|
9
|
+
// ['pin_v1', jid] twice replaces, rather than accumulates.
|
|
10
|
+
|
|
11
|
+
function mutePatch(jid, muteEndTimestampMs) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'regular_high',
|
|
14
|
+
index: ['mute', jid],
|
|
15
|
+
apiVersion: 2,
|
|
16
|
+
action: {
|
|
17
|
+
muteAction: {
|
|
18
|
+
muted: !!muteEndTimestampMs,
|
|
19
|
+
muteEndTimestamp: muteEndTimestampMs || undefined
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function archivePatch(jid, archived) {
|
|
26
|
+
return {
|
|
27
|
+
name: 'regular_low',
|
|
28
|
+
index: ['archive', jid],
|
|
29
|
+
apiVersion: 3,
|
|
30
|
+
action: { archiveChatAction: { archived: !!archived } }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function markReadPatch(jid, read) {
|
|
35
|
+
return {
|
|
36
|
+
name: 'regular_low',
|
|
37
|
+
index: ['markChatAsRead', jid],
|
|
38
|
+
apiVersion: 3,
|
|
39
|
+
action: { markChatAsReadAction: { read: !!read } }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pinPatch(jid, pinned) {
|
|
44
|
+
return {
|
|
45
|
+
name: 'regular_low',
|
|
46
|
+
index: ['pin_v1', jid],
|
|
47
|
+
apiVersion: 5,
|
|
48
|
+
action: { pinAction: { pinned: !!pinned } }
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A star is per message, so the index carries the message it belongs to as well
|
|
53
|
+
// as the chat — otherwise starring a second message would unstar the first.
|
|
54
|
+
function starPatch(jid, msgId, fromMe, starred) {
|
|
55
|
+
return {
|
|
56
|
+
name: 'regular_low',
|
|
57
|
+
index: ['star', jid, msgId, fromMe ? '1' : '0', '0'],
|
|
58
|
+
apiVersion: 2,
|
|
59
|
+
action: { starAction: { starred: !!starred } }
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pushNamePatch(name) {
|
|
64
|
+
return {
|
|
65
|
+
name: 'critical_block',
|
|
66
|
+
index: ['setting_pushName'],
|
|
67
|
+
apiVersion: 1,
|
|
68
|
+
action: { pushNameSetting: { name } }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function contactPatch(jid, contact) {
|
|
73
|
+
return {
|
|
74
|
+
name: 'critical_unblock_low',
|
|
75
|
+
index: ['contact', jid],
|
|
76
|
+
apiVersion: 2,
|
|
77
|
+
action: { contactAction: contact || {} },
|
|
78
|
+
// No contact means forget this one, which is a removal rather than an
|
|
79
|
+
// empty value.
|
|
80
|
+
operation: contact ? 0 : 1
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The index tells you what a decoded mutation was about. The first element is
|
|
85
|
+
// the kind; what follows depends on it.
|
|
86
|
+
function describeIndex(index) {
|
|
87
|
+
if (!Array.isArray(index) || !index.length) return null;
|
|
88
|
+
const [kind, jid, msgId, fromMe] = index;
|
|
89
|
+
switch (kind) {
|
|
90
|
+
case 'mute': return { kind: 'mute', jid };
|
|
91
|
+
case 'archive': return { kind: 'archive', jid };
|
|
92
|
+
case 'markChatAsRead': return { kind: 'markRead', jid };
|
|
93
|
+
case 'pin_v1': return { kind: 'pin', jid };
|
|
94
|
+
case 'star': return { kind: 'star', jid, msgId, fromMe: fromMe === '1' };
|
|
95
|
+
case 'contact': return { kind: 'contact', jid };
|
|
96
|
+
case 'setting_pushName':return { kind: 'pushName' };
|
|
97
|
+
case 'clearChat': return { kind: 'clear', jid };
|
|
98
|
+
case 'deleteChat': return { kind: 'delete', jid };
|
|
99
|
+
default: return { kind, jid };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
mutePatch, archivePatch, markReadPatch, pinPatch, starPatch,
|
|
105
|
+
pushNamePatch, contactPatch, describeIndex
|
|
106
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The wire format app state travels in.
|
|
4
|
+
//
|
|
5
|
+
// A collection ("regular_low", "critical_block", …) is a versioned list of
|
|
6
|
+
// mutations. Each mutation names an index — a JSON array like ["pin_v1", jid] —
|
|
7
|
+
// and carries the encrypted value filed under it. The server hands them over
|
|
8
|
+
// either as a snapshot (the whole collection, from scratch) or as patches (what
|
|
9
|
+
// changed since a version you name).
|
|
10
|
+
//
|
|
11
|
+
// SyncdPatch { version=1, mutations=2, externalMutations=3,
|
|
12
|
+
// snapshotMac=4, patchMac=5, keyId=6 }
|
|
13
|
+
// SyncdSnapshot{ version=1, records=2, mac=3, keyId=4 }
|
|
14
|
+
// SyncdMutation{ operation=1 (SET=0 REMOVE=1), record=2 }
|
|
15
|
+
// SyncdRecord { index=1, value=2, keyId=3 }
|
|
16
|
+
// SyncdIndex { blob=1 } SyncdValue { blob=1 } KeyId { id=1 }
|
|
17
|
+
// SyncdVersion { version=1 }
|
|
18
|
+
// SyncdMutations { mutations=1 }
|
|
19
|
+
// ExternalBlobReference { mediaKey=1, directPath=2, handle=3,
|
|
20
|
+
// fileSizeBytes=4, fileSha256=5, fileEncSha256=6 }
|
|
21
|
+
// SyncActionData { index=1, value=2, padding=3, version=4 }
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
decodeFields, field, varint, str, bytes, WIRE_VARINT, WIRE_LEN
|
|
25
|
+
} = require('../proto/MessageProto');
|
|
26
|
+
|
|
27
|
+
const SET = 0;
|
|
28
|
+
const REMOVE = 1;
|
|
29
|
+
|
|
30
|
+
const _b = v => (v == null ? null : (Buffer.isBuffer(v) ? v : Buffer.from(v)));
|
|
31
|
+
const _arr = v => (v == null ? [] : (Array.isArray(v) ? v : [v]));
|
|
32
|
+
const _num = v => {
|
|
33
|
+
if (v == null) return 0;
|
|
34
|
+
if (typeof v === 'number') return v;
|
|
35
|
+
if (Buffer.isBuffer(v)) { let n = 0; for (let i = v.length - 1; i >= 0; i--) n = n * 256 + v[i]; return n; }
|
|
36
|
+
return Number(v) || 0;
|
|
37
|
+
};
|
|
38
|
+
const _str = v => (v == null ? '' : (Buffer.isBuffer(v) ? v.toString('utf8') : String(v)));
|
|
39
|
+
|
|
40
|
+
// ─── decoding ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function decodeKeyId(buf) {
|
|
43
|
+
if (!buf) return null;
|
|
44
|
+
const f = decodeFields(buf);
|
|
45
|
+
return _b(f[1]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function decodeRecord(buf) {
|
|
49
|
+
const f = decodeFields(buf);
|
|
50
|
+
return {
|
|
51
|
+
indexBlob: f[1] ? _b(decodeFields(f[1])[1]) : null,
|
|
52
|
+
valueBlob: f[2] ? _b(decodeFields(f[2])[1]) : null,
|
|
53
|
+
keyId: decodeKeyId(f[3])
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A bare record is a SET; only a SyncdMutation carries an operation of its own.
|
|
58
|
+
function decodeMutation(buf) {
|
|
59
|
+
const f = decodeFields(buf);
|
|
60
|
+
if (!f[2]) return Object.assign({ operation: SET }, decodeRecord(buf));
|
|
61
|
+
return Object.assign({ operation: _num(f[1]) }, decodeRecord(f[2]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function decodeExternalBlobReference(buf) {
|
|
65
|
+
if (!buf) return null;
|
|
66
|
+
const f = decodeFields(buf);
|
|
67
|
+
return {
|
|
68
|
+
mediaKey: _b(f[1]),
|
|
69
|
+
directPath: _str(f[2]),
|
|
70
|
+
handle: _str(f[3]),
|
|
71
|
+
fileSizeBytes: _num(f[4]),
|
|
72
|
+
fileSha256: _b(f[5]),
|
|
73
|
+
fileEncSha256: _b(f[6])
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function decodePatch(buf) {
|
|
78
|
+
const f = decodeFields(buf);
|
|
79
|
+
return {
|
|
80
|
+
version: f[1] ? _num(decodeFields(f[1])[1]) : null,
|
|
81
|
+
mutations: _arr(f[2]).filter(Buffer.isBuffer).map(decodeMutation),
|
|
82
|
+
externalMutations: decodeExternalBlobReference(f[3]),
|
|
83
|
+
snapshotMac: _b(f[4]),
|
|
84
|
+
patchMac: _b(f[5]),
|
|
85
|
+
keyId: decodeKeyId(f[6])
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function decodeSnapshot(buf) {
|
|
90
|
+
const f = decodeFields(buf);
|
|
91
|
+
return {
|
|
92
|
+
version: f[1] ? _num(decodeFields(f[1])[1]) : 0,
|
|
93
|
+
records: _arr(f[2]).filter(Buffer.isBuffer)
|
|
94
|
+
.map(r => Object.assign({ operation: SET }, decodeRecord(r))),
|
|
95
|
+
mac: _b(f[3]),
|
|
96
|
+
keyId: decodeKeyId(f[4])
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The blob an oversized patch is moved out into.
|
|
101
|
+
function decodeMutations(buf) {
|
|
102
|
+
const f = decodeFields(buf);
|
|
103
|
+
return _arr(f[1]).filter(Buffer.isBuffer).map(decodeMutation);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function decodeSyncActionData(buf) {
|
|
107
|
+
const f = decodeFields(buf);
|
|
108
|
+
return {
|
|
109
|
+
index: _b(f[1]),
|
|
110
|
+
value: f[2] ? decodeSyncActionValue(f[2]) : null,
|
|
111
|
+
version: _num(f[4])
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── SyncActionValue ─────────────────────────────────────────────────────────
|
|
116
|
+
//
|
|
117
|
+
// One field per kind of change. Only the ones this library acts on are decoded;
|
|
118
|
+
// anything else is reported by name so a caller can see it went by.
|
|
119
|
+
const ACTION_FIELDS = {
|
|
120
|
+
2: 'starAction',
|
|
121
|
+
3: 'contactAction',
|
|
122
|
+
4: 'muteAction',
|
|
123
|
+
5: 'pinAction',
|
|
124
|
+
7: 'pushNameSetting',
|
|
125
|
+
17: 'archiveChatAction',
|
|
126
|
+
20: 'markChatAsReadAction',
|
|
127
|
+
21: 'clearChatAction',
|
|
128
|
+
22: 'deleteChatAction'
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function decodeSyncActionValue(buf) {
|
|
132
|
+
const f = decodeFields(buf);
|
|
133
|
+
const out = { timestamp: _num(f[1]) };
|
|
134
|
+
|
|
135
|
+
for (const [num, name] of Object.entries(ACTION_FIELDS)) {
|
|
136
|
+
const raw = f[num];
|
|
137
|
+
if (!Buffer.isBuffer(raw)) continue;
|
|
138
|
+
const a = decodeFields(raw);
|
|
139
|
+
switch (name) {
|
|
140
|
+
case 'starAction':
|
|
141
|
+
out.starAction = { starred: !!_num(a[1]) }; break;
|
|
142
|
+
case 'pinAction':
|
|
143
|
+
out.pinAction = { pinned: !!_num(a[1]) }; break;
|
|
144
|
+
case 'muteAction':
|
|
145
|
+
out.muteAction = {
|
|
146
|
+
muted: !!_num(a[1]),
|
|
147
|
+
muteEndTimestamp: _num(a[2]),
|
|
148
|
+
autoMuted: !!_num(a[3])
|
|
149
|
+
};
|
|
150
|
+
break;
|
|
151
|
+
case 'archiveChatAction':
|
|
152
|
+
out.archiveChatAction = { archived: !!_num(a[1]) }; break;
|
|
153
|
+
case 'markChatAsReadAction':
|
|
154
|
+
out.markChatAsReadAction = { read: !!_num(a[1]) }; break;
|
|
155
|
+
case 'clearChatAction':
|
|
156
|
+
out.clearChatAction = {}; break;
|
|
157
|
+
case 'deleteChatAction':
|
|
158
|
+
out.deleteChatAction = {}; break;
|
|
159
|
+
case 'pushNameSetting':
|
|
160
|
+
out.pushNameSetting = { name: _str(a[1]) }; break;
|
|
161
|
+
case 'contactAction':
|
|
162
|
+
out.contactAction = {
|
|
163
|
+
fullName: _str(a[1]) || null,
|
|
164
|
+
firstName: _str(a[2]) || null,
|
|
165
|
+
lidJid: _str(a[3]) || null,
|
|
166
|
+
pnJid: _str(a[5]) || null,
|
|
167
|
+
username: _str(a[6]) || null
|
|
168
|
+
};
|
|
169
|
+
break;
|
|
170
|
+
default: break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Whatever else the server sent, so a caller is not left guessing why nothing
|
|
175
|
+
// happened for a mutation that plainly said something.
|
|
176
|
+
const known = new Set(['1', ...Object.keys(ACTION_FIELDS)]);
|
|
177
|
+
out.otherFields = Object.keys(f).filter(k => !known.has(k)).map(Number);
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── encoding (the write path) ───────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function encodeKeyId(id) { return field(1, WIRE_LEN, bytes(id)); }
|
|
184
|
+
function encodeVersion(v) { return field(1, WIRE_VARINT, varint(v)); }
|
|
185
|
+
|
|
186
|
+
function encodeSyncActionValue(action, timestampMs) {
|
|
187
|
+
const parts = [field(1, WIRE_VARINT, varint(timestampMs))];
|
|
188
|
+
|
|
189
|
+
if (action.starAction) {
|
|
190
|
+
parts.push(field(2, WIRE_LEN,
|
|
191
|
+
field(1, WIRE_VARINT, varint(action.starAction.starred ? 1 : 0))));
|
|
192
|
+
}
|
|
193
|
+
if (action.contactAction) {
|
|
194
|
+
const c = action.contactAction;
|
|
195
|
+
parts.push(field(3, WIRE_LEN, Buffer.concat([
|
|
196
|
+
c.fullName ? field(1, WIRE_LEN, str(c.fullName)) : Buffer.alloc(0),
|
|
197
|
+
c.firstName ? field(2, WIRE_LEN, str(c.firstName)) : Buffer.alloc(0)
|
|
198
|
+
])));
|
|
199
|
+
}
|
|
200
|
+
if (action.muteAction) {
|
|
201
|
+
const m = action.muteAction;
|
|
202
|
+
parts.push(field(4, WIRE_LEN, Buffer.concat([
|
|
203
|
+
field(1, WIRE_VARINT, varint(m.muted ? 1 : 0)),
|
|
204
|
+
m.muteEndTimestamp
|
|
205
|
+
? field(2, WIRE_VARINT, varint(m.muteEndTimestamp))
|
|
206
|
+
: Buffer.alloc(0)
|
|
207
|
+
])));
|
|
208
|
+
}
|
|
209
|
+
if (action.pinAction) {
|
|
210
|
+
parts.push(field(5, WIRE_LEN,
|
|
211
|
+
field(1, WIRE_VARINT, varint(action.pinAction.pinned ? 1 : 0))));
|
|
212
|
+
}
|
|
213
|
+
if (action.pushNameSetting) {
|
|
214
|
+
parts.push(field(7, WIRE_LEN, field(1, WIRE_LEN, str(action.pushNameSetting.name))));
|
|
215
|
+
}
|
|
216
|
+
if (action.archiveChatAction) {
|
|
217
|
+
parts.push(field(17, WIRE_LEN,
|
|
218
|
+
field(1, WIRE_VARINT, varint(action.archiveChatAction.archived ? 1 : 0))));
|
|
219
|
+
}
|
|
220
|
+
if (action.markChatAsReadAction) {
|
|
221
|
+
parts.push(field(20, WIRE_LEN,
|
|
222
|
+
field(1, WIRE_VARINT, varint(action.markChatAsReadAction.read ? 1 : 0))));
|
|
223
|
+
}
|
|
224
|
+
return Buffer.concat(parts);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function encodeSyncActionData({ index, value, apiVersion, timestampMs }) {
|
|
228
|
+
return Buffer.concat([
|
|
229
|
+
field(1, WIRE_LEN, bytes(index)),
|
|
230
|
+
field(2, WIRE_LEN, encodeSyncActionValue(value, timestampMs)),
|
|
231
|
+
field(3, WIRE_LEN, Buffer.alloc(0)), // padding, always empty
|
|
232
|
+
field(4, WIRE_VARINT, varint(apiVersion || 1))
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function encodeMutation({ operation, indexMac, valueBlob, keyId }) {
|
|
237
|
+
const record = Buffer.concat([
|
|
238
|
+
field(1, WIRE_LEN, field(1, WIRE_LEN, bytes(indexMac))),
|
|
239
|
+
field(2, WIRE_LEN, field(1, WIRE_LEN, bytes(valueBlob))),
|
|
240
|
+
field(3, WIRE_LEN, encodeKeyId(keyId))
|
|
241
|
+
]);
|
|
242
|
+
return Buffer.concat([
|
|
243
|
+
field(1, WIRE_VARINT, varint(operation)),
|
|
244
|
+
field(2, WIRE_LEN, record)
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function encodePatch({ version, mutations, snapshotMac, patchMac, keyId }) {
|
|
249
|
+
return Buffer.concat([
|
|
250
|
+
field(1, WIRE_LEN, encodeVersion(version)),
|
|
251
|
+
...mutations.map(m => field(2, WIRE_LEN, encodeMutation(m))),
|
|
252
|
+
field(4, WIRE_LEN, bytes(snapshotMac)),
|
|
253
|
+
field(5, WIRE_LEN, bytes(patchMac)),
|
|
254
|
+
field(6, WIRE_LEN, encodeKeyId(keyId))
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = {
|
|
259
|
+
SET, REMOVE,
|
|
260
|
+
decodePatch, decodeSnapshot, decodeMutations, decodeMutation,
|
|
261
|
+
decodeExternalBlobReference, decodeSyncActionData, decodeSyncActionValue,
|
|
262
|
+
encodePatch, encodeMutation, encodeSyncActionData, encodeSyncActionValue
|
|
263
|
+
};
|
|
@@ -15,6 +15,8 @@ const {
|
|
|
15
15
|
encodeStickerMessage, encodeReactionMessage,
|
|
16
16
|
encodePollCreationMessage,
|
|
17
17
|
encodeLocationMessage, encodeContactMessage,
|
|
18
|
+
encodeGroupInviteMessage,
|
|
19
|
+
GROUP_INVITE_TYPE_DEFAULT, GROUP_INVITE_TYPE_PARENT,
|
|
18
20
|
encodeDeviceSentMessage, encodeProtocolMessage,
|
|
19
21
|
encodeExtendedText,
|
|
20
22
|
encodeSenderKeyDistributionMessage,
|
|
@@ -535,6 +537,36 @@ class MessageSender {
|
|
|
535
537
|
Object.assign({}, opts, { _mediaSubtype: 'vcard' }));
|
|
536
538
|
}
|
|
537
539
|
|
|
540
|
+
// ─── Group invite ─────────────────────────────────────────────────────────
|
|
541
|
+
// sendGroupInvite(to, groupJid, inviteCode, inviteExpiration, opts)
|
|
542
|
+
//
|
|
543
|
+
// The bubble that carries a personal invitation into a group. It is what you
|
|
544
|
+
// fall back to when adding somebody outright is refused because their privacy
|
|
545
|
+
// settings do not allow it — the server hands back a code with that refusal
|
|
546
|
+
// and this is what turns the code into something the person can tap.
|
|
547
|
+
//
|
|
548
|
+
// opts: { groupName, caption, jpegThumbnail, isCommunity, id, contextInfo }
|
|
549
|
+
|
|
550
|
+
async sendGroupInvite(to, groupJid, inviteCode, inviteExpiration, opts) {
|
|
551
|
+
opts = opts || {};
|
|
552
|
+
const msgId = opts.id || generateMessageId();
|
|
553
|
+
const toJid = makeJid(to);
|
|
554
|
+
const invBuf = encodeMessage('groupInvite', encodeGroupInviteMessage({
|
|
555
|
+
groupJid: makeJid(groupJid),
|
|
556
|
+
inviteCode,
|
|
557
|
+
inviteExpiration: inviteExpiration || 0,
|
|
558
|
+
groupName: opts.groupName || '',
|
|
559
|
+
caption: opts.caption || '',
|
|
560
|
+
jpegThumbnail: opts.jpegThumbnail || opts.thumbnail || null,
|
|
561
|
+
groupType: opts.isCommunity ? GROUP_INVITE_TYPE_PARENT
|
|
562
|
+
: GROUP_INVITE_TYPE_DEFAULT,
|
|
563
|
+
contextInfo: opts.contextInfo
|
|
564
|
+
}));
|
|
565
|
+
// No media travels with it and it is not a reaction or a poll, so as far as
|
|
566
|
+
// the envelope is concerned this is an ordinary message.
|
|
567
|
+
return this._sendMessage(toJid, msgId, invBuf, 'text', opts);
|
|
568
|
+
}
|
|
569
|
+
|
|
538
570
|
// ─── Forward decoded message ───────────────────────────────────────────────
|
|
539
571
|
//
|
|
540
572
|
// Forwards a received message to a new recipient without re-uploading media.
|
|
@@ -166,6 +166,50 @@ class TcTokenStore {
|
|
|
166
166
|
return true;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Fold an entry filed under one JID into the entry for another.
|
|
171
|
+
*
|
|
172
|
+
* The same contact can end up with two entries — one under their phone JID
|
|
173
|
+
* and one under their LID — when a send happens before the LID mapping is
|
|
174
|
+
* known. That is not just untidy: whichever key the next send resolves to is
|
|
175
|
+
* the only one it looks at, so a live token under the LID goes unused and the
|
|
176
|
+
* message ships without one.
|
|
177
|
+
*
|
|
178
|
+
* The live token wins, newest first. The sender timestamp takes the newer of
|
|
179
|
+
* the two, since an older one would say we are due to issue again when we are
|
|
180
|
+
* not.
|
|
181
|
+
*
|
|
182
|
+
* @returns {boolean} whether anything was moved
|
|
183
|
+
*/
|
|
184
|
+
mergeEntry(fromJid, toJid) {
|
|
185
|
+
const fromKey = normalizeJidForTcToken(fromJid);
|
|
186
|
+
const toKey = normalizeJidForTcToken(toJid);
|
|
187
|
+
if (fromKey === toKey) return false;
|
|
188
|
+
|
|
189
|
+
const from = this._map.get(fromKey);
|
|
190
|
+
if (!from) return false;
|
|
191
|
+
|
|
192
|
+
const to = this._map.get(toKey) || {};
|
|
193
|
+
|
|
194
|
+
const fromLive = !!(from.token && from.token.length) && !tcTokenExpired(from.timestamp);
|
|
195
|
+
const toLive = !!(to.token && to.token.length) && !tcTokenExpired(to.timestamp);
|
|
196
|
+
|
|
197
|
+
let token = to.token != null ? to.token : null;
|
|
198
|
+
let timestamp = to.timestamp != null ? to.timestamp : null;
|
|
199
|
+
if (fromLive && (!toLive || Number(from.timestamp) > Number(to.timestamp || 0))) {
|
|
200
|
+
token = from.token;
|
|
201
|
+
timestamp = from.timestamp;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const senderTimestamp =
|
|
205
|
+
Math.max(Number(to.senderTimestamp || 0), Number(from.senderTimestamp || 0)) || null;
|
|
206
|
+
|
|
207
|
+
this._map.set(toKey, { token, timestamp, senderTimestamp });
|
|
208
|
+
this._map.delete(fromKey);
|
|
209
|
+
this._save();
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
169
213
|
clearToken(jid) {
|
|
170
214
|
const key = normalizeJidForTcToken(jid);
|
|
171
215
|
const existing = this._map.get(key) || {};
|
|
@@ -281,6 +281,34 @@ function encodeContactMessage(opts) {
|
|
|
281
281
|
return Buffer.concat(parts);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// ─── GroupInviteMessage (field 28 of Message) ────────────────────────────────
|
|
285
|
+
//
|
|
286
|
+
// The bubble you send someone directly when they cannot simply be added to a
|
|
287
|
+
// group: it carries the group's JID plus a one-off invite code and the moment
|
|
288
|
+
// that code stops working. Tapping it makes their client send the accept IQ.
|
|
289
|
+
//
|
|
290
|
+
// GroupInviteMessage { groupJid=1, inviteCode=2, inviteExpiration=3,
|
|
291
|
+
// groupName=4, jpegThumbnail=5, caption=6, contextInfo=7,
|
|
292
|
+
// groupType=8 }
|
|
293
|
+
//
|
|
294
|
+
// groupType is 0 for an ordinary group and 1 for a community.
|
|
295
|
+
const GROUP_INVITE_TYPE_DEFAULT = 0;
|
|
296
|
+
const GROUP_INVITE_TYPE_PARENT = 1;
|
|
297
|
+
|
|
298
|
+
function encodeGroupInviteMessage(opts) {
|
|
299
|
+
const parts = [
|
|
300
|
+
field(1, WIRE_LEN, str(opts.groupJid || '')),
|
|
301
|
+
field(2, WIRE_LEN, str(opts.inviteCode || '')),
|
|
302
|
+
field(3, WIRE_VARINT, varint(Number(opts.inviteExpiration) || 0))
|
|
303
|
+
];
|
|
304
|
+
if (opts.groupName) parts.push(field(4, WIRE_LEN, str(opts.groupName)));
|
|
305
|
+
if (opts.jpegThumbnail) parts.push(field(5, WIRE_LEN, bytes(opts.jpegThumbnail)));
|
|
306
|
+
if (opts.caption) parts.push(field(6, WIRE_LEN, str(opts.caption)));
|
|
307
|
+
if (opts.contextInfo) parts.push(field(7, WIRE_LEN, encodeContextInfo(opts.contextInfo)));
|
|
308
|
+
if (opts.groupType) parts.push(field(8, WIRE_VARINT, varint(Number(opts.groupType) || 0)));
|
|
309
|
+
return Buffer.concat(parts);
|
|
310
|
+
}
|
|
311
|
+
|
|
284
312
|
function encodeReactionMessage(opts) {
|
|
285
313
|
const parts = [
|
|
286
314
|
field(1, WIRE_LEN, encodeMessageKey(opts.key)),
|
|
@@ -306,6 +334,7 @@ function encodeMessage(type, payload) {
|
|
|
306
334
|
case 'audio': return field(8, WIRE_LEN, payload);
|
|
307
335
|
case 'video': return field(9, WIRE_LEN, payload);
|
|
308
336
|
case 'sticker': return field(26, WIRE_LEN, payload);
|
|
337
|
+
case 'groupInvite': return field(28, WIRE_LEN, payload);
|
|
309
338
|
case 'reaction': return field(46, WIRE_LEN, payload);
|
|
310
339
|
case 'protocol': return field(12, WIRE_LEN, payload);
|
|
311
340
|
// extendedTextMessage is field 6. It used to be written to field 2, which is
|
|
@@ -548,6 +577,27 @@ function decodeMessageContainer(buf) {
|
|
|
548
577
|
} catch (_) { msgResult = { type: 'location', latitude: 0, longitude: 0 }; }
|
|
549
578
|
}
|
|
550
579
|
|
|
580
|
+
// Field 28: groupInviteMessage — groupJid=1, inviteCode=2, inviteExpiration=3,
|
|
581
|
+
// groupName=4, jpegThumbnail=5, caption=6, groupType=8
|
|
582
|
+
//
|
|
583
|
+
// Everything acceptGroupInviteMessage() needs is in here, so an invite that
|
|
584
|
+
// arrives can be acted on without the caller re-parsing the payload.
|
|
585
|
+
if (!msgResult && f[28] && Buffer.isBuffer(f[28])) {
|
|
586
|
+
try {
|
|
587
|
+
const inv = _decodeFields(f[28]);
|
|
588
|
+
msgResult = {
|
|
589
|
+
type: 'groupInvite',
|
|
590
|
+
groupJid: _str(inv[1]),
|
|
591
|
+
inviteCode: _str(inv[2]),
|
|
592
|
+
inviteExpiration: _num(inv[3]),
|
|
593
|
+
groupName: _str(inv[4]),
|
|
594
|
+
jpegThumbnail: inv[5] || null,
|
|
595
|
+
caption: _str(inv[6]),
|
|
596
|
+
isCommunity: _num(inv[8]) === 1
|
|
597
|
+
};
|
|
598
|
+
} catch (_) { msgResult = { type: 'groupInvite', groupJid: '', inviteCode: '' }; }
|
|
599
|
+
}
|
|
600
|
+
|
|
551
601
|
// Field 7: documentMessage
|
|
552
602
|
if (!msgResult && f[7] && Buffer.isBuffer(f[7])) {
|
|
553
603
|
try {
|
|
@@ -757,6 +807,7 @@ module.exports = {
|
|
|
757
807
|
encodePollCreationMessage,
|
|
758
808
|
encodeLocationMessage,
|
|
759
809
|
encodeContactMessage,
|
|
810
|
+
encodeGroupInviteMessage,
|
|
760
811
|
encodeMessageKey,
|
|
761
812
|
encodeMessage,
|
|
762
813
|
encodeDeviceSentMessage,
|
|
@@ -766,6 +817,8 @@ module.exports = {
|
|
|
766
817
|
encodeSenderKeyDistributionMessage,
|
|
767
818
|
encodeMessageContextInfo,
|
|
768
819
|
encodeVarint,
|
|
820
|
+
// The generic field walker, for the protobufs that live outside this file.
|
|
821
|
+
decodeFields: _decodeFields,
|
|
769
822
|
field,
|
|
770
823
|
varint,
|
|
771
824
|
str,
|
|
@@ -774,5 +827,7 @@ module.exports = {
|
|
|
774
827
|
WIRE_LEN,
|
|
775
828
|
PROTOCOL_MSG_REVOKE,
|
|
776
829
|
PROTOCOL_MSG_EPHEMERAL,
|
|
830
|
+
GROUP_INVITE_TYPE_DEFAULT,
|
|
831
|
+
GROUP_INVITE_TYPE_PARENT,
|
|
777
832
|
decodeMessageContainer
|
|
778
833
|
};
|