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.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3677 -0
  3. package/dist/constants/constants.js +342 -0
  4. package/dist/constants/index.js +58 -0
  5. package/dist/core/client.js +419 -0
  6. package/dist/core/nav-chain.js +282 -0
  7. package/dist/core/repository.js +7 -0
  8. package/dist/core/request.js +390 -0
  9. package/dist/core/state.js +1473 -0
  10. package/dist/core/utils.js +786 -0
  11. package/dist/downloadMedia.js +381 -0
  12. package/dist/errors/index.d.ts +16 -0
  13. package/dist/errors/index.js +38 -0
  14. package/dist/errors/index.js.map +1 -0
  15. package/dist/extend.js +167 -0
  16. package/dist/fbns/fbns.client.d.ts +32 -0
  17. package/dist/fbns/fbns.client.events.d.ts +41 -0
  18. package/dist/fbns/fbns.client.events.js +3 -0
  19. package/dist/fbns/fbns.client.events.js.map +1 -0
  20. package/dist/fbns/fbns.client.js +252 -0
  21. package/dist/fbns/fbns.client.js.map +1 -0
  22. package/dist/fbns/fbns.device-auth.d.ts +17 -0
  23. package/dist/fbns/fbns.device-auth.js +54 -0
  24. package/dist/fbns/fbns.device-auth.js.map +1 -0
  25. package/dist/fbns/fbns.types.d.ts +83 -0
  26. package/dist/fbns/fbns.types.js +3 -0
  27. package/dist/fbns/fbns.types.js.map +1 -0
  28. package/dist/fbns/fbns.utilities.d.ts +2 -0
  29. package/dist/fbns/fbns.utilities.js +79 -0
  30. package/dist/fbns/fbns.utilities.js.map +1 -0
  31. package/dist/fbns/index.d.ts +4 -0
  32. package/dist/fbns/index.js +21 -0
  33. package/dist/fbns/index.js.map +1 -0
  34. package/dist/index.js +139 -0
  35. package/dist/mqtt-shim.d.ts +96 -0
  36. package/dist/mqtt-shim.js +15 -0
  37. package/dist/mqttot/index.d.ts +4 -0
  38. package/dist/mqttot/index.js +21 -0
  39. package/dist/mqttot/index.js.map +1 -0
  40. package/dist/mqttot/mqttot.client.d.ts +39 -0
  41. package/dist/mqttot/mqttot.client.js +318 -0
  42. package/dist/mqttot/mqttot.client.js.map +1 -0
  43. package/dist/mqttot/mqttot.connect.request.packet.d.ts +7 -0
  44. package/dist/mqttot/mqttot.connect.request.packet.js +9 -0
  45. package/dist/mqttot/mqttot.connect.request.packet.js.map +1 -0
  46. package/dist/mqttot/mqttot.connect.response.packet.d.ts +7 -0
  47. package/dist/mqttot/mqttot.connect.response.packet.js +24 -0
  48. package/dist/mqttot/mqttot.connect.response.packet.js.map +1 -0
  49. package/dist/mqttot/mqttot.connection.d.ts +57 -0
  50. package/dist/mqttot/mqttot.connection.js +79 -0
  51. package/dist/mqttot/mqttot.connection.js.map +1 -0
  52. package/dist/package.json +59 -0
  53. package/dist/realtime/commands/commands.d.ts +15 -0
  54. package/dist/realtime/commands/commands.js +71 -0
  55. package/dist/realtime/commands/commands.js.map +1 -0
  56. package/dist/realtime/commands/direct.commands.d.ts +75 -0
  57. package/dist/realtime/commands/direct.commands.js +417 -0
  58. package/dist/realtime/commands/direct.commands.js.map +1 -0
  59. package/dist/realtime/commands/enhanced.direct.commands.js +1731 -0
  60. package/dist/realtime/commands/enhanced.direct.commands.js.bak +967 -0
  61. package/dist/realtime/commands/index.d.ts +2 -0
  62. package/dist/realtime/commands/index.js +20 -0
  63. package/dist/realtime/commands/index.js.map +1 -0
  64. package/dist/realtime/delta-sync.manager.js +293 -0
  65. package/dist/realtime/features/dm-sender.js +88 -0
  66. package/dist/realtime/features/error-handler.js +185 -0
  67. package/dist/realtime/features/gap-handler.js +61 -0
  68. package/dist/realtime/features/persistent-logger.js +186 -0
  69. package/dist/realtime/features/presence.manager.js +66 -0
  70. package/dist/realtime/features/session-health-monitor.js +345 -0
  71. package/dist/realtime/index.js +30 -0
  72. package/dist/realtime/messages/app-presence.event.d.ts +9 -0
  73. package/dist/realtime/messages/app-presence.event.js +3 -0
  74. package/dist/realtime/messages/app-presence.event.js.map +1 -0
  75. package/dist/realtime/messages/index.d.ts +3 -0
  76. package/dist/realtime/messages/index.js +20 -0
  77. package/dist/realtime/messages/index.js.map +1 -0
  78. package/dist/realtime/messages/message-sync.message.d.ts +222 -0
  79. package/dist/realtime/messages/message-sync.message.js +43 -0
  80. package/dist/realtime/messages/message-sync.message.js.map +1 -0
  81. package/dist/realtime/messages/realtime-sub.direct.data.d.ts +11 -0
  82. package/dist/realtime/messages/realtime-sub.direct.data.js +3 -0
  83. package/dist/realtime/messages/realtime-sub.direct.data.js.map +1 -0
  84. package/dist/realtime/messages/thread-update.message.d.ts +68 -0
  85. package/dist/realtime/messages/thread-update.message.js +3 -0
  86. package/dist/realtime/messages/thread-update.message.js.map +1 -0
  87. package/dist/realtime/mixins/index.d.ts +3 -0
  88. package/dist/realtime/mixins/index.js +20 -0
  89. package/dist/realtime/mixins/index.js.map +1 -0
  90. package/dist/realtime/mixins/message-sync.mixin.d.ts +8 -0
  91. package/dist/realtime/mixins/message-sync.mixin.js +596 -0
  92. package/dist/realtime/mixins/message-sync.mixin.js.map +1 -0
  93. package/dist/realtime/mixins/mixin.d.ts +19 -0
  94. package/dist/realtime/mixins/mixin.js +41 -0
  95. package/dist/realtime/mixins/mixin.js.map +1 -0
  96. package/dist/realtime/mixins/presence-typing.mixin.js +33 -0
  97. package/dist/realtime/mixins/realtime-sub.mixin.d.ts +8 -0
  98. package/dist/realtime/mixins/realtime-sub.mixin.js +181 -0
  99. package/dist/realtime/mixins/realtime-sub.mixin.js.map +1 -0
  100. package/dist/realtime/parsers/graphql-parser.js +43 -0
  101. package/dist/realtime/parsers/graphql.parser.d.ts +15 -0
  102. package/dist/realtime/parsers/graphql.parser.js +22 -0
  103. package/dist/realtime/parsers/graphql.parser.js.map +1 -0
  104. package/dist/realtime/parsers/index.d.ts +6 -0
  105. package/dist/realtime/parsers/index.js +23 -0
  106. package/dist/realtime/parsers/index.js.map +1 -0
  107. package/dist/realtime/parsers/iris-parser.js +43 -0
  108. package/dist/realtime/parsers/iris.parser.d.ts +17 -0
  109. package/dist/realtime/parsers/iris.parser.js +10 -0
  110. package/dist/realtime/parsers/iris.parser.js.map +1 -0
  111. package/dist/realtime/parsers/json-parser.js +43 -0
  112. package/dist/realtime/parsers/json.parser.d.ts +6 -0
  113. package/dist/realtime/parsers/json.parser.js +10 -0
  114. package/dist/realtime/parsers/json.parser.js.map +1 -0
  115. package/dist/realtime/parsers/parser.d.ts +9 -0
  116. package/dist/realtime/parsers/parser.js +3 -0
  117. package/dist/realtime/parsers/parser.js.map +1 -0
  118. package/dist/realtime/parsers/region-hint-parser.js +43 -0
  119. package/dist/realtime/parsers/region-hint.parser.d.ts +12 -0
  120. package/dist/realtime/parsers/region-hint.parser.js +15 -0
  121. package/dist/realtime/parsers/region-hint.parser.js.map +1 -0
  122. package/dist/realtime/parsers/skywalker-parser.js +43 -0
  123. package/dist/realtime/parsers/skywalker.parser.d.ts +12 -0
  124. package/dist/realtime/parsers/skywalker.parser.js +15 -0
  125. package/dist/realtime/parsers/skywalker.parser.js.map +1 -0
  126. package/dist/realtime/parsers-advanced.js +158 -0
  127. package/dist/realtime/proto/common.proto +38 -0
  128. package/dist/realtime/proto/direct.proto +65 -0
  129. package/dist/realtime/proto/ig-messages.proto +83 -0
  130. package/dist/realtime/proto/iris.proto +188 -0
  131. package/dist/realtime/proto-parser.js +195 -0
  132. package/dist/realtime/protocols/iris.handshake.js +74 -0
  133. package/dist/realtime/protocols/proto-definitions.js +80 -0
  134. package/dist/realtime/protocols/skywalker.protocol.js +91 -0
  135. package/dist/realtime/realtime.client.events.js +3 -0
  136. package/dist/realtime/realtime.client.js +1915 -0
  137. package/dist/realtime/realtime.service.js +462 -0
  138. package/dist/realtime/reconnect.manager.js +88 -0
  139. package/dist/realtime/session.manager.js +121 -0
  140. package/dist/realtime/subscriptions/graphql.subscription.d.ts +47 -0
  141. package/dist/realtime/subscriptions/graphql.subscription.js +99 -0
  142. package/dist/realtime/subscriptions/graphql.subscription.js.map +1 -0
  143. package/dist/realtime/subscriptions/index.d.ts +2 -0
  144. package/dist/realtime/subscriptions/index.js +19 -0
  145. package/dist/realtime/subscriptions/index.js.map +1 -0
  146. package/dist/realtime/subscriptions/skywalker.subscription.d.ts +4 -0
  147. package/dist/realtime/subscriptions/skywalker.subscription.js +13 -0
  148. package/dist/realtime/subscriptions/skywalker.subscription.js.map +1 -0
  149. package/dist/realtime/topic-map.js +71 -0
  150. package/dist/realtime/topic.js +80 -0
  151. package/dist/repositories/account.repository.js +575 -0
  152. package/dist/repositories/bloks.repository.js +70 -0
  153. package/dist/repositories/captcha.repository.js +44 -0
  154. package/dist/repositories/challenge.repository.js +120 -0
  155. package/dist/repositories/clip.repository.js +165 -0
  156. package/dist/repositories/close-friends.repository.js +46 -0
  157. package/dist/repositories/collection.repository.js +68 -0
  158. package/dist/repositories/direct-thread.repository.js +446 -0
  159. package/dist/repositories/direct.repository.js +232 -0
  160. package/dist/repositories/explore.repository.js +70 -0
  161. package/dist/repositories/fbsearch.repository.js +140 -0
  162. package/dist/repositories/feed.repository.js +245 -0
  163. package/dist/repositories/friendship.repository.js +296 -0
  164. package/dist/repositories/fundraiser.repository.js +49 -0
  165. package/dist/repositories/hashtag.repository.js +99 -0
  166. package/dist/repositories/highlights.repository.js +121 -0
  167. package/dist/repositories/insights.repository.js +82 -0
  168. package/dist/repositories/location.repository.js +84 -0
  169. package/dist/repositories/media.repository.js +395 -0
  170. package/dist/repositories/multiple-accounts.repository.js +41 -0
  171. package/dist/repositories/news.repository.js +35 -0
  172. package/dist/repositories/note.repository.js +57 -0
  173. package/dist/repositories/notification.repository.js +79 -0
  174. package/dist/repositories/share.repository.js +35 -0
  175. package/dist/repositories/signup.repository.js +218 -0
  176. package/dist/repositories/story.repository.js +290 -0
  177. package/dist/repositories/timeline.repository.js +60 -0
  178. package/dist/repositories/totp.repository.js +139 -0
  179. package/dist/repositories/track.repository.js +53 -0
  180. package/dist/repositories/upload.repository.js +204 -0
  181. package/dist/repositories/user.repository.js +360 -0
  182. package/dist/sendmedia/index.js +27 -0
  183. package/dist/sendmedia/sendFile.js +72 -0
  184. package/dist/sendmedia/sendPhoto.js +142 -0
  185. package/dist/sendmedia/sendRavenPhoto.js +153 -0
  186. package/dist/sendmedia/sendRavenVideo.js +158 -0
  187. package/dist/sendmedia/uploadPhoto.js +107 -0
  188. package/dist/sendmedia/uploadfFile.js +130 -0
  189. package/dist/services/live.service.js +139 -0
  190. package/dist/services/search.service.js +115 -0
  191. package/dist/shared/index.js +96 -0
  192. package/dist/shared/shared.js +86 -0
  193. package/dist/thrift/index.d.ts +3 -0
  194. package/dist/thrift/index.js +20 -0
  195. package/dist/thrift/index.js.map +1 -0
  196. package/dist/thrift/thrift.d.ts +59 -0
  197. package/dist/thrift/thrift.js +101 -0
  198. package/dist/thrift/thrift.js.map +1 -0
  199. package/dist/thrift/thrift.reading.d.ts +41 -0
  200. package/dist/thrift/thrift.reading.js +327 -0
  201. package/dist/thrift/thrift.reading.js.map +1 -0
  202. package/dist/thrift/thrift.writing.d.ts +44 -0
  203. package/dist/thrift/thrift.writing.js +342 -0
  204. package/dist/thrift/thrift.writing.js.map +1 -0
  205. package/dist/types/index.js +285 -0
  206. package/dist/useMultiFileAuthState.js +1768 -0
  207. package/dist/utils/helper-1.js +1 -0
  208. package/dist/utils/helper-10.js +1 -0
  209. package/dist/utils/helper-11.js +1 -0
  210. package/dist/utils/helper-12.js +1 -0
  211. package/dist/utils/helper-13.js +1 -0
  212. package/dist/utils/helper-14.js +1 -0
  213. package/dist/utils/helper-15.js +1 -0
  214. package/dist/utils/helper-16.js +1 -0
  215. package/dist/utils/helper-17.js +1 -0
  216. package/dist/utils/helper-18.js +1 -0
  217. package/dist/utils/helper-19.js +1 -0
  218. package/dist/utils/helper-2.js +1 -0
  219. package/dist/utils/helper-20.js +1 -0
  220. package/dist/utils/helper-21.js +1 -0
  221. package/dist/utils/helper-22.js +1 -0
  222. package/dist/utils/helper-23.js +1 -0
  223. package/dist/utils/helper-24.js +1 -0
  224. package/dist/utils/helper-25.js +1 -0
  225. package/dist/utils/helper-26.js +1 -0
  226. package/dist/utils/helper-27.js +1 -0
  227. package/dist/utils/helper-28.js +1 -0
  228. package/dist/utils/helper-29.js +1 -0
  229. package/dist/utils/helper-3.js +1 -0
  230. package/dist/utils/helper-30.js +1 -0
  231. package/dist/utils/helper-4.js +1 -0
  232. package/dist/utils/helper-5.js +1 -0
  233. package/dist/utils/helper-6.js +1 -0
  234. package/dist/utils/helper-7.js +1 -0
  235. package/dist/utils/helper-8.js +1 -0
  236. package/dist/utils/helper-9.js +1 -0
  237. package/dist/utils/index.js +280 -0
  238. package/dist/utils/insta-mqtt-helper.js +128 -0
  239. package/examples/listen-to-messages.js +86 -0
  240. 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;