nodejs-insta-private-api-mqtt 1.3.20 → 1.3.21
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/mqtt-shim.js +210 -61
- package/dist/realtime/realtime.client.js +45 -5
- package/package.json +1 -1
package/dist/mqtt-shim.js
CHANGED
|
@@ -34,34 +34,56 @@ class IllegalStateError extends Error {
|
|
|
34
34
|
|
|
35
35
|
class PacketStream {
|
|
36
36
|
constructor(buffer) {
|
|
37
|
-
this.buffer = buffer;
|
|
37
|
+
this.buffer = buffer || Buffer.alloc(0);
|
|
38
38
|
this.offset = 0;
|
|
39
39
|
}
|
|
40
|
-
readByte() {
|
|
41
|
-
if (this.offset >= this.buffer.length)
|
|
42
|
-
return this.buffer[this.offset++];
|
|
40
|
+
readByte() {
|
|
41
|
+
if (this.offset >= this.buffer.length) throw new Error('Unexpected end of stream');
|
|
42
|
+
return this.buffer[this.offset++];
|
|
43
43
|
}
|
|
44
44
|
readStringAsBuffer() {
|
|
45
45
|
const len = this.readWord();
|
|
46
|
-
if (this.offset + len > this.buffer.length)
|
|
46
|
+
if (this.offset + len > this.buffer.length) throw new Error('Unexpected end of stream');
|
|
47
47
|
const buf = this.buffer.slice(this.offset, this.offset + len);
|
|
48
48
|
this.offset += len;
|
|
49
49
|
return buf;
|
|
50
50
|
}
|
|
51
51
|
readWord() {
|
|
52
|
-
if (this.offset + 2 > this.buffer.length)
|
|
52
|
+
if (this.offset + 2 > this.buffer.length) throw new Error('Unexpected end of stream');
|
|
53
53
|
const w = this.buffer.readUInt16BE(this.offset);
|
|
54
54
|
this.offset += 2;
|
|
55
55
|
return w;
|
|
56
56
|
}
|
|
57
|
+
readRemaining() {
|
|
58
|
+
const buf = this.buffer.slice(this.offset);
|
|
59
|
+
this.offset = this.buffer.length;
|
|
60
|
+
return buf;
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
class PacketWriter {
|
|
60
65
|
constructor() { this.chunks = []; }
|
|
61
66
|
writeByte(b) { const buf = Buffer.alloc(1); buf.writeUInt8(b); this.chunks.push(buf); return this; }
|
|
62
67
|
writeWord(w) { const buf = Buffer.alloc(2); buf.writeUInt16BE(w); this.chunks.push(buf); return this; }
|
|
63
|
-
writeString(s) {
|
|
64
|
-
|
|
68
|
+
writeString(s) {
|
|
69
|
+
if (Buffer.isBuffer(s)) {
|
|
70
|
+
this.writeWord(s.length);
|
|
71
|
+
this.chunks.push(s);
|
|
72
|
+
} else {
|
|
73
|
+
const str = String(s || '');
|
|
74
|
+
const b = Buffer.from(str);
|
|
75
|
+
this.writeWord(b.length);
|
|
76
|
+
this.chunks.push(b);
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
write(buf) {
|
|
81
|
+
if (!buf) return this;
|
|
82
|
+
if (Buffer.isBuffer(buf)) this.chunks.push(buf);
|
|
83
|
+
else if (typeof buf === 'string') this.chunks.push(Buffer.from(buf));
|
|
84
|
+
else if (buf instanceof Uint8Array) this.chunks.push(Buffer.from(buf));
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
65
87
|
toBuffer() { return Buffer.concat(this.chunks); }
|
|
66
88
|
}
|
|
67
89
|
|
|
@@ -77,6 +99,38 @@ class MqttClient extends EventEmitter {
|
|
|
77
99
|
this.pendingPublishes = new Map(); // PacketID -> {resolve, reject, timer}
|
|
78
100
|
this.pingTimer = null;
|
|
79
101
|
this.keepAliveInterval = 60000;
|
|
102
|
+
this.recvBuffer = Buffer.alloc(0); // buffer pentru date primite (concatenează fragmente)
|
|
103
|
+
this._defaultReadMapInstalled = false;
|
|
104
|
+
|
|
105
|
+
// install defaults if user didn't provide
|
|
106
|
+
this._installDefaultReadMap();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_installDefaultReadMap() {
|
|
110
|
+
if (this._defaultReadMapInstalled) return;
|
|
111
|
+
// Only set a default ConnAck reader if user did not provide one
|
|
112
|
+
if (!this.readMap[PacketType.ConnAck]) {
|
|
113
|
+
this.readMap[PacketType.ConnAck] = (stream, length) => {
|
|
114
|
+
// read ackFlags and returnCode (minimum)
|
|
115
|
+
// Accept extra payload (Instagram often sends extra bytes)
|
|
116
|
+
let ackFlags = 0;
|
|
117
|
+
let returnCode = 255;
|
|
118
|
+
try {
|
|
119
|
+
ackFlags = stream.readByte();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
throw new Error('Invalid CONNACK: missing ackFlags');
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
returnCode = stream.readByte();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
throw new Error('Invalid CONNACK: missing returnCode');
|
|
127
|
+
}
|
|
128
|
+
// remaining bytes as payload (could be protobuf or other)
|
|
129
|
+
const extra = stream.readRemaining();
|
|
130
|
+
return new ConnectResponsePacket(ackFlags, returnCode, extra);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
this._defaultReadMapInstalled = true;
|
|
80
134
|
}
|
|
81
135
|
|
|
82
136
|
getNextPacketId() {
|
|
@@ -116,7 +170,12 @@ class MqttClient extends EventEmitter {
|
|
|
116
170
|
fixedHeader.push(byte);
|
|
117
171
|
} while (remLen > 0);
|
|
118
172
|
|
|
119
|
-
|
|
173
|
+
// Ensure socket writable
|
|
174
|
+
if (this.socket && !this.socket.destroyed && this.socket.writable) {
|
|
175
|
+
this.socket.write(Buffer.concat([Buffer.from(fixedHeader), packetData]));
|
|
176
|
+
} else {
|
|
177
|
+
this.emit('error', new Error('Socket not writable when sending CONNECT'));
|
|
178
|
+
}
|
|
120
179
|
} else {
|
|
121
180
|
this.emit('error', new Error('No write handler for CONNECT'));
|
|
122
181
|
}
|
|
@@ -158,6 +217,9 @@ class MqttClient extends EventEmitter {
|
|
|
158
217
|
this.socket = tls.connect(port, host, { rejectUnauthorized: true }, onConnect);
|
|
159
218
|
}
|
|
160
219
|
|
|
220
|
+
// reset recvBuffer on new socket
|
|
221
|
+
this.recvBuffer = Buffer.alloc(0);
|
|
222
|
+
|
|
161
223
|
this.socket.on('data', (data) => this._handleData(data));
|
|
162
224
|
this.socket.on('error', (err) => {
|
|
163
225
|
this._clearPingTimer();
|
|
@@ -168,6 +230,7 @@ class MqttClient extends EventEmitter {
|
|
|
168
230
|
this.emit('disconnect');
|
|
169
231
|
});
|
|
170
232
|
|
|
233
|
+
// resolve when connect_success event fired (this is triggered by parsing CONNACK)
|
|
171
234
|
this.once('connect_success', resolve);
|
|
172
235
|
} catch (e) {
|
|
173
236
|
reject(e);
|
|
@@ -178,8 +241,12 @@ class MqttClient extends EventEmitter {
|
|
|
178
241
|
_setupPingTimer() {
|
|
179
242
|
this._clearPingTimer();
|
|
180
243
|
this.pingTimer = setInterval(() => {
|
|
181
|
-
if (this.socket && !this.socket.destroyed) {
|
|
182
|
-
|
|
244
|
+
if (this.socket && !this.socket.destroyed && this.socket.writable) {
|
|
245
|
+
try {
|
|
246
|
+
this.socket.write(Buffer.from([0xC0, 0x00])); // PINGREQ (12 << 4), Length 0
|
|
247
|
+
} catch (e) {
|
|
248
|
+
// ignore write errors here, they'll be handled by socket 'error'
|
|
249
|
+
}
|
|
183
250
|
}
|
|
184
251
|
}, this.keepAliveInterval);
|
|
185
252
|
}
|
|
@@ -189,60 +256,140 @@ class MqttClient extends EventEmitter {
|
|
|
189
256
|
this.pingTimer = null;
|
|
190
257
|
}
|
|
191
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Robust handler that collects incoming chunks into recvBuffer
|
|
261
|
+
* and parses complete MQTT frames. Handles variable Remaining Length
|
|
262
|
+
* encoding and fragmentation across TCP/TLS boundaries.
|
|
263
|
+
*/
|
|
192
264
|
_handleData(data) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
265
|
+
// concat incoming chunk
|
|
266
|
+
this.recvBuffer = Buffer.concat([this.recvBuffer, data]);
|
|
267
|
+
|
|
268
|
+
// parse as many full packets as present
|
|
269
|
+
while (this.recvBuffer.length > 0) {
|
|
270
|
+
// need at least 2 bytes (1 for fixed header, at least 1 for remaining length byte)
|
|
271
|
+
if (this.recvBuffer.length < 2) break;
|
|
272
|
+
|
|
273
|
+
// first byte
|
|
274
|
+
const first = this.recvBuffer[0];
|
|
275
|
+
// parse Remaining Length (variable bytes, max 4)
|
|
197
276
|
let multiplier = 1;
|
|
198
|
-
let
|
|
277
|
+
let value = 0;
|
|
278
|
+
let remLenBytes = 0;
|
|
279
|
+
let i = 1;
|
|
199
280
|
let digit;
|
|
281
|
+
let incomplete = false;
|
|
200
282
|
do {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
283
|
+
if (i >= this.recvBuffer.length) { incomplete = true; break; } // waiting for more bytes
|
|
284
|
+
digit = this.recvBuffer[i++];
|
|
285
|
+
remLenBytes++;
|
|
286
|
+
value += (digit & 127) * multiplier;
|
|
287
|
+
multiplier *= 128;
|
|
288
|
+
if (remLenBytes > 4) {
|
|
289
|
+
// malformed remaining length
|
|
290
|
+
this.emit('error', new Error('Malformed Remaining Length'));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
205
293
|
} while ((digit & 128) !== 0);
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
294
|
+
|
|
295
|
+
if (incomplete) break; // wait more data
|
|
296
|
+
|
|
297
|
+
const headerLen = 1 + remLenBytes;
|
|
298
|
+
const totalLen = headerLen + value;
|
|
299
|
+
|
|
300
|
+
if (this.recvBuffer.length < totalLen) break; // wait full packet
|
|
301
|
+
|
|
302
|
+
// we have a full packet: extract it
|
|
303
|
+
const packet = this.recvBuffer.slice(0, totalLen);
|
|
304
|
+
// payload starts after headerLen
|
|
305
|
+
const payload = packet.slice(headerLen, totalLen);
|
|
306
|
+
|
|
307
|
+
// remove processed packet from buffer
|
|
308
|
+
this.recvBuffer = this.recvBuffer.slice(totalLen);
|
|
309
|
+
|
|
310
|
+
const type = first >> 4;
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
if (type === PacketType.ConnAck) {
|
|
314
|
+
const readFunc = this.readMap[PacketType.ConnAck];
|
|
315
|
+
if (readFunc) {
|
|
316
|
+
const stream = new PacketStream(payload);
|
|
317
|
+
let packetObj;
|
|
318
|
+
try {
|
|
319
|
+
packetObj = readFunc(stream, value);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
// If parser throws, emit error but continue parsing other packets
|
|
322
|
+
this.emit('error', e);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
// Only emit success if return code indicates success
|
|
326
|
+
if (packetObj && packetObj.isSuccess) {
|
|
327
|
+
this.emit('connect_success');
|
|
328
|
+
} else if (packetObj && !packetObj.isSuccess) {
|
|
329
|
+
// emit error so upper layers can react
|
|
330
|
+
this.emit('error', new Error(packetObj.errorName || 'Connection refused'));
|
|
331
|
+
}
|
|
332
|
+
// emit connect event with raw payload (so caller can inspect extra data)
|
|
333
|
+
this.emit('connect', { payload: packetObj ? packetObj.payload : payload });
|
|
334
|
+
} else {
|
|
335
|
+
// no read handler, just emit raw connect
|
|
336
|
+
this.emit('connect', { payload });
|
|
337
|
+
}
|
|
338
|
+
} else if (type === PacketType.Publish) {
|
|
339
|
+
try {
|
|
340
|
+
// parse topic length safely
|
|
341
|
+
if (payload.length < 2) throw new Error('Publish payload too small for topic length');
|
|
342
|
+
const topicLen = payload.readUInt16BE(0);
|
|
343
|
+
if (payload.length < 2 + topicLen) throw new Error('Publish payload too small for topic');
|
|
344
|
+
const topic = payload.slice(2, 2 + topicLen).toString();
|
|
345
|
+
const qos = (first & 0x06) >> 1;
|
|
346
|
+
let msgPayload;
|
|
347
|
+
if (qos > 0) {
|
|
348
|
+
// Skip Packet ID (2 bytes)
|
|
349
|
+
msgPayload = payload.slice(2 + topicLen + 2);
|
|
350
|
+
} else {
|
|
351
|
+
msgPayload = payload.slice(2 + topicLen);
|
|
352
|
+
}
|
|
353
|
+
this.emit('message', { topic, payload: msgPayload, qosLevel: qos });
|
|
354
|
+
} catch (e) {
|
|
355
|
+
// ignore malformed publish frames but emit error
|
|
356
|
+
this.emit('error', e);
|
|
231
357
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
358
|
+
} else if (type === PacketType.PubAck) {
|
|
359
|
+
// Handle PubAck (QoS 1)
|
|
360
|
+
if (payload.length >= 2) {
|
|
361
|
+
const packetId = payload.readUInt16BE(0);
|
|
362
|
+
const pending = this.pendingPublishes.get(packetId);
|
|
363
|
+
if (pending) {
|
|
364
|
+
// clear timeout if present
|
|
365
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
366
|
+
pending.resolve();
|
|
367
|
+
this.pendingPublishes.delete(packetId);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} else if (type === PacketType.PingResp) {
|
|
371
|
+
// Pong received, good
|
|
372
|
+
// no-op
|
|
373
|
+
} else if (type === PacketType.SubAck) {
|
|
374
|
+
// for completeness - user can provide readMap[SubAck] if needed
|
|
375
|
+
const readFunc = this.readMap[PacketType.SubAck];
|
|
376
|
+
if (readFunc) {
|
|
377
|
+
try {
|
|
378
|
+
readFunc(new PacketStream(payload), payload.length);
|
|
379
|
+
} catch (e) { this.emit('error', e); }
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
// Unknown packet type - ignore or let readMap handle if provided
|
|
383
|
+
const readFunc = this.readMap[type];
|
|
384
|
+
if (readFunc) {
|
|
385
|
+
try {
|
|
386
|
+
readFunc(new PacketStream(payload), payload.length);
|
|
387
|
+
} catch (e) { this.emit('error', e); }
|
|
242
388
|
}
|
|
243
389
|
}
|
|
244
|
-
}
|
|
245
|
-
//
|
|
390
|
+
} catch (e) {
|
|
391
|
+
// catch-any to avoid breaking parse loop
|
|
392
|
+
this.emit('error', e);
|
|
246
393
|
}
|
|
247
394
|
}
|
|
248
395
|
}
|
|
@@ -272,14 +419,16 @@ class MqttClient extends EventEmitter {
|
|
|
272
419
|
try {
|
|
273
420
|
this.socket.write(Buffer.concat([Buffer.from(fixedHeader), packetData]));
|
|
274
421
|
if (msg.qosLevel > 0) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
setTimeout(() => {
|
|
422
|
+
// set timeout for ack
|
|
423
|
+
const timer = setTimeout(() => {
|
|
278
424
|
if (this.pendingPublishes.has(packetId)) {
|
|
425
|
+
const pending = this.pendingPublishes.get(packetId);
|
|
279
426
|
this.pendingPublishes.delete(packetId);
|
|
280
|
-
resolve
|
|
427
|
+
// reject rather than resolve silently - let caller handle retry
|
|
428
|
+
pending.reject(new Error('PubAck timeout'));
|
|
281
429
|
}
|
|
282
430
|
}, 5000);
|
|
431
|
+
this.pendingPublishes.set(packetId, { resolve, reject, timer });
|
|
283
432
|
} else {
|
|
284
433
|
resolve();
|
|
285
434
|
}
|
|
@@ -522,12 +522,51 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
|
|
|
522
522
|
this._attachedAuthState = authStateHelper;
|
|
523
523
|
} catch (e) {}
|
|
524
524
|
|
|
525
|
-
|
|
526
|
-
|
|
525
|
+
// Try to read any previously-saved connect options (may include graphQlSubs/irisData/skywalkerSubs)
|
|
526
|
+
const savedOptions = authStateHelper.getMqttConnectOptions?.() || {};
|
|
527
|
+
|
|
528
|
+
// Prefer options.irisData -> savedOptions.irisData -> freshly fetched inbox (guarantee)
|
|
529
|
+
let irisData = options.irisData || savedOptions.irisData || null;
|
|
530
|
+
|
|
531
|
+
// CRITICAL FIX: If irisData is missing, actively fetch a fresh IRIS snapshot from REST before connecting.
|
|
532
|
+
// This ensures IRIS seq_id / snapshot_at_ms are valid after restarts so the server will resume sending events.
|
|
533
|
+
// Additionally: even if savedOptions.irisData exists, we prefer to fetch a fresh snapshot to avoid using stale/expired snapshot.
|
|
534
|
+
let fetchedInbox = null;
|
|
535
|
+
const shouldForceFetch = true; // always attempt fresh fetch on saved-session connect to ensure live events
|
|
536
|
+
if (shouldForceFetch) {
|
|
537
|
+
const maxAttempts = 3;
|
|
538
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
539
|
+
try {
|
|
540
|
+
console.log(`[RealtimeClient] Attempting to fetch fresh IRIS inbox snapshot (attempt ${attempt}/${maxAttempts})...`);
|
|
541
|
+
fetchedInbox = await this.ig.direct.getInbox();
|
|
542
|
+
if (fetchedInbox) {
|
|
543
|
+
irisData = fetchedInbox;
|
|
544
|
+
console.log('[RealtimeClient] Fetched IRIS snapshot successfully.');
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
} catch (e) {
|
|
548
|
+
console.warn(`[RealtimeClient] Failed to fetch IRIS snapshot (attempt ${attempt}):`, e?.message || e);
|
|
549
|
+
}
|
|
550
|
+
// small backoff
|
|
551
|
+
try {
|
|
552
|
+
await (0, shared_1.delay)(500 * attempt);
|
|
553
|
+
} catch (e) {}
|
|
554
|
+
}
|
|
555
|
+
if (!fetchedInbox) {
|
|
556
|
+
// If we couldn't fetch fresh inbox, fall back to savedOptions.irisData if present, otherwise proceed (connect may still work)
|
|
557
|
+
if (savedOptions.irisData) {
|
|
558
|
+
irisData = savedOptions.irisData;
|
|
559
|
+
console.warn('[RealtimeClient] Could not fetch fresh IRIS snapshot — falling back to saved irisData (may be stale).');
|
|
560
|
+
} else if (!irisData) {
|
|
561
|
+
console.warn('[RealtimeClient] No IRIS snapshot available (neither fetched nor saved). Proceeding without irisData — server may not replay missed events.');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
527
566
|
const connectOptions = {
|
|
528
|
-
graphQlSubs: options.graphQlSubs || savedOptions
|
|
529
|
-
skywalkerSubs: options.skywalkerSubs || savedOptions
|
|
530
|
-
irisData
|
|
567
|
+
graphQlSubs: options.graphQlSubs || savedOptions.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
|
|
568
|
+
skywalkerSubs: options.skywalkerSubs || savedOptions.skywalkerSubs || ['presence_subscribe', 'typing_subscribe'],
|
|
569
|
+
irisData,
|
|
531
570
|
...options
|
|
532
571
|
};
|
|
533
572
|
|
|
@@ -621,3 +660,4 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
|
|
|
621
660
|
}
|
|
622
661
|
exports.RealtimeClient = RealtimeClient;
|
|
623
662
|
//# sourceMappingURL=realtime.client.js.map
|
|
663
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodejs-insta-private-api-mqtt",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.21",
|
|
4
4
|
"description": "Complete Instagram MQTT protocol with FULL iOS + Android support. 33 device presets (21 iOS + 12 Android). iPhone 16/15/14/13/12, iPad Pro, Samsung, Pixel, Huawei. Real-time DM messaging, view-once media extraction, sub-500ms latency.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|