nodejs-insta-private-api-mqt 1.3.70
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/LICENSE +21 -0
- package/README.md +3677 -0
- package/dist/constants/constants.js +342 -0
- package/dist/constants/index.js +58 -0
- package/dist/core/client.js +419 -0
- package/dist/core/nav-chain.js +282 -0
- package/dist/core/repository.js +7 -0
- package/dist/core/request.js +390 -0
- package/dist/core/state.js +1473 -0
- package/dist/core/utils.js +786 -0
- package/dist/downloadMedia.js +381 -0
- package/dist/errors/index.d.ts +16 -0
- package/dist/errors/index.js +38 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/extend.js +167 -0
- package/dist/fbns/fbns.client.d.ts +32 -0
- package/dist/fbns/fbns.client.events.d.ts +41 -0
- package/dist/fbns/fbns.client.events.js +3 -0
- package/dist/fbns/fbns.client.events.js.map +1 -0
- package/dist/fbns/fbns.client.js +252 -0
- package/dist/fbns/fbns.client.js.map +1 -0
- package/dist/fbns/fbns.device-auth.d.ts +17 -0
- package/dist/fbns/fbns.device-auth.js +54 -0
- package/dist/fbns/fbns.device-auth.js.map +1 -0
- package/dist/fbns/fbns.types.d.ts +83 -0
- package/dist/fbns/fbns.types.js +3 -0
- package/dist/fbns/fbns.types.js.map +1 -0
- package/dist/fbns/fbns.utilities.d.ts +2 -0
- package/dist/fbns/fbns.utilities.js +79 -0
- package/dist/fbns/fbns.utilities.js.map +1 -0
- package/dist/fbns/index.d.ts +4 -0
- package/dist/fbns/index.js +21 -0
- package/dist/fbns/index.js.map +1 -0
- package/dist/index.js +139 -0
- package/dist/mqtt-shim.d.ts +96 -0
- package/dist/mqtt-shim.js +15 -0
- package/dist/mqttot/index.d.ts +4 -0
- package/dist/mqttot/index.js +21 -0
- package/dist/mqttot/index.js.map +1 -0
- package/dist/mqttot/mqttot.client.d.ts +39 -0
- package/dist/mqttot/mqttot.client.js +318 -0
- package/dist/mqttot/mqttot.client.js.map +1 -0
- package/dist/mqttot/mqttot.connect.request.packet.d.ts +7 -0
- package/dist/mqttot/mqttot.connect.request.packet.js +9 -0
- package/dist/mqttot/mqttot.connect.request.packet.js.map +1 -0
- package/dist/mqttot/mqttot.connect.response.packet.d.ts +7 -0
- package/dist/mqttot/mqttot.connect.response.packet.js +24 -0
- package/dist/mqttot/mqttot.connect.response.packet.js.map +1 -0
- package/dist/mqttot/mqttot.connection.d.ts +57 -0
- package/dist/mqttot/mqttot.connection.js +79 -0
- package/dist/mqttot/mqttot.connection.js.map +1 -0
- package/dist/package.json +59 -0
- package/dist/realtime/commands/commands.d.ts +15 -0
- package/dist/realtime/commands/commands.js +71 -0
- package/dist/realtime/commands/commands.js.map +1 -0
- package/dist/realtime/commands/direct.commands.d.ts +75 -0
- package/dist/realtime/commands/direct.commands.js +417 -0
- package/dist/realtime/commands/direct.commands.js.map +1 -0
- package/dist/realtime/commands/enhanced.direct.commands.js +1731 -0
- package/dist/realtime/commands/enhanced.direct.commands.js.bak +967 -0
- package/dist/realtime/commands/index.d.ts +2 -0
- package/dist/realtime/commands/index.js +20 -0
- package/dist/realtime/commands/index.js.map +1 -0
- package/dist/realtime/delta-sync.manager.js +293 -0
- package/dist/realtime/features/dm-sender.js +88 -0
- package/dist/realtime/features/error-handler.js +185 -0
- package/dist/realtime/features/gap-handler.js +61 -0
- package/dist/realtime/features/persistent-logger.js +186 -0
- package/dist/realtime/features/presence.manager.js +66 -0
- package/dist/realtime/features/session-health-monitor.js +345 -0
- package/dist/realtime/index.js +30 -0
- package/dist/realtime/messages/app-presence.event.d.ts +9 -0
- package/dist/realtime/messages/app-presence.event.js +3 -0
- package/dist/realtime/messages/app-presence.event.js.map +1 -0
- package/dist/realtime/messages/index.d.ts +3 -0
- package/dist/realtime/messages/index.js +20 -0
- package/dist/realtime/messages/index.js.map +1 -0
- package/dist/realtime/messages/message-sync.message.d.ts +222 -0
- package/dist/realtime/messages/message-sync.message.js +43 -0
- package/dist/realtime/messages/message-sync.message.js.map +1 -0
- package/dist/realtime/messages/realtime-sub.direct.data.d.ts +11 -0
- package/dist/realtime/messages/realtime-sub.direct.data.js +3 -0
- package/dist/realtime/messages/realtime-sub.direct.data.js.map +1 -0
- package/dist/realtime/messages/thread-update.message.d.ts +68 -0
- package/dist/realtime/messages/thread-update.message.js +3 -0
- package/dist/realtime/messages/thread-update.message.js.map +1 -0
- package/dist/realtime/mixins/index.d.ts +3 -0
- package/dist/realtime/mixins/index.js +20 -0
- package/dist/realtime/mixins/index.js.map +1 -0
- package/dist/realtime/mixins/message-sync.mixin.d.ts +8 -0
- package/dist/realtime/mixins/message-sync.mixin.js +596 -0
- package/dist/realtime/mixins/message-sync.mixin.js.map +1 -0
- package/dist/realtime/mixins/mixin.d.ts +19 -0
- package/dist/realtime/mixins/mixin.js +41 -0
- package/dist/realtime/mixins/mixin.js.map +1 -0
- package/dist/realtime/mixins/presence-typing.mixin.js +33 -0
- package/dist/realtime/mixins/realtime-sub.mixin.d.ts +8 -0
- package/dist/realtime/mixins/realtime-sub.mixin.js +181 -0
- package/dist/realtime/mixins/realtime-sub.mixin.js.map +1 -0
- package/dist/realtime/parsers/graphql-parser.js +43 -0
- package/dist/realtime/parsers/graphql.parser.d.ts +15 -0
- package/dist/realtime/parsers/graphql.parser.js +22 -0
- package/dist/realtime/parsers/graphql.parser.js.map +1 -0
- package/dist/realtime/parsers/index.d.ts +6 -0
- package/dist/realtime/parsers/index.js +23 -0
- package/dist/realtime/parsers/index.js.map +1 -0
- package/dist/realtime/parsers/iris-parser.js +43 -0
- package/dist/realtime/parsers/iris.parser.d.ts +17 -0
- package/dist/realtime/parsers/iris.parser.js +10 -0
- package/dist/realtime/parsers/iris.parser.js.map +1 -0
- package/dist/realtime/parsers/json-parser.js +43 -0
- package/dist/realtime/parsers/json.parser.d.ts +6 -0
- package/dist/realtime/parsers/json.parser.js +10 -0
- package/dist/realtime/parsers/json.parser.js.map +1 -0
- package/dist/realtime/parsers/parser.d.ts +9 -0
- package/dist/realtime/parsers/parser.js +3 -0
- package/dist/realtime/parsers/parser.js.map +1 -0
- package/dist/realtime/parsers/region-hint-parser.js +43 -0
- package/dist/realtime/parsers/region-hint.parser.d.ts +12 -0
- package/dist/realtime/parsers/region-hint.parser.js +15 -0
- package/dist/realtime/parsers/region-hint.parser.js.map +1 -0
- package/dist/realtime/parsers/skywalker-parser.js +43 -0
- package/dist/realtime/parsers/skywalker.parser.d.ts +12 -0
- package/dist/realtime/parsers/skywalker.parser.js +15 -0
- package/dist/realtime/parsers/skywalker.parser.js.map +1 -0
- package/dist/realtime/parsers-advanced.js +158 -0
- package/dist/realtime/proto/common.proto +38 -0
- package/dist/realtime/proto/direct.proto +65 -0
- package/dist/realtime/proto/ig-messages.proto +83 -0
- package/dist/realtime/proto/iris.proto +188 -0
- package/dist/realtime/proto-parser.js +195 -0
- package/dist/realtime/protocols/iris.handshake.js +74 -0
- package/dist/realtime/protocols/proto-definitions.js +80 -0
- package/dist/realtime/protocols/skywalker.protocol.js +91 -0
- package/dist/realtime/realtime.client.events.js +3 -0
- package/dist/realtime/realtime.client.js +1915 -0
- package/dist/realtime/realtime.service.js +462 -0
- package/dist/realtime/reconnect.manager.js +88 -0
- package/dist/realtime/session.manager.js +121 -0
- package/dist/realtime/subscriptions/graphql.subscription.d.ts +47 -0
- package/dist/realtime/subscriptions/graphql.subscription.js +99 -0
- package/dist/realtime/subscriptions/graphql.subscription.js.map +1 -0
- package/dist/realtime/subscriptions/index.d.ts +2 -0
- package/dist/realtime/subscriptions/index.js +19 -0
- package/dist/realtime/subscriptions/index.js.map +1 -0
- package/dist/realtime/subscriptions/skywalker.subscription.d.ts +4 -0
- package/dist/realtime/subscriptions/skywalker.subscription.js +13 -0
- package/dist/realtime/subscriptions/skywalker.subscription.js.map +1 -0
- package/dist/realtime/topic-map.js +71 -0
- package/dist/realtime/topic.js +80 -0
- package/dist/repositories/account.repository.js +575 -0
- package/dist/repositories/bloks.repository.js +70 -0
- package/dist/repositories/captcha.repository.js +44 -0
- package/dist/repositories/challenge.repository.js +120 -0
- package/dist/repositories/clip.repository.js +165 -0
- package/dist/repositories/close-friends.repository.js +46 -0
- package/dist/repositories/collection.repository.js +68 -0
- package/dist/repositories/direct-thread.repository.js +446 -0
- package/dist/repositories/direct.repository.js +232 -0
- package/dist/repositories/explore.repository.js +70 -0
- package/dist/repositories/fbsearch.repository.js +140 -0
- package/dist/repositories/feed.repository.js +245 -0
- package/dist/repositories/friendship.repository.js +296 -0
- package/dist/repositories/fundraiser.repository.js +49 -0
- package/dist/repositories/hashtag.repository.js +99 -0
- package/dist/repositories/highlights.repository.js +121 -0
- package/dist/repositories/insights.repository.js +82 -0
- package/dist/repositories/location.repository.js +84 -0
- package/dist/repositories/media.repository.js +395 -0
- package/dist/repositories/multiple-accounts.repository.js +41 -0
- package/dist/repositories/news.repository.js +35 -0
- package/dist/repositories/note.repository.js +57 -0
- package/dist/repositories/notification.repository.js +79 -0
- package/dist/repositories/share.repository.js +35 -0
- package/dist/repositories/signup.repository.js +218 -0
- package/dist/repositories/story.repository.js +290 -0
- package/dist/repositories/timeline.repository.js +60 -0
- package/dist/repositories/totp.repository.js +139 -0
- package/dist/repositories/track.repository.js +53 -0
- package/dist/repositories/upload.repository.js +204 -0
- package/dist/repositories/user.repository.js +360 -0
- package/dist/sendmedia/index.js +27 -0
- package/dist/sendmedia/sendFile.js +72 -0
- package/dist/sendmedia/sendPhoto.js +142 -0
- package/dist/sendmedia/sendRavenPhoto.js +153 -0
- package/dist/sendmedia/sendRavenVideo.js +158 -0
- package/dist/sendmedia/uploadPhoto.js +107 -0
- package/dist/sendmedia/uploadfFile.js +130 -0
- package/dist/services/live.service.js +139 -0
- package/dist/services/search.service.js +115 -0
- package/dist/shared/index.js +96 -0
- package/dist/shared/shared.js +86 -0
- package/dist/thrift/index.d.ts +3 -0
- package/dist/thrift/index.js +20 -0
- package/dist/thrift/index.js.map +1 -0
- package/dist/thrift/thrift.d.ts +59 -0
- package/dist/thrift/thrift.js +101 -0
- package/dist/thrift/thrift.js.map +1 -0
- package/dist/thrift/thrift.reading.d.ts +41 -0
- package/dist/thrift/thrift.reading.js +327 -0
- package/dist/thrift/thrift.reading.js.map +1 -0
- package/dist/thrift/thrift.writing.d.ts +44 -0
- package/dist/thrift/thrift.writing.js +342 -0
- package/dist/thrift/thrift.writing.js.map +1 -0
- package/dist/types/index.js +285 -0
- package/dist/useMultiFileAuthState.js +1768 -0
- package/dist/utils/helper-1.js +1 -0
- package/dist/utils/helper-10.js +1 -0
- package/dist/utils/helper-11.js +1 -0
- package/dist/utils/helper-12.js +1 -0
- package/dist/utils/helper-13.js +1 -0
- package/dist/utils/helper-14.js +1 -0
- package/dist/utils/helper-15.js +1 -0
- package/dist/utils/helper-16.js +1 -0
- package/dist/utils/helper-17.js +1 -0
- package/dist/utils/helper-18.js +1 -0
- package/dist/utils/helper-19.js +1 -0
- package/dist/utils/helper-2.js +1 -0
- package/dist/utils/helper-20.js +1 -0
- package/dist/utils/helper-21.js +1 -0
- package/dist/utils/helper-22.js +1 -0
- package/dist/utils/helper-23.js +1 -0
- package/dist/utils/helper-24.js +1 -0
- package/dist/utils/helper-25.js +1 -0
- package/dist/utils/helper-26.js +1 -0
- package/dist/utils/helper-27.js +1 -0
- package/dist/utils/helper-28.js +1 -0
- package/dist/utils/helper-29.js +1 -0
- package/dist/utils/helper-3.js +1 -0
- package/dist/utils/helper-30.js +1 -0
- package/dist/utils/helper-4.js +1 -0
- package/dist/utils/helper-5.js +1 -0
- package/dist/utils/helper-6.js +1 -0
- package/dist/utils/helper-7.js +1 -0
- package/dist/utils/helper-8.js +1 -0
- package/dist/utils/helper-9.js +1 -0
- package/dist/utils/index.js +280 -0
- package/dist/utils/insta-mqtt-helper.js +128 -0
- package/examples/listen-to-messages.js +86 -0
- package/package.json +82 -0
|
@@ -0,0 +1,1915 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
RealtimeClient
|
|
5
|
+
--------------
|
|
6
|
+
This file implements the RealtimeClient which manages:
|
|
7
|
+
- constructing the FBNS/MQTT connection payload (Thrift)
|
|
8
|
+
- starting the MQTT client (MQTToTClient)
|
|
9
|
+
- attaching lifecycle handlers (connect/close/error)
|
|
10
|
+
- keepalives, message-sync refresh and traffic watchdogs
|
|
11
|
+
- wiring MQTT messages into higher-level events (message, iris, receive)
|
|
12
|
+
|
|
13
|
+
The implementation keeps original structure while applying compatibility tweaks:
|
|
14
|
+
- persistent clientMqttSessionId and client_context handling
|
|
15
|
+
- robust sessionid / mqttJwt / deviceSecret fallbacks
|
|
16
|
+
- slightly adjusted appSpecificInfo to match mobile client fields
|
|
17
|
+
- uses mqttot.MQTToTConnection to build a thrift connection payload
|
|
18
|
+
- MQTT URL remains 'edge-mqtt.facebook.com' (Instagram/Meta edge host)
|
|
19
|
+
|
|
20
|
+
Additional improvements added to keep bots alive across long idle periods:
|
|
21
|
+
- persistent mqtt-session load/save on disk at startup & after connect
|
|
22
|
+
- heartbeat improvements: MQTT-level ping (if available), numeric-topic pings and foreground keepalive
|
|
23
|
+
- credential refresh hook (if auth helper exposes refreshMqttAuth or similar)
|
|
24
|
+
- robust reconnect loop with exponential backoff and credential refresh attempts
|
|
25
|
+
- enhanced logging and defensive guards so long-idle reconnections work better
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Keep module imports similar to your original code
|
|
29
|
+
const constants_1 = require("../constants");
|
|
30
|
+
const commands_1 = require("./commands");
|
|
31
|
+
const shared_1 = require("../shared");
|
|
32
|
+
const mqttot_1 = require("../mqttot");
|
|
33
|
+
// Use mqtts for errors / IllegalStateError compatibility
|
|
34
|
+
const mqtts_1 = require("mqtts");
|
|
35
|
+
const errors_1 = require("../errors");
|
|
36
|
+
const eventemitter3_1 = require("eventemitter3");
|
|
37
|
+
const mixins_1 = require("./mixins");
|
|
38
|
+
const iris_handshake_1 = require("./protocols/iris.handshake");
|
|
39
|
+
const skywalker_protocol_1 = require("./protocols/skywalker.protocol");
|
|
40
|
+
const presence_manager_1 = require("./features/presence.manager");
|
|
41
|
+
const dm_sender_1 = require("./features/dm-sender");
|
|
42
|
+
const error_handler_1 = require("./features/error-handler");
|
|
43
|
+
const gap_handler_1 = require("./features/gap-handler");
|
|
44
|
+
const enhanced_direct_commands_1 = require("./commands/enhanced.direct.commands");
|
|
45
|
+
const presence_typing_mixin_1 = require("./mixins/presence-typing.mixin");
|
|
46
|
+
const { SessionHealthMonitor } = require("./features/session-health-monitor");
|
|
47
|
+
const { PersistentLogger } = require("./features/persistent-logger");
|
|
48
|
+
const fs = require('fs');
|
|
49
|
+
const path = require('path');
|
|
50
|
+
|
|
51
|
+
// Use INSTAGRAM_VERSION exported from mqttot so it is applied automatically where available
|
|
52
|
+
const INSTAGRAM_VERSION = mqttot_1.INSTAGRAM_VERSION;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* RealtimeClient
|
|
56
|
+
* - Extends EventEmitter to emit high-level events for consumers.
|
|
57
|
+
* - Responsible for constructing the thrift payload, creating the MQTToTClient and wiring its events.
|
|
58
|
+
*/
|
|
59
|
+
class RealtimeClient extends eventemitter3_1.EventEmitter {
|
|
60
|
+
// getter to expose the underlying mqtt client instance
|
|
61
|
+
get mqtt() {
|
|
62
|
+
return this._mqtt;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* constructor(ig, mixins)
|
|
67
|
+
* - ig: instance of instagram client (used for building payloads & fetching inbox)
|
|
68
|
+
* - mixins: collection of mixins applied to this realtime client (message-sync, realtime-sub, presence/typing)
|
|
69
|
+
*/
|
|
70
|
+
constructor(ig, mixins = [new mixins_1.MessageSyncMixin(), new mixins_1.RealtimeSubMixin(), new presence_typing_mixin_1.PresenceTypingMixin()]) {
|
|
71
|
+
super();
|
|
72
|
+
// debug helpers
|
|
73
|
+
this.realtimeDebug = (0, shared_1.debugChannel)('realtime');
|
|
74
|
+
this.messageDebug = this.realtimeDebug.extend('message');
|
|
75
|
+
|
|
76
|
+
// enhanced direct commands debug (exposed for compatibility with the enhanced commands)
|
|
77
|
+
this.enhancedDebug = this.realtimeDebug.extend('enhanced-commands');
|
|
78
|
+
|
|
79
|
+
// safeDisconnect flag used when user intentionally disconnects
|
|
80
|
+
this.safeDisconnect = false;
|
|
81
|
+
|
|
82
|
+
// convenience wrappers to emit events
|
|
83
|
+
this.emitError = (e) => this.emit('error', e);
|
|
84
|
+
this.emitWarning = (e) => this.emit('warning', e);
|
|
85
|
+
|
|
86
|
+
// persistent references
|
|
87
|
+
this.ig = ig;
|
|
88
|
+
this.threads = new Map();
|
|
89
|
+
|
|
90
|
+
// instantiate features / protocols / mixins
|
|
91
|
+
this.irisHandshake = new iris_handshake_1.IrisHandshake(this);
|
|
92
|
+
this.skywalkerProtocol = new skywalker_protocol_1.SkywalkerProtocol(this);
|
|
93
|
+
this.presenceManager = new presence_manager_1.PresenceManager(this);
|
|
94
|
+
this.dmSender = new dm_sender_1.DMSender(this);
|
|
95
|
+
this.errorHandler = new error_handler_1.ErrorHandler(this);
|
|
96
|
+
this.gapHandler = new gap_handler_1.GapHandler(this);
|
|
97
|
+
this.directCommands = new enhanced_direct_commands_1.EnhancedDirectCommands(this);
|
|
98
|
+
|
|
99
|
+
// Add transaction and ordering capability
|
|
100
|
+
try {
|
|
101
|
+
const { addTransactionCapability } = require('../utils/insta-mqtt-helper');
|
|
102
|
+
addTransactionCapability(this);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
this.realtimeDebug('Failed to add transaction capability:', e.message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.realtimeDebug(`Applying mixins: ${mixins.map(m => m.name).join(', ')}`);
|
|
108
|
+
// apply mixins to this instance (keeps modular features separated)
|
|
109
|
+
(0, mixins_1.applyMixins)(mixins, this, this.ig);
|
|
110
|
+
|
|
111
|
+
// Default subscriptions (force-enable if saved ones are empty)
|
|
112
|
+
this.defaultGraphQlSubs = ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'];
|
|
113
|
+
this.defaultSkywalkerSubs = ['presence_subscribe', 'typing_subscribe'];
|
|
114
|
+
|
|
115
|
+
// internal flags & timers
|
|
116
|
+
this._attachedAuthState = null;
|
|
117
|
+
this._messageSyncAttached = false;
|
|
118
|
+
this._reconnectInProgress = false;
|
|
119
|
+
this._lastMessageAt = Date.now();
|
|
120
|
+
this._foregroundTimer = null;
|
|
121
|
+
this._syncTimer = null;
|
|
122
|
+
this._trafficWatchdog = null;
|
|
123
|
+
this._heartbeatTimer = null; // new: heartbeat timer
|
|
124
|
+
this._reconnectDebounceMs = 500;
|
|
125
|
+
this._reconnectTimeoutId = null;
|
|
126
|
+
|
|
127
|
+
// MQTT session tracking & persistence
|
|
128
|
+
this._mqttSessionId = null; // actual server-provided mqtt session id (string)
|
|
129
|
+
this._mqttSessionPersistIntervalId = null; // periodic persist interval handler
|
|
130
|
+
|
|
131
|
+
// Active keepalive/query timer (added)
|
|
132
|
+
this._activeKeepaliveTimer = null;
|
|
133
|
+
|
|
134
|
+
// Timer for credential refresh
|
|
135
|
+
this._mqttAuthRefreshTimer = null;
|
|
136
|
+
|
|
137
|
+
// Session health monitor (initialized on connect with enableHealthMonitor option)
|
|
138
|
+
this.healthMonitor = null;
|
|
139
|
+
// Persistent logger (initialized on connect with enablePersistentLogger option)
|
|
140
|
+
this.persistentLogger = null;
|
|
141
|
+
|
|
142
|
+
// Persisted identity items that must survive reconnects:
|
|
143
|
+
// - clientMqttSessionId must NOT be re-generated each connect
|
|
144
|
+
// - _clientContext for EnhancedDirectCommands (for message identity)
|
|
145
|
+
if (!this._clientMqttSessionId) {
|
|
146
|
+
// generate once for lifetime of this RealtimeClient instance
|
|
147
|
+
try {
|
|
148
|
+
this._clientMqttSessionId = (BigInt(Date.now()) & BigInt(0xffffffff));
|
|
149
|
+
} catch (e) {
|
|
150
|
+
this._clientMqttSessionId = BigInt(0);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!this._clientContext) {
|
|
154
|
+
// persistent client_context for message sends — single per session
|
|
155
|
+
try {
|
|
156
|
+
this._clientContext = require('uuid').v4();
|
|
157
|
+
} catch (e) {
|
|
158
|
+
this._clientContext = `cc_${Date.now()}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//
|
|
163
|
+
// AUTH/PERSISTENCE FOLDER RESOLUTION
|
|
164
|
+
// - unify folder selection so we don't miss persisted mqtt-session.json saved under
|
|
165
|
+
// different folder names in various versions.
|
|
166
|
+
//
|
|
167
|
+
try {
|
|
168
|
+
const candidates = ['./authinfo_instagram', './auth_info_ig', './auth_info_ig'];
|
|
169
|
+
let chosen = null;
|
|
170
|
+
for (const c of candidates) {
|
|
171
|
+
try {
|
|
172
|
+
if (fs.existsSync(c)) {
|
|
173
|
+
chosen = c;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {}
|
|
177
|
+
}
|
|
178
|
+
if (!chosen) chosen = './authinfo_instagram';
|
|
179
|
+
this._authFolder = chosen;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
this._authFolder = './authinfo_instagram';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// attempt to load persisted mqtt-session information (if available) to keep continuity across restarts
|
|
185
|
+
try {
|
|
186
|
+
this._loadPersistedMqttSession();
|
|
187
|
+
} catch (e) {
|
|
188
|
+
this.realtimeDebug('[MQTT] loadPersistedMqttSession failed:', e?.message || e);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// auto-connect block preserved (no change)
|
|
192
|
+
const { useMultiFileAuthState } = require('../useMultiFileAuthState');
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* waitForMqttCredentials(auth, timeoutMs, pollMs)
|
|
196
|
+
* - Poll the auth state for device / mqtt credentials before proceeding with auto-connect.
|
|
197
|
+
* - This ensures the saved auth state contains the fields required to build the MQTT payload.
|
|
198
|
+
*/
|
|
199
|
+
const waitForMqttCredentials = async (auth, timeoutMs = 15000, pollMs = 250) => {
|
|
200
|
+
const start = Date.now();
|
|
201
|
+
const hasCreds = () => {
|
|
202
|
+
try {
|
|
203
|
+
const d = (auth && typeof auth.getData === 'function') ? auth.getData() : (auth && auth.data ? auth.data : null);
|
|
204
|
+
if (!d) return false;
|
|
205
|
+
if (d.device && (d.device.deviceSecret || d.device.secret)) return true;
|
|
206
|
+
if (d.mqttAuth && (d.mqttAuth.jwt || d.mqttAuth.deviceSecret)) return true;
|
|
207
|
+
if (d.creds && (d.creds.sessionId || d.creds.csrfToken || d.creds.authorization)) return true;
|
|
208
|
+
return false;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
while (Date.now() - start < timeoutMs) {
|
|
214
|
+
if (hasCreds()) return true;
|
|
215
|
+
await new Promise(r => setTimeout(r, pollMs));
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Auto-start if saved creds exist on disk (non-blocking)
|
|
221
|
+
if (fs.existsSync(path.join(this._authFolder, 'creds.json'))) {
|
|
222
|
+
setTimeout(async () => {
|
|
223
|
+
try {
|
|
224
|
+
if (this._mqttConnected || this._connectInProgress) {
|
|
225
|
+
console.log('[REALTIME] Auto-start skipped — MQTT already connected or connect in progress.');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const auth = await useMultiFileAuthState(this._authFolder);
|
|
229
|
+
this._attachedAuthState = auth;
|
|
230
|
+
if (auth.hasSession && auth.hasSession()) {
|
|
231
|
+
if (this._mqttConnected || this._connectInProgress) {
|
|
232
|
+
console.log('[REALTIME] Auto-start skipped (after auth load) — MQTT already connected or connect in progress.');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
console.log('[REALTIME] Auto-start candidate session detected — loading creds...');
|
|
236
|
+
try {
|
|
237
|
+
await auth.loadCreds(this.ig);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.warn('[REALTIME] loadCreds warning:', e?.message || e);
|
|
240
|
+
}
|
|
241
|
+
const ready = await waitForMqttCredentials(auth, 20000, 300);
|
|
242
|
+
if (!ready) {
|
|
243
|
+
console.warn('[REALTIME] MQTT/device credentials not found within timeout — auto-connect aborted (will still allow manual connect).');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (this._mqttConnected || this._connectInProgress) {
|
|
247
|
+
console.log('[REALTIME] Auto-start skipped (after wait) — MQTT already connected or connect in progress.');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
console.log('[REALTIME] Device/MQTT credentials present — attempting connectFromSavedSession...');
|
|
251
|
+
try {
|
|
252
|
+
await this.connectFromSavedSession(auth);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
console.error('[REALTIME] Constructor auto-connect failed:', e?.message || e);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
console.error('[REALTIME] Constructor auto-start exception:', e?.message || e);
|
|
259
|
+
}
|
|
260
|
+
}, 100);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* _loadPersistedMqttSession()
|
|
266
|
+
* - Attempt to read <authFolder>/mqtt-session.json on startup and restore
|
|
267
|
+
* client-generated ids so the client can reuse previous session identifiers.
|
|
268
|
+
*/
|
|
269
|
+
_loadPersistedMqttSession() {
|
|
270
|
+
try {
|
|
271
|
+
const folder = this._authFolder || './authinfo_instagram';
|
|
272
|
+
const file = path.join(folder, 'mqtt-session.json');
|
|
273
|
+
if (!fs.existsSync(file)) return false;
|
|
274
|
+
const raw = fs.readFileSync(file, { encoding: 'utf8' });
|
|
275
|
+
const data = JSON.parse(raw);
|
|
276
|
+
if (data && data.mqttSessionId) {
|
|
277
|
+
// restore clientMqttSessionId (if possible)
|
|
278
|
+
try {
|
|
279
|
+
// prefer to set local client id if server one is missing
|
|
280
|
+
this._mqttSessionId = data.sessionId || null;
|
|
281
|
+
if (!this._clientMqttSessionId && data.mqttSessionId) {
|
|
282
|
+
// preserve numeric/persistent value as BigInt if possible
|
|
283
|
+
try {
|
|
284
|
+
this._clientMqttSessionId = BigInt(data.mqttSessionId);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
// leave as-is if not parseable
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.realtimeDebug('[MQTT] Loaded persisted mqtt session data from disk.');
|
|
290
|
+
return true;
|
|
291
|
+
} catch (e) {}
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {
|
|
294
|
+
this.realtimeDebug('[MQTT] _loadPersistedMqttSession error', e?.message || e);
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* startRealTimeListener(options)
|
|
301
|
+
* - Convenience method to fetch initial inbox (IRIS) and connect with those subscriptions.
|
|
302
|
+
*/
|
|
303
|
+
async startRealTimeListener(options = {}) {
|
|
304
|
+
this._connectInProgress = true;
|
|
305
|
+
try {
|
|
306
|
+
console.log('[REALTIME] Starting Real-Time Listener...');
|
|
307
|
+
console.log('[REALTIME] Fetching inbox (IRIS data)...');
|
|
308
|
+
const inboxData = await this.ig.direct.getInbox();
|
|
309
|
+
console.log('[REALTIME] Connecting to MQTT with IRIS subscription...');
|
|
310
|
+
await this.connect({
|
|
311
|
+
graphQlSubs: [
|
|
312
|
+
'ig_sub_direct',
|
|
313
|
+
'ig_sub_direct_v2_message_create',
|
|
314
|
+
],
|
|
315
|
+
skywalkerSubs: [
|
|
316
|
+
'presence_subscribe',
|
|
317
|
+
'typing_subscribe',
|
|
318
|
+
],
|
|
319
|
+
irisData: inboxData
|
|
320
|
+
});
|
|
321
|
+
console.log('[REALTIME] MQTT Connected with IRIS');
|
|
322
|
+
console.log('----------------------------------------');
|
|
323
|
+
console.log('[REALTIME] Real-Time Listener ACTIVE');
|
|
324
|
+
console.log('[REALTIME] Waiting for messages...');
|
|
325
|
+
console.log('----------------------------------------');
|
|
326
|
+
this._setupMessageHandlers();
|
|
327
|
+
|
|
328
|
+
if (options.enableHealthMonitor !== false) {
|
|
329
|
+
this.enableHealthMonitor(options.healthMonitorOptions || options);
|
|
330
|
+
}
|
|
331
|
+
if (options.enablePersistentLogger || options.logDir) {
|
|
332
|
+
this.enablePersistentLogger(options.persistentLoggerOptions || options);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { success: true };
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error('[REALTIME] Failed:', error.message);
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_setupMessageHandlers() {
|
|
343
|
+
this.on('message', (data) => {
|
|
344
|
+
const msg = this._parseMessage(data);
|
|
345
|
+
if (msg) this.emit('message_live', msg);
|
|
346
|
+
});
|
|
347
|
+
this.on('iris', (data) => {
|
|
348
|
+
const msg = this._parseIrisMessage(data);
|
|
349
|
+
if (msg) this.emit('message_live', msg);
|
|
350
|
+
});
|
|
351
|
+
this.on('receive', (topic, messages) => {
|
|
352
|
+
try {
|
|
353
|
+
const topicPath = topic.path || '';
|
|
354
|
+
for (const msg of (Array.isArray(messages) ? messages : [messages])) {
|
|
355
|
+
const data = msg?.data || msg;
|
|
356
|
+
if (!data) continue;
|
|
357
|
+
switch (topicPath) {
|
|
358
|
+
case '/ig_msg_dr':
|
|
359
|
+
this.emit('deliveryReceipt', {
|
|
360
|
+
threadId: data.thread_id || data.threadId,
|
|
361
|
+
itemId: data.item_id || data.itemId,
|
|
362
|
+
userId: data.user_id || data.userId,
|
|
363
|
+
timestamp: data.timestamp || Date.now(),
|
|
364
|
+
raw: data,
|
|
365
|
+
});
|
|
366
|
+
break;
|
|
367
|
+
case '/ig_conn_update':
|
|
368
|
+
this.emit('connectionUpdate', {
|
|
369
|
+
type: data.type || data.event,
|
|
370
|
+
reason: data.reason,
|
|
371
|
+
raw: data,
|
|
372
|
+
});
|
|
373
|
+
break;
|
|
374
|
+
case '/notify_disconnect':
|
|
375
|
+
this.emit('notifyDisconnect', {
|
|
376
|
+
reason: data.reason || data.message,
|
|
377
|
+
code: data.code,
|
|
378
|
+
raw: data,
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
case '/t_thread_typing':
|
|
382
|
+
this.emit('threadTyping', {
|
|
383
|
+
threadId: data.thread_id || data.threadId,
|
|
384
|
+
userId: data.user_id || data.sender_id,
|
|
385
|
+
isTyping: data.activity_status !== undefined ? data.activity_status === 1 : (data.is_typing !== false),
|
|
386
|
+
timestamp: data.timestamp || Date.now(),
|
|
387
|
+
raw: data,
|
|
388
|
+
});
|
|
389
|
+
break;
|
|
390
|
+
case '/iris_server_reset':
|
|
391
|
+
this.realtimeDebug('[IRIS] Server reset received, re-subscribing...');
|
|
392
|
+
this.emit('irisServerReset', { raw: data });
|
|
393
|
+
this._handleIrisReset();
|
|
394
|
+
break;
|
|
395
|
+
case '/t_ig_family_navigation_badge':
|
|
396
|
+
this.emit('badgeCount', {
|
|
397
|
+
count: data.badge_count ?? data.count ?? data.total,
|
|
398
|
+
dmCount: data.dm_count ?? data.direct_count,
|
|
399
|
+
activityCount: data.activity_count,
|
|
400
|
+
raw: data,
|
|
401
|
+
});
|
|
402
|
+
break;
|
|
403
|
+
case '/t_entity_presence':
|
|
404
|
+
this.emit('entityPresence', {
|
|
405
|
+
userId: data.user_id || data.entity_id,
|
|
406
|
+
isActive: data.is_active ?? data.active ?? false,
|
|
407
|
+
lastActivityAt: data.last_activity_at_ms || data.last_activity_at,
|
|
408
|
+
raw: data,
|
|
409
|
+
});
|
|
410
|
+
break;
|
|
411
|
+
case '/opened_thread':
|
|
412
|
+
this.emit('threadOpened', {
|
|
413
|
+
threadId: data.thread_id || data.threadId,
|
|
414
|
+
userId: data.user_id,
|
|
415
|
+
raw: data,
|
|
416
|
+
});
|
|
417
|
+
break;
|
|
418
|
+
case '/buddy_list':
|
|
419
|
+
this.emit('buddyList', {
|
|
420
|
+
users: data.overlay || data.buddies || data,
|
|
421
|
+
raw: data,
|
|
422
|
+
});
|
|
423
|
+
break;
|
|
424
|
+
case '/webrtc':
|
|
425
|
+
case '/webrtc_response':
|
|
426
|
+
this.emit('callEvent', {
|
|
427
|
+
type: data.event || data.type || (topicPath === '/webrtc' ? 'offer' : 'answer'),
|
|
428
|
+
callId: data.call_id || data.video_call_id,
|
|
429
|
+
peerId: data.peer_id || data.caller_id || data.user_id,
|
|
430
|
+
sdp: data.sdp,
|
|
431
|
+
raw: data,
|
|
432
|
+
});
|
|
433
|
+
break;
|
|
434
|
+
case '/onevc':
|
|
435
|
+
this.emit('callEvent', {
|
|
436
|
+
type: data.event || data.action || 'onevc',
|
|
437
|
+
callId: data.call_id || data.video_call_id,
|
|
438
|
+
peerId: data.peer_id || data.caller_id,
|
|
439
|
+
raw: data,
|
|
440
|
+
});
|
|
441
|
+
break;
|
|
442
|
+
case '/graphql':
|
|
443
|
+
this._handleGraphQLEvent(data);
|
|
444
|
+
break;
|
|
445
|
+
case '/pubsub':
|
|
446
|
+
this._handlePubsubEvent(data);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch (e) {
|
|
451
|
+
this.realtimeDebug('[EVENT_HANDLER] Error processing topic event:', e?.message || e);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async _handleIrisReset() {
|
|
457
|
+
try {
|
|
458
|
+
this.realtimeDebug('[IRIS_RESET] Attempting to re-subscribe after server reset...');
|
|
459
|
+
const inboxData = await this.ig.direct.getInbox();
|
|
460
|
+
if (inboxData) {
|
|
461
|
+
await this.irisSubscribe(inboxData);
|
|
462
|
+
this.realtimeDebug('[IRIS_RESET] Re-subscribed successfully');
|
|
463
|
+
}
|
|
464
|
+
} catch (e) {
|
|
465
|
+
this.realtimeDebug('[IRIS_RESET] Failed to re-subscribe:', e?.message || e);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
_handleGraphQLEvent(data) {
|
|
470
|
+
try {
|
|
471
|
+
const json = data.json || data;
|
|
472
|
+
const event = json?.event || json?.data?.event;
|
|
473
|
+
const payload = json?.data || json;
|
|
474
|
+
if (!payload) return;
|
|
475
|
+
|
|
476
|
+
if (payload.live_broadcast_comments) {
|
|
477
|
+
this.emit('liveComment', {
|
|
478
|
+
broadcastId: payload.broadcast_id,
|
|
479
|
+
comments: payload.live_broadcast_comments,
|
|
480
|
+
raw: payload,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
if (payload.live_broadcast_like_count !== undefined) {
|
|
484
|
+
this.emit('liveLikeCount', {
|
|
485
|
+
broadcastId: payload.broadcast_id,
|
|
486
|
+
likeCount: payload.live_broadcast_like_count,
|
|
487
|
+
raw: payload,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (payload.live_broadcast_wave) {
|
|
491
|
+
this.emit('liveWave', {
|
|
492
|
+
broadcastId: payload.broadcast_id,
|
|
493
|
+
wave: payload.live_broadcast_wave,
|
|
494
|
+
raw: payload,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
if (payload.live_broadcast_typing_indicator) {
|
|
498
|
+
this.emit('liveTyping', {
|
|
499
|
+
broadcastId: payload.broadcast_id,
|
|
500
|
+
userId: payload.user_id,
|
|
501
|
+
raw: payload,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (payload.live_viewer_count !== undefined) {
|
|
505
|
+
this.emit('liveViewerCount', {
|
|
506
|
+
broadcastId: payload.broadcast_id,
|
|
507
|
+
viewerCount: payload.live_viewer_count,
|
|
508
|
+
raw: payload,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (payload.media_feedback || payload.feedback_action) {
|
|
512
|
+
this.emit('mediaFeedback', {
|
|
513
|
+
mediaId: payload.media_id || payload.feedback_id,
|
|
514
|
+
action: payload.feedback_action || payload.action_type,
|
|
515
|
+
userId: payload.user_id,
|
|
516
|
+
raw: payload,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (payload.direct_typing || payload.activity_indicator_id) {
|
|
520
|
+
this.emit('directTyping', {
|
|
521
|
+
userId: payload.user_id || payload.sender_id,
|
|
522
|
+
threadId: payload.thread_id || payload.activity_indicator_id,
|
|
523
|
+
timestamp: payload.timestamp,
|
|
524
|
+
raw: payload,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (payload.presence_event || payload.user_presence) {
|
|
528
|
+
this.emit('appPresence', {
|
|
529
|
+
userId: payload.user_id,
|
|
530
|
+
isActive: payload.is_active ?? payload.active,
|
|
531
|
+
lastActivityAt: payload.last_activity_at_ms,
|
|
532
|
+
raw: payload,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (payload.direct_status) {
|
|
536
|
+
this.emit('directStatus', payload);
|
|
537
|
+
}
|
|
538
|
+
if (payload.interactivity) {
|
|
539
|
+
this.emit('liveInteractivity', {
|
|
540
|
+
broadcastId: payload.broadcast_id,
|
|
541
|
+
data: payload.interactivity,
|
|
542
|
+
raw: payload,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (payload.video_call_participant_state) {
|
|
546
|
+
this.emit('callStateChange', {
|
|
547
|
+
callId: payload.video_call_id,
|
|
548
|
+
participants: payload.video_call_participant_state,
|
|
549
|
+
raw: payload,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
this.emit('graphqlEvent', { event, payload, raw: data });
|
|
553
|
+
} catch (e) {
|
|
554
|
+
this.realtimeDebug('[GRAPHQL_EVENT] Parse error:', e?.message || e);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_handlePubsubEvent(data) {
|
|
559
|
+
try {
|
|
560
|
+
const json = data.json || data;
|
|
561
|
+
const payload = json?.data || json;
|
|
562
|
+
if (!payload) return;
|
|
563
|
+
|
|
564
|
+
if (payload.doublePublish) return;
|
|
565
|
+
|
|
566
|
+
if (payload.event === 'patch' || payload.op) {
|
|
567
|
+
this.emit('direct', {
|
|
568
|
+
op: payload.op,
|
|
569
|
+
path: payload.path,
|
|
570
|
+
value: payload.value,
|
|
571
|
+
threadId: payload.thread_id,
|
|
572
|
+
raw: payload,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (payload.activity_indicator_id || (payload.event === 'typing')) {
|
|
576
|
+
this.emit('directTyping', {
|
|
577
|
+
userId: payload.user_id || payload.sender_id,
|
|
578
|
+
threadId: payload.thread_id || payload.activity_indicator_id,
|
|
579
|
+
isTyping: payload.activity_status !== 0,
|
|
580
|
+
raw: payload,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
this.emit('pubsubEvent', { payload, raw: data });
|
|
584
|
+
} catch (e) {
|
|
585
|
+
this.realtimeDebug('[PUBSUB_EVENT] Parse error:', e?.message || e);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Parse a standard realtime message packet into a simplified message object
|
|
590
|
+
_parseMessage(data) {
|
|
591
|
+
try {
|
|
592
|
+
const msg = data.message;
|
|
593
|
+
if (!msg) return null;
|
|
594
|
+
if (data.parsed) return data.parsed;
|
|
595
|
+
const threadInfo = this.threads.get(msg.thread_id);
|
|
596
|
+
return {
|
|
597
|
+
id: msg.item_id || msg.id,
|
|
598
|
+
userId: msg.user_id || msg.from_user_id,
|
|
599
|
+
username: msg.username || msg.from_username || `user_${msg.user_id || 'unknown'}`,
|
|
600
|
+
text: msg.text || msg.body || '',
|
|
601
|
+
itemType: msg.item_type || 'text',
|
|
602
|
+
thread: threadInfo?.title || `Thread ${msg.thread_id}`,
|
|
603
|
+
thread_id: msg.thread_id,
|
|
604
|
+
timestamp: msg.timestamp,
|
|
605
|
+
isGroup: threadInfo?.isGroup,
|
|
606
|
+
status: 'good'
|
|
607
|
+
};
|
|
608
|
+
} catch (e) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Parse IRIS message data into a simplified message object
|
|
614
|
+
_parseIrisMessage(data) {
|
|
615
|
+
try {
|
|
616
|
+
if (data.event !== 'message_create' && !data.type?.includes('message')) return null;
|
|
617
|
+
return {
|
|
618
|
+
id: data.item_id || data.id,
|
|
619
|
+
userId: data.user_id || data.from_user_id,
|
|
620
|
+
username: data.username || data.from_username || `user_${data.user_id || 'unknown'}`,
|
|
621
|
+
text: data.text || '',
|
|
622
|
+
itemType: data.item_type || 'text',
|
|
623
|
+
thread_id: data.thread_id,
|
|
624
|
+
timestamp: data.timestamp,
|
|
625
|
+
status: 'good'
|
|
626
|
+
};
|
|
627
|
+
} catch (e) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* setInitOptions(initOptions)
|
|
634
|
+
* - Normalizes init options for connect calls (accepts array or object)
|
|
635
|
+
*/
|
|
636
|
+
setInitOptions(initOptions) {
|
|
637
|
+
if (Array.isArray(initOptions))
|
|
638
|
+
initOptions = { graphQlSubs: initOptions };
|
|
639
|
+
this.initOptions = {
|
|
640
|
+
graphQlSubs: [],
|
|
641
|
+
skywalkerSubs: [],
|
|
642
|
+
...(initOptions || {}),
|
|
643
|
+
socksOptions: typeof initOptions === 'object' && !Array.isArray(initOptions) ? initOptions.socksOptions : undefined,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Extract session ID from a JWT-like authorization header if present
|
|
648
|
+
extractSessionIdFromJWT() {
|
|
649
|
+
try {
|
|
650
|
+
const authHeader = this.ig.state.authorization;
|
|
651
|
+
if (!authHeader) return null;
|
|
652
|
+
const raw = String(authHeader || '');
|
|
653
|
+
const candidate = raw.replace(/^Bearer\s*/i, '').replace(/^IGT:2:/i, '');
|
|
654
|
+
if (candidate.includes('.')) {
|
|
655
|
+
const parts = candidate.split('.');
|
|
656
|
+
if (parts.length >= 2) {
|
|
657
|
+
try {
|
|
658
|
+
const payload = Buffer.from(parts[1], 'base64').toString('utf8');
|
|
659
|
+
const parsed = JSON.parse(payload);
|
|
660
|
+
return parsed.sessionid || parsed.session_id || parsed.session || null;
|
|
661
|
+
} catch (e) {}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
const decoded = Buffer.from(candidate, 'base64').toString('utf8');
|
|
666
|
+
const parsed = JSON.parse(decoded);
|
|
667
|
+
return parsed.sessionid || parsed.session_id || null;
|
|
668
|
+
} catch (e) {}
|
|
669
|
+
return null;
|
|
670
|
+
} catch (e) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* constructConnection()
|
|
677
|
+
* - Build the MQTToTConnection payload (thrift object) used to connect.
|
|
678
|
+
* - Pulls deviceId, sessionid, device secrets and app-specific headers to populate the payload.
|
|
679
|
+
*/
|
|
680
|
+
constructConnection() {
|
|
681
|
+
// Choose the best available user agent value from state fallbacks
|
|
682
|
+
const userAgent =
|
|
683
|
+
typeof this.ig.state.userAgent === 'string'
|
|
684
|
+
? this.ig.state.userAgent
|
|
685
|
+
: typeof this.ig.state.appUserAgent === 'string'
|
|
686
|
+
? this.ig.state.appUserAgent
|
|
687
|
+
: typeof this.ig.state.deviceString === 'string'
|
|
688
|
+
? this.ig.state.deviceString
|
|
689
|
+
: 'Instagram 415.0.0.36.76 Android (34/14; 420dpi; 1080x2340; samsung; SM-S911B; e1s; exynos2400; en_US; 580610226)';
|
|
690
|
+
|
|
691
|
+
// deviceId / phoneId fallback handling (string coercion)
|
|
692
|
+
const deviceId = String(this.ig.state.phoneId || this.ig.state.deviceId || 'device_unknown');
|
|
693
|
+
|
|
694
|
+
// attempt to extract sessionid from various locations: Authorization JWT, cookie helpers, state fields, or cookie jar
|
|
695
|
+
let sessionid = null;
|
|
696
|
+
try {
|
|
697
|
+
sessionid = this.extractSessionIdFromJWT();
|
|
698
|
+
} catch (e) { sessionid = null; }
|
|
699
|
+
|
|
700
|
+
if (!sessionid) {
|
|
701
|
+
try {
|
|
702
|
+
if (typeof this.ig.state.extractCookieValue === 'function') {
|
|
703
|
+
sessionid = this.ig.state.extractCookieValue('sessionid');
|
|
704
|
+
}
|
|
705
|
+
} catch (e) { sessionid = null; }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!sessionid) {
|
|
709
|
+
try {
|
|
710
|
+
sessionid = this.ig.state.sessionId || this.ig.state.sessionid || this.ig.state.cookies?.sessionid || null;
|
|
711
|
+
} catch (e) { sessionid = null; }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (!sessionid) {
|
|
715
|
+
try {
|
|
716
|
+
if (this.ig.state.cookieJar && typeof this.ig.state.cookieJar.getCookiesSync === 'function') {
|
|
717
|
+
const cookies = this.ig.state.cookieJar.getCookiesSync('https://i.instagram.com/') || [];
|
|
718
|
+
const found = cookies.find(c => (c.key === 'sessionid' || c.name === 'sessionid'));
|
|
719
|
+
if (found) sessionid = found.value;
|
|
720
|
+
}
|
|
721
|
+
} catch (e) {}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// fallback sessionid generation — only used if nothing else found
|
|
725
|
+
if (!sessionid) {
|
|
726
|
+
const userId = this.ig.state.cookieUserId || this.ig.state.userId || '0';
|
|
727
|
+
sessionid = String(userId) + '_' + Date.now();
|
|
728
|
+
this.realtimeDebug(`SessionID generated (fallback): ${sessionid}`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// device secret retrieval (from attached auth helper or state)
|
|
732
|
+
let deviceSecret = null;
|
|
733
|
+
try {
|
|
734
|
+
if (this._attachedAuthState && typeof this._attachedAuthState.getData === 'function') {
|
|
735
|
+
const d = this._attachedAuthState.getData();
|
|
736
|
+
if (d && d.device && (d.device.deviceSecret || d.device.secret)) {
|
|
737
|
+
deviceSecret = d.device.deviceSecret || d.device.secret;
|
|
738
|
+
}
|
|
739
|
+
if (!deviceSecret && d && d.mqttAuth && (d.mqttAuth.deviceSecret || d.mqttAuth.secret)) {
|
|
740
|
+
deviceSecret = d.mqttAuth.deviceSecret || d.mqttAuth.secret;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
} catch (e) {}
|
|
744
|
+
try {
|
|
745
|
+
if (!deviceSecret && (this.ig.state.deviceSecret || this.ig.state.mqttDeviceSecret)) {
|
|
746
|
+
deviceSecret = this.ig.state.deviceSecret || this.ig.state.mqttDeviceSecret;
|
|
747
|
+
}
|
|
748
|
+
} catch (e) {}
|
|
749
|
+
|
|
750
|
+
// mqtt JWT token if available (preferred)
|
|
751
|
+
let mqttJwt = null;
|
|
752
|
+
try {
|
|
753
|
+
if (this._attachedAuthState && typeof this._attachedAuthState.getData === 'function') {
|
|
754
|
+
const d = this._attachedAuthState.getData();
|
|
755
|
+
if (d && d.mqttAuth && d.mqttAuth.jwt) mqttJwt = d.mqttAuth.jwt;
|
|
756
|
+
}
|
|
757
|
+
if (!mqttJwt && this.ig.state.mqttJwt) mqttJwt = this.ig.state.mqttJwt;
|
|
758
|
+
} catch (e) {}
|
|
759
|
+
|
|
760
|
+
// password is either "jwt=..." or "sessionid=..."
|
|
761
|
+
let password;
|
|
762
|
+
if (mqttJwt) {
|
|
763
|
+
password = `jwt=${mqttJwt}`;
|
|
764
|
+
} else {
|
|
765
|
+
password = `sessionid=${sessionid}`;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// client type indicates if device secret is available
|
|
769
|
+
const clientType = deviceSecret ? 'secure_cookie_auth' : 'cookie_auth';
|
|
770
|
+
|
|
771
|
+
// IMPORTANT: keep clientMqttSessionId persistent across reconnects (already set in constructor)
|
|
772
|
+
if (!this._clientMqttSessionId) {
|
|
773
|
+
try {
|
|
774
|
+
this._clientMqttSessionId = (BigInt(Date.now()) & BigInt(0xffffffff));
|
|
775
|
+
} catch (e) {
|
|
776
|
+
this._clientMqttSessionId = BigInt(0);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const clientMqttSessionId = this._clientMqttSessionId;
|
|
780
|
+
|
|
781
|
+
const subscribeTopics = [88, 135, 149, 150, 133, 146, 165, 164, 176, 195, 141, 152, 160, 139, 0, 62, 63, 211];
|
|
782
|
+
|
|
783
|
+
// Build the thrift connection object using mqttot.MQTToTConnection
|
|
784
|
+
// NOTE: use deviceId.substring(0,20) for clientIdentifier because the mobile app often truncates it.
|
|
785
|
+
// endpointCapabilities set to 128 for broader compatibility (some clients use 0, 128 provides better capability advertising).
|
|
786
|
+
this.connection = new mqttot_1.MQTToTConnection({
|
|
787
|
+
clientIdentifier: deviceId.substring(0, 20),
|
|
788
|
+
clientInfo: {
|
|
789
|
+
userId: BigInt(Number(this.ig.state.cookieUserId || this.ig.state.userId || 0)),
|
|
790
|
+
userAgent,
|
|
791
|
+
clientCapabilities: 439,
|
|
792
|
+
endpointCapabilities: 128,
|
|
793
|
+
publishFormat: 1,
|
|
794
|
+
noAutomaticForeground: false,
|
|
795
|
+
makeUserAvailableInForeground: true,
|
|
796
|
+
deviceId,
|
|
797
|
+
isInitiallyForeground: true,
|
|
798
|
+
networkType: 1,
|
|
799
|
+
networkSubtype: 0,
|
|
800
|
+
clientMqttSessionId: clientMqttSessionId,
|
|
801
|
+
subscribeTopics,
|
|
802
|
+
clientType,
|
|
803
|
+
appId: BigInt(567067343352427),
|
|
804
|
+
deviceSecret: deviceSecret || '',
|
|
805
|
+
clientStack: 3,
|
|
806
|
+
...(this.initOptions?.connectOverrides || {}),
|
|
807
|
+
},
|
|
808
|
+
password,
|
|
809
|
+
appSpecificInfo: {
|
|
810
|
+
// Use exported INSTAGRAM_VERSION automatically, fallback to this.ig.state.appVersion if missing
|
|
811
|
+
app_version: INSTAGRAM_VERSION || this.ig.state.appVersion,
|
|
812
|
+
'X-IG-Capabilities': this.ig.state.capabilitiesHeader,
|
|
813
|
+
everclear_subscriptions: JSON.stringify({
|
|
814
|
+
inapp_notification_subscribe_comment: '17899377895239777',
|
|
815
|
+
inapp_notification_subscribe_comment_mention_and_reply: '17899377895239777',
|
|
816
|
+
video_call_participant_state_delivery: '17977239895057311',
|
|
817
|
+
presence_subscribe: '17846944882223835',
|
|
818
|
+
}),
|
|
819
|
+
// Extra app-specific fields observed in the mobile APK (helps the server identify the client capabilities)
|
|
820
|
+
'User-Agent': userAgent,
|
|
821
|
+
'Accept-Language': (this.ig.state.language || 'en_US').replace('_', '-'),
|
|
822
|
+
platform: 'android',
|
|
823
|
+
ig_mqtt_route: 'django',
|
|
824
|
+
pubsub_msg_type_blacklist: 'direct, typing_type',
|
|
825
|
+
auth_cache_enabled: '0',
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* connect(options)
|
|
832
|
+
* - Creates the MQTToTClient using mqttot.MQTToTClient and the payloadProvider.
|
|
833
|
+
* - Attaches idempotent lifecycle handlers for connect/close/error.
|
|
834
|
+
* - Starts keepalive/watchdog timers and message listeners after successful connect.
|
|
835
|
+
*/
|
|
836
|
+
async connect(options) {
|
|
837
|
+
this._connectInProgress = true;
|
|
838
|
+
try {
|
|
839
|
+
this.setInitOptions(options);
|
|
840
|
+
this.constructConnection();
|
|
841
|
+
const { MQTToTClient } = require("../mqttot");
|
|
842
|
+
const { compressDeflate } = require("../shared");
|
|
843
|
+
|
|
844
|
+
if (this._mqtt) {
|
|
845
|
+
try { this._mqtt.removeAllListeners(); } catch (_e) {}
|
|
846
|
+
try { this._mqtt._stopKeepalive(); } catch (_e) {}
|
|
847
|
+
try { this._mqtt.disconnect(); } catch (_e) {}
|
|
848
|
+
this._mqtt = null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
this._mqtt = new MQTToTClient({
|
|
852
|
+
url: 'edge-mqtt.facebook.com',
|
|
853
|
+
payloadProvider: async () => {
|
|
854
|
+
return await compressDeflate(this.connection.toThrift());
|
|
855
|
+
},
|
|
856
|
+
autoReconnect: false,
|
|
857
|
+
requirePayload: false,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// attach lifecycle handlers idempotent (create once)
|
|
861
|
+
try {
|
|
862
|
+
if (typeof this._attachMqttLifecycle !== 'function') {
|
|
863
|
+
this._attachMqttLifecycle = async () => {
|
|
864
|
+
if (!this._mqtt) return;
|
|
865
|
+
try {
|
|
866
|
+
this._mqtt.on('connect', async () => {
|
|
867
|
+
this.realtimeDebug('[MQTT] client emitted connect');
|
|
868
|
+
try {
|
|
869
|
+
// After normal after-connect handlers, try extract session id and persist it
|
|
870
|
+
await this._afterConnectHandlers();
|
|
871
|
+
} catch (e) {
|
|
872
|
+
this.realtimeDebug('[MQTT] afterConnect error', e?.message || e);
|
|
873
|
+
}
|
|
874
|
+
// After everything, attempt to detect and persist MQTT session id
|
|
875
|
+
try {
|
|
876
|
+
await this._onMqttConnected();
|
|
877
|
+
} catch (e) {
|
|
878
|
+
this.realtimeDebug('[MQTT] _onMqttConnected error', e?.message || e);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
} catch (e) {}
|
|
882
|
+
try {
|
|
883
|
+
this._mqtt.on('close', async () => {
|
|
884
|
+
this.realtimeDebug('[MQTT] client close event');
|
|
885
|
+
this._lastMessageAt = Date.now();
|
|
886
|
+
try { await this._persistMqttSession(); } catch (e) {}
|
|
887
|
+
this._mqttConnected = false;
|
|
888
|
+
this._connectInProgress = false;
|
|
889
|
+
this.emit('mqtt_disconnected');
|
|
890
|
+
if (this._reconnectInProgress) {
|
|
891
|
+
this.realtimeDebug('[MQTT] close event ignored — reconnect already in progress');
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (this.safeDisconnect) {
|
|
895
|
+
this.realtimeDebug('[MQTT] close event ignored — safe disconnect');
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (this._reconnectTimeoutId) clearTimeout(this._reconnectTimeoutId);
|
|
899
|
+
this._reconnectTimeoutId = setTimeout(async () => {
|
|
900
|
+
try { await this._attemptReconnectSafely(); } catch (e) {}
|
|
901
|
+
}, this._reconnectDebounceMs);
|
|
902
|
+
});
|
|
903
|
+
} catch (e) {}
|
|
904
|
+
try {
|
|
905
|
+
this._mqtt.on('error', (err) => {
|
|
906
|
+
this.realtimeDebug('[MQTT] client error:', err?.message || err);
|
|
907
|
+
this.emit('mqtt_error', err);
|
|
908
|
+
if (this._reconnectInProgress) {
|
|
909
|
+
this.realtimeDebug('[MQTT] error event ignored — reconnect already in progress');
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (this.safeDisconnect) return;
|
|
913
|
+
if (this._reconnectTimeoutId) clearTimeout(this._reconnectTimeoutId);
|
|
914
|
+
const debounceMs = (this.errorHandler && this.errorHandler.isRateLimited())
|
|
915
|
+
? this.errorHandler.getRateLimitRemainingMs()
|
|
916
|
+
: this._reconnectDebounceMs;
|
|
917
|
+
this._reconnectTimeoutId = setTimeout(async () => {
|
|
918
|
+
try { await this._attemptReconnectSafely(err); } catch (e) {}
|
|
919
|
+
}, debounceMs);
|
|
920
|
+
});
|
|
921
|
+
} catch (e) {}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
try { await this._attachMqttLifecycle(); } catch (e) {}
|
|
925
|
+
} catch (e) {}
|
|
926
|
+
|
|
927
|
+
// actually connect the mqtt client (this will emit connect when done)
|
|
928
|
+
await this._mqtt.connect();
|
|
929
|
+
|
|
930
|
+
// Commands uses mqtt client; Commands.updateSubscriptions has been set to use qos 0.
|
|
931
|
+
this.commands = new commands_1.Commands(this._mqtt);
|
|
932
|
+
|
|
933
|
+
// Notify higher-level code that we are connected
|
|
934
|
+
this._mqttConnected = true;
|
|
935
|
+
this._connectInProgress = false;
|
|
936
|
+
this.emit('connected');
|
|
937
|
+
this.emit('mqtt_connected');
|
|
938
|
+
|
|
939
|
+
// WATCHDOG / KEEPALIVE / TRAFFIC MONITOR (rationalized - 2 primary timers + 1 watchdog)
|
|
940
|
+
try {
|
|
941
|
+
this._lastMessageAt = Date.now();
|
|
942
|
+
this._lastServerTrafficAt = Date.now();
|
|
943
|
+
const updateLast = () => { try { this._lastMessageAt = Date.now(); this._lastServerTrafficAt = Date.now(); } catch (e) {} };
|
|
944
|
+
this.on('receive', updateLast);
|
|
945
|
+
this.on('receiveRaw', updateLast);
|
|
946
|
+
this.on('message', updateLast);
|
|
947
|
+
this.on('iris', updateLast);
|
|
948
|
+
|
|
949
|
+
const KEEPALIVE_FOREGROUND_MS = (this.initOptions && this.initOptions.keepaliveForegroundMs) ? this.initOptions.keepaliveForegroundMs : 60000;
|
|
950
|
+
const MESSAGE_SYNC_REFRESH_MS = (this.initOptions && this.initOptions.messageSyncRefreshMs) ? this.initOptions.messageSyncRefreshMs : 300000;
|
|
951
|
+
const TRAFFIC_INACTIVITY_MS = (this.initOptions && this.initOptions.trafficInactivityMs) ? this.initOptions.trafficInactivityMs : 300000;
|
|
952
|
+
const HEARTBEAT_MS = (this.initOptions && this.initOptions.heartbeatMs) ? this.initOptions.heartbeatMs : 240000;
|
|
953
|
+
|
|
954
|
+
try {
|
|
955
|
+
if (this._foregroundTimer) clearInterval(this._foregroundTimer);
|
|
956
|
+
this._foregroundTimer = setInterval(async () => {
|
|
957
|
+
try {
|
|
958
|
+
if (!this.commands) return;
|
|
959
|
+
await this.commands.updateSubscriptions({
|
|
960
|
+
topic: constants_1.Topics.PUBSUB,
|
|
961
|
+
data: { foreground: true }
|
|
962
|
+
});
|
|
963
|
+
this._lastMessageAt = Date.now();
|
|
964
|
+
this.realtimeDebug('[KEEPALIVE] Foreground pulse sent.');
|
|
965
|
+
} catch (e) {
|
|
966
|
+
this.realtimeDebug('[KEEPALIVE] Foreground pulse failed:', e?.message || e);
|
|
967
|
+
}
|
|
968
|
+
}, KEEPALIVE_FOREGROUND_MS + Math.floor(Math.random() * 5000));
|
|
969
|
+
} catch (e) {
|
|
970
|
+
this.realtimeDebug('[KEEPALIVE] Could not start foreground timer:', e?.message || e);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
try {
|
|
974
|
+
if (this._syncTimer) clearInterval(this._syncTimer);
|
|
975
|
+
this._syncTimer = setInterval(async () => {
|
|
976
|
+
try {
|
|
977
|
+
if (!this.commands) return;
|
|
978
|
+
const subs = (this.initOptions && this.initOptions.graphQlSubs && this.initOptions.graphQlSubs.length) ? this.initOptions.graphQlSubs : this.defaultGraphQlSubs;
|
|
979
|
+
await this.graphQlSubscribe(subs);
|
|
980
|
+
this._lastMessageAt = Date.now();
|
|
981
|
+
this.realtimeDebug('[KEEPALIVE] GraphQL subs refreshed.');
|
|
982
|
+
} catch (e) {
|
|
983
|
+
this.realtimeDebug('[KEEPALIVE] GraphQL refresh failed:', e?.message || e);
|
|
984
|
+
}
|
|
985
|
+
}, MESSAGE_SYNC_REFRESH_MS + Math.floor(Math.random() * 10000));
|
|
986
|
+
} catch (e) {
|
|
987
|
+
this.realtimeDebug('[KEEPALIVE] Could not start sync timer:', e?.message || e);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
try {
|
|
991
|
+
if (this._trafficWatchdog) clearInterval(this._trafficWatchdog);
|
|
992
|
+
this._trafficWatchdog = setInterval(async () => {
|
|
993
|
+
try {
|
|
994
|
+
const idle = Date.now() - (this._lastServerTrafficAt || 0);
|
|
995
|
+
if (idle > TRAFFIC_INACTIVITY_MS) {
|
|
996
|
+
this.realtimeDebug(`[WATCHDOG] No server traffic for ${Math.round(idle/1000)}s -> attempting reconnect`);
|
|
997
|
+
try {
|
|
998
|
+
if (this._mqtt && typeof this._mqtt.ping === 'function') {
|
|
999
|
+
await this._mqtt.ping();
|
|
1000
|
+
this._lastMessageAt = Date.now();
|
|
1001
|
+
this.realtimeDebug('[WATCHDOG] Ping succeeded, connection is alive.');
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
} catch (e) {
|
|
1005
|
+
this.realtimeDebug('[WATCHDOG] Ping failed, proceeding with reconnect:', e?.message || e);
|
|
1006
|
+
}
|
|
1007
|
+
await this._attemptReconnectSafely();
|
|
1008
|
+
}
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
this.realtimeDebug('[WATCHDOG] trafficWatchdog fault:', e?.message || e);
|
|
1011
|
+
}
|
|
1012
|
+
}, 60000);
|
|
1013
|
+
} catch (e) {
|
|
1014
|
+
this.realtimeDebug('[WATCHDOG] Could not start traffic watchdog:', e?.message || e);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
try {
|
|
1018
|
+
if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
|
|
1019
|
+
this._heartbeatTimer = setInterval(async () => {
|
|
1020
|
+
try {
|
|
1021
|
+
if (this._mqtt && typeof this._mqtt.ping === 'function') {
|
|
1022
|
+
try {
|
|
1023
|
+
await this._mqtt.ping();
|
|
1024
|
+
this._lastMessageAt = Date.now();
|
|
1025
|
+
this.realtimeDebug('[HEARTBEAT] mqtt.ping() ok.');
|
|
1026
|
+
} catch (e) {
|
|
1027
|
+
this.realtimeDebug('[HEARTBEAT] mqtt.ping failed:', e?.message || e);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
} catch (e) {
|
|
1031
|
+
this.realtimeDebug('[HEARTBEAT] fault:', e?.message || e);
|
|
1032
|
+
}
|
|
1033
|
+
}, HEARTBEAT_MS + Math.floor(Math.random() * 5000));
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
this.realtimeDebug('[HEARTBEAT] Could not start heartbeat timer:', e?.message || e);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
this.realtimeDebug('[WATCHDOG] initialization error:', e?.message || e);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Set up on-message handler:
|
|
1044
|
+
* - Messages arriving from low-level MQTT are looked up in mqtt.topicMap,
|
|
1045
|
+
* then parsed by the topic's parser if available. Otherwise payload is emitted as raw.
|
|
1046
|
+
*/
|
|
1047
|
+
this._mqtt.on('message', async (msg) => {
|
|
1048
|
+
const topicMap = this.mqtt?.topicMap;
|
|
1049
|
+
const topic = topicMap?.get(msg.topic);
|
|
1050
|
+
if (topic && topic.parser && !topic.noParse) {
|
|
1051
|
+
try {
|
|
1052
|
+
const unzipped = await (0, shared_1.tryUnzipAsync)(msg.payload);
|
|
1053
|
+
const parsedMessages = topic.parser.parseMessage(topic, unzipped);
|
|
1054
|
+
this.emit('receive', topic, Array.isArray(parsedMessages) ? parsedMessages : [parsedMessages]);
|
|
1055
|
+
} catch(e) {}
|
|
1056
|
+
} else {
|
|
1057
|
+
try {
|
|
1058
|
+
await (0, shared_1.tryUnzipAsync)(msg.payload);
|
|
1059
|
+
this.emit('receiveRaw', msg);
|
|
1060
|
+
} catch(e) {}
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
// propagate mqtt errors to RealtimeClient error handler
|
|
1064
|
+
this._mqtt.on('error', this.emitError);
|
|
1065
|
+
|
|
1066
|
+
try { await this._afterConnectHandlers(); } catch (e) { this.realtimeDebug('[MQTT] afterConnectHandlers failed', e?.message || e); }
|
|
1067
|
+
|
|
1068
|
+
// Initial subscriptions / iris
|
|
1069
|
+
await (0, shared_1.delay)(100);
|
|
1070
|
+
if (this.initOptions.graphQlSubs && this.initOptions.graphQlSubs.length > 0) {
|
|
1071
|
+
await this.graphQlSubscribe(this.initOptions.graphQlSubs);
|
|
1072
|
+
} else {
|
|
1073
|
+
// ensure defaults if none provided
|
|
1074
|
+
await this.graphQlSubscribe(this.defaultGraphQlSubs);
|
|
1075
|
+
}
|
|
1076
|
+
if (this.initOptions.irisData) {
|
|
1077
|
+
await this.irisSubscribe(this.initOptions.irisData);
|
|
1078
|
+
} else {
|
|
1079
|
+
try {
|
|
1080
|
+
console.log('[REALTIME] Auto-fetching IRIS data...');
|
|
1081
|
+
const autoIrisData = await this.ig.direct.getInbox();
|
|
1082
|
+
if (autoIrisData) {
|
|
1083
|
+
await this.irisSubscribe(autoIrisData);
|
|
1084
|
+
console.log('[REALTIME] IRIS subscription successful');
|
|
1085
|
+
}
|
|
1086
|
+
} catch (e) {
|
|
1087
|
+
console.log('[REALTIME] Could not auto-fetch IRIS data:', e.message);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if ((this.initOptions.skywalkerSubs ?? []).length > 0) {
|
|
1091
|
+
await this.skywalkerSubscribe(this.initOptions.skywalkerSubs);
|
|
1092
|
+
} else {
|
|
1093
|
+
// ensure default skywalker subs if none provided
|
|
1094
|
+
await this.skywalkerSubscribe(this.defaultSkywalkerSubs);
|
|
1095
|
+
}
|
|
1096
|
+
await (0, shared_1.delay)(100);
|
|
1097
|
+
try {
|
|
1098
|
+
await this.ig.direct.getInbox();
|
|
1099
|
+
try {
|
|
1100
|
+
await this.ig.request.send({
|
|
1101
|
+
url: '/api/v1/direct_v2/threads/get_most_recent_message/',
|
|
1102
|
+
method: 'POST',
|
|
1103
|
+
});
|
|
1104
|
+
} catch(e) {}
|
|
1105
|
+
} catch (error) {}
|
|
1106
|
+
this._setupMessageHandlers();
|
|
1107
|
+
|
|
1108
|
+
// Active query keepalive disabled - redundant with rationalized foreground + heartbeat timers
|
|
1109
|
+
// The foreground timer (60s) and heartbeat (90s) provide sufficient keepalive coverage
|
|
1110
|
+
// without generating excessive traffic that could trigger Instagram rate limiting
|
|
1111
|
+
} catch (connectError) {
|
|
1112
|
+
this._connectInProgress = false;
|
|
1113
|
+
throw connectError;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Attempt to detect server-provided MQTT session id and persist it.
|
|
1119
|
+
* This method is defensive: it tries multiple places on the mqtt client object.
|
|
1120
|
+
*
|
|
1121
|
+
* Also: schedule credential refresh if mqttAuth.expiresAt is present (so tokens are refreshed
|
|
1122
|
+
* before they expire).
|
|
1123
|
+
*/
|
|
1124
|
+
async _onMqttConnected() {
|
|
1125
|
+
try {
|
|
1126
|
+
let found = null;
|
|
1127
|
+
try {
|
|
1128
|
+
const mqtt = this._mqtt;
|
|
1129
|
+
if (mqtt) {
|
|
1130
|
+
// try common accessor first
|
|
1131
|
+
if (typeof mqtt.getSessionId === 'function') {
|
|
1132
|
+
try { found = mqtt.getSessionId(); } catch (e) {}
|
|
1133
|
+
}
|
|
1134
|
+
// try common property names (defensive)
|
|
1135
|
+
if (!found && mqtt.sessionId) found = mqtt.sessionId;
|
|
1136
|
+
if (!found && mqtt._sessionId) found = mqtt._sessionId;
|
|
1137
|
+
// some clients keep last connack info
|
|
1138
|
+
if (!found && mqtt.lastConnack && mqtt.lastConnack.sessionId) found = mqtt.lastConnack.sessionId;
|
|
1139
|
+
if (!found && mqtt.connack && mqtt.connack.sessionId) found = mqtt.connack.sessionId;
|
|
1140
|
+
// fallback to connection.clientInfo.clientMqttSessionId (local id)
|
|
1141
|
+
if (!found && this.connection && this.connection.clientInfo && this.connection.clientInfo.clientMqttSessionId) {
|
|
1142
|
+
try { found = String(this.connection.clientInfo.clientMqttSessionId); } catch (e) {}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
} catch (e) {}
|
|
1146
|
+
// If no server-provided id was found, fall back to the persistent client id (if present) or 'boot'
|
|
1147
|
+
if (!found) {
|
|
1148
|
+
if (this._clientMqttSessionId) {
|
|
1149
|
+
try {
|
|
1150
|
+
found = String(this._clientMqttSessionId);
|
|
1151
|
+
this.realtimeDebug('[MQTT] No server mqttSessionId yet — falling back to clientMqttSessionId.');
|
|
1152
|
+
} catch (e) {
|
|
1153
|
+
// ignore and keep found null
|
|
1154
|
+
}
|
|
1155
|
+
} else {
|
|
1156
|
+
// final fallback
|
|
1157
|
+
found = 'boot';
|
|
1158
|
+
this.realtimeDebug('[MQTT] No mqttSessionId available — using boot fallback.');
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (found) {
|
|
1162
|
+
this._mqttSessionId = String(found);
|
|
1163
|
+
this.realtimeDebug(`[MQTT] detected mqttSessionId: ${this._mqttSessionId}`);
|
|
1164
|
+
// emit event for consumers
|
|
1165
|
+
try { this.emit('mqtt_session', this._mqttSessionId); } catch (e) {}
|
|
1166
|
+
// persist it right away
|
|
1167
|
+
try { await this._persistMqttSession(); } catch (e) { this.realtimeDebug('[MQTT] persist after connect failed', e?.message || e); }
|
|
1168
|
+
// start periodic persist if not already (persist every 4 hours to avoid excessive I/O and detection)
|
|
1169
|
+
if (!this._mqttSessionPersistIntervalId) {
|
|
1170
|
+
try {
|
|
1171
|
+
this._mqttSessionPersistIntervalId = setInterval(() => {
|
|
1172
|
+
try { this._persistMqttSession(); } catch (e) {}
|
|
1173
|
+
}, 4 * 60 * 60 * 1000); // every 4 hours
|
|
1174
|
+
} catch (e) {}
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
this.realtimeDebug('[MQTT] mqttSessionId not found on client after connect (will attempt again on subsequent connects)');
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
//
|
|
1181
|
+
// SCHEDULE CREDENTIAL REFRESH (if auth helper provided mqttAuth.expiresAt).
|
|
1182
|
+
// We schedule a timer to refresh credentials some seconds before expiry and then attempt a
|
|
1183
|
+
// graceful reconnect so the new token is used.
|
|
1184
|
+
//
|
|
1185
|
+
try {
|
|
1186
|
+
// clear any previous timer
|
|
1187
|
+
if (this._mqttAuthRefreshTimer) {
|
|
1188
|
+
try { clearTimeout(this._mqttAuthRefreshTimer); } catch (e) {}
|
|
1189
|
+
this._mqttAuthRefreshTimer = null;
|
|
1190
|
+
}
|
|
1191
|
+
if (this._attachedAuthState && typeof this._attachedAuthState.getData === 'function') {
|
|
1192
|
+
try {
|
|
1193
|
+
const d = this._attachedAuthState.getData();
|
|
1194
|
+
const mqttAuth = d?.mqttAuth;
|
|
1195
|
+
if (mqttAuth && mqttAuth.expiresAt) {
|
|
1196
|
+
const expires = new Date(mqttAuth.expiresAt).getTime();
|
|
1197
|
+
if (!isNaN(expires)) {
|
|
1198
|
+
// refresh 60 seconds before expiry (buffer)
|
|
1199
|
+
const msBefore = expires - Date.now() - (60 * 1000);
|
|
1200
|
+
if (msBefore > 0) {
|
|
1201
|
+
this.realtimeDebug(`[CREDENTIAL_REFRESH] scheduling mqttAuth refresh in ${msBefore}ms (expiresAt: ${mqttAuth.expiresAt})`);
|
|
1202
|
+
this._mqttAuthRefreshTimer = setTimeout(async () => {
|
|
1203
|
+
try {
|
|
1204
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] timer fired - attempting refresh');
|
|
1205
|
+
const refreshed = await this._refreshMqttCredentials();
|
|
1206
|
+
if (refreshed) {
|
|
1207
|
+
try {
|
|
1208
|
+
// rebuild payload and attempt reconnect so new token is used
|
|
1209
|
+
this.constructConnection();
|
|
1210
|
+
} catch (e) {}
|
|
1211
|
+
try {
|
|
1212
|
+
await this._attemptReconnectSafely();
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] reconnect after refresh failed:', e?.message || e);
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] refresh attempt did not produce new mqtt credentials.');
|
|
1218
|
+
}
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] unexpected error during scheduled refresh:', e?.message || e);
|
|
1221
|
+
}
|
|
1222
|
+
}, msBefore);
|
|
1223
|
+
} else {
|
|
1224
|
+
// expiry is imminent — attempt immediate refresh now
|
|
1225
|
+
(async () => {
|
|
1226
|
+
try {
|
|
1227
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] mqttAuth near-expired or expired — attempting immediate refresh');
|
|
1228
|
+
const refreshedNow = await this._refreshMqttCredentials();
|
|
1229
|
+
if (refreshedNow) {
|
|
1230
|
+
try { this.constructConnection(); } catch (e) {}
|
|
1231
|
+
try { await this._attemptReconnectSafely(); } catch (e) {}
|
|
1232
|
+
}
|
|
1233
|
+
} catch (e) {}
|
|
1234
|
+
})();
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] scheduling failed:', e?.message || e);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
} catch (e) {
|
|
1243
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] schedule block failed:', e?.message || e);
|
|
1244
|
+
}
|
|
1245
|
+
} catch (e) {
|
|
1246
|
+
this.realtimeDebug('[MQTT] _onMqttConnected fatal', e?.message || e);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* _refreshMqttCredentials()
|
|
1252
|
+
* - Attempt to refresh mqtt credentials using attached auth helper if possible.
|
|
1253
|
+
* - This method is defensive: many auth helpers won't expose refresh methods, so it tries possible hooks and logs results.
|
|
1254
|
+
*/
|
|
1255
|
+
async _refreshMqttCredentials() {
|
|
1256
|
+
try {
|
|
1257
|
+
if (!this._attachedAuthState) {
|
|
1258
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] No attached auth helper to refresh credentials.');
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
const auth = this._attachedAuthState;
|
|
1262
|
+
// Preferred hook: auth.refreshMqttAuth() or auth.refreshAuth()
|
|
1263
|
+
if (typeof auth.refreshMqttAuth === 'function') {
|
|
1264
|
+
try {
|
|
1265
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] Calling auth.refreshMqttAuth()');
|
|
1266
|
+
await auth.refreshMqttAuth();
|
|
1267
|
+
// reload data into ig or auth helper if possible
|
|
1268
|
+
if (typeof auth.getData === 'function') {
|
|
1269
|
+
const d = auth.getData();
|
|
1270
|
+
if (d && d.mqttAuth && d.mqttAuth.jwt) {
|
|
1271
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] mqttAuth refreshed via refreshMqttAuth.');
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] refreshMqttAuth failed:', e?.message || e);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
// Secondary hook: auth.refresh() or auth.loadCreds()
|
|
1280
|
+
if (typeof auth.refresh === 'function') {
|
|
1281
|
+
try {
|
|
1282
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] Calling auth.refresh()');
|
|
1283
|
+
await auth.refresh();
|
|
1284
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] auth.refresh() completed.');
|
|
1285
|
+
return true;
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] auth.refresh() failed:', e?.message || e);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (typeof auth.loadCreds === 'function') {
|
|
1291
|
+
try {
|
|
1292
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] Calling auth.loadCreds(this.ig)');
|
|
1293
|
+
await auth.loadCreds(this.ig);
|
|
1294
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] auth.loadCreds() completed.');
|
|
1295
|
+
return true;
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] auth.loadCreds() failed:', e?.message || e);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// If none of the above worked, attempt to re-run the useMultiFileAuthState load path if available (best-effort)
|
|
1301
|
+
try {
|
|
1302
|
+
if (auth.folder && typeof require('../useMultiFileAuthState') === 'function') {
|
|
1303
|
+
try {
|
|
1304
|
+
const { useMultiFileAuthState } = require('../useMultiFileAuthState');
|
|
1305
|
+
const reloaded = await useMultiFileAuthState(auth.folder);
|
|
1306
|
+
if (reloaded && reloaded.getData && typeof reloaded.getData === 'function') {
|
|
1307
|
+
this._attachedAuthState = reloaded;
|
|
1308
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] reloaded auth helper from folder.');
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
} catch (e) {
|
|
1312
|
+
// not critical
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
} catch (e) {}
|
|
1316
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] No refresh hook available or refresh did not produce new mqtt credentials.');
|
|
1317
|
+
return false;
|
|
1318
|
+
} catch (e) {
|
|
1319
|
+
this.realtimeDebug('[CREDENTIAL_REFRESH] unexpected error', e?.message || e);
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Persist mqtt session details locally and via attached auth helper if available.
|
|
1326
|
+
* Writes <authFolder>/mqtt-session.json as a local backup.
|
|
1327
|
+
*/
|
|
1328
|
+
async _persistMqttSession() {
|
|
1329
|
+
try {
|
|
1330
|
+
const folder = this._authFolder || './authinfo_instagram';
|
|
1331
|
+
try {
|
|
1332
|
+
if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
|
|
1333
|
+
} catch (e) {}
|
|
1334
|
+
|
|
1335
|
+
// IMPORTANT: prefer server-provided mqtt session id when available.
|
|
1336
|
+
// Fallback order:
|
|
1337
|
+
// 1) this._mqttSessionId (server-provided)
|
|
1338
|
+
// 2) this._clientMqttSessionId (client-generated persistent id)
|
|
1339
|
+
// 3) 'boot'
|
|
1340
|
+
const obj = {
|
|
1341
|
+
sessionId: this._mqttSessionId || null,
|
|
1342
|
+
mqttSessionId: this._mqttSessionId ? String(this._mqttSessionId) : (this._clientMqttSessionId ? String(this._clientMqttSessionId) : 'boot'),
|
|
1343
|
+
lastConnected: new Date().toISOString(),
|
|
1344
|
+
userId: String(this.ig?.state?.cookieUserId || this.ig?.state?.userId || '')
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
// If an auth helper supports saveMqttSession, call that first (so it can save to the canonical auth state)
|
|
1348
|
+
try {
|
|
1349
|
+
if (this._attachedAuthState && typeof this._attachedAuthState.saveMqttSession === 'function') {
|
|
1350
|
+
try {
|
|
1351
|
+
// Many helpers expect the realtime client instance
|
|
1352
|
+
await this._attachedAuthState.saveMqttSession(this);
|
|
1353
|
+
this.realtimeDebug('[MQTT] authState.saveMqttSession called');
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
this.realtimeDebug('[MQTT] authState.saveMqttSession failed', e?.message || e);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
} catch (e) {}
|
|
1359
|
+
|
|
1360
|
+
// Always write local backup file
|
|
1361
|
+
try {
|
|
1362
|
+
fs.writeFileSync(path.join(folder, 'mqtt-session.json'), JSON.stringify(obj, null, 2));
|
|
1363
|
+
this.realtimeDebug('[MQTT] mqtt-session.json written locally');
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
this.realtimeDebug('[MQTT] failed writing mqtt-session.json', e?.message || e);
|
|
1366
|
+
}
|
|
1367
|
+
} catch (e) {
|
|
1368
|
+
// swallow errors to not break main flow
|
|
1369
|
+
this.realtimeDebug('[MQTT] _persistMqttSession error', e?.message || e);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* _attemptReconnectSafely()
|
|
1375
|
+
* - Ensure only a single reconnect flow runs at once.
|
|
1376
|
+
* - Disconnects existing mqtt client, waits briefly, refreshes credentials when appropriate,
|
|
1377
|
+
* and calls connect() again with exponential backoff retries.
|
|
1378
|
+
*/
|
|
1379
|
+
async _attemptReconnectSafely(lastError) {
|
|
1380
|
+
if (this._reconnectInProgress) {
|
|
1381
|
+
this.realtimeDebug('[RECONNECT] Skipped — reconnect already in progress');
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (this._mqttConnected) {
|
|
1385
|
+
this.realtimeDebug('[RECONNECT] Skipped — MQTT is already connected');
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (this.safeDisconnect) {
|
|
1389
|
+
this.realtimeDebug('[RECONNECT] Skipped — safe disconnect active');
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
this._reconnectInProgress = true;
|
|
1393
|
+
if (this._reconnectTimeoutId) {
|
|
1394
|
+
clearTimeout(this._reconnectTimeoutId);
|
|
1395
|
+
this._reconnectTimeoutId = null;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (this.errorHandler && this.errorHandler.isRateLimited()) {
|
|
1399
|
+
const remaining = this.errorHandler.getRateLimitRemainingMs();
|
|
1400
|
+
this.realtimeDebug(`[RECONNECT] Rate limited. Waiting ${Math.round(remaining/1000)}s before attempting reconnect.`);
|
|
1401
|
+
await (0, shared_1.delay)(remaining);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
try {
|
|
1405
|
+
let attempt = 0;
|
|
1406
|
+
const maxAttempts = 20;
|
|
1407
|
+
let lastErrorType = lastError ? (this.errorHandler ? this.errorHandler.classifyError(lastError) : 'unknown') : 'unknown';
|
|
1408
|
+
|
|
1409
|
+
while (attempt < maxAttempts) {
|
|
1410
|
+
attempt++;
|
|
1411
|
+
try {
|
|
1412
|
+
if (this._mqttConnected) {
|
|
1413
|
+
this.realtimeDebug('[RECONNECT] MQTT connected during loop — stopping reconnect');
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
const shouldRefreshCreds = lastErrorType === 'auth_failure' || attempt > 3;
|
|
1418
|
+
if (shouldRefreshCreds) {
|
|
1419
|
+
try {
|
|
1420
|
+
const refreshed = await this._refreshMqttCredentials();
|
|
1421
|
+
if (refreshed) {
|
|
1422
|
+
this.realtimeDebug('[RECONNECT] Credentials refreshed successfully.');
|
|
1423
|
+
}
|
|
1424
|
+
} catch (e) {
|
|
1425
|
+
this.realtimeDebug('[RECONNECT] Credential refresh failed:', e?.message || e);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
this.realtimeDebug(`[RECONNECT] Attempt #${attempt}...`);
|
|
1430
|
+
try {
|
|
1431
|
+
await this.connect(this.initOptions);
|
|
1432
|
+
this.realtimeDebug(`[RECONNECT] Reconnect succeeded on attempt #${attempt}.`);
|
|
1433
|
+
if (this.errorHandler) this.errorHandler.resetErrorCounter();
|
|
1434
|
+
this.emit('reconnected', { attempt });
|
|
1435
|
+
return;
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
lastErrorType = this.errorHandler ? this.errorHandler.classifyError(e) : 'unknown';
|
|
1438
|
+
this.realtimeDebug(`[RECONNECT] Attempt #${attempt} failed (${lastErrorType}):`, e?.message || e);
|
|
1439
|
+
}
|
|
1440
|
+
} catch (e) {
|
|
1441
|
+
this.realtimeDebug('[RECONNECT] Loop error:', e?.message || e);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
let backoffMs;
|
|
1445
|
+
if (this.errorHandler) {
|
|
1446
|
+
backoffMs = this.errorHandler.getBackoffForType(lastErrorType, attempt);
|
|
1447
|
+
} else {
|
|
1448
|
+
const jitter = Math.floor(Math.random() * 3000);
|
|
1449
|
+
backoffMs = Math.min(60000, 2000 * Math.pow(2, attempt)) + jitter;
|
|
1450
|
+
}
|
|
1451
|
+
this.realtimeDebug(`[RECONNECT] Waiting ${Math.round(backoffMs/1000)}s before attempt #${attempt + 1} (type: ${lastErrorType}).`);
|
|
1452
|
+
await (0, shared_1.delay)(backoffMs);
|
|
1453
|
+
|
|
1454
|
+
if (attempt >= maxAttempts) {
|
|
1455
|
+
this.realtimeDebug(`[RECONNECT] Max attempts (${maxAttempts}) reached. Persisting state and stopping.`);
|
|
1456
|
+
try { await this._persistMqttSession(); } catch (e) {}
|
|
1457
|
+
this.emit('reconnect_failed', { attempts: attempt, lastErrorType });
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
} finally {
|
|
1462
|
+
this._reconnectInProgress = false;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* _ensureIrisSnapshotAndSubscribe()
|
|
1468
|
+
* - Ensure IRIS snapshot is present and subscribe to it.
|
|
1469
|
+
* - Tries a fresh fetch after connect to get the latest snapshot.
|
|
1470
|
+
*/
|
|
1471
|
+
async _ensureIrisSnapshotAndSubscribe() {
|
|
1472
|
+
try {
|
|
1473
|
+
let iris = this.initOptions?.irisData || null;
|
|
1474
|
+
let fetched = false;
|
|
1475
|
+
try {
|
|
1476
|
+
const fetchedInbox = await this.ig.direct.getInbox();
|
|
1477
|
+
if (fetchedInbox) {
|
|
1478
|
+
iris = fetchedInbox;
|
|
1479
|
+
fetched = true;
|
|
1480
|
+
this.realtimeDebug('[IRIS] Fetched fresh snapshot after connect.');
|
|
1481
|
+
}
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
this.realtimeDebug('[IRIS] Fresh fetch failed:', e?.message || e);
|
|
1484
|
+
}
|
|
1485
|
+
if (iris) {
|
|
1486
|
+
try {
|
|
1487
|
+
await this.irisSubscribe(iris);
|
|
1488
|
+
this.realtimeDebug('[IRIS] irisSubscribe executed.');
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
this.realtimeDebug('[IRIS] irisSubscribe failed:', e?.message || e);
|
|
1491
|
+
}
|
|
1492
|
+
} else {
|
|
1493
|
+
this.realtimeDebug('[IRIS] No iris data available to subscribe.');
|
|
1494
|
+
}
|
|
1495
|
+
return fetched;
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Attach message_sync listener to mqtt if available
|
|
1503
|
+
* - Some mqtt clients provide a listen() helper; attach to both numeric id and string names for compatibility.
|
|
1504
|
+
*/
|
|
1505
|
+
async _ensureMessageSyncListener() {
|
|
1506
|
+
try {
|
|
1507
|
+
if (this._messageSyncAttached) return;
|
|
1508
|
+
if (this._mqtt && typeof this._mqtt.listen === 'function') {
|
|
1509
|
+
try {
|
|
1510
|
+
const bound = (payload) => {
|
|
1511
|
+
try { this.emit('message_sync', payload); } catch (e) {}
|
|
1512
|
+
};
|
|
1513
|
+
try { this._mqtt.listen(146, bound); } catch (e) {}
|
|
1514
|
+
try { this._mqtt.listen('message_sync', bound); } catch (e) {}
|
|
1515
|
+
this._messageSyncAttached = true;
|
|
1516
|
+
this.realtimeDebug('[MESSAGE_SYNC] listener attached to mqtt (idempotent).');
|
|
1517
|
+
} catch (e) {
|
|
1518
|
+
this.realtimeDebug('[MESSAGE_SYNC] attach failed:', e?.message || e);
|
|
1519
|
+
}
|
|
1520
|
+
} else {
|
|
1521
|
+
this.realtimeDebug('[MESSAGE_SYNC] mqtt.listen not available on client.');
|
|
1522
|
+
}
|
|
1523
|
+
} catch (e) {}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
async _afterConnectHandlers() {
|
|
1527
|
+
try {
|
|
1528
|
+
this._lastMessageAt = Date.now();
|
|
1529
|
+
await this._ensureMessageSyncListener();
|
|
1530
|
+
await this._ensureIrisSnapshotAndSubscribe();
|
|
1531
|
+
} catch (e) {
|
|
1532
|
+
this.realtimeDebug('[AFTER_CONNECT] handlers error:', e?.message || e);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
enableHealthMonitor(options = {}) {
|
|
1537
|
+
if (this.healthMonitor) {
|
|
1538
|
+
this.healthMonitor.stop();
|
|
1539
|
+
}
|
|
1540
|
+
this.healthMonitor = new SessionHealthMonitor(this, {
|
|
1541
|
+
checkIntervalMs: options.checkIntervalMs || 30 * 60 * 1000,
|
|
1542
|
+
jitterMs: options.jitterMs || 5 * 60 * 1000,
|
|
1543
|
+
autoRelogin: options.autoRelogin !== undefined ? options.autoRelogin : !!options.credentials,
|
|
1544
|
+
credentials: options.credentials || null,
|
|
1545
|
+
maxConsecutiveFailures: options.maxConsecutiveFailures || 5,
|
|
1546
|
+
onSessionExpired: options.onSessionExpired || null,
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
this.healthMonitor.on('health_check', (data) => {
|
|
1550
|
+
this.emit('health_check', data);
|
|
1551
|
+
});
|
|
1552
|
+
this.healthMonitor.on('session_expired', (data) => {
|
|
1553
|
+
this.emit('session_expired', data);
|
|
1554
|
+
});
|
|
1555
|
+
this.healthMonitor.on('relogin_start', (data) => {
|
|
1556
|
+
this.emit('relogin_start', data);
|
|
1557
|
+
});
|
|
1558
|
+
this.healthMonitor.on('relogin_success', (data) => {
|
|
1559
|
+
this.emit('relogin_success', data);
|
|
1560
|
+
});
|
|
1561
|
+
this.healthMonitor.on('relogin_failed', (data) => {
|
|
1562
|
+
this.emit('relogin_failed', data);
|
|
1563
|
+
});
|
|
1564
|
+
this.healthMonitor.on('relogin_challenge', (data) => {
|
|
1565
|
+
this.emit('relogin_challenge', data);
|
|
1566
|
+
});
|
|
1567
|
+
this.healthMonitor.on('relogin_needed', (data) => {
|
|
1568
|
+
this.emit('relogin_needed', data);
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
if (this.persistentLogger) {
|
|
1572
|
+
this.healthMonitor.on('log', (line) => {
|
|
1573
|
+
this.persistentLogger.info(line);
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
this.healthMonitor.start();
|
|
1578
|
+
this.realtimeDebug('[HEALTH] Session health monitor enabled');
|
|
1579
|
+
return this.healthMonitor;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
enablePersistentLogger(options = {}) {
|
|
1583
|
+
if (this.persistentLogger) {
|
|
1584
|
+
this.persistentLogger.stop();
|
|
1585
|
+
}
|
|
1586
|
+
this.persistentLogger = new PersistentLogger({
|
|
1587
|
+
logDir: options.logDir || './logs',
|
|
1588
|
+
prefix: options.logPrefix || 'instagram-mqtt',
|
|
1589
|
+
maxFileSize: options.maxLogFileSize || 10 * 1024 * 1024,
|
|
1590
|
+
maxFiles: options.maxLogFiles || 5,
|
|
1591
|
+
flushIntervalMs: options.logFlushIntervalMs || 30000,
|
|
1592
|
+
logToConsole: options.logToConsole !== false,
|
|
1593
|
+
logLevel: options.logLevel || 'info',
|
|
1594
|
+
});
|
|
1595
|
+
this.persistentLogger.start();
|
|
1596
|
+
|
|
1597
|
+
const originalDebug = this.realtimeDebug.bind(this);
|
|
1598
|
+
this.realtimeDebug = (...args) => {
|
|
1599
|
+
originalDebug(...args);
|
|
1600
|
+
if (this.persistentLogger && this.persistentLogger._started) {
|
|
1601
|
+
this.persistentLogger.info(...args);
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
this.on('error', (err) => {
|
|
1606
|
+
if (this.persistentLogger) this.persistentLogger.error('[MQTT_ERROR]', err?.message || err);
|
|
1607
|
+
});
|
|
1608
|
+
this.on('disconnect', (reason) => {
|
|
1609
|
+
if (this.persistentLogger) this.persistentLogger.warn('[DISCONNECT]', reason || 'unknown');
|
|
1610
|
+
});
|
|
1611
|
+
this.on('reconnected', (data) => {
|
|
1612
|
+
if (this.persistentLogger) this.persistentLogger.info('[RECONNECTED]', JSON.stringify(data));
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
this.realtimeDebug('[LOGGER] Persistent file logger enabled at', options.logDir || './logs');
|
|
1616
|
+
return this.persistentLogger;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
getHealthStats() {
|
|
1620
|
+
if (!this.healthMonitor) return null;
|
|
1621
|
+
return this.healthMonitor.getStats();
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
getLoggerStats() {
|
|
1625
|
+
if (!this.persistentLogger) return null;
|
|
1626
|
+
return this.persistentLogger.getStats();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* connectFromSavedSession(authStateHelper, options)
|
|
1631
|
+
* - Reconstructs connect options from saved authState and then calls connect()
|
|
1632
|
+
* - Attempts to fetch a fresh IRIS snapshot (up to a few attempts) for safety.
|
|
1633
|
+
*/
|
|
1634
|
+
async connectFromSavedSession(authStateHelper, options = {}) {
|
|
1635
|
+
if (!authStateHelper) {
|
|
1636
|
+
throw new Error('authStateHelper is required - use useMultiFileAuthState()');
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
if (this._mqttConnected) {
|
|
1640
|
+
console.log('[RealtimeClient] connectFromSavedSession skipped — already connected.');
|
|
1641
|
+
return this;
|
|
1642
|
+
}
|
|
1643
|
+
if (this._connectInProgress) {
|
|
1644
|
+
console.log('[RealtimeClient] connectFromSavedSession skipped — connect already in progress.');
|
|
1645
|
+
return this;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
this._connectInProgress = true;
|
|
1649
|
+
console.log('[RealtimeClient] Connecting from saved session...');
|
|
1650
|
+
try { this._attachedAuthState = authStateHelper; } catch (e) {}
|
|
1651
|
+
|
|
1652
|
+
const savedOptions = authStateHelper.getMqttConnectOptions?.() || {};
|
|
1653
|
+
let irisData = options.irisData || savedOptions.irisData || null;
|
|
1654
|
+
|
|
1655
|
+
let fetchedInbox = null;
|
|
1656
|
+
const shouldForceFetch = true;
|
|
1657
|
+
if (shouldForceFetch) {
|
|
1658
|
+
const maxAttempts = 3;
|
|
1659
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1660
|
+
try {
|
|
1661
|
+
console.log(`[RealtimeClient] Attempting to fetch fresh IRIS inbox snapshot (attempt ${attempt}/${maxAttempts})...`);
|
|
1662
|
+
fetchedInbox = await this.ig.direct.getInbox();
|
|
1663
|
+
if (fetchedInbox) {
|
|
1664
|
+
irisData = fetchedInbox;
|
|
1665
|
+
console.log('[RealtimeClient] Fetched IRIS snapshot successfully.');
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
} catch (e) {
|
|
1669
|
+
const msg = (e?.message || String(e)).toLowerCase();
|
|
1670
|
+
const isAuthIssue = msg.includes('login_required') || msg.includes('401') || msg.includes('403') || msg.includes('not authorized') || msg.includes('checkpoint');
|
|
1671
|
+
console.warn(`[RealtimeClient] Failed to fetch IRIS snapshot (attempt ${attempt}):`, e?.message || e);
|
|
1672
|
+
if (isAuthIssue) {
|
|
1673
|
+
console.warn('[RealtimeClient] IRIS fetch failed due to auth issue — session may be expired.');
|
|
1674
|
+
this.emit('warning', { type: 'auth_issue', message: 'Session may be expired - IRIS fetch returned auth error', error: e?.message });
|
|
1675
|
+
break;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
try { await (0, shared_1.delay)(1000 * attempt); } catch (e) {}
|
|
1679
|
+
}
|
|
1680
|
+
if (!fetchedInbox) {
|
|
1681
|
+
if (savedOptions.irisData) {
|
|
1682
|
+
irisData = savedOptions.irisData;
|
|
1683
|
+
console.warn('[RealtimeClient] Could not fetch fresh IRIS snapshot — falling back to saved irisData (may be stale).');
|
|
1684
|
+
} else if (!irisData) {
|
|
1685
|
+
console.warn('[RealtimeClient] No IRIS snapshot available (neither fetched nor saved). Proceeding without irisData — server may not replay missed events.');
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const connectOptions = {
|
|
1691
|
+
graphQlSubs: options.graphQlSubs || savedOptions.graphQlSubs || this.defaultGraphQlSubs,
|
|
1692
|
+
skywalkerSubs: options.skywalkerSubs || savedOptions.skywalkerSubs || this.defaultSkywalkerSubs,
|
|
1693
|
+
irisData,
|
|
1694
|
+
...options
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
console.log('[RealtimeClient] Using saved subscriptions:', {
|
|
1698
|
+
graphQlSubs: connectOptions.graphQlSubs,
|
|
1699
|
+
skywalkerSubs: connectOptions.skywalkerSubs,
|
|
1700
|
+
hasIrisData: !!connectOptions.irisData
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
try {
|
|
1704
|
+
const d = authStateHelper.getData?.() || authStateHelper.data || {};
|
|
1705
|
+
const mqttAuth = d.mqttAuth || null;
|
|
1706
|
+
if (mqttAuth && mqttAuth.expiresAt) {
|
|
1707
|
+
const t = new Date(mqttAuth.expiresAt).getTime();
|
|
1708
|
+
if (!isNaN(t) && Date.now() > t) {
|
|
1709
|
+
console.warn('[RealtimeClient] Warning: saved mqttAuth token appears expired.');
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
} catch (e) {}
|
|
1713
|
+
|
|
1714
|
+
await this.connect(connectOptions);
|
|
1715
|
+
|
|
1716
|
+
if (authStateHelper.saveMqttSession) {
|
|
1717
|
+
try {
|
|
1718
|
+
await authStateHelper.saveMqttSession(this);
|
|
1719
|
+
console.log('[RealtimeClient] MQTT session saved after connect');
|
|
1720
|
+
} catch (e) {
|
|
1721
|
+
console.warn('[RealtimeClient] Failed to save MQTT session:', e.message);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
return this;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* saveSession(authStateHelper)
|
|
1730
|
+
* - Helper to persist current MQTT session to auth helper if available.
|
|
1731
|
+
*/
|
|
1732
|
+
async saveSession(authStateHelper) {
|
|
1733
|
+
if (!authStateHelper || !authStateHelper.saveMqttSession) {
|
|
1734
|
+
console.warn('[RealtimeClient] No authStateHelper provided');
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
await authStateHelper.saveMqttSession(this);
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* disconnect()
|
|
1743
|
+
* - Perform a graceful shutdown: clear timers and disconnect mqtt client.
|
|
1744
|
+
*/
|
|
1745
|
+
disconnect() {
|
|
1746
|
+
this.safeDisconnect = true;
|
|
1747
|
+
try {
|
|
1748
|
+
if (this._foregroundTimer) {
|
|
1749
|
+
clearInterval(this._foregroundTimer);
|
|
1750
|
+
this._foregroundTimer = null;
|
|
1751
|
+
}
|
|
1752
|
+
if (this._syncTimer) {
|
|
1753
|
+
clearInterval(this._syncTimer);
|
|
1754
|
+
this._syncTimer = null;
|
|
1755
|
+
}
|
|
1756
|
+
if (this._trafficWatchdog) {
|
|
1757
|
+
clearInterval(this._trafficWatchdog);
|
|
1758
|
+
this._trafficWatchdog = null;
|
|
1759
|
+
}
|
|
1760
|
+
if (this._heartbeatTimer) {
|
|
1761
|
+
clearInterval(this._heartbeatTimer);
|
|
1762
|
+
this._heartbeatTimer = null;
|
|
1763
|
+
}
|
|
1764
|
+
// clear periodic mqtt persist
|
|
1765
|
+
if (this._mqttSessionPersistIntervalId) {
|
|
1766
|
+
clearInterval(this._mqttSessionPersistIntervalId);
|
|
1767
|
+
this._mqttSessionPersistIntervalId = null;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Clear the active keepalive/query timer if running
|
|
1771
|
+
if (this._activeKeepaliveTimer) {
|
|
1772
|
+
clearInterval(this._activeKeepaliveTimer);
|
|
1773
|
+
this._activeKeepaliveTimer = null;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// clear credential refresh timer
|
|
1777
|
+
if (this._mqttAuthRefreshTimer) {
|
|
1778
|
+
try { clearTimeout(this._mqttAuthRefreshTimer); } catch (e) {}
|
|
1779
|
+
this._mqttAuthRefreshTimer = null;
|
|
1780
|
+
}
|
|
1781
|
+
} catch (e) {}
|
|
1782
|
+
// persist final session snapshot
|
|
1783
|
+
try { this._persistMqttSession(); } catch (e) {}
|
|
1784
|
+
return this.mqtt?.disconnect() ?? Promise.resolve();
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* graphQlSubscribe(sub)
|
|
1789
|
+
* - Helper that delegates to Commands.updateSubscriptions (which uses qos 0).
|
|
1790
|
+
*/
|
|
1791
|
+
graphQlSubscribe(sub) {
|
|
1792
|
+
sub = typeof sub === 'string' ? [sub] : sub;
|
|
1793
|
+
if (!this.commands) {
|
|
1794
|
+
throw new mqtts_1.IllegalStateError('connect() must be called before graphQlSubscribe()');
|
|
1795
|
+
}
|
|
1796
|
+
// If the caller provided an empty array, ensure defaults are used
|
|
1797
|
+
if (Array.isArray(sub) && sub.length === 0) sub = this.defaultGraphQlSubs;
|
|
1798
|
+
this.realtimeDebug(`Subscribing with GraphQL to ${sub.join(', ')}`);
|
|
1799
|
+
return this.commands.updateSubscriptions({
|
|
1800
|
+
topic: constants_1.Topics.REALTIME_SUB,
|
|
1801
|
+
data: {
|
|
1802
|
+
sub,
|
|
1803
|
+
},
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* skywalkerSubscribe(sub)
|
|
1809
|
+
* - Delegate to Commands.updateSubscriptions (pubsub topic).
|
|
1810
|
+
*/
|
|
1811
|
+
skywalkerSubscribe(sub) {
|
|
1812
|
+
sub = typeof sub === 'string' ? [sub] : sub;
|
|
1813
|
+
if (!this.commands) {
|
|
1814
|
+
throw new mqtts_1.IllegalStateError('connect() must be called before skywalkerSubscribe()');
|
|
1815
|
+
}
|
|
1816
|
+
// If empty, use defaults
|
|
1817
|
+
if (Array.isArray(sub) && sub.length === 0) sub = this.defaultSkywalkerSubs;
|
|
1818
|
+
this.realtimeDebug(`Subscribing with Skywalker to ${sub.join(', ')}`);
|
|
1819
|
+
return this.commands.updateSubscriptions({
|
|
1820
|
+
topic: constants_1.Topics.PUBSUB,
|
|
1821
|
+
data: {
|
|
1822
|
+
sub,
|
|
1823
|
+
},
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* irisSubscribe({ seq_id, snapshot_at_ms })
|
|
1829
|
+
* - Subscribe to IRIS using the provided snapshot properties.
|
|
1830
|
+
*/
|
|
1831
|
+
irisSubscribe({ seq_id, snapshot_at_ms, }) {
|
|
1832
|
+
if (!this.commands) {
|
|
1833
|
+
throw new mqtts_1.IllegalStateError('connect() must be called before irisSubscribe()');
|
|
1834
|
+
}
|
|
1835
|
+
this.realtimeDebug(`Iris Sub to: seqId: ${seq_id}, snapshot: ${snapshot_at_ms}`);
|
|
1836
|
+
return this.commands.updateSubscriptions({
|
|
1837
|
+
topic: constants_1.Topics.IRIS_SUB,
|
|
1838
|
+
data: {
|
|
1839
|
+
seq_id,
|
|
1840
|
+
snapshot_at_ms,
|
|
1841
|
+
snapshot_app_version: this.ig.state.appVersion,
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Start an "active query" keepalive loop
|
|
1848
|
+
* - This sends a lightweight PUBSUB foreground pulse and a REALTIME_SUB reaffirmation
|
|
1849
|
+
* on a periodic basis to keep the server-side connection state active.
|
|
1850
|
+
*
|
|
1851
|
+
* Behavior & protections:
|
|
1852
|
+
* - Idle-aware: will skip sending if client received traffic within idleThresholdMs (to avoid unnecessary traffic).
|
|
1853
|
+
* - Interval configurable via initOptions.activeKeepaliveMs (default 45s).
|
|
1854
|
+
* - GraphQL subs reaffirmation uses initOptions.graphQlSubs or defaultGraphQlSubs.
|
|
1855
|
+
*/
|
|
1856
|
+
_startActiveQueryKeepalive() {
|
|
1857
|
+
try {
|
|
1858
|
+
// Clear any existing timer idempotently
|
|
1859
|
+
if (this._activeKeepaliveTimer) {
|
|
1860
|
+
clearInterval(this._activeKeepaliveTimer);
|
|
1861
|
+
this._activeKeepaliveTimer = null;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const ms = (this.initOptions && this.initOptions.activeKeepaliveMs) ? this.initOptions.activeKeepaliveMs : 25000;
|
|
1865
|
+
const idleThresholdMs = (this.initOptions && this.initOptions.activeKeepaliveIdleThresholdMs) ? this.initOptions.activeKeepaliveIdleThresholdMs : 15000;
|
|
1866
|
+
|
|
1867
|
+
// small wrapper to avoid unhandled rejection inside setInterval
|
|
1868
|
+
this._activeKeepaliveTimer = setInterval(async () => {
|
|
1869
|
+
try {
|
|
1870
|
+
if (!this.commands) return;
|
|
1871
|
+
|
|
1872
|
+
// Do not send keepalive if we received traffic recently — avoids noisy pulses during active use.
|
|
1873
|
+
const idle = Date.now() - (this._lastMessageAt || 0);
|
|
1874
|
+
if (idle < idleThresholdMs) {
|
|
1875
|
+
// skip sending if not idle enough
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// 1) PUBSUB "foreground pulse" with a tiny timestamp payload (safe, no side-effects)
|
|
1880
|
+
try {
|
|
1881
|
+
await this.commands.updateSubscriptions({
|
|
1882
|
+
topic: constants_1.Topics.PUBSUB,
|
|
1883
|
+
data: { foreground: true, keepalive_ts: Date.now() }
|
|
1884
|
+
});
|
|
1885
|
+
// mark activity so the traffic watchdog won't be triggered
|
|
1886
|
+
this._lastMessageAt = Date.now(); // <--- update last-activity timestamp after active keepalive
|
|
1887
|
+
} catch (e) {
|
|
1888
|
+
// log but continue to attempt realtime-sub reaffirmation
|
|
1889
|
+
this.realtimeDebug('[ACTIVE_QUERY] PUBSUB foreground pulse failed:', e?.message || e);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// 2) REALTIME_SUB reaffirmation of GraphQL subs (lightweight)
|
|
1893
|
+
try {
|
|
1894
|
+
const subs = (this.initOptions && this.initOptions.graphQlSubs && this.initOptions.graphQlSubs.length) ? this.initOptions.graphQlSubs : this.defaultGraphQlSubs;
|
|
1895
|
+
await this.commands.updateSubscriptions({
|
|
1896
|
+
topic: constants_1.Topics.REALTIME_SUB,
|
|
1897
|
+
data: { sub: subs, keepalive_ts: Date.now() }
|
|
1898
|
+
});
|
|
1899
|
+
// mark activity after realtime-sub reaffirmation as well
|
|
1900
|
+
this._lastMessageAt = Date.now(); // <--- update last-activity timestamp after realtime-sub reaffirmation
|
|
1901
|
+
} catch (e) {
|
|
1902
|
+
this.realtimeDebug('[ACTIVE_QUERY] REALTIME_SUB reaffirmation failed:', e?.message || e);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
this.realtimeDebug('[ACTIVE_QUERY] keepalive query sent (idle ms: ' + idle + ')');
|
|
1906
|
+
} catch (e) {
|
|
1907
|
+
this.realtimeDebug('[ACTIVE_QUERY] unexpected error in keepalive loop:', e?.message || e);
|
|
1908
|
+
}
|
|
1909
|
+
}, ms);
|
|
1910
|
+
} catch (e) {
|
|
1911
|
+
this.realtimeDebug('[ACTIVE_QUERY] could not start keepalive timer:', e?.message || e);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
exports.RealtimeClient = RealtimeClient;
|