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,1768 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { CookieJar } = require('tough-cookie');
|
|
6
|
+
const util = require('util');
|
|
7
|
+
const EventEmitter = require('events');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
const FILE_NAMES = {
|
|
11
|
+
creds: 'creds.json',
|
|
12
|
+
device: 'device.json',
|
|
13
|
+
cookies: 'cookies.json',
|
|
14
|
+
mqttSession: 'mqtt-session.json',
|
|
15
|
+
subscriptions: 'subscriptions.json',
|
|
16
|
+
seqIds: 'seq-ids.json',
|
|
17
|
+
appState: 'app-state.json',
|
|
18
|
+
|
|
19
|
+
// Newly added MQTT / realtime persistence files
|
|
20
|
+
irisState: 'iris-state.json', // subscription / iris specific state
|
|
21
|
+
mqttTopics: 'mqtt-topics.json', // observed topics
|
|
22
|
+
mqttCapabilities: 'mqtt-capabilities.json', // realtime capabilities per device/version
|
|
23
|
+
mqttAuth: 'mqtt-auth.json', // mqtt auth token / jwt (if available)
|
|
24
|
+
lastPublishIds: 'last-publish-ids.json', // last published message ids / ack info
|
|
25
|
+
|
|
26
|
+
// Added utility files
|
|
27
|
+
loginHistory: 'login-history.json',
|
|
28
|
+
usageStats: 'usage-stats.json',
|
|
29
|
+
accountsIndex: 'accounts.json',
|
|
30
|
+
|
|
31
|
+
// MQTT helpers files
|
|
32
|
+
mqttHealth: 'mqtt-health.json',
|
|
33
|
+
mqttReconnect: 'mqtt-reconnect.json',
|
|
34
|
+
mqttMessageCache: 'mqtt-message-cache.json',
|
|
35
|
+
mqttOutbox: 'mqtt-outbox.json',
|
|
36
|
+
mqttSubscriptionHealth: 'mqtt-subscription-health.json',
|
|
37
|
+
mqttTraffic: 'mqtt-traffic.json',
|
|
38
|
+
mqttRisk: 'mqtt-risk.json'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
class MultiFileAuthState extends EventEmitter {
|
|
42
|
+
constructor(folder) {
|
|
43
|
+
super();
|
|
44
|
+
this.folder = folder;
|
|
45
|
+
this._saveDebounceTimer = null;
|
|
46
|
+
this._saveDebounceMs = 500;
|
|
47
|
+
this._dirty = new Set();
|
|
48
|
+
|
|
49
|
+
// Auto-refresh fields
|
|
50
|
+
this._autoRefreshTimer = null;
|
|
51
|
+
this._autoRefreshIntervalMs = 5 * 60 * 1000; // default 5 minutes
|
|
52
|
+
this._autoRefreshClient = null;
|
|
53
|
+
|
|
54
|
+
// Reconnect/backoff runtime
|
|
55
|
+
this._reconnectBackoff = [2000, 5000, 15000, 60000];
|
|
56
|
+
this._reconnectAttempts = 0;
|
|
57
|
+
this._reconnectTimer = null;
|
|
58
|
+
|
|
59
|
+
this.data = {
|
|
60
|
+
creds: null,
|
|
61
|
+
device: null,
|
|
62
|
+
cookies: null,
|
|
63
|
+
mqttSession: null,
|
|
64
|
+
subscriptions: null,
|
|
65
|
+
seqIds: null,
|
|
66
|
+
appState: null,
|
|
67
|
+
|
|
68
|
+
// new
|
|
69
|
+
irisState: null,
|
|
70
|
+
mqttTopics: null,
|
|
71
|
+
mqttCapabilities: null,
|
|
72
|
+
mqttAuth: null,
|
|
73
|
+
lastPublishIds: null,
|
|
74
|
+
|
|
75
|
+
// utility
|
|
76
|
+
loginHistory: null,
|
|
77
|
+
usageStats: null,
|
|
78
|
+
accountsIndex: null,
|
|
79
|
+
|
|
80
|
+
// mqtt helpers
|
|
81
|
+
mqttHealth: null,
|
|
82
|
+
mqttReconnect: null,
|
|
83
|
+
mqttMessageCache: null,
|
|
84
|
+
mqttOutbox: null,
|
|
85
|
+
mqttSubscriptionHealth: null,
|
|
86
|
+
mqttTraffic: null,
|
|
87
|
+
mqttRisk: null
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ensure folder structure
|
|
91
|
+
this._ensureFolder();
|
|
92
|
+
this._ensureBackupFolder();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_getFilePath(key) {
|
|
96
|
+
return path.join(this.folder, FILE_NAMES[key]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_getBackupFolder() {
|
|
100
|
+
return path.join(this.folder, 'backup');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_ensureFolder() {
|
|
104
|
+
if (!fs.existsSync(this.folder)) {
|
|
105
|
+
fs.mkdirSync(this.folder, { recursive: true, mode: 0o700 });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_ensureBackupFolder() {
|
|
110
|
+
// backups disabled — no-op
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// sanitize state to avoid nulls for key fields (useful to ensure instant non-null)
|
|
115
|
+
_sanitizeForSave(key, data) {
|
|
116
|
+
try {
|
|
117
|
+
if (!data) return data;
|
|
118
|
+
if (key === 'creds') {
|
|
119
|
+
if (typeof data.userId === 'undefined' || data.userId === null) data.userId = data.userId || 'pending';
|
|
120
|
+
if (typeof data.username === 'undefined' || data.username === null) data.username = data.username || 'pending';
|
|
121
|
+
if (typeof data.sessionId === 'undefined' || data.sessionId === null) data.sessionId = data.sessionId || `pending-${Date.now()}`;
|
|
122
|
+
if (typeof data.csrfToken === 'undefined' || data.csrfToken === null) data.csrfToken = data.csrfToken || 'pending';
|
|
123
|
+
if (typeof data.isLoggedIn === 'undefined') data.isLoggedIn = !!data.sessionId || !!data.authorization;
|
|
124
|
+
if (!data.loginAt) data.loginAt = data.loginAt || new Date().toISOString();
|
|
125
|
+
}
|
|
126
|
+
if (key === 'mqttSession') {
|
|
127
|
+
if (!data.sessionId) data.sessionId = data.sessionId || `local-mqtt-${Date.now()}`;
|
|
128
|
+
if (!data.mqttSessionId) data.mqttSessionId = data.mqttSessionId || 'boot';
|
|
129
|
+
if (!data.lastConnected) data.lastConnected = new Date().toISOString();
|
|
130
|
+
}
|
|
131
|
+
if (key === 'seqIds') {
|
|
132
|
+
if (typeof data.seq_id === 'undefined' || data.seq_id === null) data.seq_id = data.seq_id || 0;
|
|
133
|
+
if (typeof data.snapshot_at_ms === 'undefined' || data.snapshot_at_ms === null) data.snapshot_at_ms = data.snapshot_at_ms || Date.now();
|
|
134
|
+
}
|
|
135
|
+
if (key === 'mqttTopics') {
|
|
136
|
+
if (!data.topics) data.topics = data.topics || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync', 'presence_subscribe'];
|
|
137
|
+
if (!data.updatedAt) data.updatedAt = new Date().toISOString();
|
|
138
|
+
}
|
|
139
|
+
if (key === 'mqttCapabilities') {
|
|
140
|
+
data.supportsTyping = Boolean(data.supportsTyping ?? true);
|
|
141
|
+
data.supportsReactions = Boolean(data.supportsReactions ?? true);
|
|
142
|
+
data.supportsVoice = Boolean(data.supportsVoice ?? false);
|
|
143
|
+
data.protocolVersion = data.protocolVersion || 3;
|
|
144
|
+
data.collectedAt = data.collectedAt || new Date().toISOString();
|
|
145
|
+
}
|
|
146
|
+
if (key === 'mqttHealth') {
|
|
147
|
+
data.lastPingAt = data.lastPingAt || null;
|
|
148
|
+
data.lastPongAt = data.lastPongAt || null;
|
|
149
|
+
data.latencyMs = data.latencyMs || 0;
|
|
150
|
+
data.missedPings = data.missedPings || 0;
|
|
151
|
+
data.status = data.status || 'unknown';
|
|
152
|
+
}
|
|
153
|
+
if (key === 'mqttMessageCache') {
|
|
154
|
+
if (!Array.isArray(data.lastIds)) data.lastIds = [];
|
|
155
|
+
if (!data.max) data.max = data.max || 500;
|
|
156
|
+
}
|
|
157
|
+
if (key === 'mqttOutbox') {
|
|
158
|
+
if (!Array.isArray(data)) return [];
|
|
159
|
+
}
|
|
160
|
+
if (key === 'mqttSubscriptionHealth') {
|
|
161
|
+
data.expected = data.expected || (this.getMqttTopics()?.topics || ['ig_sub_direct']);
|
|
162
|
+
data.active = data.active || data.expected.slice();
|
|
163
|
+
data.lastCheck = data.lastCheck || new Date().toISOString();
|
|
164
|
+
data.needsResubscribe = data.needsResubscribe || false;
|
|
165
|
+
}
|
|
166
|
+
if (key === 'mqttTraffic') {
|
|
167
|
+
data.messagesInPerMin = data.messagesInPerMin || 0;
|
|
168
|
+
data.messagesOutPerMin = data.messagesOutPerMin || 0;
|
|
169
|
+
data.lastReset = data.lastReset || new Date().toISOString();
|
|
170
|
+
}
|
|
171
|
+
if (key === 'mqttReconnect') {
|
|
172
|
+
data.attempts = data.attempts || 0;
|
|
173
|
+
data.lastReason = data.lastReason || null;
|
|
174
|
+
data.lastAttemptAt = data.lastAttemptAt || null;
|
|
175
|
+
data.nextRetryInMs = data.nextRetryInMs || 0;
|
|
176
|
+
}
|
|
177
|
+
if (key === 'mqttRisk') {
|
|
178
|
+
data.riskScore = typeof data.riskScore === 'number' ? data.riskScore : 0;
|
|
179
|
+
data.level = data.level || 'low';
|
|
180
|
+
data.updatedAt = data.updatedAt || new Date().toISOString();
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// ignore sanitize errors
|
|
184
|
+
}
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// create a timestamped backup of existing file (if present)
|
|
189
|
+
async _createBackup(key) {
|
|
190
|
+
// backups disabled — do nothing
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async _writeFileAtomic(filePath, data) {
|
|
195
|
+
const tempPath = filePath + '.tmp';
|
|
196
|
+
const jsonData = JSON.stringify(data, null, 2);
|
|
197
|
+
await fs.promises.writeFile(tempPath, jsonData, { mode: 0o600 });
|
|
198
|
+
await fs.promises.rename(tempPath, filePath);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async _readFile(key) {
|
|
202
|
+
const filePath = this._getFilePath(key);
|
|
203
|
+
try {
|
|
204
|
+
if (fs.existsSync(filePath)) {
|
|
205
|
+
const raw = await fs.promises.readFile(filePath, 'utf8');
|
|
206
|
+
return JSON.parse(raw);
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.warn(`[MultiFileAuthState] Error reading ${key}:`, e.message);
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async _writeFile(key, data) {
|
|
215
|
+
this._ensureFolder();
|
|
216
|
+
const filePath = this._getFilePath(key);
|
|
217
|
+
try {
|
|
218
|
+
// sanitize to avoid nulls for key fields
|
|
219
|
+
const sanitized = this._sanitizeForSave(key, JSON.parse(JSON.stringify(data)));
|
|
220
|
+
// create backup before overwriting existing file
|
|
221
|
+
await this._createBackup(key);
|
|
222
|
+
await this._writeFileAtomic(filePath, sanitized);
|
|
223
|
+
return true;
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.error(`[MultiFileAuthState] Error writing ${key}:`, e.message);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// restore the latest backup for a key (returns path of restored file or null)
|
|
231
|
+
async restoreLatestBackup(key) {
|
|
232
|
+
try {
|
|
233
|
+
const base = FILE_NAMES[key];
|
|
234
|
+
const bfolder = this._getBackupFolder();
|
|
235
|
+
if (!fs.existsSync(bfolder)) return null;
|
|
236
|
+
const files = await fs.promises.readdir(bfolder);
|
|
237
|
+
const matches = files.filter(f => f.startsWith(base + '.'));
|
|
238
|
+
if (!matches.length) return null;
|
|
239
|
+
// sort descending by timestamp embedded in filename
|
|
240
|
+
matches.sort().reverse();
|
|
241
|
+
const latest = matches[0];
|
|
242
|
+
const src = path.join(bfolder, latest);
|
|
243
|
+
const dest = this._getFilePath(key);
|
|
244
|
+
await fs.promises.copyFile(src, dest);
|
|
245
|
+
this.emit('backup-restored', { key, file: src });
|
|
246
|
+
// reload into memory
|
|
247
|
+
this.data[key] = await this._readFile(key);
|
|
248
|
+
return src;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
console.warn('[MultiFileAuthState] restoreLatestBackup error:', e.message);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async loadAll() {
|
|
256
|
+
this._ensureFolder();
|
|
257
|
+
this._ensureBackupFolder();
|
|
258
|
+
|
|
259
|
+
const loadPromises = Object.keys(FILE_NAMES).map(async (key) => {
|
|
260
|
+
this.data[key] = await this._readFile(key);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await Promise.all(loadPromises);
|
|
264
|
+
|
|
265
|
+
// ensure usageStats and loginHistory objects exist
|
|
266
|
+
if (!this.data.loginHistory) this.data.loginHistory = [];
|
|
267
|
+
if (!this.data.usageStats) this.data.usageStats = {
|
|
268
|
+
apiRequests: 0,
|
|
269
|
+
mqttMessages: 0,
|
|
270
|
+
errors: 0,
|
|
271
|
+
reconnects: 0,
|
|
272
|
+
lastReset: new Date().toISOString()
|
|
273
|
+
};
|
|
274
|
+
if (!this.data.accountsIndex) this.data.accountsIndex = {};
|
|
275
|
+
|
|
276
|
+
// ensure mqtt helper objects exist
|
|
277
|
+
if (!this.data.mqttHealth) this.data.mqttHealth = { lastPingAt: null, lastPongAt: null, latencyMs: 0, missedPings: 0, status: 'unknown' };
|
|
278
|
+
if (!this.data.mqttReconnect) this.data.mqttReconnect = { attempts: 0, lastReason: null, lastAttemptAt: null, nextRetryInMs: 0 };
|
|
279
|
+
if (!this.data.mqttMessageCache) this.data.mqttMessageCache = { lastIds: [], max: 500 };
|
|
280
|
+
if (!this.data.mqttOutbox) this.data.mqttOutbox = [];
|
|
281
|
+
if (!this.data.mqttSubscriptionHealth) this.data.mqttSubscriptionHealth = { expected: (this.data.mqttTopics?.topics || ['ig_sub_direct']), active: (this.data.mqttTopics?.topics || ['ig_sub_direct']).slice(), lastCheck: new Date().toISOString(), needsResubscribe: false };
|
|
282
|
+
if (!this.data.mqttTraffic) this.data.mqttTraffic = { messagesInPerMin: 0, messagesOutPerMin: 0, lastReset: new Date().toISOString() };
|
|
283
|
+
if (!this.data.mqttRisk) this.data.mqttRisk = { riskScore: 0, level: 'low', updatedAt: new Date().toISOString() };
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
creds: this.data.creds,
|
|
287
|
+
device: this.data.device,
|
|
288
|
+
cookies: this.data.cookies,
|
|
289
|
+
mqttSession: this.data.mqttSession,
|
|
290
|
+
subscriptions: this.data.subscriptions,
|
|
291
|
+
seqIds: this.data.seqIds,
|
|
292
|
+
appState: this.data.appState,
|
|
293
|
+
irisState: this.data.irisState,
|
|
294
|
+
mqttTopics: this.data.mqttTopics,
|
|
295
|
+
mqttCapabilities: this.data.mqttCapabilities,
|
|
296
|
+
mqttAuth: this.data.mqttAuth,
|
|
297
|
+
lastPublishIds: this.data.lastPublishIds,
|
|
298
|
+
loginHistory: this.data.loginHistory,
|
|
299
|
+
usageStats: this.data.usageStats,
|
|
300
|
+
accountsIndex: this.data.accountsIndex,
|
|
301
|
+
mqttHealth: this.data.mqttHealth,
|
|
302
|
+
mqttReconnect: this.data.mqttReconnect,
|
|
303
|
+
mqttMessageCache: this.data.mqttMessageCache,
|
|
304
|
+
mqttOutbox: this.data.mqttOutbox,
|
|
305
|
+
mqttSubscriptionHealth: this.data.mqttSubscriptionHealth,
|
|
306
|
+
mqttTraffic: this.data.mqttTraffic,
|
|
307
|
+
mqttRisk: this.data.mqttRisk,
|
|
308
|
+
hasSession: !!(this.data.creds && this.data.cookies)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async saveAll() {
|
|
313
|
+
const savePromises = Object.keys(FILE_NAMES).map(async (key) => {
|
|
314
|
+
if (this.data[key] !== null && this.data[key] !== undefined) {
|
|
315
|
+
await this._writeFile(key, this.data[key]);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
// also save usageStats and loginHistory explicitly
|
|
319
|
+
if (this.data.loginHistory) {
|
|
320
|
+
try { await this._writeFile('loginHistory', this.data.loginHistory); } catch (e) {}
|
|
321
|
+
}
|
|
322
|
+
if (this.data.usageStats) {
|
|
323
|
+
try { await this._writeFile('usageStats', this.data.usageStats); } catch (e) {}
|
|
324
|
+
}
|
|
325
|
+
if (this.data.accountsIndex) {
|
|
326
|
+
try { await this._writeFile('accountsIndex', this.data.accountsIndex); } catch (e) {}
|
|
327
|
+
}
|
|
328
|
+
// mqtt helpers
|
|
329
|
+
try { await this._writeFile('mqttHealth', this.data.mqttHealth); } catch (e) {}
|
|
330
|
+
try { await this._writeFile('mqttReconnect', this.data.mqttReconnect); } catch (e) {}
|
|
331
|
+
try { await this._writeFile('mqttMessageCache', this.data.mqttMessageCache); } catch (e) {}
|
|
332
|
+
try { await this._writeFile('mqttOutbox', this.data.mqttOutbox); } catch (e) {}
|
|
333
|
+
try { await this._writeFile('mqttSubscriptionHealth', this.data.mqttSubscriptionHealth); } catch (e) {}
|
|
334
|
+
try { await this._writeFile('mqttTraffic', this.data.mqttTraffic); } catch (e) {}
|
|
335
|
+
try { await this._writeFile('mqttRisk', this.data.mqttRisk); } catch (e) {}
|
|
336
|
+
|
|
337
|
+
await Promise.all(savePromises);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async saveCreds() {
|
|
341
|
+
const promises = [];
|
|
342
|
+
if (this.data.creds) promises.push(this._writeFile('creds', this.data.creds));
|
|
343
|
+
if (this.data.device) promises.push(this._writeFile('device', this.data.device));
|
|
344
|
+
if (this.data.cookies) promises.push(this._writeFile('cookies', this.data.cookies));
|
|
345
|
+
// also persist loginHistory and usageStats after creds save
|
|
346
|
+
if (this.data.loginHistory) promises.push(this._writeFile('loginHistory', this.data.loginHistory));
|
|
347
|
+
if (this.data.usageStats) promises.push(this._writeFile('usageStats', this.data.usageStats));
|
|
348
|
+
await Promise.all(promises);
|
|
349
|
+
this.emit('creds-saved');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async saveMqttState() {
|
|
353
|
+
// Save base mqtt data
|
|
354
|
+
const promises = [];
|
|
355
|
+
if (this.data.mqttSession) promises.push(this._writeFile('mqttSession', this.data.mqttSession));
|
|
356
|
+
if (this.data.subscriptions) promises.push(this._writeFile('subscriptions', this.data.subscriptions));
|
|
357
|
+
if (this.data.seqIds) promises.push(this._writeFile('seqIds', this.data.seqIds));
|
|
358
|
+
|
|
359
|
+
// Save extended mqtt/realtime data (new files)
|
|
360
|
+
if (this.data.irisState) promises.push(this._writeFile('irisState', this.data.irisState));
|
|
361
|
+
if (this.data.mqttTopics) promises.push(this._writeFile('mqttTopics', this.data.mqttTopics));
|
|
362
|
+
if (this.data.mqttCapabilities) promises.push(this._writeFile('mqttCapabilities', this.data.mqttCapabilities));
|
|
363
|
+
if (this.data.mqttAuth) promises.push(this._writeFile('mqttAuth', this.data.mqttAuth));
|
|
364
|
+
if (this.data.lastPublishIds) promises.push(this._writeFile('lastPublishIds', this.data.lastPublishIds));
|
|
365
|
+
|
|
366
|
+
// mqtt helpers
|
|
367
|
+
promises.push(this._writeFile('mqttHealth', this.data.mqttHealth));
|
|
368
|
+
promises.push(this._writeFile('mqttReconnect', this.data.mqttReconnect));
|
|
369
|
+
promises.push(this._writeFile('mqttMessageCache', this.data.mqttMessageCache));
|
|
370
|
+
promises.push(this._writeFile('mqttOutbox', this.data.mqttOutbox));
|
|
371
|
+
promises.push(this._writeFile('mqttSubscriptionHealth', this.data.mqttSubscriptionHealth));
|
|
372
|
+
promises.push(this._writeFile('mqttTraffic', this.data.mqttTraffic));
|
|
373
|
+
promises.push(this._writeFile('mqttRisk', this.data.mqttRisk));
|
|
374
|
+
|
|
375
|
+
await Promise.all(promises);
|
|
376
|
+
this.emit('mqtt-state-saved');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async saveAppState() {
|
|
380
|
+
if (this.data.appState) {
|
|
381
|
+
await this._writeFile('appState', this.data.appState);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
_debouncedSave(keys) {
|
|
386
|
+
keys.forEach(k => this._dirty.add(k));
|
|
387
|
+
|
|
388
|
+
if (this._saveDebounceTimer) {
|
|
389
|
+
clearTimeout(this._saveDebounceTimer);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
this._saveDebounceTimer = setTimeout(async () => {
|
|
393
|
+
const toSave = Array.from(this._dirty);
|
|
394
|
+
this._dirty.clear();
|
|
395
|
+
|
|
396
|
+
for (const key of toSave) {
|
|
397
|
+
if (this.data[key] !== null && this.data[key] !== undefined) {
|
|
398
|
+
try {
|
|
399
|
+
// ensure backup for critical files
|
|
400
|
+
if (['creds', 'cookies', 'device'].includes(key)) {
|
|
401
|
+
await this._createBackup(key);
|
|
402
|
+
}
|
|
403
|
+
await this._writeFile(key, this.data[key]);
|
|
404
|
+
} catch (e) {
|
|
405
|
+
console.error(`[MultiFileAuthState] Debounced write error for ${key}:`, e.message);
|
|
406
|
+
// increment usageStats.errors
|
|
407
|
+
try { this.incrementStat('errors'); } catch (e2) {}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.emit('debounced-save-complete', toSave);
|
|
412
|
+
}, this._saveDebounceMs);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// --- Setters that schedule debounced saves ---
|
|
416
|
+
setCreds(creds) {
|
|
417
|
+
this.data.creds = creds;
|
|
418
|
+
this._debouncedSave(['creds']);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
setDevice(device) {
|
|
422
|
+
this.data.device = device;
|
|
423
|
+
this._debouncedSave(['device']);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
setCookies(cookies) {
|
|
427
|
+
this.data.cookies = cookies;
|
|
428
|
+
this._debouncedSave(['cookies']);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
setMqttSession(session) {
|
|
432
|
+
this.data.mqttSession = session;
|
|
433
|
+
this._debouncedSave(['mqttSession']);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
setSubscriptions(subs) {
|
|
437
|
+
this.data.subscriptions = subs;
|
|
438
|
+
this._debouncedSave(['subscriptions']);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
setSeqIds(seqIds) {
|
|
442
|
+
this.data.seqIds = seqIds;
|
|
443
|
+
this._debouncedSave(['seqIds']);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
setAppState(state) {
|
|
447
|
+
this.data.appState = state;
|
|
448
|
+
this._debouncedSave(['appState']);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// --- New setters for additional MQTT files ---
|
|
452
|
+
setIrisState(irisState) {
|
|
453
|
+
this.data.irisState = irisState;
|
|
454
|
+
this._debouncedSave(['irisState']);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
setMqttTopics(topics) {
|
|
458
|
+
this.data.mqttTopics = topics;
|
|
459
|
+
this._debouncedSave(['mqttTopics']);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
setMqttCapabilities(capabilities) {
|
|
463
|
+
this.data.mqttCapabilities = capabilities;
|
|
464
|
+
this._debouncedSave(['mqttCapabilities']);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
setMqttAuth(auth) {
|
|
468
|
+
this.data.mqttAuth = auth;
|
|
469
|
+
this._debouncedSave(['mqttAuth']);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
setLastPublishIds(lastPublishIds) {
|
|
473
|
+
this.data.lastPublishIds = lastPublishIds;
|
|
474
|
+
this._debouncedSave(['lastPublishIds']);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// --- Utility setters ---
|
|
478
|
+
addLoginHistory(entry) {
|
|
479
|
+
if (!this.data.loginHistory) this.data.loginHistory = [];
|
|
480
|
+
this.data.loginHistory.unshift(entry);
|
|
481
|
+
// keep bounded history length
|
|
482
|
+
if (this.data.loginHistory.length > 200) this.data.loginHistory.length = 200;
|
|
483
|
+
this._debouncedSave(['loginHistory']);
|
|
484
|
+
this.emit('login-attempt', entry);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
incrementStat(statName, by = 1) {
|
|
488
|
+
if (!this.data.usageStats) this.data.usageStats = {
|
|
489
|
+
apiRequests: 0,
|
|
490
|
+
mqttMessages: 0,
|
|
491
|
+
errors: 0,
|
|
492
|
+
reconnects: 0,
|
|
493
|
+
lastReset: new Date().toISOString()
|
|
494
|
+
};
|
|
495
|
+
if (typeof this.data.usageStats[statName] === 'number') {
|
|
496
|
+
this.data.usageStats[statName] += by;
|
|
497
|
+
} else {
|
|
498
|
+
this.data.usageStats[statName] = (this.data.usageStats[statName] || 0) + by;
|
|
499
|
+
}
|
|
500
|
+
this._debouncedSave(['usageStats']);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// --- MQTT helper methods ---
|
|
504
|
+
|
|
505
|
+
// update mqtt health and persist
|
|
506
|
+
_updateMqttHealth(partial) {
|
|
507
|
+
try {
|
|
508
|
+
if (!this.data.mqttHealth) this.data.mqttHealth = {};
|
|
509
|
+
Object.assign(this.data.mqttHealth, partial);
|
|
510
|
+
// recompute status
|
|
511
|
+
if (this.data.mqttHealth.missedPings > 3) {
|
|
512
|
+
this.data.mqttHealth.status = 'degraded';
|
|
513
|
+
} else if (this.data.mqttHealth.lastPongAt) {
|
|
514
|
+
this.data.mqttHealth.status = 'ok';
|
|
515
|
+
} else {
|
|
516
|
+
this.data.mqttHealth.status = this.data.mqttHealth.status || 'unknown';
|
|
517
|
+
}
|
|
518
|
+
this._debouncedSave(['mqttHealth']);
|
|
519
|
+
} catch (e) {}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// mark ping sent
|
|
523
|
+
markPing() {
|
|
524
|
+
this._updateMqttHealth({ lastPingAt: new Date().toISOString() });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// mark pong received and update latency
|
|
528
|
+
markPong() {
|
|
529
|
+
const now = Date.now();
|
|
530
|
+
const lastPing = this.data.mqttHealth?.lastPingAt ? new Date(this.data.mqttHealth.lastPingAt).getTime() : null;
|
|
531
|
+
const latency = lastPing ? (now - lastPing) : 0;
|
|
532
|
+
const missed = Math.max(0, (this.data.mqttHealth?.missedPings || 0) - 1);
|
|
533
|
+
this._updateMqttHealth({ lastPongAt: new Date().toISOString(), latencyMs: latency, missedPings: missed });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// increment missed ping
|
|
537
|
+
markMissedPing() {
|
|
538
|
+
const missed = (this.data.mqttHealth?.missedPings || 0) + 1;
|
|
539
|
+
this._updateMqttHealth({ missedPings: missed });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// message deduplication: return true if duplicate
|
|
543
|
+
isDuplicateMessage(messageId) {
|
|
544
|
+
if (!messageId) return false;
|
|
545
|
+
if (!this.data.mqttMessageCache) this.data.mqttMessageCache = { lastIds: [], max: 500 };
|
|
546
|
+
const idx = this.data.mqttMessageCache.lastIds.indexOf(messageId);
|
|
547
|
+
if (idx !== -1) return true;
|
|
548
|
+
// push and cap
|
|
549
|
+
this.data.mqttMessageCache.lastIds.push(messageId);
|
|
550
|
+
if (this.data.mqttMessageCache.lastIds.length > (this.data.mqttMessageCache.max || 500)) {
|
|
551
|
+
this.data.mqttMessageCache.lastIds.shift();
|
|
552
|
+
}
|
|
553
|
+
this._debouncedSave(['mqttMessageCache']);
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// enqueue outgoing message to outbox
|
|
558
|
+
enqueueOutbox(item) {
|
|
559
|
+
if (!this.data.mqttOutbox) this.data.mqttOutbox = [];
|
|
560
|
+
// item: { topic, payload, qos, createdAt, tries }
|
|
561
|
+
const i = Object.assign({ qos: 1, createdAt: new Date().toISOString(), tries: 0 }, item);
|
|
562
|
+
this.data.mqttOutbox.push(i);
|
|
563
|
+
this._debouncedSave(['mqttOutbox']);
|
|
564
|
+
this.emit('outbox-enqueued', i);
|
|
565
|
+
return i;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// flush outbox (attempt send). Tries to use common publish/send methods on client
|
|
569
|
+
async flushOutbox(realtimeClient, maxPerRun = 50) {
|
|
570
|
+
if (!this.data.mqttOutbox || !this.data.mqttOutbox.length) return 0;
|
|
571
|
+
const sent = [];
|
|
572
|
+
const keep = [];
|
|
573
|
+
let processed = 0;
|
|
574
|
+
for (const item of this.data.mqttOutbox) {
|
|
575
|
+
if (processed >= maxPerRun) {
|
|
576
|
+
keep.push(item);
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
processed++;
|
|
580
|
+
try {
|
|
581
|
+
item.tries = (item.tries || 0) + 1;
|
|
582
|
+
// try publish methods in order of likelihood
|
|
583
|
+
let ok = false;
|
|
584
|
+
if (realtimeClient && typeof realtimeClient.publish === 'function') {
|
|
585
|
+
// mqtt.js style
|
|
586
|
+
try {
|
|
587
|
+
realtimeClient.publish(item.topic, typeof item.payload === 'string' ? item.payload : JSON.stringify(item.payload), { qos: item.qos });
|
|
588
|
+
ok = true;
|
|
589
|
+
} catch (e) {
|
|
590
|
+
ok = false;
|
|
591
|
+
}
|
|
592
|
+
} else if (realtimeClient && typeof realtimeClient.send === 'function') {
|
|
593
|
+
try {
|
|
594
|
+
realtimeClient.send(item.topic, item.payload);
|
|
595
|
+
ok = true;
|
|
596
|
+
} catch (e) { ok = false; }
|
|
597
|
+
} else if (realtimeClient && typeof realtimeClient.sendMessage === 'function') {
|
|
598
|
+
try {
|
|
599
|
+
realtimeClient.sendMessage(item.payload);
|
|
600
|
+
ok = true;
|
|
601
|
+
} catch (e) { ok = false; }
|
|
602
|
+
} else {
|
|
603
|
+
// cannot send: keep in outbox
|
|
604
|
+
ok = false;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (ok) {
|
|
608
|
+
sent.push(item);
|
|
609
|
+
this.incrementStat('mqttMessages', 1);
|
|
610
|
+
} else {
|
|
611
|
+
// keep for retry, but if tries exceed threshold move to dead-letter (drop)
|
|
612
|
+
if (item.tries >= 10) {
|
|
613
|
+
this.emit('outbox-dropped', item);
|
|
614
|
+
} else {
|
|
615
|
+
keep.push(item);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch (e) {
|
|
619
|
+
keep.push(item);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// replace queue
|
|
623
|
+
this.data.mqttOutbox = keep.concat(this.data.mqttOutbox.slice(processed));
|
|
624
|
+
this._debouncedSave(['mqttOutbox']);
|
|
625
|
+
this.emit('outbox-flushed', { sentCount: sent.length, remaining: this.data.mqttOutbox.length });
|
|
626
|
+
return sent.length;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// subscription watchdog: check and attempt resubscribe
|
|
630
|
+
async checkSubscriptions(realtimeClient) {
|
|
631
|
+
try {
|
|
632
|
+
const expected = this.data.mqttSubscriptionHealth?.expected || (this.data.mqttTopics?.topics || []);
|
|
633
|
+
let active = [];
|
|
634
|
+
// try to read from client
|
|
635
|
+
if (realtimeClient?.connection?.subscribedTopics) active = realtimeClient.connection.subscribedTopics;
|
|
636
|
+
else if (realtimeClient?._subscribedTopics) active = realtimeClient._subscribedTopics;
|
|
637
|
+
else if (Array.isArray(this.data.mqttTopics?.topics)) active = this.data.mqttTopics.topics;
|
|
638
|
+
|
|
639
|
+
const needs = expected.filter(t => !active.includes(t));
|
|
640
|
+
this.data.mqttSubscriptionHealth = {
|
|
641
|
+
expected,
|
|
642
|
+
active,
|
|
643
|
+
lastCheck: new Date().toISOString(),
|
|
644
|
+
needsResubscribe: needs.length > 0
|
|
645
|
+
};
|
|
646
|
+
this._debouncedSave(['mqttSubscriptionHealth']);
|
|
647
|
+
|
|
648
|
+
if (needs.length && realtimeClient) {
|
|
649
|
+
for (const t of needs) {
|
|
650
|
+
try {
|
|
651
|
+
// call common subscribe methods
|
|
652
|
+
if (typeof realtimeClient.subscribe === 'function') {
|
|
653
|
+
realtimeClient.subscribe(t);
|
|
654
|
+
} else if (realtimeClient.connection && typeof realtimeClient.connection.subscribe === 'function') {
|
|
655
|
+
realtimeClient.connection.subscribe(t);
|
|
656
|
+
} else if (typeof realtimeClient.subscriptions === 'function') {
|
|
657
|
+
realtimeClient.subscriptions([t]);
|
|
658
|
+
}
|
|
659
|
+
// update active
|
|
660
|
+
if (!this.data.mqttSubscriptionHealth.active.includes(t)) this.data.mqttSubscriptionHealth.active.push(t);
|
|
661
|
+
this.emit('resubscribe-attempt', { topic: t });
|
|
662
|
+
} catch (e) {
|
|
663
|
+
this.emit('resubscribe-failed', { topic: t, error: e.message });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
this._debouncedSave(['mqttSubscriptionHealth']);
|
|
667
|
+
}
|
|
668
|
+
return this.data.mqttSubscriptionHealth;
|
|
669
|
+
} catch (e) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// traffic profiler: update incoming/outgoing counters
|
|
675
|
+
_updateTraffic(incoming = 0, outgoing = 0) {
|
|
676
|
+
try {
|
|
677
|
+
if (!this.data.mqttTraffic) this.data.mqttTraffic = { messagesInPerMin: 0, messagesOutPerMin: 0, lastReset: new Date().toISOString() };
|
|
678
|
+
const lastReset = new Date(this.data.mqttTraffic.lastReset).getTime();
|
|
679
|
+
if (Date.now() - lastReset > 60 * 1000) {
|
|
680
|
+
// reset every minute
|
|
681
|
+
this.data.mqttTraffic.messagesInPerMin = 0;
|
|
682
|
+
this.data.mqttTraffic.messagesOutPerMin = 0;
|
|
683
|
+
this.data.mqttTraffic.lastReset = new Date().toISOString();
|
|
684
|
+
}
|
|
685
|
+
this.data.mqttTraffic.messagesInPerMin += incoming;
|
|
686
|
+
this.data.mqttTraffic.messagesOutPerMin += outgoing;
|
|
687
|
+
this._debouncedSave(['mqttTraffic']);
|
|
688
|
+
} catch (e) {}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// compute simple risk score from reconnects, errors, traffic
|
|
692
|
+
_computeRiskScore() {
|
|
693
|
+
try {
|
|
694
|
+
const reconnects = this.data.usageStats?.reconnects || 0;
|
|
695
|
+
const errors = this.data.usageStats?.errors || 0;
|
|
696
|
+
const outPerMin = this.data.mqttTraffic?.messagesOutPerMin || 0;
|
|
697
|
+
// simple heuristic
|
|
698
|
+
let score = 0;
|
|
699
|
+
score += Math.min(1, reconnects / 10) * 0.4;
|
|
700
|
+
score += Math.min(1, errors / 20) * 0.3;
|
|
701
|
+
score += Math.min(1, outPerMin / 200) * 0.3;
|
|
702
|
+
const level = score > 0.7 ? 'high' : score > 0.35 ? 'medium' : 'low';
|
|
703
|
+
this.data.mqttRisk = { riskScore: Number(score.toFixed(3)), level, updatedAt: new Date().toISOString() };
|
|
704
|
+
this._debouncedSave(['mqttRisk']);
|
|
705
|
+
this.emit('risk-updated', this.data.mqttRisk);
|
|
706
|
+
return this.data.mqttRisk;
|
|
707
|
+
} catch (e) {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// schedule auto reconnect using backoff; will attempt to call client.reconnect() if available or emit event
|
|
713
|
+
_scheduleReconnect(realtimeClient, reason = 'unknown') {
|
|
714
|
+
try {
|
|
715
|
+
this._reconnectAttempts = (this._reconnectAttempts || 0) + 1;
|
|
716
|
+
const idx = Math.min(this._reconnectAttempts - 1, this._reconnectBackoff.length - 1);
|
|
717
|
+
const delay = this._reconnectBackoff[idx] || this._reconnectBackoff[this._reconnectBackoff.length - 1];
|
|
718
|
+
this.data.mqttReconnect = {
|
|
719
|
+
attempts: this._reconnectAttempts,
|
|
720
|
+
lastReason: reason,
|
|
721
|
+
lastAttemptAt: new Date().toISOString(),
|
|
722
|
+
nextRetryInMs: delay
|
|
723
|
+
};
|
|
724
|
+
this.incrementStat('reconnects', 1);
|
|
725
|
+
this._debouncedSave(['mqttReconnect']);
|
|
726
|
+
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
727
|
+
this._reconnectTimer = setTimeout(async () => {
|
|
728
|
+
try {
|
|
729
|
+
if (realtimeClient) {
|
|
730
|
+
if (typeof realtimeClient.reconnect === 'function') {
|
|
731
|
+
realtimeClient.reconnect();
|
|
732
|
+
} else if (typeof realtimeClient.connect === 'function') {
|
|
733
|
+
// some libs have connect
|
|
734
|
+
realtimeClient.connect();
|
|
735
|
+
} else {
|
|
736
|
+
// can't auto-call: emit event so app can handle
|
|
737
|
+
this.emit('should-reconnect', { reason });
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
this.emit('should-reconnect', { reason });
|
|
741
|
+
}
|
|
742
|
+
} catch (e) {
|
|
743
|
+
this.emit('reconnect-failed', { reason, error: e.message });
|
|
744
|
+
}
|
|
745
|
+
}, delay);
|
|
746
|
+
} catch (e) {}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// reset reconnect attempts after successful connect
|
|
750
|
+
_resetReconnect() {
|
|
751
|
+
this._reconnectAttempts = 0;
|
|
752
|
+
if (this._reconnectTimer) {
|
|
753
|
+
clearTimeout(this._reconnectTimer);
|
|
754
|
+
this._reconnectTimer = null;
|
|
755
|
+
}
|
|
756
|
+
if (this.data.mqttReconnect) {
|
|
757
|
+
this.data.mqttReconnect.attempts = 0;
|
|
758
|
+
this.data.mqttReconnect.nextRetryInMs = 0;
|
|
759
|
+
this._debouncedSave(['mqttReconnect']);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// warm-restore: apply saved options into realtimeClient before connect to speed startup
|
|
764
|
+
warmRestoreClient(realtimeClient) {
|
|
765
|
+
try {
|
|
766
|
+
const opts = this.getMqttConnectOptions();
|
|
767
|
+
if (!opts) return false;
|
|
768
|
+
// Merge irisData, mqttAuthToken etc. into client init options if possible
|
|
769
|
+
if (realtimeClient.initOptions && typeof realtimeClient.initOptions === 'object') {
|
|
770
|
+
Object.assign(realtimeClient.initOptions, opts);
|
|
771
|
+
} else {
|
|
772
|
+
realtimeClient.initOptions = opts;
|
|
773
|
+
}
|
|
774
|
+
// also set connection clientInfo if supported
|
|
775
|
+
if (realtimeClient.connection && realtimeClient.connection.clientInfo && this.data.mqttSession?.mqttSessionId) {
|
|
776
|
+
realtimeClient.connection.clientInfo.clientMqttSessionId = this.data.mqttSession.mqttSessionId;
|
|
777
|
+
}
|
|
778
|
+
this.emit('warm-restore', { options: opts });
|
|
779
|
+
return true;
|
|
780
|
+
} catch (e) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// attach runtime helpers to realtimeClient to manage instant improvements
|
|
786
|
+
attachRealtimeClient(realtimeClient, opts = {}) {
|
|
787
|
+
try {
|
|
788
|
+
if (!realtimeClient) return;
|
|
789
|
+
|
|
790
|
+
// warm restore immediately
|
|
791
|
+
try { this.warmRestoreClient(realtimeClient); } catch (e) {}
|
|
792
|
+
|
|
793
|
+
// ping/pong monitoring
|
|
794
|
+
const pingIntervalMs = opts.pingIntervalMs || 30 * 1000; // 30s
|
|
795
|
+
let pingTimer = null;
|
|
796
|
+
const startPing = () => {
|
|
797
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
798
|
+
pingTimer = setInterval(() => {
|
|
799
|
+
try {
|
|
800
|
+
this.markPing();
|
|
801
|
+
if (typeof realtimeClient.ping === 'function') {
|
|
802
|
+
realtimeClient.ping();
|
|
803
|
+
} else if (typeof realtimeClient.pingReq === 'function') {
|
|
804
|
+
realtimeClient.pingReq();
|
|
805
|
+
} else if (typeof realtimeClient.pingServer === 'function') {
|
|
806
|
+
realtimeClient.pingServer();
|
|
807
|
+
} else {
|
|
808
|
+
// some libraries use publish to $SYS topic or nothing; we still mark ping
|
|
809
|
+
}
|
|
810
|
+
// after ping, schedule missed ping detection
|
|
811
|
+
setTimeout(() => {
|
|
812
|
+
// if lastPongAt older than lastPingAt, consider missed
|
|
813
|
+
const lastPing = new Date(this.data.mqttHealth?.lastPingAt || 0).getTime();
|
|
814
|
+
const lastPong = new Date(this.data.mqttHealth?.lastPongAt || 0).getTime();
|
|
815
|
+
if (lastPing && (!lastPong || lastPong < lastPing)) {
|
|
816
|
+
this.markMissedPing();
|
|
817
|
+
}
|
|
818
|
+
}, Math.min(5000, Math.floor(pingIntervalMs / 2)));
|
|
819
|
+
} catch (e) {}
|
|
820
|
+
}, pingIntervalMs);
|
|
821
|
+
};
|
|
822
|
+
const stopPing = () => { if (pingTimer) clearInterval(pingTimer); pingTimer = null; };
|
|
823
|
+
|
|
824
|
+
// event handlers (subscribe to multiple common event names)
|
|
825
|
+
const onConnect = async (...args) => {
|
|
826
|
+
this._resetReconnect();
|
|
827
|
+
this.data.mqttSession = this.data.mqttSession || {};
|
|
828
|
+
this.data.mqttSession.lastConnected = new Date().toISOString();
|
|
829
|
+
this._debouncedSave(['mqttSession']);
|
|
830
|
+
this._updateMqttHealth({ status: 'ok', lastPongAt: new Date().toISOString(), missedPings: 0 });
|
|
831
|
+
this.emit('realtime-connected', { args });
|
|
832
|
+
// flush outbox on connect
|
|
833
|
+
try { await this.flushOutbox(realtimeClient); } catch (e) {}
|
|
834
|
+
// check subscriptions
|
|
835
|
+
try { await this.checkSubscriptions(realtimeClient); } catch (e) {}
|
|
836
|
+
startPing();
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
const onClose = (...args) => {
|
|
840
|
+
stopPing();
|
|
841
|
+
this._updateMqttHealth({ status: 'dead' });
|
|
842
|
+
this._scheduleReconnect(realtimeClient, 'close');
|
|
843
|
+
this.emit('realtime-closed', { args });
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const onReconnect = (...args) => {
|
|
847
|
+
this.emit('realtime-reconnect', { args });
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const onOffline = (...args) => {
|
|
851
|
+
this._updateMqttHealth({ status: 'degraded' });
|
|
852
|
+
this._scheduleReconnect(realtimeClient, 'offline');
|
|
853
|
+
this.emit('realtime-offline', { args });
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const onError = (err) => {
|
|
857
|
+
this.incrementStat('errors', 1);
|
|
858
|
+
this._scheduleReconnect(realtimeClient, err?.message || 'error');
|
|
859
|
+
this.emit('realtime-error', { error: err?.message || String(err) });
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const onMessage = async (topic, payload, packet) => {
|
|
863
|
+
// generic message handler: try to extract id for dedupe
|
|
864
|
+
// Many IG messages contain an id field in JSON payload; try to parse
|
|
865
|
+
let messageId = null;
|
|
866
|
+
try {
|
|
867
|
+
const s = (typeof payload === 'string') ? payload : (payload && payload.toString ? payload.toString() : null);
|
|
868
|
+
if (s) {
|
|
869
|
+
const j = JSON.parse(s);
|
|
870
|
+
if (j && (j.message_id || j.id || j.msg_id)) messageId = j.message_id || j.id || j.msg_id;
|
|
871
|
+
}
|
|
872
|
+
} catch (e) {
|
|
873
|
+
// not json, ignore
|
|
874
|
+
}
|
|
875
|
+
if (messageId && this.isDuplicateMessage(messageId)) {
|
|
876
|
+
this.emit('message-duplicate', { topic, messageId });
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
// update traffic counters
|
|
880
|
+
this._updateTraffic(1, 0);
|
|
881
|
+
this.incrementStat('mqttMessages', 1);
|
|
882
|
+
// mark pong if message looks like pong or heartbeat
|
|
883
|
+
if (topic && topic.includes('pong')) this.markPong();
|
|
884
|
+
this.emit('realtime-message', { topic, payload, packet });
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
// wire up events safely for common handlers
|
|
888
|
+
try {
|
|
889
|
+
// mqtt.js style
|
|
890
|
+
if (typeof realtimeClient.on === 'function') {
|
|
891
|
+
realtimeClient.on('connect', onConnect);
|
|
892
|
+
realtimeClient.on('reconnect', onReconnect);
|
|
893
|
+
realtimeClient.on('close', onClose);
|
|
894
|
+
realtimeClient.on('offline', onOffline);
|
|
895
|
+
realtimeClient.on('error', onError);
|
|
896
|
+
realtimeClient.on('message', onMessage);
|
|
897
|
+
}
|
|
898
|
+
// websockets or other: attempt to attach possible event names
|
|
899
|
+
if (realtimeClient.addEventListener && typeof realtimeClient.addEventListener === 'function') {
|
|
900
|
+
try { realtimeClient.addEventListener('open', onConnect); } catch (e) {}
|
|
901
|
+
try { realtimeClient.addEventListener('close', onClose); } catch (e) {}
|
|
902
|
+
try { realtimeClient.addEventListener('error', onError); } catch (e) {}
|
|
903
|
+
try { realtimeClient.addEventListener('message', (ev) => onMessage(ev.topic || ev.type, ev.data || ev.payload, ev)); } catch (e) {}
|
|
904
|
+
}
|
|
905
|
+
} catch (e) {}
|
|
906
|
+
|
|
907
|
+
// also attach one-time status: if the client supports events differently, let app listen to 'should-reconnect'
|
|
908
|
+
// attach a cleanup handle
|
|
909
|
+
const cleanup = () => {
|
|
910
|
+
stopPing();
|
|
911
|
+
try {
|
|
912
|
+
if (typeof realtimeClient.off === 'function') {
|
|
913
|
+
realtimeClient.off('connect', onConnect);
|
|
914
|
+
realtimeClient.off('reconnect', onReconnect);
|
|
915
|
+
realtimeClient.off('close', onClose);
|
|
916
|
+
realtimeClient.off('offline', onOffline);
|
|
917
|
+
realtimeClient.off('error', onError);
|
|
918
|
+
realtimeClient.off('message', onMessage);
|
|
919
|
+
}
|
|
920
|
+
} catch (e) {}
|
|
921
|
+
this.emit('realtime-detach');
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// expose cleanup on client for convenience
|
|
925
|
+
try { realtimeClient._authStateCleanup = cleanup; } catch (e) {}
|
|
926
|
+
|
|
927
|
+
// return helper object to caller
|
|
928
|
+
return {
|
|
929
|
+
pingIntervalMs,
|
|
930
|
+
stop: cleanup,
|
|
931
|
+
flushOutbox: async (max) => { return await this.flushOutbox(realtimeClient, max); },
|
|
932
|
+
checkSubscriptions: async () => { return await this.checkSubscriptions(realtimeClient); },
|
|
933
|
+
warmRestore: () => { return this.warmRestoreClient(realtimeClient); }
|
|
934
|
+
};
|
|
935
|
+
} catch (e) {
|
|
936
|
+
// emit attach failure
|
|
937
|
+
this.emit('attach-failed', { error: e.message });
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// --- Getters ---
|
|
943
|
+
getCreds() { return this.data.creds; }
|
|
944
|
+
getDevice() { return this.data.device; }
|
|
945
|
+
getCookies() { return this.data.cookies; }
|
|
946
|
+
getMqttSession() { return this.data.mqttSession; }
|
|
947
|
+
getSubscriptions() { return this.data.subscriptions; }
|
|
948
|
+
getSeqIds() { return this.data.seqIds; }
|
|
949
|
+
getAppState() { return this.data.appState; }
|
|
950
|
+
getIrisState() { return this.data.irisState; }
|
|
951
|
+
getMqttTopics() { return this.data.mqttTopics; }
|
|
952
|
+
getMqttCapabilities() { return this.data.mqttCapabilities; }
|
|
953
|
+
getMqttAuth() { return this.data.mqttAuth; }
|
|
954
|
+
getLastPublishIds() { return this.data.lastPublishIds; }
|
|
955
|
+
getLoginHistory() { return this.data.loginHistory || []; }
|
|
956
|
+
getUsageStats() { return this.data.usageStats || {}; }
|
|
957
|
+
getAccountsIndex() { return this.data.accountsIndex || {}; }
|
|
958
|
+
getMqttHealth() { return this.data.mqttHealth || {}; }
|
|
959
|
+
getMqttReconnect() { return this.data.mqttReconnect || {}; }
|
|
960
|
+
getMqttMessageCache() { return this.data.mqttMessageCache || {}; }
|
|
961
|
+
getMqttOutbox() { return this.data.mqttOutbox || []; }
|
|
962
|
+
getMqttSubscriptionHealth() { return this.data.mqttSubscriptionHealth || {}; }
|
|
963
|
+
getMqttTraffic() { return this.data.mqttTraffic || {}; }
|
|
964
|
+
getMqttRisk() { return this.data.mqttRisk || {}; }
|
|
965
|
+
|
|
966
|
+
hasValidSession() {
|
|
967
|
+
return !!(this.data.creds && this.data.cookies && this.data.creds.authorization);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
hasMqttSession() {
|
|
971
|
+
return !!(
|
|
972
|
+
(this.data.mqttSession && this.data.mqttSession.sessionId) ||
|
|
973
|
+
(this.data.mqttAuth && this.data.mqttAuth.jwt)
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async clearAll() {
|
|
978
|
+
for (const key of Object.keys(FILE_NAMES)) {
|
|
979
|
+
const filePath = this._getFilePath(key);
|
|
980
|
+
try {
|
|
981
|
+
if (fs.existsSync(filePath)) {
|
|
982
|
+
await fs.promises.unlink(filePath);
|
|
983
|
+
}
|
|
984
|
+
} catch (e) {}
|
|
985
|
+
this.data[key] = null;
|
|
986
|
+
}
|
|
987
|
+
// clear backups too? leave backups intact but emit event
|
|
988
|
+
this.emit('cleared-all');
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// --- Backup utilities exposed ---
|
|
992
|
+
async listBackups() {
|
|
993
|
+
try {
|
|
994
|
+
const b = this._getBackupFolder();
|
|
995
|
+
if (!fs.existsSync(b)) return [];
|
|
996
|
+
const files = await fs.promises.readdir(b);
|
|
997
|
+
return files.map(f => path.join(b, f));
|
|
998
|
+
} catch (e) {
|
|
999
|
+
return [];
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// --- Device fingerprint helpers ---
|
|
1004
|
+
computeDeviceFingerprint(deviceObj) {
|
|
1005
|
+
try {
|
|
1006
|
+
const s = JSON.stringify(deviceObj || this.data.device || {});
|
|
1007
|
+
return crypto.createHash('sha256').update(s).digest('hex');
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// lock device fingerprint: saves hash into device.json (device.locked = true)
|
|
1014
|
+
async lockDeviceFingerprint() {
|
|
1015
|
+
try {
|
|
1016
|
+
if (!this.data.device) this.data.device = {};
|
|
1017
|
+
const hash = this.computeDeviceFingerprint(this.data.device);
|
|
1018
|
+
this.data.device.fingerprintHash = hash;
|
|
1019
|
+
this.data.device.locked = true;
|
|
1020
|
+
await this._writeFile('device', this.data.device);
|
|
1021
|
+
this.emit('device-locked', { fingerprintHash: hash });
|
|
1022
|
+
return hash;
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
console.warn('[MultiFileAuthState] lockDeviceFingerprint failed:', e.message);
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
verifyDeviceFingerprint(deviceObj) {
|
|
1030
|
+
try {
|
|
1031
|
+
const stored = this.data.device?.fingerprintHash || null;
|
|
1032
|
+
if (!stored) return true; // no lock present
|
|
1033
|
+
const hash = this.computeDeviceFingerprint(deviceObj || this.data.device);
|
|
1034
|
+
return stored === hash;
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// --- Login history helpers ---
|
|
1041
|
+
recordLoginAttempt({ success, ip, userAgent, method = 'password', reason = null }) {
|
|
1042
|
+
const entry = {
|
|
1043
|
+
date: new Date().toISOString(),
|
|
1044
|
+
success: Boolean(success),
|
|
1045
|
+
ip: ip || null,
|
|
1046
|
+
userAgent: userAgent || null,
|
|
1047
|
+
method,
|
|
1048
|
+
reason
|
|
1049
|
+
};
|
|
1050
|
+
this.addLoginHistory(entry);
|
|
1051
|
+
return entry;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// mark account restricted / checkpoint
|
|
1055
|
+
markAccountRestricted({ status = 'checkpoint', reason = null }) {
|
|
1056
|
+
if (!this.data.creds) this.data.creds = {};
|
|
1057
|
+
this.data.creds.accountStatus = status;
|
|
1058
|
+
this.data.creds.restrictedAt = new Date().toISOString();
|
|
1059
|
+
this.data.creds.lastError = reason;
|
|
1060
|
+
this._debouncedSave(['creds']);
|
|
1061
|
+
this.emit('account-restricted', { status, reason });
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// clear restriction
|
|
1065
|
+
clearAccountRestriction() {
|
|
1066
|
+
if (this.data.creds) {
|
|
1067
|
+
delete this.data.creds.accountStatus;
|
|
1068
|
+
delete this.data.creds.restrictedAt;
|
|
1069
|
+
delete this.data.creds.lastError;
|
|
1070
|
+
this._debouncedSave(['creds']);
|
|
1071
|
+
this.emit('account-unrestricted');
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// --- Health check ---
|
|
1076
|
+
getHealth() {
|
|
1077
|
+
return {
|
|
1078
|
+
validSession: this.hasValidSession(),
|
|
1079
|
+
hasCookies: !!this.data.cookies,
|
|
1080
|
+
hasMqtt: this.hasMqttSession(),
|
|
1081
|
+
lastLogin: (this.data.loginHistory && this.data.loginHistory[0]?.date) || null,
|
|
1082
|
+
usageStats: this.getUsageStats(),
|
|
1083
|
+
accountStatus: this.data.creds?.accountStatus || 'ok',
|
|
1084
|
+
deviceLocked: !!this.data.device?.locked,
|
|
1085
|
+
mqttHealth: this.getMqttHealth(),
|
|
1086
|
+
mqttRisk: this.getMqttRisk()
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Helper: try to safely extract cookieJar serialization if exists
|
|
1092
|
+
async function trySerializeCookieJar(igState) {
|
|
1093
|
+
let cookies = null;
|
|
1094
|
+
try {
|
|
1095
|
+
if (igState.cookieJar && typeof igState.serializeCookieJar === 'function') {
|
|
1096
|
+
cookies = await igState.serializeCookieJar();
|
|
1097
|
+
} else if (igState.cookieJar && typeof igState.cookieJar.serialize === 'function') {
|
|
1098
|
+
// fallback: tough-cookie jar serialize
|
|
1099
|
+
const ser = await util.promisify(igState.cookieJar.serialize).bind(igState.cookieJar)();
|
|
1100
|
+
cookies = ser;
|
|
1101
|
+
}
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
console.warn('[MultiFileAuthState] Could not serialize cookies:', e.message);
|
|
1104
|
+
}
|
|
1105
|
+
return cookies;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Helper: try to read a cookie value from a tough-cookie Jar or equivalent
|
|
1109
|
+
async function getCookieValueFromJar(cookieJar, name) {
|
|
1110
|
+
if (!cookieJar) return null;
|
|
1111
|
+
|
|
1112
|
+
// try getCookieString (tough-cookie)
|
|
1113
|
+
try {
|
|
1114
|
+
if (typeof cookieJar.getCookieString === 'function') {
|
|
1115
|
+
const getCookieString = util.promisify(cookieJar.getCookieString).bind(cookieJar);
|
|
1116
|
+
const urls = ['https://www.instagram.com', 'https://instagram.com', 'https://i.instagram.com'];
|
|
1117
|
+
for (const url of urls) {
|
|
1118
|
+
try {
|
|
1119
|
+
const cookieString = await getCookieString(url);
|
|
1120
|
+
if (cookieString && typeof cookieString === 'string') {
|
|
1121
|
+
const pairs = cookieString.split(';').map(s => s.trim());
|
|
1122
|
+
for (const p of pairs) {
|
|
1123
|
+
const [k, ...rest] = p.split('=');
|
|
1124
|
+
if (k === name) return rest.join('=');
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
} catch (e) {
|
|
1128
|
+
// ignore and try next url
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
// ignore
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// try getCookies which returns Cookie objects
|
|
1137
|
+
try {
|
|
1138
|
+
if (typeof cookieJar.getCookies === 'function') {
|
|
1139
|
+
const getCookies = util.promisify(cookieJar.getCookies).bind(cookieJar);
|
|
1140
|
+
const urls = ['https://www.instagram.com', 'https://instagram.com', 'https://i.instagram.com'];
|
|
1141
|
+
for (const url of urls) {
|
|
1142
|
+
try {
|
|
1143
|
+
const cookies = await getCookies(url);
|
|
1144
|
+
if (Array.isArray(cookies)) {
|
|
1145
|
+
for (const c of cookies) {
|
|
1146
|
+
// cookie object shapes vary: check common keys
|
|
1147
|
+
const key = c.key ?? c.name ?? c.name;
|
|
1148
|
+
const val = c.value ?? c.value ?? c.value;
|
|
1149
|
+
if (key === name) return val;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
} catch (e) {
|
|
1153
|
+
// ignore and try next url
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} catch (e) {
|
|
1158
|
+
// ignore
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// try serialized cookie jar structure if present
|
|
1162
|
+
try {
|
|
1163
|
+
if (cookieJar && typeof cookieJar === 'object' && cookieJar.cookies && Array.isArray(cookieJar.cookies)) {
|
|
1164
|
+
const found = cookieJar.cookies.find(c => (c.key === name || c.name === name || c.name === name));
|
|
1165
|
+
if (found) return found.value ?? found.value;
|
|
1166
|
+
}
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
// ignore
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Extract cookie-derived fields (sessionid, csrftoken, ds_user_id, mid)
|
|
1175
|
+
async function extractCookieFields(igState) {
|
|
1176
|
+
const out = {
|
|
1177
|
+
sessionIdCookie: null,
|
|
1178
|
+
csrfTokenCookie: null,
|
|
1179
|
+
dsUserIdCookie: null,
|
|
1180
|
+
midCookie: null
|
|
1181
|
+
};
|
|
1182
|
+
try {
|
|
1183
|
+
const cookieJar = igState.cookieJar;
|
|
1184
|
+
out.sessionIdCookie = await getCookieValueFromJar(cookieJar, 'sessionid');
|
|
1185
|
+
out.csrfTokenCookie = await getCookieValueFromJar(cookieJar, 'csrftoken');
|
|
1186
|
+
out.dsUserIdCookie = await getCookieValueFromJar(cookieJar, 'ds_user_id') || await getCookieValueFromJar(cookieJar, 'ds_user');
|
|
1187
|
+
out.midCookie = await getCookieValueFromJar(cookieJar, 'mid');
|
|
1188
|
+
} catch (e) {
|
|
1189
|
+
// ignore
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async function extractStateData(igState) {
|
|
1195
|
+
// Basic creds (existing)
|
|
1196
|
+
const creds = {
|
|
1197
|
+
authorization: igState.authorization || null,
|
|
1198
|
+
igWWWClaim: igState.igWWWClaim || null,
|
|
1199
|
+
passwordEncryptionKeyId: igState.passwordEncryptionKeyId || null,
|
|
1200
|
+
passwordEncryptionPubKey: igState.passwordEncryptionPubKey || null
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
// Device remains same
|
|
1204
|
+
const device = {
|
|
1205
|
+
deviceString: igState.deviceString || null,
|
|
1206
|
+
deviceId: igState.deviceId || null,
|
|
1207
|
+
uuid: igState.uuid || null,
|
|
1208
|
+
phoneId: igState.phoneId || null,
|
|
1209
|
+
adid: igState.adid || null,
|
|
1210
|
+
build: igState.build || null
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
// Try to serialize cookies (kept separate)
|
|
1214
|
+
const cookies = await trySerializeCookieJar(igState);
|
|
1215
|
+
|
|
1216
|
+
// App state remains same
|
|
1217
|
+
const appState = {
|
|
1218
|
+
language: igState.language || 'en_US',
|
|
1219
|
+
timezoneOffset: igState.timezoneOffset || null,
|
|
1220
|
+
connectionTypeHeader: igState.connectionTypeHeader || 'WIFI',
|
|
1221
|
+
capabilitiesHeader: igState.capabilitiesHeader || null,
|
|
1222
|
+
checkpoint: igState.checkpoint || null,
|
|
1223
|
+
challenge: igState.challenge || null
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
// --- NEW: richer creds fields derived from igState + cookies ---
|
|
1227
|
+
// try to obtain cookie fields
|
|
1228
|
+
const cookieFields = await extractCookieFields(igState);
|
|
1229
|
+
|
|
1230
|
+
// Primary identifiers
|
|
1231
|
+
const userIdFromState = igState.cookieUserId || igState.userId || igState.user_id || null;
|
|
1232
|
+
const dsUserIdFromCookies = cookieFields.dsUserIdCookie || igState.dsUserId || igState.ds_user_id || null;
|
|
1233
|
+
const sessionIdFromCookies = cookieFields.sessionIdCookie || null;
|
|
1234
|
+
const csrfFromCookies = cookieFields.csrfTokenCookie || null;
|
|
1235
|
+
const midFromCookies = cookieFields.midCookie || igState.mid || null;
|
|
1236
|
+
|
|
1237
|
+
// username if exposed on state
|
|
1238
|
+
const usernameFromState = igState.username || igState.userName || igState.user?.username || null;
|
|
1239
|
+
|
|
1240
|
+
// rankToken: if userId + uuid available, form `${userId}_${uuid}`
|
|
1241
|
+
let rankToken = null;
|
|
1242
|
+
try {
|
|
1243
|
+
if (userIdFromState && (igState.uuid || device.uuid)) {
|
|
1244
|
+
rankToken = `${userIdFromState}_${igState.uuid || device.uuid}`;
|
|
1245
|
+
} else if (igState.rankToken) {
|
|
1246
|
+
rankToken = igState.rankToken;
|
|
1247
|
+
}
|
|
1248
|
+
} catch (e) {
|
|
1249
|
+
rankToken = igState.rankToken || null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// sessionId and csrfToken - prefer cookies, fallback to state fields if present
|
|
1253
|
+
const sessionId = sessionIdFromCookies || igState.sessionId || igState.sessionid || null;
|
|
1254
|
+
const csrfToken = csrfFromCookies || igState.csrfToken || igState.csrftoken || null;
|
|
1255
|
+
|
|
1256
|
+
// mid fallback
|
|
1257
|
+
const mid = midFromCookies || igState.mid || null;
|
|
1258
|
+
|
|
1259
|
+
// isLoggedIn heuristic: presence of sessionid cookie or an 'isLoggedIn' flag or authorization header
|
|
1260
|
+
const isLoggedIn = Boolean(
|
|
1261
|
+
sessionId ||
|
|
1262
|
+
igState.isLoggedIn ||
|
|
1263
|
+
(creds.authorization && creds.authorization.length)
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
// loginAt / lastValidatedAt: try to take from igState if present, otherwise null
|
|
1267
|
+
const loginAt = igState.loginAt || igState.loggedInAt || null;
|
|
1268
|
+
const lastValidatedAt = igState.lastValidatedAt || igState.lastValidated || null;
|
|
1269
|
+
|
|
1270
|
+
// Attach all new fields into creds object (non-destructive)
|
|
1271
|
+
creds.userId = userIdFromState || dsUserIdFromCookies || creds.userId || null;
|
|
1272
|
+
creds.dsUserId = dsUserIdFromCookies || null;
|
|
1273
|
+
creds.username = usernameFromState || null;
|
|
1274
|
+
creds.rankToken = rankToken;
|
|
1275
|
+
creds.sessionId = sessionId;
|
|
1276
|
+
creds.csrfToken = csrfToken;
|
|
1277
|
+
creds.mid = mid;
|
|
1278
|
+
creds.isLoggedIn = isLoggedIn;
|
|
1279
|
+
creds.loginAt = loginAt;
|
|
1280
|
+
creds.lastValidatedAt = lastValidatedAt;
|
|
1281
|
+
|
|
1282
|
+
return { creds, device, cookies, appState };
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async function applyStateData(igState, authState) {
|
|
1286
|
+
const { creds, device, cookies, appState } = authState.data;
|
|
1287
|
+
|
|
1288
|
+
if (creds) {
|
|
1289
|
+
if (creds.authorization) igState.authorization = creds.authorization;
|
|
1290
|
+
if (creds.igWWWClaim) igState.igWWWClaim = creds.igWWWClaim;
|
|
1291
|
+
if (creds.passwordEncryptionKeyId) igState.passwordEncryptionKeyId = creds.passwordEncryptionKeyId;
|
|
1292
|
+
if (creds.passwordEncryptionPubKey) igState.passwordEncryptionPubKey = creds.passwordEncryptionPubKey;
|
|
1293
|
+
// if igState provides a helper to update authorization, call it
|
|
1294
|
+
if (typeof igState.updateAuthorization === 'function') {
|
|
1295
|
+
try { igState.updateAuthorization(); } catch (e) {}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// apply new creds-derived fields if state supports them (non-intrusive)
|
|
1299
|
+
try {
|
|
1300
|
+
if (creds.userId && !igState.cookieUserId) igState.cookieUserId = creds.userId;
|
|
1301
|
+
if (creds.username && !igState.username) igState.username = creds.username;
|
|
1302
|
+
if (creds.rankToken && !igState.rankToken) igState.rankToken = creds.rankToken;
|
|
1303
|
+
if (creds.sessionId && !igState.sessionId) igState.sessionId = creds.sessionId;
|
|
1304
|
+
if (creds.csrfToken && !igState.csrfToken) igState.csrfToken = creds.csrfToken;
|
|
1305
|
+
if (creds.mid && !igState.mid) igState.mid = creds.mid;
|
|
1306
|
+
if (typeof creds.isLoggedIn !== 'undefined' && !igState.isLoggedIn) igState.isLoggedIn = creds.isLoggedIn;
|
|
1307
|
+
if (creds.loginAt && !igState.loginAt) igState.loginAt = creds.loginAt;
|
|
1308
|
+
if (creds.lastValidatedAt && !igState.lastValidatedAt) igState.lastValidatedAt = creds.lastValidatedAt;
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
// ignore if igState shape different
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (device) {
|
|
1315
|
+
if (device.deviceString) igState.deviceString = device.deviceString;
|
|
1316
|
+
if (device.deviceId) igState.deviceId = device.deviceId;
|
|
1317
|
+
if (device.uuid) igState.uuid = device.uuid;
|
|
1318
|
+
if (device.phoneId) igState.phoneId = device.phoneId;
|
|
1319
|
+
if (device.adid) igState.adid = device.adid;
|
|
1320
|
+
if (device.build) igState.build = device.build;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (cookies) {
|
|
1324
|
+
try {
|
|
1325
|
+
if (typeof igState.deserializeCookieJar === 'function') {
|
|
1326
|
+
await igState.deserializeCookieJar(cookies);
|
|
1327
|
+
} else if (igState.cookieJar && typeof igState.cookieJar.restore === 'function') {
|
|
1328
|
+
// fallback restore
|
|
1329
|
+
await util.promisify(igState.cookieJar.restore).bind(igState.cookieJar)(cookies);
|
|
1330
|
+
} else if (igState.cookieJar && typeof igState.cookieJar._importCookies === 'function') {
|
|
1331
|
+
// last-resort, not likely
|
|
1332
|
+
try { igState.cookieJar._importCookies(cookies); } catch (e) {}
|
|
1333
|
+
}
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
console.warn('[MultiFileAuthState] Could not deserialize cookies:', e.message);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (appState) {
|
|
1340
|
+
if (appState.language) igState.language = appState.language;
|
|
1341
|
+
if (appState.timezoneOffset) igState.timezoneOffset = appState.timezoneOffset;
|
|
1342
|
+
if (appState.connectionTypeHeader) igState.connectionTypeHeader = appState.connectionTypeHeader;
|
|
1343
|
+
if (appState.capabilitiesHeader) igState.capabilitiesHeader = appState.capabilitiesHeader;
|
|
1344
|
+
if (appState.checkpoint) igState.checkpoint = appState.checkpoint;
|
|
1345
|
+
if (appState.challenge) igState.challenge = appState.challenge;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Main exported helper that wires MultiFileAuthState to your clients
|
|
1350
|
+
async function useMultiFileAuthState(folder) {
|
|
1351
|
+
const authState = new MultiFileAuthState(folder);
|
|
1352
|
+
await authState.loadAll();
|
|
1353
|
+
|
|
1354
|
+
// Helper: persist additional derived creds fields when saving creds
|
|
1355
|
+
const enrichAndSaveCreds = async (igClientState) => {
|
|
1356
|
+
const { creds, device, cookies, appState } = await extractStateData(igClientState);
|
|
1357
|
+
// merge with existing creds non-destructive
|
|
1358
|
+
authState.data.creds = Object.assign({}, authState.data.creds || {}, creds);
|
|
1359
|
+
authState.data.device = Object.assign({}, authState.data.device || {}, device);
|
|
1360
|
+
authState.data.cookies = cookies || authState.data.cookies;
|
|
1361
|
+
authState.data.appState = Object.assign({}, authState.data.appState || {}, appState);
|
|
1362
|
+
await authState.saveCreds();
|
|
1363
|
+
await authState.saveAppState();
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
const saveCreds = async (igClient) => {
|
|
1367
|
+
if (!igClient || !igClient.state) {
|
|
1368
|
+
console.warn('[useMultiFileAuthState] No igClient provided to saveCreds');
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Use the enriched saver
|
|
1373
|
+
await enrichAndSaveCreds(igClient.state);
|
|
1374
|
+
|
|
1375
|
+
// Also attempt to extract some cookie-derived fields for convenience
|
|
1376
|
+
try {
|
|
1377
|
+
const cookieFields = await extractCookieFields(igClient.state);
|
|
1378
|
+
if (cookieFields.sessionIdCookie) {
|
|
1379
|
+
if (!authState.data.creds) authState.data.creds = {};
|
|
1380
|
+
authState.data.creds.sessionId = cookieFields.sessionIdCookie;
|
|
1381
|
+
}
|
|
1382
|
+
if (cookieFields.dsUserIdCookie) {
|
|
1383
|
+
if (!authState.data.creds) authState.data.creds = {};
|
|
1384
|
+
authState.data.creds.dsUserId = cookieFields.dsUserIdCookie;
|
|
1385
|
+
}
|
|
1386
|
+
authState._debouncedSave(['creds']);
|
|
1387
|
+
} catch (e) {}
|
|
1388
|
+
|
|
1389
|
+
console.log('[useMultiFileAuthState] Credentials saved to', folder);
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
const saveMqttSession = async (realtimeClient) => {
|
|
1393
|
+
if (!realtimeClient) {
|
|
1394
|
+
console.warn('[useMultiFileAuthState] No realtimeClient provided');
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// base mqtt session
|
|
1399
|
+
const mqttSession = {
|
|
1400
|
+
sessionId: null,
|
|
1401
|
+
mqttSessionId: null,
|
|
1402
|
+
lastConnected: new Date().toISOString(),
|
|
1403
|
+
userId: null
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
try {
|
|
1407
|
+
if (realtimeClient.ig && realtimeClient.ig.state) {
|
|
1408
|
+
mqttSession.userId = realtimeClient.ig.state.cookieUserId || realtimeClient.ig.state.userId || null;
|
|
1409
|
+
// attempt to extract sessionId from JWT helper if available
|
|
1410
|
+
mqttSession.sessionId = (typeof realtimeClient.extractSessionIdFromJWT === 'function') ? realtimeClient.extractSessionIdFromJWT() : null;
|
|
1411
|
+
}
|
|
1412
|
+
if (realtimeClient.connection) {
|
|
1413
|
+
mqttSession.mqttSessionId = realtimeClient.connection?.clientInfo?.clientMqttSessionId?.toString() || null;
|
|
1414
|
+
}
|
|
1415
|
+
} catch (e) {
|
|
1416
|
+
// ignore
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// subscriptions
|
|
1420
|
+
const subscriptions = {
|
|
1421
|
+
graphQlSubs: realtimeClient.initOptions?.graphQlSubs || [],
|
|
1422
|
+
skywalkerSubs: realtimeClient.initOptions?.skywalkerSubs || [],
|
|
1423
|
+
subscribedAt: new Date().toISOString()
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
// seq ids (iris)
|
|
1427
|
+
const seqIds = {};
|
|
1428
|
+
if (realtimeClient.initOptions?.irisData) {
|
|
1429
|
+
seqIds.seq_id = realtimeClient.initOptions.irisData.seq_id || null;
|
|
1430
|
+
seqIds.snapshot_at_ms = realtimeClient.initOptions.irisData.snapshot_at_ms || null;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// --- Extended MQTT / realtime info (new) ---
|
|
1434
|
+
// irisState: things like subscription_id / device id / other iris metadata
|
|
1435
|
+
const irisState = {};
|
|
1436
|
+
try {
|
|
1437
|
+
// try to glean from various possible places on realtimeClient
|
|
1438
|
+
irisState.subscription_id = realtimeClient.initOptions?.irisData?.subscription_id || realtimeClient.iris?.subscriptionId || realtimeClient.irisState?.subscription_id || null;
|
|
1439
|
+
irisState.user_id = realtimeClient.ig?.state?.cookieUserId || realtimeClient.ig?.state?.userId || irisState.user_id || null;
|
|
1440
|
+
irisState.device_id = realtimeClient.connection?.clientInfo?.deviceId || realtimeClient.initOptions?.deviceId || null;
|
|
1441
|
+
irisState.created_at = new Date().toISOString();
|
|
1442
|
+
} catch (e) {}
|
|
1443
|
+
|
|
1444
|
+
// mqttTopics: topics observed/subscribed
|
|
1445
|
+
const mqttTopics = {};
|
|
1446
|
+
try {
|
|
1447
|
+
mqttTopics.topics = realtimeClient.connection?.subscribedTopics || realtimeClient._subscribedTopics || realtimeClient.subscribedTopics || [];
|
|
1448
|
+
mqttTopics.updatedAt = new Date().toISOString();
|
|
1449
|
+
} catch (e) {
|
|
1450
|
+
mqttTopics.topics = [];
|
|
1451
|
+
mqttTopics.updatedAt = new Date().toISOString();
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// mqttCapabilities: features supported / protocol version
|
|
1455
|
+
const mqttCapabilities = {};
|
|
1456
|
+
try {
|
|
1457
|
+
mqttCapabilities.supportsTyping = Boolean(realtimeClient.initOptions?.supportsTyping ?? realtimeClient.capabilities?.supportsTyping);
|
|
1458
|
+
mqttCapabilities.supportsReactions = Boolean(realtimeClient.initOptions?.supportsReactions ?? realtimeClient.capabilities?.supportsReactions);
|
|
1459
|
+
mqttCapabilities.supportsVoice = Boolean(realtimeClient.initOptions?.supportsVoice ?? realtimeClient.capabilities?.supportsVoice);
|
|
1460
|
+
mqttCapabilities.protocolVersion = realtimeClient.connection?.protocolVersion || realtimeClient.initOptions?.protocolVersion || null;
|
|
1461
|
+
mqttCapabilities.collectedAt = new Date().toISOString();
|
|
1462
|
+
} catch (e) {}
|
|
1463
|
+
|
|
1464
|
+
// mqttAuth: if realtimeClient keeps an auth token / jwt for mqtt, store (but be careful: short lived)
|
|
1465
|
+
const mqttAuth = {};
|
|
1466
|
+
try {
|
|
1467
|
+
mqttAuth.jwt = realtimeClient.connection?.authToken || realtimeClient.mqttAuth?.jwt || realtimeClient.initOptions?.mqttJwt || null;
|
|
1468
|
+
mqttAuth.expiresAt = realtimeClient.connection?.authExpiresAt || realtimeClient.mqttAuth?.expiresAt || null;
|
|
1469
|
+
mqttAuth.collectedAt = new Date().toISOString();
|
|
1470
|
+
} catch (e) {}
|
|
1471
|
+
|
|
1472
|
+
// lastPublishIds: tracking last message ids for reliable publish/acks
|
|
1473
|
+
const lastPublishIds = {};
|
|
1474
|
+
try {
|
|
1475
|
+
lastPublishIds.lastMessageId = realtimeClient._lastMessageId || realtimeClient.lastMessageId || null;
|
|
1476
|
+
lastPublishIds.lastAckedAt = realtimeClient._lastAckedAt || null;
|
|
1477
|
+
lastPublishIds.collectedAt = new Date().toISOString();
|
|
1478
|
+
} catch (e) {}
|
|
1479
|
+
|
|
1480
|
+
// Assign all into authState and persist
|
|
1481
|
+
authState.data.mqttSession = mqttSession;
|
|
1482
|
+
authState.data.subscriptions = subscriptions;
|
|
1483
|
+
authState.data.seqIds = seqIds;
|
|
1484
|
+
|
|
1485
|
+
authState.data.irisState = irisState;
|
|
1486
|
+
authState.data.mqttTopics = mqttTopics;
|
|
1487
|
+
authState.data.mqttCapabilities = mqttCapabilities;
|
|
1488
|
+
authState.data.mqttAuth = mqttAuth;
|
|
1489
|
+
authState.data.lastPublishIds = lastPublishIds;
|
|
1490
|
+
|
|
1491
|
+
// Increment stats
|
|
1492
|
+
authState.incrementStat('mqttMessages');
|
|
1493
|
+
|
|
1494
|
+
await authState.saveMqttState();
|
|
1495
|
+
console.log('[useMultiFileAuthState] MQTT session saved to', folder);
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
const loadCreds = async (igClient) => {
|
|
1499
|
+
if (!igClient || !igClient.state) {
|
|
1500
|
+
console.warn('[useMultiFileAuthState] No igClient provided to loadCreds');
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (!authState.hasValidSession()) {
|
|
1505
|
+
console.log('[useMultiFileAuthState] No valid session found');
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
await applyStateData(igClient.state, authState);
|
|
1510
|
+
console.log('[useMultiFileAuthState] Credentials loaded from', folder);
|
|
1511
|
+
return true;
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
const getMqttConnectOptions = () => {
|
|
1515
|
+
if (!authState.hasMqttSession()) {
|
|
1516
|
+
// still return a warm default to help warm-restore
|
|
1517
|
+
const defaultTopics = ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'];
|
|
1518
|
+
return {
|
|
1519
|
+
graphQlSubs: defaultTopics,
|
|
1520
|
+
skywalkerSubs: ['presence_subscribe', 'typing_subscribe'],
|
|
1521
|
+
irisData: { seq_id: authState.getSeqIds()?.seq_id || 0, snapshot_at_ms: authState.getSeqIds()?.snapshot_at_ms || Date.now() },
|
|
1522
|
+
mqttAuthToken: authState.getMqttAuth()?.jwt || null,
|
|
1523
|
+
mqttAuthExpiresAt: authState.getMqttAuth()?.expiresAt || null
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
const subs = authState.getSubscriptions() || {};
|
|
1528
|
+
const seqIds = authState.getSeqIds() || {};
|
|
1529
|
+
const mqttAuth = authState.getMqttAuth() || null;
|
|
1530
|
+
|
|
1531
|
+
return {
|
|
1532
|
+
graphQlSubs: subs.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
|
|
1533
|
+
skywalkerSubs: subs.skywalkerSubs || ['presence_subscribe', 'typing_subscribe'],
|
|
1534
|
+
irisData: seqIds.seq_id ? {
|
|
1535
|
+
seq_id: seqIds.seq_id,
|
|
1536
|
+
snapshot_at_ms: seqIds.snapshot_at_ms
|
|
1537
|
+
} : { seq_id: 0, snapshot_at_ms: Date.now() },
|
|
1538
|
+
mqttAuthToken: mqttAuth?.jwt || null,
|
|
1539
|
+
mqttAuthExpiresAt: mqttAuth?.expiresAt || null
|
|
1540
|
+
};
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
const clearSession = async () => {
|
|
1544
|
+
await authState.clearAll();
|
|
1545
|
+
console.log('[useMultiFileAuthState] Session cleared');
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
const isSessionValid = async (igClient) => {
|
|
1549
|
+
if (!authState.hasValidSession()) return false;
|
|
1550
|
+
if (!igClient) return true;
|
|
1551
|
+
|
|
1552
|
+
try {
|
|
1553
|
+
// quick check by calling account/currentUser or equivalent
|
|
1554
|
+
if (igClient.account && typeof igClient.account.currentUser === 'function') {
|
|
1555
|
+
await igClient.account.currentUser();
|
|
1556
|
+
return true;
|
|
1557
|
+
}
|
|
1558
|
+
// fallback assume valid if we have creds - up to caller to verify
|
|
1559
|
+
return true;
|
|
1560
|
+
} catch (e) {
|
|
1561
|
+
console.warn('[useMultiFileAuthState] Session validation failed:', e.message);
|
|
1562
|
+
// emit session-expired if credentials appear invalid
|
|
1563
|
+
authState.emit('session-expired', { reason: e.message });
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// --- Auto-refresh / revalidation helpers ---
|
|
1569
|
+
const _autoRefreshCheck = async () => {
|
|
1570
|
+
try {
|
|
1571
|
+
if (!authState.data.creds) return;
|
|
1572
|
+
// If creds indicate expiry timestamp, check it
|
|
1573
|
+
const expiresAt = authState.data.creds.sessionExpiresAt ? new Date(authState.data.creds.sessionExpiresAt).getTime() : null;
|
|
1574
|
+
if (expiresAt && Date.now() > expiresAt) {
|
|
1575
|
+
authState.data.creds.isExpired = true;
|
|
1576
|
+
authState._debouncedSave(['creds']);
|
|
1577
|
+
authState.emit('session-expired', { reason: 'sessionExpiresAt' });
|
|
1578
|
+
} else {
|
|
1579
|
+
// if igClient provided, optionally validate remote
|
|
1580
|
+
if (authState._autoRefreshClient) {
|
|
1581
|
+
try {
|
|
1582
|
+
const valid = await isSessionValid(authState._autoRefreshClient);
|
|
1583
|
+
if (!valid) {
|
|
1584
|
+
authState.emit('session-expired', { reason: 'remote-check' });
|
|
1585
|
+
} else {
|
|
1586
|
+
// update lastValidatedAt
|
|
1587
|
+
if (!authState.data.creds) authState.data.creds = {};
|
|
1588
|
+
authState.data.creds.lastValidatedAt = new Date().toISOString();
|
|
1589
|
+
authState._debouncedSave(['creds']);
|
|
1590
|
+
}
|
|
1591
|
+
} catch (e) {}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
} catch (e) {
|
|
1595
|
+
// ignore
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1599
|
+
const enableAutoRefresh = (igClient, intervalMs = 5 * 60 * 1000) => {
|
|
1600
|
+
// igClient optional: if provided, will be used to do remote validation
|
|
1601
|
+
try {
|
|
1602
|
+
disableAutoRefresh();
|
|
1603
|
+
authState._autoRefreshClient = igClient || null;
|
|
1604
|
+
authState._autoRefreshIntervalMs = intervalMs;
|
|
1605
|
+
authState._autoRefreshTimer = setInterval(_autoRefreshCheck, intervalMs);
|
|
1606
|
+
// run immediately once
|
|
1607
|
+
_autoRefreshCheck();
|
|
1608
|
+
authState.emit('auto-refresh-enabled', { intervalMs });
|
|
1609
|
+
} catch (e) {
|
|
1610
|
+
console.warn('[useMultiFileAuthState] enableAutoRefresh error:', e.message);
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
const disableAutoRefresh = () => {
|
|
1615
|
+
try {
|
|
1616
|
+
if (authState._autoRefreshTimer) {
|
|
1617
|
+
clearInterval(authState._autoRefreshTimer);
|
|
1618
|
+
authState._autoRefreshTimer = null;
|
|
1619
|
+
authState._autoRefreshClient = null;
|
|
1620
|
+
authState.emit('auto-refresh-disabled');
|
|
1621
|
+
}
|
|
1622
|
+
} catch (e) {}
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
// --- Login history / audit helpers ---
|
|
1626
|
+
const recordLoginAttempt = async ({ success, ip = null, userAgent = null, method = 'password', reason = null }) => {
|
|
1627
|
+
const entry = authState.recordLoginAttempt({ success, ip, userAgent, method, reason });
|
|
1628
|
+
// return entry for caller
|
|
1629
|
+
return entry;
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
// --- Account restriction helpers ---
|
|
1633
|
+
const markAccountRestricted = ({ status = 'checkpoint', reason = null }) => {
|
|
1634
|
+
authState.markAccountRestricted({ status, reason });
|
|
1635
|
+
};
|
|
1636
|
+
|
|
1637
|
+
const clearAccountRestriction = () => {
|
|
1638
|
+
authState.clearAccountRestriction();
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
// --- Usage stats helpers ---
|
|
1642
|
+
const incrementStat = (statName, by = 1) => {
|
|
1643
|
+
authState.incrementStat(statName, by);
|
|
1644
|
+
};
|
|
1645
|
+
|
|
1646
|
+
// --- Backup / restore helpers exposed ---
|
|
1647
|
+
const restoreLatestBackup = async (key) => {
|
|
1648
|
+
return await authState.restoreLatestBackup(key);
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
// --- Device fingerprinting ---
|
|
1652
|
+
const lockDeviceFingerprint = async () => {
|
|
1653
|
+
return await authState.lockDeviceFingerprint();
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
const verifyDeviceFingerprint = (deviceObj) => {
|
|
1657
|
+
return authState.verifyDeviceFingerprint(deviceObj);
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
// --- Accounts manager ---
|
|
1661
|
+
const registerAccount = async (name, folderPath) => {
|
|
1662
|
+
if (!authState.data.accountsIndex) authState.data.accountsIndex = {};
|
|
1663
|
+
authState.data.accountsIndex[name] = folderPath;
|
|
1664
|
+
await authState._writeFile('accountsIndex', authState.data.accountsIndex);
|
|
1665
|
+
authState.emit('account-registered', { name, folderPath });
|
|
1666
|
+
return authState.data.accountsIndex;
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
const listAccounts = () => {
|
|
1670
|
+
return authState.getAccountsIndex();
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
// --- Health check ---
|
|
1674
|
+
const getHealth = () => {
|
|
1675
|
+
return authState.getHealth();
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
// --- MQTT helpers exposing internal features ---
|
|
1679
|
+
const attachRealtimeClient = (realtimeClient, opts = {}) => {
|
|
1680
|
+
return authState.attachRealtimeClient(realtimeClient, opts);
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
const enqueueOutbox = (item) => authState.enqueueOutbox(item);
|
|
1684
|
+
const flushOutbox = (client, max) => authState.flushOutbox(client, max);
|
|
1685
|
+
const checkSubscriptions = (client) => authState.checkSubscriptions(client);
|
|
1686
|
+
const computeRisk = () => authState._computeRiskScore();
|
|
1687
|
+
const warmRestore = (client) => authState.warmRestoreClient(client);
|
|
1688
|
+
const getMqttHealth = () => authState.getMqttHealth();
|
|
1689
|
+
const getMqttTraffic = () => authState.getMqttTraffic();
|
|
1690
|
+
const getMqttRisk = () => authState.getMqttRisk();
|
|
1691
|
+
const isDuplicateMessage = (id) => authState.isDuplicateMessage(id);
|
|
1692
|
+
const markPing = () => authState.markPing();
|
|
1693
|
+
const markPong = () => authState.markPong();
|
|
1694
|
+
|
|
1695
|
+
// --- Misc helpers: loginHistory & usageStats getters ---
|
|
1696
|
+
const getLoginHistory = () => authState.getLoginHistory();
|
|
1697
|
+
const getUsageStats = () => authState.getUsageStats();
|
|
1698
|
+
|
|
1699
|
+
return {
|
|
1700
|
+
state: authState,
|
|
1701
|
+
|
|
1702
|
+
saveCreds,
|
|
1703
|
+
loadCreds,
|
|
1704
|
+
|
|
1705
|
+
saveMqttSession,
|
|
1706
|
+
getMqttConnectOptions,
|
|
1707
|
+
|
|
1708
|
+
clearSession,
|
|
1709
|
+
isSessionValid,
|
|
1710
|
+
|
|
1711
|
+
hasSession: () => authState.hasValidSession(),
|
|
1712
|
+
hasMqttSession: () => authState.hasMqttSession(),
|
|
1713
|
+
|
|
1714
|
+
folder,
|
|
1715
|
+
|
|
1716
|
+
// pass-through for convenience
|
|
1717
|
+
getData: () => authState.data,
|
|
1718
|
+
|
|
1719
|
+
// convenience setters/getters for the new data
|
|
1720
|
+
setIrisState: (s) => authState.setIrisState(s),
|
|
1721
|
+
setMqttTopics: (t) => authState.setMqttTopics(t),
|
|
1722
|
+
setMqttCapabilities: (c) => authState.setMqttCapabilities(c),
|
|
1723
|
+
setMqttAuth: (a) => authState.setMqttAuth(a),
|
|
1724
|
+
setLastPublishIds: (p) => authState.setLastPublishIds(p),
|
|
1725
|
+
getIrisState: () => authState.getIrisState(),
|
|
1726
|
+
getMqttTopics: () => authState.getMqttTopics(),
|
|
1727
|
+
getMqttCapabilities: () => authState.getMqttCapabilities(),
|
|
1728
|
+
getMqttAuth: () => authState.getMqttAuth(),
|
|
1729
|
+
getLastPublishIds: () => authState.getLastPublishIds(),
|
|
1730
|
+
|
|
1731
|
+
// new utilities
|
|
1732
|
+
enableAutoRefresh,
|
|
1733
|
+
disableAutoRefresh,
|
|
1734
|
+
recordLoginAttempt,
|
|
1735
|
+
getLoginHistory,
|
|
1736
|
+
markAccountRestricted,
|
|
1737
|
+
clearAccountRestriction,
|
|
1738
|
+
incrementStat,
|
|
1739
|
+
getUsageStats,
|
|
1740
|
+
restoreLatestBackup,
|
|
1741
|
+
lockDeviceFingerprint,
|
|
1742
|
+
verifyDeviceFingerprint,
|
|
1743
|
+
registerAccount,
|
|
1744
|
+
listAccounts,
|
|
1745
|
+
getHealth,
|
|
1746
|
+
|
|
1747
|
+
// mqtt helpers
|
|
1748
|
+
attachRealtimeClient,
|
|
1749
|
+
enqueueOutbox,
|
|
1750
|
+
flushOutbox,
|
|
1751
|
+
checkSubscriptions,
|
|
1752
|
+
computeRisk,
|
|
1753
|
+
warmRestore,
|
|
1754
|
+
getMqttHealth,
|
|
1755
|
+
getMqttTraffic,
|
|
1756
|
+
getMqttRisk,
|
|
1757
|
+
isDuplicateMessage,
|
|
1758
|
+
markPing,
|
|
1759
|
+
markPong
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
module.exports = {
|
|
1764
|
+
useMultiFileAuthState,
|
|
1765
|
+
MultiFileAuthState,
|
|
1766
|
+
extractStateData,
|
|
1767
|
+
applyStateData
|
|
1768
|
+
};
|