nodejs-insta-private-api-mqtt 1.2.10 → 1.3.10
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/useMultiFileAuthState.js +627 -7
- package/package.json +1 -1
|
@@ -26,7 +26,16 @@ const FILE_NAMES = {
|
|
|
26
26
|
// Added utility files
|
|
27
27
|
loginHistory: 'login-history.json',
|
|
28
28
|
usageStats: 'usage-stats.json',
|
|
29
|
-
accountsIndex: 'accounts.json'
|
|
29
|
+
accountsIndex: 'accounts.json',
|
|
30
|
+
|
|
31
|
+
// MQTT helpers files
|
|
32
|
+
mqttHealth: 'mqtt-health.json',
|
|
33
|
+
mqttReconnect: 'mqtt-reconnect.json',
|
|
34
|
+
mqttMessageCache: 'mqtt-message-cache.json',
|
|
35
|
+
mqttOutbox: 'mqtt-outbox.json',
|
|
36
|
+
mqttSubscriptionHealth: 'mqtt-subscription-health.json',
|
|
37
|
+
mqttTraffic: 'mqtt-traffic.json',
|
|
38
|
+
mqttRisk: 'mqtt-risk.json'
|
|
30
39
|
};
|
|
31
40
|
|
|
32
41
|
class MultiFileAuthState extends EventEmitter {
|
|
@@ -42,6 +51,11 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
42
51
|
this._autoRefreshIntervalMs = 5 * 60 * 1000; // default 5 minutes
|
|
43
52
|
this._autoRefreshClient = null;
|
|
44
53
|
|
|
54
|
+
// Reconnect/backoff runtime
|
|
55
|
+
this._reconnectBackoff = [2000, 5000, 15000, 60000];
|
|
56
|
+
this._reconnectAttempts = 0;
|
|
57
|
+
this._reconnectTimer = null;
|
|
58
|
+
|
|
45
59
|
this.data = {
|
|
46
60
|
creds: null,
|
|
47
61
|
device: null,
|
|
@@ -61,7 +75,16 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
61
75
|
// utility
|
|
62
76
|
loginHistory: null,
|
|
63
77
|
usageStats: null,
|
|
64
|
-
accountsIndex: null
|
|
78
|
+
accountsIndex: null,
|
|
79
|
+
|
|
80
|
+
// mqtt helpers
|
|
81
|
+
mqttHealth: null,
|
|
82
|
+
mqttReconnect: null,
|
|
83
|
+
mqttMessageCache: null,
|
|
84
|
+
mqttOutbox: null,
|
|
85
|
+
mqttSubscriptionHealth: null,
|
|
86
|
+
mqttTraffic: null,
|
|
87
|
+
mqttRisk: null
|
|
65
88
|
};
|
|
66
89
|
|
|
67
90
|
// ensure folder structure
|
|
@@ -90,6 +113,80 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
90
113
|
}
|
|
91
114
|
}
|
|
92
115
|
|
|
116
|
+
// sanitize state to avoid nulls for key fields (useful to ensure instant non-null)
|
|
117
|
+
_sanitizeForSave(key, data) {
|
|
118
|
+
try {
|
|
119
|
+
if (!data) return data;
|
|
120
|
+
if (key === 'creds') {
|
|
121
|
+
if (typeof data.userId === 'undefined' || data.userId === null) data.userId = data.userId || 'pending';
|
|
122
|
+
if (typeof data.username === 'undefined' || data.username === null) data.username = data.username || 'pending';
|
|
123
|
+
if (typeof data.sessionId === 'undefined' || data.sessionId === null) data.sessionId = data.sessionId || `pending-${Date.now()}`;
|
|
124
|
+
if (typeof data.csrfToken === 'undefined' || data.csrfToken === null) data.csrfToken = data.csrfToken || 'pending';
|
|
125
|
+
if (typeof data.isLoggedIn === 'undefined') data.isLoggedIn = !!data.sessionId || !!data.authorization;
|
|
126
|
+
if (!data.loginAt) data.loginAt = data.loginAt || new Date().toISOString();
|
|
127
|
+
}
|
|
128
|
+
if (key === 'mqttSession') {
|
|
129
|
+
if (!data.sessionId) data.sessionId = data.sessionId || `local-mqtt-${Date.now()}`;
|
|
130
|
+
if (!data.mqttSessionId) data.mqttSessionId = data.mqttSessionId || 'boot';
|
|
131
|
+
if (!data.lastConnected) data.lastConnected = new Date().toISOString();
|
|
132
|
+
}
|
|
133
|
+
if (key === 'seqIds') {
|
|
134
|
+
if (typeof data.seq_id === 'undefined' || data.seq_id === null) data.seq_id = data.seq_id || 0;
|
|
135
|
+
if (typeof data.snapshot_at_ms === 'undefined' || data.snapshot_at_ms === null) data.snapshot_at_ms = data.snapshot_at_ms || Date.now();
|
|
136
|
+
}
|
|
137
|
+
if (key === 'mqttTopics') {
|
|
138
|
+
if (!data.topics) data.topics = data.topics || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync', 'presence_subscribe'];
|
|
139
|
+
if (!data.updatedAt) data.updatedAt = new Date().toISOString();
|
|
140
|
+
}
|
|
141
|
+
if (key === 'mqttCapabilities') {
|
|
142
|
+
data.supportsTyping = Boolean(data.supportsTyping ?? true);
|
|
143
|
+
data.supportsReactions = Boolean(data.supportsReactions ?? true);
|
|
144
|
+
data.supportsVoice = Boolean(data.supportsVoice ?? false);
|
|
145
|
+
data.protocolVersion = data.protocolVersion || 3;
|
|
146
|
+
data.collectedAt = data.collectedAt || new Date().toISOString();
|
|
147
|
+
}
|
|
148
|
+
if (key === 'mqttHealth') {
|
|
149
|
+
data.lastPingAt = data.lastPingAt || null;
|
|
150
|
+
data.lastPongAt = data.lastPongAt || null;
|
|
151
|
+
data.latencyMs = data.latencyMs || 0;
|
|
152
|
+
data.missedPings = data.missedPings || 0;
|
|
153
|
+
data.status = data.status || 'unknown';
|
|
154
|
+
}
|
|
155
|
+
if (key === 'mqttMessageCache') {
|
|
156
|
+
if (!Array.isArray(data.lastIds)) data.lastIds = [];
|
|
157
|
+
if (!data.max) data.max = data.max || 500;
|
|
158
|
+
}
|
|
159
|
+
if (key === 'mqttOutbox') {
|
|
160
|
+
if (!Array.isArray(data)) return [];
|
|
161
|
+
}
|
|
162
|
+
if (key === 'mqttSubscriptionHealth') {
|
|
163
|
+
data.expected = data.expected || (this.getMqttTopics()?.topics || ['ig_sub_direct']);
|
|
164
|
+
data.active = data.active || data.expected.slice();
|
|
165
|
+
data.lastCheck = data.lastCheck || new Date().toISOString();
|
|
166
|
+
data.needsResubscribe = data.needsResubscribe || false;
|
|
167
|
+
}
|
|
168
|
+
if (key === 'mqttTraffic') {
|
|
169
|
+
data.messagesInPerMin = data.messagesInPerMin || 0;
|
|
170
|
+
data.messagesOutPerMin = data.messagesOutPerMin || 0;
|
|
171
|
+
data.lastReset = data.lastReset || new Date().toISOString();
|
|
172
|
+
}
|
|
173
|
+
if (key === 'mqttReconnect') {
|
|
174
|
+
data.attempts = data.attempts || 0;
|
|
175
|
+
data.lastReason = data.lastReason || null;
|
|
176
|
+
data.lastAttemptAt = data.lastAttemptAt || null;
|
|
177
|
+
data.nextRetryInMs = data.nextRetryInMs || 0;
|
|
178
|
+
}
|
|
179
|
+
if (key === 'mqttRisk') {
|
|
180
|
+
data.riskScore = typeof data.riskScore === 'number' ? data.riskScore : 0;
|
|
181
|
+
data.level = data.level || 'low';
|
|
182
|
+
data.updatedAt = data.updatedAt || new Date().toISOString();
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
// ignore sanitize errors
|
|
186
|
+
}
|
|
187
|
+
return data;
|
|
188
|
+
}
|
|
189
|
+
|
|
93
190
|
// create a timestamped backup of existing file (if present)
|
|
94
191
|
async _createBackup(key) {
|
|
95
192
|
try {
|
|
@@ -133,9 +230,11 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
133
230
|
this._ensureFolder();
|
|
134
231
|
const filePath = this._getFilePath(key);
|
|
135
232
|
try {
|
|
233
|
+
// sanitize to avoid nulls for key fields
|
|
234
|
+
const sanitized = this._sanitizeForSave(key, JSON.parse(JSON.stringify(data)));
|
|
136
235
|
// create backup before overwriting existing file
|
|
137
236
|
await this._createBackup(key);
|
|
138
|
-
await this._writeFileAtomic(filePath,
|
|
237
|
+
await this._writeFileAtomic(filePath, sanitized);
|
|
139
238
|
return true;
|
|
140
239
|
} catch (e) {
|
|
141
240
|
console.error(`[MultiFileAuthState] Error writing ${key}:`, e.message);
|
|
@@ -189,6 +288,15 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
189
288
|
};
|
|
190
289
|
if (!this.data.accountsIndex) this.data.accountsIndex = {};
|
|
191
290
|
|
|
291
|
+
// ensure mqtt helper objects exist
|
|
292
|
+
if (!this.data.mqttHealth) this.data.mqttHealth = { lastPingAt: null, lastPongAt: null, latencyMs: 0, missedPings: 0, status: 'unknown' };
|
|
293
|
+
if (!this.data.mqttReconnect) this.data.mqttReconnect = { attempts: 0, lastReason: null, lastAttemptAt: null, nextRetryInMs: 0 };
|
|
294
|
+
if (!this.data.mqttMessageCache) this.data.mqttMessageCache = { lastIds: [], max: 500 };
|
|
295
|
+
if (!this.data.mqttOutbox) this.data.mqttOutbox = [];
|
|
296
|
+
if (!this.data.mqttSubscriptionHealth) this.data.mqttSubscriptionHealth = { expected: (this.data.mqttTopics?.topics || ['ig_sub_direct']), active: (this.data.mqttTopics?.topics || ['ig_sub_direct']).slice(), lastCheck: new Date().toISOString(), needsResubscribe: false };
|
|
297
|
+
if (!this.data.mqttTraffic) this.data.mqttTraffic = { messagesInPerMin: 0, messagesOutPerMin: 0, lastReset: new Date().toISOString() };
|
|
298
|
+
if (!this.data.mqttRisk) this.data.mqttRisk = { riskScore: 0, level: 'low', updatedAt: new Date().toISOString() };
|
|
299
|
+
|
|
192
300
|
return {
|
|
193
301
|
creds: this.data.creds,
|
|
194
302
|
device: this.data.device,
|
|
@@ -205,6 +313,13 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
205
313
|
loginHistory: this.data.loginHistory,
|
|
206
314
|
usageStats: this.data.usageStats,
|
|
207
315
|
accountsIndex: this.data.accountsIndex,
|
|
316
|
+
mqttHealth: this.data.mqttHealth,
|
|
317
|
+
mqttReconnect: this.data.mqttReconnect,
|
|
318
|
+
mqttMessageCache: this.data.mqttMessageCache,
|
|
319
|
+
mqttOutbox: this.data.mqttOutbox,
|
|
320
|
+
mqttSubscriptionHealth: this.data.mqttSubscriptionHealth,
|
|
321
|
+
mqttTraffic: this.data.mqttTraffic,
|
|
322
|
+
mqttRisk: this.data.mqttRisk,
|
|
208
323
|
hasSession: !!(this.data.creds && this.data.cookies)
|
|
209
324
|
};
|
|
210
325
|
}
|
|
@@ -225,6 +340,15 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
225
340
|
if (this.data.accountsIndex) {
|
|
226
341
|
try { await this._writeFile('accountsIndex', this.data.accountsIndex); } catch (e) {}
|
|
227
342
|
}
|
|
343
|
+
// mqtt helpers
|
|
344
|
+
try { await this._writeFile('mqttHealth', this.data.mqttHealth); } catch (e) {}
|
|
345
|
+
try { await this._writeFile('mqttReconnect', this.data.mqttReconnect); } catch (e) {}
|
|
346
|
+
try { await this._writeFile('mqttMessageCache', this.data.mqttMessageCache); } catch (e) {}
|
|
347
|
+
try { await this._writeFile('mqttOutbox', this.data.mqttOutbox); } catch (e) {}
|
|
348
|
+
try { await this._writeFile('mqttSubscriptionHealth', this.data.mqttSubscriptionHealth); } catch (e) {}
|
|
349
|
+
try { await this._writeFile('mqttTraffic', this.data.mqttTraffic); } catch (e) {}
|
|
350
|
+
try { await this._writeFile('mqttRisk', this.data.mqttRisk); } catch (e) {}
|
|
351
|
+
|
|
228
352
|
await Promise.all(savePromises);
|
|
229
353
|
}
|
|
230
354
|
|
|
@@ -254,6 +378,15 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
254
378
|
if (this.data.mqttAuth) promises.push(this._writeFile('mqttAuth', this.data.mqttAuth));
|
|
255
379
|
if (this.data.lastPublishIds) promises.push(this._writeFile('lastPublishIds', this.data.lastPublishIds));
|
|
256
380
|
|
|
381
|
+
// mqtt helpers
|
|
382
|
+
promises.push(this._writeFile('mqttHealth', this.data.mqttHealth));
|
|
383
|
+
promises.push(this._writeFile('mqttReconnect', this.data.mqttReconnect));
|
|
384
|
+
promises.push(this._writeFile('mqttMessageCache', this.data.mqttMessageCache));
|
|
385
|
+
promises.push(this._writeFile('mqttOutbox', this.data.mqttOutbox));
|
|
386
|
+
promises.push(this._writeFile('mqttSubscriptionHealth', this.data.mqttSubscriptionHealth));
|
|
387
|
+
promises.push(this._writeFile('mqttTraffic', this.data.mqttTraffic));
|
|
388
|
+
promises.push(this._writeFile('mqttRisk', this.data.mqttRisk));
|
|
389
|
+
|
|
257
390
|
await Promise.all(promises);
|
|
258
391
|
this.emit('mqtt-state-saved');
|
|
259
392
|
}
|
|
@@ -382,6 +515,445 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
382
515
|
this._debouncedSave(['usageStats']);
|
|
383
516
|
}
|
|
384
517
|
|
|
518
|
+
// --- MQTT helper methods ---
|
|
519
|
+
|
|
520
|
+
// update mqtt health and persist
|
|
521
|
+
_updateMqttHealth(partial) {
|
|
522
|
+
try {
|
|
523
|
+
if (!this.data.mqttHealth) this.data.mqttHealth = {};
|
|
524
|
+
Object.assign(this.data.mqttHealth, partial);
|
|
525
|
+
// recompute status
|
|
526
|
+
if (this.data.mqttHealth.missedPings > 3) {
|
|
527
|
+
this.data.mqttHealth.status = 'degraded';
|
|
528
|
+
} else if (this.data.mqttHealth.lastPongAt) {
|
|
529
|
+
this.data.mqttHealth.status = 'ok';
|
|
530
|
+
} else {
|
|
531
|
+
this.data.mqttHealth.status = this.data.mqttHealth.status || 'unknown';
|
|
532
|
+
}
|
|
533
|
+
this._debouncedSave(['mqttHealth']);
|
|
534
|
+
} catch (e) {}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// mark ping sent
|
|
538
|
+
markPing() {
|
|
539
|
+
this._updateMqttHealth({ lastPingAt: new Date().toISOString() });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// mark pong received and update latency
|
|
543
|
+
markPong() {
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
const lastPing = this.data.mqttHealth?.lastPingAt ? new Date(this.data.mqttHealth.lastPingAt).getTime() : null;
|
|
546
|
+
const latency = lastPing ? (now - lastPing) : 0;
|
|
547
|
+
const missed = Math.max(0, (this.data.mqttHealth?.missedPings || 0) - 1);
|
|
548
|
+
this._updateMqttHealth({ lastPongAt: new Date().toISOString(), latencyMs: latency, missedPings: missed });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// increment missed ping
|
|
552
|
+
markMissedPing() {
|
|
553
|
+
const missed = (this.data.mqttHealth?.missedPings || 0) + 1;
|
|
554
|
+
this._updateMqttHealth({ missedPings: missed });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// message deduplication: return true if duplicate
|
|
558
|
+
isDuplicateMessage(messageId) {
|
|
559
|
+
if (!messageId) return false;
|
|
560
|
+
if (!this.data.mqttMessageCache) this.data.mqttMessageCache = { lastIds: [], max: 500 };
|
|
561
|
+
const idx = this.data.mqttMessageCache.lastIds.indexOf(messageId);
|
|
562
|
+
if (idx !== -1) return true;
|
|
563
|
+
// push and cap
|
|
564
|
+
this.data.mqttMessageCache.lastIds.push(messageId);
|
|
565
|
+
if (this.data.mqttMessageCache.lastIds.length > (this.data.mqttMessageCache.max || 500)) {
|
|
566
|
+
this.data.mqttMessageCache.lastIds.shift();
|
|
567
|
+
}
|
|
568
|
+
this._debouncedSave(['mqttMessageCache']);
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// enqueue outgoing message to outbox
|
|
573
|
+
enqueueOutbox(item) {
|
|
574
|
+
if (!this.data.mqttOutbox) this.data.mqttOutbox = [];
|
|
575
|
+
// item: { topic, payload, qos, createdAt, tries }
|
|
576
|
+
const i = Object.assign({ qos: 1, createdAt: new Date().toISOString(), tries: 0 }, item);
|
|
577
|
+
this.data.mqttOutbox.push(i);
|
|
578
|
+
this._debouncedSave(['mqttOutbox']);
|
|
579
|
+
this.emit('outbox-enqueued', i);
|
|
580
|
+
return i;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// flush outbox (attempt send). Tries to use common publish/send methods on client
|
|
584
|
+
async flushOutbox(realtimeClient, maxPerRun = 50) {
|
|
585
|
+
if (!this.data.mqttOutbox || !this.data.mqttOutbox.length) return 0;
|
|
586
|
+
const sent = [];
|
|
587
|
+
const keep = [];
|
|
588
|
+
let processed = 0;
|
|
589
|
+
for (const item of this.data.mqttOutbox) {
|
|
590
|
+
if (processed >= maxPerRun) {
|
|
591
|
+
keep.push(item);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
processed++;
|
|
595
|
+
try {
|
|
596
|
+
item.tries = (item.tries || 0) + 1;
|
|
597
|
+
// try publish methods in order of likelihood
|
|
598
|
+
let ok = false;
|
|
599
|
+
if (realtimeClient && typeof realtimeClient.publish === 'function') {
|
|
600
|
+
// mqtt.js style
|
|
601
|
+
try {
|
|
602
|
+
realtimeClient.publish(item.topic, typeof item.payload === 'string' ? item.payload : JSON.stringify(item.payload), { qos: item.qos });
|
|
603
|
+
ok = true;
|
|
604
|
+
} catch (e) {
|
|
605
|
+
ok = false;
|
|
606
|
+
}
|
|
607
|
+
} else if (realtimeClient && typeof realtimeClient.send === 'function') {
|
|
608
|
+
try {
|
|
609
|
+
realtimeClient.send(item.topic, item.payload);
|
|
610
|
+
ok = true;
|
|
611
|
+
} catch (e) { ok = false; }
|
|
612
|
+
} else if (realtimeClient && typeof realtimeClient.sendMessage === 'function') {
|
|
613
|
+
try {
|
|
614
|
+
realtimeClient.sendMessage(item.payload);
|
|
615
|
+
ok = true;
|
|
616
|
+
} catch (e) { ok = false; }
|
|
617
|
+
} else {
|
|
618
|
+
// cannot send: keep in outbox
|
|
619
|
+
ok = false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (ok) {
|
|
623
|
+
sent.push(item);
|
|
624
|
+
this.incrementStat('mqttMessages', 1);
|
|
625
|
+
} else {
|
|
626
|
+
// keep for retry, but if tries exceed threshold move to dead-letter (drop)
|
|
627
|
+
if (item.tries >= 10) {
|
|
628
|
+
this.emit('outbox-dropped', item);
|
|
629
|
+
} else {
|
|
630
|
+
keep.push(item);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} catch (e) {
|
|
634
|
+
keep.push(item);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// replace queue
|
|
638
|
+
this.data.mqttOutbox = keep.concat(this.data.mqttOutbox.slice(processed));
|
|
639
|
+
this._debouncedSave(['mqttOutbox']);
|
|
640
|
+
this.emit('outbox-flushed', { sentCount: sent.length, remaining: this.data.mqttOutbox.length });
|
|
641
|
+
return sent.length;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// subscription watchdog: check and attempt resubscribe
|
|
645
|
+
async checkSubscriptions(realtimeClient) {
|
|
646
|
+
try {
|
|
647
|
+
const expected = this.data.mqttSubscriptionHealth?.expected || (this.data.mqttTopics?.topics || []);
|
|
648
|
+
let active = [];
|
|
649
|
+
// try to read from client
|
|
650
|
+
if (realtimeClient?.connection?.subscribedTopics) active = realtimeClient.connection.subscribedTopics;
|
|
651
|
+
else if (realtimeClient?._subscribedTopics) active = realtimeClient._subscribedTopics;
|
|
652
|
+
else if (Array.isArray(this.data.mqttTopics?.topics)) active = this.data.mqttTopics.topics;
|
|
653
|
+
|
|
654
|
+
const needs = expected.filter(t => !active.includes(t));
|
|
655
|
+
this.data.mqttSubscriptionHealth = {
|
|
656
|
+
expected,
|
|
657
|
+
active,
|
|
658
|
+
lastCheck: new Date().toISOString(),
|
|
659
|
+
needsResubscribe: needs.length > 0
|
|
660
|
+
};
|
|
661
|
+
this._debouncedSave(['mqttSubscriptionHealth']);
|
|
662
|
+
|
|
663
|
+
if (needs.length && realtimeClient) {
|
|
664
|
+
for (const t of needs) {
|
|
665
|
+
try {
|
|
666
|
+
// call common subscribe methods
|
|
667
|
+
if (typeof realtimeClient.subscribe === 'function') {
|
|
668
|
+
realtimeClient.subscribe(t);
|
|
669
|
+
} else if (realtimeClient.connection && typeof realtimeClient.connection.subscribe === 'function') {
|
|
670
|
+
realtimeClient.connection.subscribe(t);
|
|
671
|
+
} else if (typeof realtimeClient.subscriptions === 'function') {
|
|
672
|
+
realtimeClient.subscriptions([t]);
|
|
673
|
+
}
|
|
674
|
+
// update active
|
|
675
|
+
if (!this.data.mqttSubscriptionHealth.active.includes(t)) this.data.mqttSubscriptionHealth.active.push(t);
|
|
676
|
+
this.emit('resubscribe-attempt', { topic: t });
|
|
677
|
+
} catch (e) {
|
|
678
|
+
this.emit('resubscribe-failed', { topic: t, error: e.message });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
this._debouncedSave(['mqttSubscriptionHealth']);
|
|
682
|
+
}
|
|
683
|
+
return this.data.mqttSubscriptionHealth;
|
|
684
|
+
} catch (e) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// traffic profiler: update incoming/outgoing counters
|
|
690
|
+
_updateTraffic(incoming = 0, outgoing = 0) {
|
|
691
|
+
try {
|
|
692
|
+
if (!this.data.mqttTraffic) this.data.mqttTraffic = { messagesInPerMin: 0, messagesOutPerMin: 0, lastReset: new Date().toISOString() };
|
|
693
|
+
const lastReset = new Date(this.data.mqttTraffic.lastReset).getTime();
|
|
694
|
+
if (Date.now() - lastReset > 60 * 1000) {
|
|
695
|
+
// reset every minute
|
|
696
|
+
this.data.mqttTraffic.messagesInPerMin = 0;
|
|
697
|
+
this.data.mqttTraffic.messagesOutPerMin = 0;
|
|
698
|
+
this.data.mqttTraffic.lastReset = new Date().toISOString();
|
|
699
|
+
}
|
|
700
|
+
this.data.mqttTraffic.messagesInPerMin += incoming;
|
|
701
|
+
this.data.mqttTraffic.messagesOutPerMin += outgoing;
|
|
702
|
+
this._debouncedSave(['mqttTraffic']);
|
|
703
|
+
} catch (e) {}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// compute simple risk score from reconnects, errors, traffic
|
|
707
|
+
_computeRiskScore() {
|
|
708
|
+
try {
|
|
709
|
+
const reconnects = this.data.usageStats?.reconnects || 0;
|
|
710
|
+
const errors = this.data.usageStats?.errors || 0;
|
|
711
|
+
const outPerMin = this.data.mqttTraffic?.messagesOutPerMin || 0;
|
|
712
|
+
// simple heuristic
|
|
713
|
+
let score = 0;
|
|
714
|
+
score += Math.min(1, reconnects / 10) * 0.4;
|
|
715
|
+
score += Math.min(1, errors / 20) * 0.3;
|
|
716
|
+
score += Math.min(1, outPerMin / 200) * 0.3;
|
|
717
|
+
const level = score > 0.7 ? 'high' : score > 0.35 ? 'medium' : 'low';
|
|
718
|
+
this.data.mqttRisk = { riskScore: Number(score.toFixed(3)), level, updatedAt: new Date().toISOString() };
|
|
719
|
+
this._debouncedSave(['mqttRisk']);
|
|
720
|
+
this.emit('risk-updated', this.data.mqttRisk);
|
|
721
|
+
return this.data.mqttRisk;
|
|
722
|
+
} catch (e) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// schedule auto reconnect using backoff; will attempt to call client.reconnect() if available or emit event
|
|
728
|
+
_scheduleReconnect(realtimeClient, reason = 'unknown') {
|
|
729
|
+
try {
|
|
730
|
+
this._reconnectAttempts = (this._reconnectAttempts || 0) + 1;
|
|
731
|
+
const idx = Math.min(this._reconnectAttempts - 1, this._reconnectBackoff.length - 1);
|
|
732
|
+
const delay = this._reconnectBackoff[idx] || this._reconnectBackoff[this._reconnectBackoff.length - 1];
|
|
733
|
+
this.data.mqttReconnect = {
|
|
734
|
+
attempts: this._reconnectAttempts,
|
|
735
|
+
lastReason: reason,
|
|
736
|
+
lastAttemptAt: new Date().toISOString(),
|
|
737
|
+
nextRetryInMs: delay
|
|
738
|
+
};
|
|
739
|
+
this.incrementStat('reconnects', 1);
|
|
740
|
+
this._debouncedSave(['mqttReconnect']);
|
|
741
|
+
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
742
|
+
this._reconnectTimer = setTimeout(async () => {
|
|
743
|
+
try {
|
|
744
|
+
if (realtimeClient) {
|
|
745
|
+
if (typeof realtimeClient.reconnect === 'function') {
|
|
746
|
+
realtimeClient.reconnect();
|
|
747
|
+
} else if (typeof realtimeClient.connect === 'function') {
|
|
748
|
+
// some libs have connect
|
|
749
|
+
realtimeClient.connect();
|
|
750
|
+
} else {
|
|
751
|
+
// can't auto-call: emit event so app can handle
|
|
752
|
+
this.emit('should-reconnect', { reason });
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
this.emit('should-reconnect', { reason });
|
|
756
|
+
}
|
|
757
|
+
} catch (e) {
|
|
758
|
+
this.emit('reconnect-failed', { reason, error: e.message });
|
|
759
|
+
}
|
|
760
|
+
}, delay);
|
|
761
|
+
} catch (e) {}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// reset reconnect attempts after successful connect
|
|
765
|
+
_resetReconnect() {
|
|
766
|
+
this._reconnectAttempts = 0;
|
|
767
|
+
if (this._reconnectTimer) {
|
|
768
|
+
clearTimeout(this._reconnectTimer);
|
|
769
|
+
this._reconnectTimer = null;
|
|
770
|
+
}
|
|
771
|
+
if (this.data.mqttReconnect) {
|
|
772
|
+
this.data.mqttReconnect.attempts = 0;
|
|
773
|
+
this.data.mqttReconnect.nextRetryInMs = 0;
|
|
774
|
+
this._debouncedSave(['mqttReconnect']);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// warm-restore: apply saved options into realtimeClient before connect to speed startup
|
|
779
|
+
warmRestoreClient(realtimeClient) {
|
|
780
|
+
try {
|
|
781
|
+
const opts = this.getMqttConnectOptions();
|
|
782
|
+
if (!opts) return false;
|
|
783
|
+
// Merge irisData, mqttAuthToken etc. into client init options if possible
|
|
784
|
+
if (realtimeClient.initOptions && typeof realtimeClient.initOptions === 'object') {
|
|
785
|
+
Object.assign(realtimeClient.initOptions, opts);
|
|
786
|
+
} else {
|
|
787
|
+
realtimeClient.initOptions = opts;
|
|
788
|
+
}
|
|
789
|
+
// also set connection clientInfo if supported
|
|
790
|
+
if (realtimeClient.connection && realtimeClient.connection.clientInfo && this.data.mqttSession?.mqttSessionId) {
|
|
791
|
+
realtimeClient.connection.clientInfo.clientMqttSessionId = this.data.mqttSession.mqttSessionId;
|
|
792
|
+
}
|
|
793
|
+
this.emit('warm-restore', { options: opts });
|
|
794
|
+
return true;
|
|
795
|
+
} catch (e) {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// attach runtime helpers to realtimeClient to manage instant improvements
|
|
801
|
+
attachRealtimeClient(realtimeClient, opts = {}) {
|
|
802
|
+
try {
|
|
803
|
+
if (!realtimeClient) return;
|
|
804
|
+
|
|
805
|
+
// warm restore immediately
|
|
806
|
+
try { this.warmRestoreClient(realtimeClient); } catch (e) {}
|
|
807
|
+
|
|
808
|
+
// ping/pong monitoring
|
|
809
|
+
const pingIntervalMs = opts.pingIntervalMs || 30 * 1000; // 30s
|
|
810
|
+
let pingTimer = null;
|
|
811
|
+
const startPing = () => {
|
|
812
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
813
|
+
pingTimer = setInterval(() => {
|
|
814
|
+
try {
|
|
815
|
+
this.markPing();
|
|
816
|
+
if (typeof realtimeClient.ping === 'function') {
|
|
817
|
+
realtimeClient.ping();
|
|
818
|
+
} else if (typeof realtimeClient.pingReq === 'function') {
|
|
819
|
+
realtimeClient.pingReq();
|
|
820
|
+
} else if (typeof realtimeClient.pingServer === 'function') {
|
|
821
|
+
realtimeClient.pingServer();
|
|
822
|
+
} else {
|
|
823
|
+
// some libraries use publish to $SYS topic or nothing; we still mark ping
|
|
824
|
+
}
|
|
825
|
+
// after ping, schedule missed ping detection
|
|
826
|
+
setTimeout(() => {
|
|
827
|
+
// if lastPongAt older than lastPingAt, consider missed
|
|
828
|
+
const lastPing = new Date(this.data.mqttHealth?.lastPingAt || 0).getTime();
|
|
829
|
+
const lastPong = new Date(this.data.mqttHealth?.lastPongAt || 0).getTime();
|
|
830
|
+
if (lastPing && (!lastPong || lastPong < lastPing)) {
|
|
831
|
+
this.markMissedPing();
|
|
832
|
+
}
|
|
833
|
+
}, Math.min(5000, Math.floor(pingIntervalMs / 2)));
|
|
834
|
+
} catch (e) {}
|
|
835
|
+
}, pingIntervalMs);
|
|
836
|
+
};
|
|
837
|
+
const stopPing = () => { if (pingTimer) clearInterval(pingTimer); pingTimer = null; };
|
|
838
|
+
|
|
839
|
+
// event handlers (subscribe to multiple common event names)
|
|
840
|
+
const onConnect = async (...args) => {
|
|
841
|
+
this._resetReconnect();
|
|
842
|
+
this.data.mqttSession = this.data.mqttSession || {};
|
|
843
|
+
this.data.mqttSession.lastConnected = new Date().toISOString();
|
|
844
|
+
this._debouncedSave(['mqttSession']);
|
|
845
|
+
this._updateMqttHealth({ status: 'ok', lastPongAt: new Date().toISOString(), missedPings: 0 });
|
|
846
|
+
this.emit('realtime-connected', { args });
|
|
847
|
+
// flush outbox on connect
|
|
848
|
+
try { await this.flushOutbox(realtimeClient); } catch (e) {}
|
|
849
|
+
// check subscriptions
|
|
850
|
+
try { await this.checkSubscriptions(realtimeClient); } catch (e) {}
|
|
851
|
+
startPing();
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const onClose = (...args) => {
|
|
855
|
+
stopPing();
|
|
856
|
+
this._updateMqttHealth({ status: 'dead' });
|
|
857
|
+
this._scheduleReconnect(realtimeClient, 'close');
|
|
858
|
+
this.emit('realtime-closed', { args });
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const onReconnect = (...args) => {
|
|
862
|
+
this.emit('realtime-reconnect', { args });
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const onOffline = (...args) => {
|
|
866
|
+
this._updateMqttHealth({ status: 'degraded' });
|
|
867
|
+
this._scheduleReconnect(realtimeClient, 'offline');
|
|
868
|
+
this.emit('realtime-offline', { args });
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const onError = (err) => {
|
|
872
|
+
this.incrementStat('errors', 1);
|
|
873
|
+
this._scheduleReconnect(realtimeClient, err?.message || 'error');
|
|
874
|
+
this.emit('realtime-error', { error: err?.message || String(err) });
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const onMessage = async (topic, payload, packet) => {
|
|
878
|
+
// generic message handler: try to extract id for dedupe
|
|
879
|
+
// Many IG messages contain an id field in JSON payload; try to parse
|
|
880
|
+
let messageId = null;
|
|
881
|
+
try {
|
|
882
|
+
const s = (typeof payload === 'string') ? payload : (payload && payload.toString ? payload.toString() : null);
|
|
883
|
+
if (s) {
|
|
884
|
+
const j = JSON.parse(s);
|
|
885
|
+
if (j && (j.message_id || j.id || j.msg_id)) messageId = j.message_id || j.id || j.msg_id;
|
|
886
|
+
}
|
|
887
|
+
} catch (e) {
|
|
888
|
+
// not json, ignore
|
|
889
|
+
}
|
|
890
|
+
if (messageId && this.isDuplicateMessage(messageId)) {
|
|
891
|
+
this.emit('message-duplicate', { topic, messageId });
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
// update traffic counters
|
|
895
|
+
this._updateTraffic(1, 0);
|
|
896
|
+
this.incrementStat('mqttMessages', 1);
|
|
897
|
+
// mark pong if message looks like pong or heartbeat
|
|
898
|
+
if (topic && topic.includes('pong')) this.markPong();
|
|
899
|
+
this.emit('realtime-message', { topic, payload, packet });
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// wire up events safely for common handlers
|
|
903
|
+
try {
|
|
904
|
+
// mqtt.js style
|
|
905
|
+
if (typeof realtimeClient.on === 'function') {
|
|
906
|
+
realtimeClient.on('connect', onConnect);
|
|
907
|
+
realtimeClient.on('reconnect', onReconnect);
|
|
908
|
+
realtimeClient.on('close', onClose);
|
|
909
|
+
realtimeClient.on('offline', onOffline);
|
|
910
|
+
realtimeClient.on('error', onError);
|
|
911
|
+
realtimeClient.on('message', onMessage);
|
|
912
|
+
}
|
|
913
|
+
// websockets or other: attempt to attach possible event names
|
|
914
|
+
if (realtimeClient.addEventListener && typeof realtimeClient.addEventListener === 'function') {
|
|
915
|
+
try { realtimeClient.addEventListener('open', onConnect); } catch (e) {}
|
|
916
|
+
try { realtimeClient.addEventListener('close', onClose); } catch (e) {}
|
|
917
|
+
try { realtimeClient.addEventListener('error', onError); } catch (e) {}
|
|
918
|
+
try { realtimeClient.addEventListener('message', (ev) => onMessage(ev.topic || ev.type, ev.data || ev.payload, ev)); } catch (e) {}
|
|
919
|
+
}
|
|
920
|
+
} catch (e) {}
|
|
921
|
+
|
|
922
|
+
// also attach one-time status: if the client supports events differently, let app listen to 'should-reconnect'
|
|
923
|
+
// attach a cleanup handle
|
|
924
|
+
const cleanup = () => {
|
|
925
|
+
stopPing();
|
|
926
|
+
try {
|
|
927
|
+
if (typeof realtimeClient.off === 'function') {
|
|
928
|
+
realtimeClient.off('connect', onConnect);
|
|
929
|
+
realtimeClient.off('reconnect', onReconnect);
|
|
930
|
+
realtimeClient.off('close', onClose);
|
|
931
|
+
realtimeClient.off('offline', onOffline);
|
|
932
|
+
realtimeClient.off('error', onError);
|
|
933
|
+
realtimeClient.off('message', onMessage);
|
|
934
|
+
}
|
|
935
|
+
} catch (e) {}
|
|
936
|
+
this.emit('realtime-detach');
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// expose cleanup on client for convenience
|
|
940
|
+
try { realtimeClient._authStateCleanup = cleanup; } catch (e) {}
|
|
941
|
+
|
|
942
|
+
// return helper object to caller
|
|
943
|
+
return {
|
|
944
|
+
pingIntervalMs,
|
|
945
|
+
stop: cleanup,
|
|
946
|
+
flushOutbox: async (max) => { return await this.flushOutbox(realtimeClient, max); },
|
|
947
|
+
checkSubscriptions: async () => { return await this.checkSubscriptions(realtimeClient); },
|
|
948
|
+
warmRestore: () => { return this.warmRestoreClient(realtimeClient); }
|
|
949
|
+
};
|
|
950
|
+
} catch (e) {
|
|
951
|
+
// emit attach failure
|
|
952
|
+
this.emit('attach-failed', { error: e.message });
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
385
957
|
// --- Getters ---
|
|
386
958
|
getCreds() { return this.data.creds; }
|
|
387
959
|
getDevice() { return this.data.device; }
|
|
@@ -398,6 +970,13 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
398
970
|
getLoginHistory() { return this.data.loginHistory || []; }
|
|
399
971
|
getUsageStats() { return this.data.usageStats || {}; }
|
|
400
972
|
getAccountsIndex() { return this.data.accountsIndex || {}; }
|
|
973
|
+
getMqttHealth() { return this.data.mqttHealth || {}; }
|
|
974
|
+
getMqttReconnect() { return this.data.mqttReconnect || {}; }
|
|
975
|
+
getMqttMessageCache() { return this.data.mqttMessageCache || {}; }
|
|
976
|
+
getMqttOutbox() { return this.data.mqttOutbox || []; }
|
|
977
|
+
getMqttSubscriptionHealth() { return this.data.mqttSubscriptionHealth || {}; }
|
|
978
|
+
getMqttTraffic() { return this.data.mqttTraffic || {}; }
|
|
979
|
+
getMqttRisk() { return this.data.mqttRisk || {}; }
|
|
401
980
|
|
|
402
981
|
hasValidSession() {
|
|
403
982
|
return !!(this.data.creds && this.data.cookies && this.data.creds.authorization);
|
|
@@ -517,7 +1096,9 @@ class MultiFileAuthState extends EventEmitter {
|
|
|
517
1096
|
lastLogin: (this.data.loginHistory && this.data.loginHistory[0]?.date) || null,
|
|
518
1097
|
usageStats: this.getUsageStats(),
|
|
519
1098
|
accountStatus: this.data.creds?.accountStatus || 'ok',
|
|
520
|
-
deviceLocked: !!this.data.device?.locked
|
|
1099
|
+
deviceLocked: !!this.data.device?.locked,
|
|
1100
|
+
mqttHealth: this.getMqttHealth(),
|
|
1101
|
+
mqttRisk: this.getMqttRisk()
|
|
521
1102
|
};
|
|
522
1103
|
}
|
|
523
1104
|
}
|
|
@@ -947,7 +1528,15 @@ async function useMultiFileAuthState(folder) {
|
|
|
947
1528
|
|
|
948
1529
|
const getMqttConnectOptions = () => {
|
|
949
1530
|
if (!authState.hasMqttSession()) {
|
|
950
|
-
return
|
|
1531
|
+
// still return a warm default to help warm-restore
|
|
1532
|
+
const defaultTopics = ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'];
|
|
1533
|
+
return {
|
|
1534
|
+
graphQlSubs: defaultTopics,
|
|
1535
|
+
skywalkerSubs: ['presence_subscribe', 'typing_subscribe'],
|
|
1536
|
+
irisData: { seq_id: authState.getSeqIds()?.seq_id || 0, snapshot_at_ms: authState.getSeqIds()?.snapshot_at_ms || Date.now() },
|
|
1537
|
+
mqttAuthToken: authState.getMqttAuth()?.jwt || null,
|
|
1538
|
+
mqttAuthExpiresAt: authState.getMqttAuth()?.expiresAt || null
|
|
1539
|
+
};
|
|
951
1540
|
}
|
|
952
1541
|
|
|
953
1542
|
const subs = authState.getSubscriptions() || {};
|
|
@@ -960,7 +1549,7 @@ async function useMultiFileAuthState(folder) {
|
|
|
960
1549
|
irisData: seqIds.seq_id ? {
|
|
961
1550
|
seq_id: seqIds.seq_id,
|
|
962
1551
|
snapshot_at_ms: seqIds.snapshot_at_ms
|
|
963
|
-
} :
|
|
1552
|
+
} : { seq_id: 0, snapshot_at_ms: Date.now() },
|
|
964
1553
|
mqttAuthToken: mqttAuth?.jwt || null,
|
|
965
1554
|
mqttAuthExpiresAt: mqttAuth?.expiresAt || null
|
|
966
1555
|
};
|
|
@@ -1101,6 +1690,23 @@ async function useMultiFileAuthState(folder) {
|
|
|
1101
1690
|
return authState.getHealth();
|
|
1102
1691
|
};
|
|
1103
1692
|
|
|
1693
|
+
// --- MQTT helpers exposing internal features ---
|
|
1694
|
+
const attachRealtimeClient = (realtimeClient, opts = {}) => {
|
|
1695
|
+
return authState.attachRealtimeClient(realtimeClient, opts);
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
const enqueueOutbox = (item) => authState.enqueueOutbox(item);
|
|
1699
|
+
const flushOutbox = (client, max) => authState.flushOutbox(client, max);
|
|
1700
|
+
const checkSubscriptions = (client) => authState.checkSubscriptions(client);
|
|
1701
|
+
const computeRisk = () => authState._computeRiskScore();
|
|
1702
|
+
const warmRestore = (client) => authState.warmRestoreClient(client);
|
|
1703
|
+
const getMqttHealth = () => authState.getMqttHealth();
|
|
1704
|
+
const getMqttTraffic = () => authState.getMqttTraffic();
|
|
1705
|
+
const getMqttRisk = () => authState.getMqttRisk();
|
|
1706
|
+
const isDuplicateMessage = (id) => authState.isDuplicateMessage(id);
|
|
1707
|
+
const markPing = () => authState.markPing();
|
|
1708
|
+
const markPong = () => authState.markPong();
|
|
1709
|
+
|
|
1104
1710
|
// --- Misc helpers: loginHistory & usageStats getters ---
|
|
1105
1711
|
const getLoginHistory = () => authState.getLoginHistory();
|
|
1106
1712
|
const getUsageStats = () => authState.getUsageStats();
|
|
@@ -1151,7 +1757,21 @@ async function useMultiFileAuthState(folder) {
|
|
|
1151
1757
|
verifyDeviceFingerprint,
|
|
1152
1758
|
registerAccount,
|
|
1153
1759
|
listAccounts,
|
|
1154
|
-
getHealth
|
|
1760
|
+
getHealth,
|
|
1761
|
+
|
|
1762
|
+
// mqtt helpers
|
|
1763
|
+
attachRealtimeClient,
|
|
1764
|
+
enqueueOutbox,
|
|
1765
|
+
flushOutbox,
|
|
1766
|
+
checkSubscriptions,
|
|
1767
|
+
computeRisk,
|
|
1768
|
+
warmRestore,
|
|
1769
|
+
getMqttHealth,
|
|
1770
|
+
getMqttTraffic,
|
|
1771
|
+
getMqttRisk,
|
|
1772
|
+
isDuplicateMessage,
|
|
1773
|
+
markPing,
|
|
1774
|
+
markPong
|
|
1155
1775
|
};
|
|
1156
1776
|
}
|
|
1157
1777
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodejs-insta-private-api-mqtt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.10",
|
|
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": {
|