nodejs-insta-private-api-mqtt 1.0.0
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/README.md +1650 -0
- package/dist/constants/constants.js +280 -0
- package/dist/constants/index.js +41 -0
- package/dist/core/client.js +243 -0
- package/dist/core/repository.js +7 -0
- package/dist/core/request.js +212 -0
- package/dist/core/state.js +1456 -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 +30 -0
- package/dist/errors/index.js.map +1 -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 +179 -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 +39 -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 +120 -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 +56 -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 +21 -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 +186 -0
- package/dist/realtime/commands/direct.commands.js.map +1 -0
- package/dist/realtime/commands/enhanced.direct.commands.js +987 -0
- package/dist/realtime/commands/index.d.ts +2 -0
- package/dist/realtime/commands/index.js +19 -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 +73 -0
- package/dist/realtime/features/gap-handler.js +61 -0
- package/dist/realtime/features/presence.manager.js +66 -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 +381 -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 +55 -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 +449 -0
- package/dist/realtime/realtime.service.js +462 -0
- package/dist/realtime/reconnect.manager.js +94 -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 +261 -0
- package/dist/repositories/direct-thread.repository.js +247 -0
- package/dist/repositories/direct.repository.js +153 -0
- package/dist/repositories/feed.repository.js +233 -0
- package/dist/repositories/friendship.repository.js +190 -0
- package/dist/repositories/hashtag.repository.js +101 -0
- package/dist/repositories/highlights.repository.js +127 -0
- package/dist/repositories/location.repository.js +84 -0
- package/dist/repositories/media.repository.js +165 -0
- package/dist/repositories/story.repository.js +156 -0
- package/dist/repositories/upload.repository.js +167 -0
- package/dist/repositories/user.repository.js +94 -0
- package/dist/sendmedia/index.js +11 -0
- package/dist/sendmedia/sendFile.js +154 -0
- package/dist/sendmedia/sendPhoto.js +145 -0
- package/dist/sendmedia/uploadPhoto.js +175 -0
- package/dist/sendmedia/uploadfFile.js +264 -0
- package/dist/services/live.service.js +147 -0
- package/dist/services/search.service.js +116 -0
- package/dist/shared/index.js +35 -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 +437 -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/examples/listen-to-messages.js +86 -0
- package/package.json +79 -0
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
// state.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const util = require('util');
|
|
7
|
+
const { CookieJar } = require('tough-cookie');
|
|
8
|
+
const Chance = require('chance');
|
|
9
|
+
const Constants = require('../constants');
|
|
10
|
+
const EventEmitter = require('events');
|
|
11
|
+
|
|
12
|
+
const SESSION_FILE = path.resolve(process.cwd(), 'session.json');
|
|
13
|
+
const SESSION_BACKUP = path.resolve(process.cwd(), 'session_backup.json');
|
|
14
|
+
|
|
15
|
+
class State {
|
|
16
|
+
constructor() {
|
|
17
|
+
// public constants reference (kept as property for backwards compat)
|
|
18
|
+
this.constants = Constants;
|
|
19
|
+
|
|
20
|
+
// basic defaults
|
|
21
|
+
this.language = 'en_US';
|
|
22
|
+
this.timezoneOffset = String(new Date().getTimezoneOffset() * -60);
|
|
23
|
+
this.radioType = 'wifi-none';
|
|
24
|
+
this.capabilitiesHeader = '3brTv10=';
|
|
25
|
+
this.connectionTypeHeader = 'WIFI';
|
|
26
|
+
this.isLayoutRTL = false;
|
|
27
|
+
this.adsOptOut = false;
|
|
28
|
+
this.thumbnailCacheBustingValue = 1000;
|
|
29
|
+
this.proxyUrl = null;
|
|
30
|
+
this.checkpoint = null;
|
|
31
|
+
this.challenge = null;
|
|
32
|
+
this.clientSessionIdLifetime = 1200000;
|
|
33
|
+
this.pigeonSessionIdLifetime = 1200000;
|
|
34
|
+
this.parsedAuthorization = undefined;
|
|
35
|
+
|
|
36
|
+
// ===== PLATFORM SUPPORT (iOS + Android) =====
|
|
37
|
+
this.platform = 'android'; // 'android' or 'ios'
|
|
38
|
+
this.iosVersion = '18.1';
|
|
39
|
+
this.iosAppVersion = '347.0.0.36.89';
|
|
40
|
+
this.iosAppVersionCode = '618023787';
|
|
41
|
+
this.iosDeviceModel = 'iPhone16,2'; // iPhone 15 Pro Max
|
|
42
|
+
this.iosDeviceName = 'iPhone';
|
|
43
|
+
this.iosBundleId = 'com.burbn.instagram';
|
|
44
|
+
|
|
45
|
+
// cookie jar (tough-cookie)
|
|
46
|
+
this.cookieJar = new CookieJar();
|
|
47
|
+
|
|
48
|
+
// device defaults
|
|
49
|
+
this.generateDevice('instagram-private-api');
|
|
50
|
+
|
|
51
|
+
// internal event emitter (non-invasive - backward-compatible)
|
|
52
|
+
this._emitter = new EventEmitter();
|
|
53
|
+
|
|
54
|
+
// internal watcher handle for session file (if used)
|
|
55
|
+
this._sessionFileWatcher = null;
|
|
56
|
+
|
|
57
|
+
// Default values for added utilities
|
|
58
|
+
this._saveRetries = 3;
|
|
59
|
+
this._saveRetryDelayMs = 300;
|
|
60
|
+
this._maxBackupCopies = 5; // rotate backups up to this many
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ===== getters mapping to constants (read-only) =====
|
|
64
|
+
get appVersion() {
|
|
65
|
+
return this.constants.APP_VERSION;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get appVersionCode() {
|
|
69
|
+
return this.constants.APP_VERSION_CODE;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get signatureKey() {
|
|
73
|
+
return this.constants.SIGNATURE_KEY;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get signatureVersion() {
|
|
77
|
+
return this.constants.SIGNATURE_VERSION;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get fbAnalyticsApplicationId() {
|
|
81
|
+
return this.constants.FACEBOOK_ANALYTICS_APPLICATION_ID;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get bloksVersionId() {
|
|
85
|
+
return this.constants.BLOKS_VERSION_ID;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get clientSessionId() {
|
|
89
|
+
return this.generateTemporaryGuid('clientSessionId', this.clientSessionIdLifetime);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get pigeonSessionId() {
|
|
93
|
+
return this.generateTemporaryGuid('pigeonSessionId', this.pigeonSessionIdLifetime);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get appUserAgent() {
|
|
97
|
+
if (this.platform === 'ios') {
|
|
98
|
+
return this.iosUserAgent;
|
|
99
|
+
}
|
|
100
|
+
return `Instagram ${this.appVersion} Android (${this.deviceString}; ${this.language}; ${this.appVersionCode})`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get iosUserAgent() {
|
|
104
|
+
return `Instagram ${this.iosAppVersion} (${this.iosDeviceModel}; iOS ${this.iosVersion}; ${this.language}; ${this.language}; scale=3.00; ${this.iosResolution}; ${this.iosAppVersionCode}) AppleWebKit/420+`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get iosResolution() {
|
|
108
|
+
const resolutions = {
|
|
109
|
+
'iPhone17,1': '1320x2868', // iPhone 16 Pro Max
|
|
110
|
+
'iPhone17,2': '1206x2622', // iPhone 16 Pro
|
|
111
|
+
'iPhone17,3': '1290x2796', // iPhone 16 Plus
|
|
112
|
+
'iPhone17,4': '1179x2556', // iPhone 16
|
|
113
|
+
'iPhone16,1': '1179x2556', // iPhone 15 Pro
|
|
114
|
+
'iPhone16,2': '1290x2796', // iPhone 15 Pro Max
|
|
115
|
+
'iPhone15,4': '1179x2556', // iPhone 15
|
|
116
|
+
'iPhone15,5': '1290x2796', // iPhone 15 Plus
|
|
117
|
+
'iPhone15,2': '1179x2556', // iPhone 14 Pro
|
|
118
|
+
'iPhone15,3': '1290x2796', // iPhone 14 Pro Max
|
|
119
|
+
'iPhone14,7': '1170x2532', // iPhone 14
|
|
120
|
+
'iPhone14,8': '1284x2778', // iPhone 14 Plus
|
|
121
|
+
'iPhone14,2': '1170x2532', // iPhone 13 Pro
|
|
122
|
+
'iPhone14,3': '1284x2778', // iPhone 13 Pro Max
|
|
123
|
+
'iPhone14,5': '1170x2532', // iPhone 13
|
|
124
|
+
'iPhone13,2': '1170x2532', // iPhone 12
|
|
125
|
+
'iPhone13,3': '1170x2532', // iPhone 12 Pro
|
|
126
|
+
'iPhone13,4': '1284x2778', // iPhone 12 Pro Max
|
|
127
|
+
'iPad14,3': '2048x2732', // iPad Pro 12.9" (6th gen)
|
|
128
|
+
'iPad14,4': '2048x2732', // iPad Pro 12.9" (6th gen)
|
|
129
|
+
'iPad14,5': '1668x2388', // iPad Pro 11" (4th gen)
|
|
130
|
+
'iPad14,6': '1668x2388', // iPad Pro 11" (4th gen)
|
|
131
|
+
'iPad13,18': '2360x1640', // iPad Air (5th gen)
|
|
132
|
+
'iPad13,19': '2360x1640', // iPad Air (5th gen)
|
|
133
|
+
};
|
|
134
|
+
return resolutions[this.iosDeviceModel] || '1290x2796';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get packageName() {
|
|
138
|
+
return this.platform === 'ios' ? this.iosBundleId : 'com.instagram.android';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ===== cookies/auth helpers =====
|
|
142
|
+
extractCookie(key) {
|
|
143
|
+
// tough-cookie CookieJar returns array via getCookiesSync in some versions; use synchronous API if present
|
|
144
|
+
try {
|
|
145
|
+
const cookies = this.cookieJar.getCookiesSync
|
|
146
|
+
? this.cookieJar.getCookiesSync(this.constants.HOST)
|
|
147
|
+
: this.cookieJar.getCookies(this.constants.HOST);
|
|
148
|
+
// cookies might be an array or a Promise; if array, find
|
|
149
|
+
if (Array.isArray(cookies)) {
|
|
150
|
+
const found = cookies.find(c => c.key === key);
|
|
151
|
+
return found || null;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// fallback: try jar serialized introspection (rare)
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
extractCookieValue(key) {
|
|
161
|
+
const cookie = this.extractCookie(key);
|
|
162
|
+
if (!cookie) {
|
|
163
|
+
throw new Error(`Could not find cookie: ${key}`);
|
|
164
|
+
}
|
|
165
|
+
return cookie.value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get cookieCsrfToken() {
|
|
169
|
+
try {
|
|
170
|
+
return this.extractCookieValue('csrftoken');
|
|
171
|
+
} catch {
|
|
172
|
+
return 'missing';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get cookieUserId() {
|
|
177
|
+
try {
|
|
178
|
+
return this.extractCookieValue('ds_user_id');
|
|
179
|
+
} catch {
|
|
180
|
+
// fallback to parsed authorization if available
|
|
181
|
+
this.updateAuthorization();
|
|
182
|
+
if (!this.parsedAuthorization) throw new Error('Could not find ds_user_id');
|
|
183
|
+
return this.parsedAuthorization.ds_user_id;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
get cookieUsername() {
|
|
188
|
+
try {
|
|
189
|
+
return this.extractCookieValue('ds_user');
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
hasValidAuthorization() {
|
|
196
|
+
return this.parsedAuthorization && this.parsedAuthorization.authorizationTag === this.authorization;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
updateAuthorization() {
|
|
200
|
+
if (!this.authorization) {
|
|
201
|
+
this.parsedAuthorization = undefined;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (this.hasValidAuthorization()) return;
|
|
205
|
+
if (typeof this.authorization === 'string' && this.authorization.startsWith('Bearer IGT:2:')) {
|
|
206
|
+
try {
|
|
207
|
+
const json = Buffer.from(this.authorization.substring('Bearer IGT:2:'.length), 'base64').toString();
|
|
208
|
+
const parsed = JSON.parse(json);
|
|
209
|
+
// keep an extra tag to detect equality later
|
|
210
|
+
parsed.authorizationTag = this.authorization;
|
|
211
|
+
this.parsedAuthorization = parsed;
|
|
212
|
+
} catch (e) {
|
|
213
|
+
this.parsedAuthorization = undefined;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
this.parsedAuthorization = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
refreshAuthorization(newAuthToken) {
|
|
221
|
+
if (!newAuthToken || typeof newAuthToken !== 'string') return false;
|
|
222
|
+
this.authorization = newAuthToken;
|
|
223
|
+
this.updateAuthorization();
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ===== serialization helpers for cookieJar =====
|
|
228
|
+
async serializeCookieJar() {
|
|
229
|
+
// CookieJar.serialize(cb) exists in tough-cookie; wrap it
|
|
230
|
+
const serializeFn = util.promisify((cb) => {
|
|
231
|
+
try {
|
|
232
|
+
this.cookieJar.serialize(cb);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
cb(err);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
const data = await serializeFn();
|
|
238
|
+
// return an object safe to JSON.stringify
|
|
239
|
+
return data;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async deserializeCookieJar(serialized) {
|
|
243
|
+
// Accept serialized either as string (JSON) or object
|
|
244
|
+
let obj = serialized;
|
|
245
|
+
if (typeof serialized === 'string') {
|
|
246
|
+
try {
|
|
247
|
+
obj = JSON.parse(serialized);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
obj = serialized;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const deserializeFn = util.promisify((input, cb) => {
|
|
253
|
+
try {
|
|
254
|
+
CookieJar.deserialize(input, cb);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
cb(err);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
// CookieJar.deserialize returns a CookieJar instance
|
|
260
|
+
const jar = await deserializeFn(obj);
|
|
261
|
+
if (jar && typeof jar === 'object') {
|
|
262
|
+
this.cookieJar = jar;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ===== main serialize / deserialize for whole state =====
|
|
267
|
+
/**
|
|
268
|
+
* Return a plain-object ready to be JSON.stringify-ed and saved to disk.
|
|
269
|
+
*/
|
|
270
|
+
async serialize() {
|
|
271
|
+
const cookieData = await this.serializeCookieJar();
|
|
272
|
+
const obj = {
|
|
273
|
+
constants: this.constants,
|
|
274
|
+
cookies: cookieData,
|
|
275
|
+
// include selective state fields (device + auth + extra fields commonly expected)
|
|
276
|
+
deviceString: this.deviceString,
|
|
277
|
+
deviceId: this.deviceId,
|
|
278
|
+
uuid: this.uuid,
|
|
279
|
+
phoneId: this.phoneId,
|
|
280
|
+
adid: this.adid,
|
|
281
|
+
build: this.build,
|
|
282
|
+
authorization: this.authorization,
|
|
283
|
+
igWWWClaim: this.igWWWClaim,
|
|
284
|
+
passwordEncryptionKeyId: this.passwordEncryptionKeyId,
|
|
285
|
+
passwordEncryptionPubKey: this.passwordEncryptionPubKey,
|
|
286
|
+
// keep other runtime fields that may be present
|
|
287
|
+
language: this.language,
|
|
288
|
+
timezoneOffset: this.timezoneOffset,
|
|
289
|
+
connectionTypeHeader: this.connectionTypeHeader,
|
|
290
|
+
capabilitiesHeader: this.capabilitiesHeader
|
|
291
|
+
};
|
|
292
|
+
return obj;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Merge data from a saved session into this State instance.
|
|
297
|
+
* Safe: does NOT overwrite prototype getters (like appVersion).
|
|
298
|
+
*/
|
|
299
|
+
async deserialize(state) {
|
|
300
|
+
const obj = typeof state === 'string' ? JSON.parse(state) : state;
|
|
301
|
+
if (!obj || typeof obj !== 'object') {
|
|
302
|
+
throw new TypeError("State isn't an object or serialized JSON");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// If constants present and looks like an object, restore it
|
|
306
|
+
if (obj.constants) {
|
|
307
|
+
this.constants = obj.constants;
|
|
308
|
+
// don't delete - but won't assign later
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Restore cookieJar if present
|
|
312
|
+
if (obj.cookies) {
|
|
313
|
+
try {
|
|
314
|
+
await this.deserializeCookieJar(obj.cookies);
|
|
315
|
+
} catch (e) {
|
|
316
|
+
// best-effort: ignore cookie restore failures
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Assign every other top-level property carefully, skipping prototype getters
|
|
321
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
322
|
+
if (key === 'cookies' || key === 'constants') continue;
|
|
323
|
+
// skip if prototype defines a getter for this key
|
|
324
|
+
const desc = Object.getOwnPropertyDescriptor(State.prototype, key);
|
|
325
|
+
if (desc && (typeof desc.get === 'function' || typeof desc.set === 'function')) {
|
|
326
|
+
// skip assigning to avoid "Cannot set property X of #<State> which has only a getter"
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
// otherwise set on this
|
|
330
|
+
try {
|
|
331
|
+
this[key] = value;
|
|
332
|
+
} catch (e) {
|
|
333
|
+
// ignore property set failures (non-critical)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// refresh parsed authorization (if any)
|
|
338
|
+
this.updateAuthorization();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ===== file helpers: save/load to disk (session + backup) =====
|
|
342
|
+
async saveSessionToFile(filePath = SESSION_FILE, backupPath = SESSION_BACKUP) {
|
|
343
|
+
try {
|
|
344
|
+
const data = await this.serialize();
|
|
345
|
+
// Save cookies field as object (not string) — caller may JSON.stringify whole obj
|
|
346
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
347
|
+
// Also write a backup
|
|
348
|
+
try {
|
|
349
|
+
fs.writeFileSync(backupPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
350
|
+
} catch (_) {}
|
|
351
|
+
return true;
|
|
352
|
+
} catch (e) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async loadSessionFromFile(filePath = SESSION_FILE, backupPath = SESSION_BACKUP) {
|
|
358
|
+
try {
|
|
359
|
+
if (!fs.existsSync(filePath)) return false;
|
|
360
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
361
|
+
const obj = JSON.parse(raw);
|
|
362
|
+
await this.deserialize(obj);
|
|
363
|
+
return true;
|
|
364
|
+
} catch (e) {
|
|
365
|
+
// try backup
|
|
366
|
+
try {
|
|
367
|
+
if (fs.existsSync(backupPath)) {
|
|
368
|
+
const rawb = fs.readFileSync(backupPath, 'utf8');
|
|
369
|
+
const objb = JSON.parse(rawb);
|
|
370
|
+
await this.deserialize(objb);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
} catch (_) {}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// old convenience names so library code that calls state.saveSession()/loadSession() still works
|
|
379
|
+
async saveSession() {
|
|
380
|
+
return await this.saveSessionToFile();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async loadSession() {
|
|
384
|
+
return await this.loadSessionFromFile();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ===== device / cookie utilities =====
|
|
388
|
+
generateDevice(seed) {
|
|
389
|
+
const chance = new Chance(seed);
|
|
390
|
+
// Default device: Samsung Galaxy S25 Ultra (2025 flagship)
|
|
391
|
+
// Users can override this with setCustomDevice() or usePresetDevice()
|
|
392
|
+
this.deviceString = `35/15; 505dpi; 1440x3120; samsung; SM-S928B; e3q; qcom`;
|
|
393
|
+
this.deviceId = `android-${chance.string({ pool: 'abcdef0123456789', length: 16 })}`;
|
|
394
|
+
this.uuid = chance.guid();
|
|
395
|
+
this.phoneId = chance.guid();
|
|
396
|
+
this.adid = chance.guid();
|
|
397
|
+
this.build = 'UP1A.231005.007';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
regenerateDevice(seed = 'instagram-private-api') {
|
|
401
|
+
this.generateDevice(seed);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ===== CUSTOM DEVICE EMULATION =====
|
|
405
|
+
// Allows users to set their own phone model for Instagram emulation
|
|
406
|
+
// Example devices included: Samsung S25 Ultra, Huawei P60 Pro, Google Pixel 8, etc.
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Set a custom device to emulate when connecting to Instagram.
|
|
410
|
+
* This allows you to choose which phone model Instagram sees.
|
|
411
|
+
*
|
|
412
|
+
* @param {Object} deviceConfig - The device configuration
|
|
413
|
+
* @param {string} deviceConfig.manufacturer - Phone manufacturer (e.g., 'samsung', 'huawei', 'google')
|
|
414
|
+
* @param {string} deviceConfig.model - Phone model code (e.g., 'SM-S928B' for Samsung S25 Ultra)
|
|
415
|
+
* @param {string} deviceConfig.device - Device codename (e.g., 'e3q' for S25 Ultra)
|
|
416
|
+
* @param {string} deviceConfig.androidVersion - Android version (e.g., '15')
|
|
417
|
+
* @param {number} deviceConfig.androidApiLevel - Android API level (e.g., 35 for Android 15)
|
|
418
|
+
* @param {string} deviceConfig.resolution - Screen resolution (e.g., '1440x3120')
|
|
419
|
+
* @param {string} deviceConfig.dpi - Screen density (e.g., '505dpi')
|
|
420
|
+
* @param {string} deviceConfig.chipset - Chipset name (e.g., 'qcom')
|
|
421
|
+
* @param {string} deviceConfig.build - Build number (optional)
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* // Samsung Galaxy S25 Ultra
|
|
425
|
+
* state.setCustomDevice({
|
|
426
|
+
* manufacturer: 'samsung',
|
|
427
|
+
* model: 'SM-S928B',
|
|
428
|
+
* device: 'e3q',
|
|
429
|
+
* androidVersion: '15',
|
|
430
|
+
* androidApiLevel: 35,
|
|
431
|
+
* resolution: '1440x3120',
|
|
432
|
+
* dpi: '505dpi',
|
|
433
|
+
* chipset: 'qcom'
|
|
434
|
+
* });
|
|
435
|
+
*/
|
|
436
|
+
setCustomDevice(deviceConfig) {
|
|
437
|
+
const {
|
|
438
|
+
manufacturer = 'samsung',
|
|
439
|
+
model = 'SM-G930F',
|
|
440
|
+
device = 'herolte',
|
|
441
|
+
androidVersion = '8.0.0',
|
|
442
|
+
androidApiLevel = 26,
|
|
443
|
+
resolution = '1080x1920',
|
|
444
|
+
dpi = '480dpi',
|
|
445
|
+
chipset = 'samsungexynos8890',
|
|
446
|
+
build = null
|
|
447
|
+
} = deviceConfig || {};
|
|
448
|
+
|
|
449
|
+
// Build the device string in Instagram format
|
|
450
|
+
this.deviceString = `${androidApiLevel}/${androidVersion}; ${dpi}; ${resolution}; ${manufacturer}; ${model}; ${device}; ${chipset}`;
|
|
451
|
+
|
|
452
|
+
// Optionally update build if provided
|
|
453
|
+
if (build) {
|
|
454
|
+
this.build = build;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Emit event for tracking
|
|
458
|
+
if (this._emitter) {
|
|
459
|
+
this._emitter.emit('device_changed', { deviceString: this.deviceString, model, manufacturer });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return this.deviceString;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get a list of popular preset devices that can be used with setCustomDevice()
|
|
467
|
+
* @returns {Object} Object containing preset device configurations
|
|
468
|
+
*/
|
|
469
|
+
getPresetDevices() {
|
|
470
|
+
return {
|
|
471
|
+
// Samsung devices
|
|
472
|
+
'Samsung Galaxy S25 Ultra': {
|
|
473
|
+
manufacturer: 'samsung',
|
|
474
|
+
model: 'SM-S928B',
|
|
475
|
+
device: 'e3q',
|
|
476
|
+
androidVersion: '15',
|
|
477
|
+
androidApiLevel: 35,
|
|
478
|
+
resolution: '1440x3120',
|
|
479
|
+
dpi: '505dpi',
|
|
480
|
+
chipset: 'qcom',
|
|
481
|
+
build: 'UP1A.231005.007'
|
|
482
|
+
},
|
|
483
|
+
'Samsung Galaxy S24 Ultra': {
|
|
484
|
+
manufacturer: 'samsung',
|
|
485
|
+
model: 'SM-S928B',
|
|
486
|
+
device: 'e2q',
|
|
487
|
+
androidVersion: '14',
|
|
488
|
+
androidApiLevel: 34,
|
|
489
|
+
resolution: '1440x3088',
|
|
490
|
+
dpi: '480dpi',
|
|
491
|
+
chipset: 'qcom',
|
|
492
|
+
build: 'UP1A.231005.007'
|
|
493
|
+
},
|
|
494
|
+
'Samsung Galaxy S23 Ultra': {
|
|
495
|
+
manufacturer: 'samsung',
|
|
496
|
+
model: 'SM-S918B',
|
|
497
|
+
device: 'dm3q',
|
|
498
|
+
androidVersion: '14',
|
|
499
|
+
androidApiLevel: 34,
|
|
500
|
+
resolution: '1440x3088',
|
|
501
|
+
dpi: '480dpi',
|
|
502
|
+
chipset: 'qcom',
|
|
503
|
+
build: 'UP1A.231005.007'
|
|
504
|
+
},
|
|
505
|
+
'Samsung Galaxy Z Fold 5': {
|
|
506
|
+
manufacturer: 'samsung',
|
|
507
|
+
model: 'SM-F946B',
|
|
508
|
+
device: 'q5q',
|
|
509
|
+
androidVersion: '14',
|
|
510
|
+
androidApiLevel: 34,
|
|
511
|
+
resolution: '1812x2176',
|
|
512
|
+
dpi: '420dpi',
|
|
513
|
+
chipset: 'qcom',
|
|
514
|
+
build: 'UP1A.231005.007'
|
|
515
|
+
},
|
|
516
|
+
// Huawei devices
|
|
517
|
+
'Huawei P60 Pro': {
|
|
518
|
+
manufacturer: 'HUAWEI',
|
|
519
|
+
model: 'MNA-AL00',
|
|
520
|
+
device: 'mona',
|
|
521
|
+
androidVersion: '12',
|
|
522
|
+
androidApiLevel: 31,
|
|
523
|
+
resolution: '1260x2720',
|
|
524
|
+
dpi: '480dpi',
|
|
525
|
+
chipset: 'kirin',
|
|
526
|
+
build: 'HUAWEIMNA-AL00'
|
|
527
|
+
},
|
|
528
|
+
'Huawei Mate 60 Pro': {
|
|
529
|
+
manufacturer: 'HUAWEI',
|
|
530
|
+
model: 'ALN-AL10',
|
|
531
|
+
device: 'aln',
|
|
532
|
+
androidVersion: '12',
|
|
533
|
+
androidApiLevel: 31,
|
|
534
|
+
resolution: '1260x2720',
|
|
535
|
+
dpi: '480dpi',
|
|
536
|
+
chipset: 'kirin',
|
|
537
|
+
build: 'HUAWEIALN-AL10'
|
|
538
|
+
},
|
|
539
|
+
// Google Pixel devices
|
|
540
|
+
'Google Pixel 8 Pro': {
|
|
541
|
+
manufacturer: 'Google',
|
|
542
|
+
model: 'Pixel 8 Pro',
|
|
543
|
+
device: 'husky',
|
|
544
|
+
androidVersion: '14',
|
|
545
|
+
androidApiLevel: 34,
|
|
546
|
+
resolution: '1344x2992',
|
|
547
|
+
dpi: '480dpi',
|
|
548
|
+
chipset: 'google',
|
|
549
|
+
build: 'AP2A.240805.005'
|
|
550
|
+
},
|
|
551
|
+
'Google Pixel 9 Pro': {
|
|
552
|
+
manufacturer: 'Google',
|
|
553
|
+
model: 'Pixel 9 Pro',
|
|
554
|
+
device: 'caiman',
|
|
555
|
+
androidVersion: '15',
|
|
556
|
+
androidApiLevel: 35,
|
|
557
|
+
resolution: '1280x2856',
|
|
558
|
+
dpi: '480dpi',
|
|
559
|
+
chipset: 'google',
|
|
560
|
+
build: 'AP3A.241005.015'
|
|
561
|
+
},
|
|
562
|
+
// OnePlus devices
|
|
563
|
+
'OnePlus 12': {
|
|
564
|
+
manufacturer: 'OnePlus',
|
|
565
|
+
model: 'CPH2573',
|
|
566
|
+
device: 'aston',
|
|
567
|
+
androidVersion: '14',
|
|
568
|
+
androidApiLevel: 34,
|
|
569
|
+
resolution: '1440x3168',
|
|
570
|
+
dpi: '525dpi',
|
|
571
|
+
chipset: 'qcom',
|
|
572
|
+
build: 'UP1A.231005.007'
|
|
573
|
+
},
|
|
574
|
+
// Xiaomi devices
|
|
575
|
+
'Xiaomi 14 Ultra': {
|
|
576
|
+
manufacturer: 'Xiaomi',
|
|
577
|
+
model: '24030PN60G',
|
|
578
|
+
device: 'aurora',
|
|
579
|
+
androidVersion: '14',
|
|
580
|
+
androidApiLevel: 34,
|
|
581
|
+
resolution: '1440x3200',
|
|
582
|
+
dpi: '522dpi',
|
|
583
|
+
chipset: 'qcom',
|
|
584
|
+
build: 'UP1A.231005.007'
|
|
585
|
+
},
|
|
586
|
+
'Xiaomi Redmi Note 13 Pro': {
|
|
587
|
+
manufacturer: 'Xiaomi',
|
|
588
|
+
model: '2312DRA50G',
|
|
589
|
+
device: 'emerald',
|
|
590
|
+
androidVersion: '14',
|
|
591
|
+
androidApiLevel: 34,
|
|
592
|
+
resolution: '1220x2712',
|
|
593
|
+
dpi: '446dpi',
|
|
594
|
+
chipset: 'qcom',
|
|
595
|
+
build: 'UP1A.231005.007'
|
|
596
|
+
},
|
|
597
|
+
// OPPO devices
|
|
598
|
+
'OPPO Find X7 Ultra': {
|
|
599
|
+
manufacturer: 'OPPO',
|
|
600
|
+
model: 'PHZ110',
|
|
601
|
+
device: 'OP5D4BL1',
|
|
602
|
+
androidVersion: '14',
|
|
603
|
+
androidApiLevel: 34,
|
|
604
|
+
resolution: '1440x3168',
|
|
605
|
+
dpi: '525dpi',
|
|
606
|
+
chipset: 'qcom',
|
|
607
|
+
build: 'UP1A.231005.007',
|
|
608
|
+
platform: 'android'
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
// ===== iOS DEVICES =====
|
|
612
|
+
// iPhone 16 Series (2024)
|
|
613
|
+
'iPhone 16 Pro Max': {
|
|
614
|
+
platform: 'ios',
|
|
615
|
+
iosDeviceModel: 'iPhone17,1',
|
|
616
|
+
iosDeviceName: 'iPhone 16 Pro Max',
|
|
617
|
+
iosVersion: '18.1',
|
|
618
|
+
resolution: '1320x2868',
|
|
619
|
+
chipset: 'A18 Pro'
|
|
620
|
+
},
|
|
621
|
+
'iPhone 16 Pro': {
|
|
622
|
+
platform: 'ios',
|
|
623
|
+
iosDeviceModel: 'iPhone17,2',
|
|
624
|
+
iosDeviceName: 'iPhone 16 Pro',
|
|
625
|
+
iosVersion: '18.1',
|
|
626
|
+
resolution: '1206x2622',
|
|
627
|
+
chipset: 'A18 Pro'
|
|
628
|
+
},
|
|
629
|
+
'iPhone 16 Plus': {
|
|
630
|
+
platform: 'ios',
|
|
631
|
+
iosDeviceModel: 'iPhone17,3',
|
|
632
|
+
iosDeviceName: 'iPhone 16 Plus',
|
|
633
|
+
iosVersion: '18.1',
|
|
634
|
+
resolution: '1290x2796',
|
|
635
|
+
chipset: 'A18'
|
|
636
|
+
},
|
|
637
|
+
'iPhone 16': {
|
|
638
|
+
platform: 'ios',
|
|
639
|
+
iosDeviceModel: 'iPhone17,4',
|
|
640
|
+
iosDeviceName: 'iPhone 16',
|
|
641
|
+
iosVersion: '18.1',
|
|
642
|
+
resolution: '1179x2556',
|
|
643
|
+
chipset: 'A18'
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
// iPhone 15 Series (2023)
|
|
647
|
+
'iPhone 15 Pro Max': {
|
|
648
|
+
platform: 'ios',
|
|
649
|
+
iosDeviceModel: 'iPhone16,2',
|
|
650
|
+
iosDeviceName: 'iPhone 15 Pro Max',
|
|
651
|
+
iosVersion: '18.1',
|
|
652
|
+
resolution: '1290x2796',
|
|
653
|
+
chipset: 'A17 Pro'
|
|
654
|
+
},
|
|
655
|
+
'iPhone 15 Pro': {
|
|
656
|
+
platform: 'ios',
|
|
657
|
+
iosDeviceModel: 'iPhone16,1',
|
|
658
|
+
iosDeviceName: 'iPhone 15 Pro',
|
|
659
|
+
iosVersion: '18.1',
|
|
660
|
+
resolution: '1179x2556',
|
|
661
|
+
chipset: 'A17 Pro'
|
|
662
|
+
},
|
|
663
|
+
'iPhone 15 Plus': {
|
|
664
|
+
platform: 'ios',
|
|
665
|
+
iosDeviceModel: 'iPhone15,5',
|
|
666
|
+
iosDeviceName: 'iPhone 15 Plus',
|
|
667
|
+
iosVersion: '18.1',
|
|
668
|
+
resolution: '1290x2796',
|
|
669
|
+
chipset: 'A16'
|
|
670
|
+
},
|
|
671
|
+
'iPhone 15': {
|
|
672
|
+
platform: 'ios',
|
|
673
|
+
iosDeviceModel: 'iPhone15,4',
|
|
674
|
+
iosDeviceName: 'iPhone 15',
|
|
675
|
+
iosVersion: '18.1',
|
|
676
|
+
resolution: '1179x2556',
|
|
677
|
+
chipset: 'A16'
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
// iPhone 14 Series (2022)
|
|
681
|
+
'iPhone 14 Pro Max': {
|
|
682
|
+
platform: 'ios',
|
|
683
|
+
iosDeviceModel: 'iPhone15,3',
|
|
684
|
+
iosDeviceName: 'iPhone 14 Pro Max',
|
|
685
|
+
iosVersion: '18.1',
|
|
686
|
+
resolution: '1290x2796',
|
|
687
|
+
chipset: 'A16'
|
|
688
|
+
},
|
|
689
|
+
'iPhone 14 Pro': {
|
|
690
|
+
platform: 'ios',
|
|
691
|
+
iosDeviceModel: 'iPhone15,2',
|
|
692
|
+
iosDeviceName: 'iPhone 14 Pro',
|
|
693
|
+
iosVersion: '18.1',
|
|
694
|
+
resolution: '1179x2556',
|
|
695
|
+
chipset: 'A16'
|
|
696
|
+
},
|
|
697
|
+
'iPhone 14 Plus': {
|
|
698
|
+
platform: 'ios',
|
|
699
|
+
iosDeviceModel: 'iPhone14,8',
|
|
700
|
+
iosDeviceName: 'iPhone 14 Plus',
|
|
701
|
+
iosVersion: '18.1',
|
|
702
|
+
resolution: '1284x2778',
|
|
703
|
+
chipset: 'A15'
|
|
704
|
+
},
|
|
705
|
+
'iPhone 14': {
|
|
706
|
+
platform: 'ios',
|
|
707
|
+
iosDeviceModel: 'iPhone14,7',
|
|
708
|
+
iosDeviceName: 'iPhone 14',
|
|
709
|
+
iosVersion: '18.1',
|
|
710
|
+
resolution: '1170x2532',
|
|
711
|
+
chipset: 'A15'
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
// iPhone 13 Series (2021)
|
|
715
|
+
'iPhone 13 Pro Max': {
|
|
716
|
+
platform: 'ios',
|
|
717
|
+
iosDeviceModel: 'iPhone14,3',
|
|
718
|
+
iosDeviceName: 'iPhone 13 Pro Max',
|
|
719
|
+
iosVersion: '17.6',
|
|
720
|
+
resolution: '1284x2778',
|
|
721
|
+
chipset: 'A15'
|
|
722
|
+
},
|
|
723
|
+
'iPhone 13 Pro': {
|
|
724
|
+
platform: 'ios',
|
|
725
|
+
iosDeviceModel: 'iPhone14,2',
|
|
726
|
+
iosDeviceName: 'iPhone 13 Pro',
|
|
727
|
+
iosVersion: '17.6',
|
|
728
|
+
resolution: '1170x2532',
|
|
729
|
+
chipset: 'A15'
|
|
730
|
+
},
|
|
731
|
+
'iPhone 13': {
|
|
732
|
+
platform: 'ios',
|
|
733
|
+
iosDeviceModel: 'iPhone14,5',
|
|
734
|
+
iosDeviceName: 'iPhone 13',
|
|
735
|
+
iosVersion: '17.6',
|
|
736
|
+
resolution: '1170x2532',
|
|
737
|
+
chipset: 'A15'
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
// iPhone 12 Series (2020)
|
|
741
|
+
'iPhone 12 Pro Max': {
|
|
742
|
+
platform: 'ios',
|
|
743
|
+
iosDeviceModel: 'iPhone13,4',
|
|
744
|
+
iosDeviceName: 'iPhone 12 Pro Max',
|
|
745
|
+
iosVersion: '17.6',
|
|
746
|
+
resolution: '1284x2778',
|
|
747
|
+
chipset: 'A14'
|
|
748
|
+
},
|
|
749
|
+
'iPhone 12 Pro': {
|
|
750
|
+
platform: 'ios',
|
|
751
|
+
iosDeviceModel: 'iPhone13,3',
|
|
752
|
+
iosDeviceName: 'iPhone 12 Pro',
|
|
753
|
+
iosVersion: '17.6',
|
|
754
|
+
resolution: '1170x2532',
|
|
755
|
+
chipset: 'A14'
|
|
756
|
+
},
|
|
757
|
+
'iPhone 12': {
|
|
758
|
+
platform: 'ios',
|
|
759
|
+
iosDeviceModel: 'iPhone13,2',
|
|
760
|
+
iosDeviceName: 'iPhone 12',
|
|
761
|
+
iosVersion: '17.6',
|
|
762
|
+
resolution: '1170x2532',
|
|
763
|
+
chipset: 'A14'
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
// iPad Pro Series
|
|
767
|
+
'iPad Pro 12.9 (6th gen)': {
|
|
768
|
+
platform: 'ios',
|
|
769
|
+
iosDeviceModel: 'iPad14,3',
|
|
770
|
+
iosDeviceName: 'iPad Pro 12.9-inch (6th generation)',
|
|
771
|
+
iosVersion: '18.1',
|
|
772
|
+
resolution: '2048x2732',
|
|
773
|
+
chipset: 'M2'
|
|
774
|
+
},
|
|
775
|
+
'iPad Pro 11 (4th gen)': {
|
|
776
|
+
platform: 'ios',
|
|
777
|
+
iosDeviceModel: 'iPad14,5',
|
|
778
|
+
iosDeviceName: 'iPad Pro 11-inch (4th generation)',
|
|
779
|
+
iosVersion: '18.1',
|
|
780
|
+
resolution: '1668x2388',
|
|
781
|
+
chipset: 'M2'
|
|
782
|
+
},
|
|
783
|
+
'iPad Air (5th gen)': {
|
|
784
|
+
platform: 'ios',
|
|
785
|
+
iosDeviceModel: 'iPad13,18',
|
|
786
|
+
iosDeviceName: 'iPad Air (5th generation)',
|
|
787
|
+
iosVersion: '18.1',
|
|
788
|
+
resolution: '2360x1640',
|
|
789
|
+
chipset: 'M1'
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Get only iOS device presets
|
|
796
|
+
* @returns {Object} Object containing iOS device configurations
|
|
797
|
+
*/
|
|
798
|
+
getIOSDevices() {
|
|
799
|
+
const all = this.getPresetDevices();
|
|
800
|
+
const iosDevices = {};
|
|
801
|
+
for (const [name, config] of Object.entries(all)) {
|
|
802
|
+
if (config.platform === 'ios') {
|
|
803
|
+
iosDevices[name] = config;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return iosDevices;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Get only Android device presets
|
|
811
|
+
* @returns {Object} Object containing Android device configurations
|
|
812
|
+
*/
|
|
813
|
+
getAndroidDevices() {
|
|
814
|
+
const all = this.getPresetDevices();
|
|
815
|
+
const androidDevices = {};
|
|
816
|
+
for (const [name, config] of Object.entries(all)) {
|
|
817
|
+
if (config.platform !== 'ios') {
|
|
818
|
+
androidDevices[name] = config;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return androidDevices;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Set an iOS device to emulate
|
|
826
|
+
* @param {Object} iosConfig - iOS device configuration
|
|
827
|
+
* @returns {string} The iOS User-Agent
|
|
828
|
+
*/
|
|
829
|
+
setIOSDevice(iosConfig) {
|
|
830
|
+
const {
|
|
831
|
+
iosDeviceModel = 'iPhone16,2',
|
|
832
|
+
iosDeviceName = 'iPhone 15 Pro Max',
|
|
833
|
+
iosVersion = '18.1',
|
|
834
|
+
iosAppVersion = '347.0.0.36.89',
|
|
835
|
+
iosAppVersionCode = '618023787'
|
|
836
|
+
} = iosConfig || {};
|
|
837
|
+
|
|
838
|
+
this.platform = 'ios';
|
|
839
|
+
this.iosDeviceModel = iosDeviceModel;
|
|
840
|
+
this.iosDeviceName = iosDeviceName;
|
|
841
|
+
this.iosVersion = iosVersion;
|
|
842
|
+
this.iosAppVersion = iosAppVersion;
|
|
843
|
+
this.iosAppVersionCode = iosAppVersionCode;
|
|
844
|
+
|
|
845
|
+
// Generate iOS-specific device identifiers
|
|
846
|
+
const chance = new Chance(`ios-${iosDeviceModel}-${Date.now()}`);
|
|
847
|
+
this.deviceId = `ios-${chance.string({ pool: 'ABCDEF0123456789', length: 40 })}`;
|
|
848
|
+
this.uuid = chance.guid().toUpperCase();
|
|
849
|
+
this.phoneId = chance.guid().toUpperCase();
|
|
850
|
+
this.adid = chance.guid().toUpperCase();
|
|
851
|
+
|
|
852
|
+
if (this._emitter) {
|
|
853
|
+
this._emitter.emit('device_changed', {
|
|
854
|
+
platform: 'ios',
|
|
855
|
+
deviceModel: iosDeviceModel,
|
|
856
|
+
deviceName: iosDeviceName,
|
|
857
|
+
userAgent: this.iosUserAgent
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return this.iosUserAgent;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Switch to iOS platform with a preset device
|
|
866
|
+
* @param {string} presetName - Name of the iOS preset (e.g., 'iPhone 16 Pro Max')
|
|
867
|
+
* @returns {string|null} iOS User-Agent if successful, null if preset not found
|
|
868
|
+
*/
|
|
869
|
+
useIOSDevice(presetName) {
|
|
870
|
+
const presets = this.getPresetDevices();
|
|
871
|
+
if (presets[presetName] && presets[presetName].platform === 'ios') {
|
|
872
|
+
return this.setIOSDevice(presets[presetName]);
|
|
873
|
+
}
|
|
874
|
+
const iosDevices = Object.keys(this.getIOSDevices());
|
|
875
|
+
console.warn(`iOS device "${presetName}" not found. Available iOS devices:`, iosDevices);
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Switch to Android platform with a preset device
|
|
881
|
+
* @param {string} presetName - Name of the Android preset (e.g., 'Samsung Galaxy S25 Ultra')
|
|
882
|
+
* @returns {string|null} Device string if successful, null if preset not found
|
|
883
|
+
*/
|
|
884
|
+
useAndroidDevice(presetName) {
|
|
885
|
+
const presets = this.getPresetDevices();
|
|
886
|
+
if (presets[presetName] && presets[presetName].platform !== 'ios') {
|
|
887
|
+
this.platform = 'android';
|
|
888
|
+
return this.setCustomDevice(presets[presetName]);
|
|
889
|
+
}
|
|
890
|
+
const androidDevices = Object.keys(this.getAndroidDevices());
|
|
891
|
+
console.warn(`Android device "${presetName}" not found. Available Android devices:`, androidDevices);
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Switch platform between iOS and Android
|
|
897
|
+
* @param {string} targetPlatform - 'ios' or 'android'
|
|
898
|
+
* @param {string} devicePreset - Optional device preset name
|
|
899
|
+
*/
|
|
900
|
+
switchPlatform(targetPlatform, devicePreset = null) {
|
|
901
|
+
if (targetPlatform === 'ios') {
|
|
902
|
+
if (devicePreset) {
|
|
903
|
+
return this.useIOSDevice(devicePreset);
|
|
904
|
+
}
|
|
905
|
+
return this.useIOSDevice('iPhone 16 Pro Max');
|
|
906
|
+
} else if (targetPlatform === 'android') {
|
|
907
|
+
if (devicePreset) {
|
|
908
|
+
return this.useAndroidDevice(devicePreset);
|
|
909
|
+
}
|
|
910
|
+
this.platform = 'android';
|
|
911
|
+
return this.useAndroidDevice('Samsung Galaxy S25 Ultra');
|
|
912
|
+
}
|
|
913
|
+
console.warn('Invalid platform. Use "ios" or "android".');
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Get current platform info
|
|
919
|
+
* @returns {Object} Current platform and device information
|
|
920
|
+
*/
|
|
921
|
+
getPlatformInfo() {
|
|
922
|
+
return {
|
|
923
|
+
platform: this.platform,
|
|
924
|
+
userAgent: this.appUserAgent,
|
|
925
|
+
packageName: this.packageName,
|
|
926
|
+
deviceId: this.deviceId,
|
|
927
|
+
...(this.platform === 'ios' ? {
|
|
928
|
+
iosDeviceModel: this.iosDeviceModel,
|
|
929
|
+
iosDeviceName: this.iosDeviceName,
|
|
930
|
+
iosVersion: this.iosVersion,
|
|
931
|
+
iosAppVersion: this.iosAppVersion
|
|
932
|
+
} : {
|
|
933
|
+
deviceString: this.deviceString,
|
|
934
|
+
build: this.build
|
|
935
|
+
})
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* List all available device presets grouped by platform
|
|
941
|
+
* @returns {Object} Devices grouped by platform
|
|
942
|
+
*/
|
|
943
|
+
listAllDevices() {
|
|
944
|
+
return {
|
|
945
|
+
android: Object.keys(this.getAndroidDevices()),
|
|
946
|
+
ios: Object.keys(this.getIOSDevices())
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Quick method to set a device from presets by name
|
|
952
|
+
* @param {string} presetName - Name of the preset (e.g., 'Samsung Galaxy S25 Ultra')
|
|
953
|
+
* @returns {string|null} Device string if successful, null if preset not found
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* state.usePresetDevice('Samsung Galaxy S25 Ultra');
|
|
957
|
+
* state.usePresetDevice('Huawei P60 Pro');
|
|
958
|
+
* state.usePresetDevice('Google Pixel 8 Pro');
|
|
959
|
+
*/
|
|
960
|
+
usePresetDevice(presetName) {
|
|
961
|
+
const presets = this.getPresetDevices();
|
|
962
|
+
if (presets[presetName]) {
|
|
963
|
+
return this.setCustomDevice(presets[presetName]);
|
|
964
|
+
}
|
|
965
|
+
console.warn(`Preset device "${presetName}" not found. Available presets:`, Object.keys(presets));
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Get the current device info being emulated
|
|
971
|
+
* @returns {Object} Current device configuration
|
|
972
|
+
*/
|
|
973
|
+
getCurrentDeviceInfo() {
|
|
974
|
+
return {
|
|
975
|
+
deviceString: this.deviceString,
|
|
976
|
+
deviceId: this.deviceId,
|
|
977
|
+
uuid: this.uuid,
|
|
978
|
+
phoneId: this.phoneId,
|
|
979
|
+
adid: this.adid,
|
|
980
|
+
build: this.build,
|
|
981
|
+
userAgent: this.appUserAgent
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
generateTemporaryGuid(seed, lifetime) {
|
|
986
|
+
return new Chance(`${seed}${this.deviceId}${Math.round(Date.now() / lifetime)}`).guid();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
clearCookies() {
|
|
990
|
+
this.cookieJar = new CookieJar();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
listCookies() {
|
|
994
|
+
try {
|
|
995
|
+
const cookies = this.cookieJar.getCookiesSync
|
|
996
|
+
? this.cookieJar.getCookiesSync(this.constants.HOST)
|
|
997
|
+
: this.cookieJar.getCookies(this.constants.HOST);
|
|
998
|
+
if (Array.isArray(cookies)) {
|
|
999
|
+
for (const c of cookies) {
|
|
1000
|
+
console.log(`- ${c.key}=${c.value}`);
|
|
1001
|
+
}
|
|
1002
|
+
return cookies;
|
|
1003
|
+
}
|
|
1004
|
+
return [];
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
return [];
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
logStateSummary() {
|
|
1011
|
+
console.log('--- State Summary ---');
|
|
1012
|
+
console.log(`Device ID: ${this.deviceId}`);
|
|
1013
|
+
console.log(`UUID: ${this.uuid}`);
|
|
1014
|
+
console.log(`User Agent: ${this.appUserAgent}`);
|
|
1015
|
+
console.log(`Language: ${this.language}`);
|
|
1016
|
+
console.log(`Timezone Offset: ${this.timezoneOffset}`);
|
|
1017
|
+
console.log(`Authorization: ${this.authorization ? 'Present' : 'Missing'}`);
|
|
1018
|
+
console.log('----------------------');
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
//
|
|
1022
|
+
// === NEW UTILITIES ADDED BELOW (non-destructive; keep backwards compat)
|
|
1023
|
+
//
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Subscribe to internal state events (non-invasive).
|
|
1027
|
+
* Events emitted:
|
|
1028
|
+
* - 'session_saved' => (filePath)
|
|
1029
|
+
* - 'session_save_failed' => (err)
|
|
1030
|
+
* - 'session_loaded' => (filePath)
|
|
1031
|
+
* - 'session_load_failed' => (err)
|
|
1032
|
+
* - 'cookies_cleared'
|
|
1033
|
+
* - 'device_regenerated'
|
|
1034
|
+
* - 'session_file_changed' => (eventType, filename)
|
|
1035
|
+
*/
|
|
1036
|
+
on(event, listener) {
|
|
1037
|
+
this._emitter.on(event, listener);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
off(event, listener) {
|
|
1041
|
+
this._emitter.removeListener(event, listener);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
once(event, listener) {
|
|
1045
|
+
this._emitter.once(event, listener);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Atomic save with retries and backup rotation.
|
|
1050
|
+
* Attempts to write to a temp file, rename into place (atomic on most OSes),
|
|
1051
|
+
* and maintain up to `_maxBackupCopies` rotated backups.
|
|
1052
|
+
*/
|
|
1053
|
+
async safeSaveSessionToFile(filePath = SESSION_FILE, backupPath = SESSION_BACKUP, opts = {}) {
|
|
1054
|
+
const retries = typeof opts.retries === 'number' ? opts.retries : this._saveRetries;
|
|
1055
|
+
const delayMs = typeof opts.delayMs === 'number' ? opts.delayMs : this._saveRetryDelayMs;
|
|
1056
|
+
const maxBackups = typeof opts.maxBackups === 'number' ? opts.maxBackups : this._maxBackupCopies;
|
|
1057
|
+
|
|
1058
|
+
const tempPath = `${filePath}.tmp`;
|
|
1059
|
+
|
|
1060
|
+
let lastErr;
|
|
1061
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
1062
|
+
try {
|
|
1063
|
+
const data = await this.serialize();
|
|
1064
|
+
// ensure parent dir exists
|
|
1065
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
1066
|
+
// write temp and rename (atomic on many systems)
|
|
1067
|
+
await fs.promises.writeFile(tempPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
1068
|
+
await fs.promises.rename(tempPath, filePath);
|
|
1069
|
+
// maintain backup copy
|
|
1070
|
+
try {
|
|
1071
|
+
await this._rotateAndWriteBackup(filePath, backupPath, maxBackups);
|
|
1072
|
+
} catch (_) {
|
|
1073
|
+
// non-fatal for backup rotation
|
|
1074
|
+
}
|
|
1075
|
+
this._emitter.emit('session_saved', filePath);
|
|
1076
|
+
return true;
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
lastErr = err;
|
|
1079
|
+
// small backoff
|
|
1080
|
+
await new Promise(r => setTimeout(r, delayMs * (attempt + 1)));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
this._emitter.emit('session_save_failed', lastErr);
|
|
1084
|
+
throw lastErr || new Error('safeSaveSessionToFile failed');
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// helper: rotate existing backups and write new backup copy
|
|
1088
|
+
async _rotateAndWriteBackup(filePath, backupPath, maxBackups) {
|
|
1089
|
+
try {
|
|
1090
|
+
// if no original file, just copy
|
|
1091
|
+
if (!fs.existsSync(filePath)) {
|
|
1092
|
+
const data = await this.serialize();
|
|
1093
|
+
await fs.promises.writeFile(backupPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// rotate existing numeric backups (backupPath.1, backupPath.2, ...)
|
|
1098
|
+
for (let i = maxBackups - 1; i >= 1; i--) {
|
|
1099
|
+
const src = `${backupPath}.${i}`;
|
|
1100
|
+
const dst = `${backupPath}.${i + 1}`;
|
|
1101
|
+
if (fs.existsSync(src)) {
|
|
1102
|
+
try { await fs.promises.rename(src, dst); } catch (_) {}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// move current backup to .1
|
|
1106
|
+
if (fs.existsSync(backupPath)) {
|
|
1107
|
+
try { await fs.promises.rename(backupPath, `${backupPath}.1`); } catch (_) {}
|
|
1108
|
+
}
|
|
1109
|
+
// write a new backup from current file
|
|
1110
|
+
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
1111
|
+
await fs.promises.writeFile(backupPath, content, { mode: 0o600 });
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
// ignore backup rotation issues
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Safe load with retries. Emits session_loaded/session_load_failed events.
|
|
1119
|
+
*/
|
|
1120
|
+
async safeLoadSessionFromFile(filePath = SESSION_FILE, backupPath = SESSION_BACKUP, opts = {}) {
|
|
1121
|
+
const retries = typeof opts.retries === 'number' ? opts.retries : 2;
|
|
1122
|
+
const delayMs = typeof opts.delayMs === 'number' ? opts.delayMs : 200;
|
|
1123
|
+
|
|
1124
|
+
let lastErr;
|
|
1125
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
1126
|
+
try {
|
|
1127
|
+
const ok = await this.loadSessionFromFile(filePath, backupPath);
|
|
1128
|
+
if (ok) {
|
|
1129
|
+
this._emitter.emit('session_loaded', filePath);
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
// if not ok, wait and retry
|
|
1133
|
+
await new Promise(r => setTimeout(r, delayMs * (attempt + 1)));
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
lastErr = err;
|
|
1136
|
+
await new Promise(r => setTimeout(r, delayMs * (attempt + 1)));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
this._emitter.emit('session_load_failed', lastErr);
|
|
1140
|
+
throw lastErr || new Error('safeLoadSessionFromFile failed');
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Validate minimal session integrity: ensure cookie jar has expected cookies or parsed authorization
|
|
1145
|
+
*/
|
|
1146
|
+
validateSession() {
|
|
1147
|
+
try {
|
|
1148
|
+
// try to read ds_user_id or parsed authorization
|
|
1149
|
+
let ok = false;
|
|
1150
|
+
try {
|
|
1151
|
+
const uid = this.extractCookieValue('ds_user_id');
|
|
1152
|
+
ok = !!uid;
|
|
1153
|
+
} catch (_) {
|
|
1154
|
+
// fallback to parsed authorization
|
|
1155
|
+
this.updateAuthorization();
|
|
1156
|
+
ok = !!(this.parsedAuthorization && this.parsedAuthorization.ds_user_id);
|
|
1157
|
+
}
|
|
1158
|
+
return ok;
|
|
1159
|
+
} catch (_) {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Merge cookies from another serialized jar or CookieJar instance into current cookieJar.
|
|
1166
|
+
* Accepts: serialized object/string (as used by serializeCookieJar) or CookieJar instance.
|
|
1167
|
+
*/
|
|
1168
|
+
async mergeCookieJarFrom(other) {
|
|
1169
|
+
try {
|
|
1170
|
+
if (!other) return false;
|
|
1171
|
+
// if serialized string/object
|
|
1172
|
+
if (typeof other === 'string' || (typeof other === 'object' && !other.getCookieSync)) {
|
|
1173
|
+
// deserialize into a temporary jar
|
|
1174
|
+
const deserializeFn = util.promisify((input, cb) => {
|
|
1175
|
+
try {
|
|
1176
|
+
CookieJar.deserialize(input, cb);
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
cb(err);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
const tmpJar = await deserializeFn(typeof other === 'string' ? JSON.parse(other) : other);
|
|
1182
|
+
if (!tmpJar) return false;
|
|
1183
|
+
// merge: extract cookies for host and set into current jar
|
|
1184
|
+
const host = this.constants.HOST;
|
|
1185
|
+
const cookies = tmpJar.getCookiesSync ? tmpJar.getCookiesSync(host) : await tmpJar.getCookies(host);
|
|
1186
|
+
if (Array.isArray(cookies)) {
|
|
1187
|
+
for (const c of cookies) {
|
|
1188
|
+
try {
|
|
1189
|
+
// setCookieSync may not exist in some versions; fallback to async
|
|
1190
|
+
if (typeof this.cookieJar.setCookieSync === 'function') {
|
|
1191
|
+
this.cookieJar.setCookieSync(c, host);
|
|
1192
|
+
} else if (typeof this.cookieJar.setCookie === 'function') {
|
|
1193
|
+
// promisify setCookie
|
|
1194
|
+
await util.promisify(this.cookieJar.setCookie).call(this.cookieJar, c, host);
|
|
1195
|
+
}
|
|
1196
|
+
} catch (_) {}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return true;
|
|
1200
|
+
} else if (typeof other.getCookiesSync === 'function' || typeof other.getCookies === 'function') {
|
|
1201
|
+
// assume CookieJar instance
|
|
1202
|
+
const host = this.constants.HOST;
|
|
1203
|
+
const cookies = other.getCookiesSync ? other.getCookiesSync(host) : await other.getCookies(host);
|
|
1204
|
+
if (Array.isArray(cookies)) {
|
|
1205
|
+
for (const c of cookies) {
|
|
1206
|
+
try {
|
|
1207
|
+
if (typeof this.cookieJar.setCookieSync === 'function') {
|
|
1208
|
+
this.cookieJar.setCookieSync(c, host);
|
|
1209
|
+
} else if (typeof this.cookieJar.setCookie === 'function') {
|
|
1210
|
+
await util.promisify(this.cookieJar.setCookie).call(this.cookieJar, c, host);
|
|
1211
|
+
}
|
|
1212
|
+
} catch (_) {}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Export a minimal session object (useful for sending to remote storage or IPC).
|
|
1225
|
+
*/
|
|
1226
|
+
async exportMinimalSession() {
|
|
1227
|
+
const cookieData = await this.serializeCookieJar();
|
|
1228
|
+
return {
|
|
1229
|
+
deviceId: this.deviceId,
|
|
1230
|
+
deviceString: this.deviceString,
|
|
1231
|
+
uuid: this.uuid,
|
|
1232
|
+
phoneId: this.phoneId,
|
|
1233
|
+
adid: this.adid,
|
|
1234
|
+
build: this.build,
|
|
1235
|
+
authorization: this.authorization,
|
|
1236
|
+
cookies: cookieData
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Import minimal session object (merges cookies if present and sets fields).
|
|
1242
|
+
*/
|
|
1243
|
+
async importMinimalSession(minObj = {}) {
|
|
1244
|
+
if (!minObj || typeof minObj !== 'object') return false;
|
|
1245
|
+
try {
|
|
1246
|
+
if (minObj.cookies) {
|
|
1247
|
+
try { await this.mergeCookieJarFrom(minObj.cookies); } catch (_) {}
|
|
1248
|
+
}
|
|
1249
|
+
// set other fields cautiously
|
|
1250
|
+
const fields = ['deviceId', 'deviceString', 'uuid', 'phoneId', 'adid', 'build', 'authorization'];
|
|
1251
|
+
for (const k of fields) {
|
|
1252
|
+
if (typeof minObj[k] !== 'undefined') this[k] = minObj[k];
|
|
1253
|
+
}
|
|
1254
|
+
this.updateAuthorization();
|
|
1255
|
+
return true;
|
|
1256
|
+
} catch (e) {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Set proxy URL (persists only in-memory; use saveSessionToFile to persist).
|
|
1263
|
+
*/
|
|
1264
|
+
setProxyUrl(url) {
|
|
1265
|
+
this.proxyUrl = url || null;
|
|
1266
|
+
return this.proxyUrl;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
clearProxyUrl() {
|
|
1270
|
+
this.proxyUrl = null;
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Mark checkpoint metadata object (stores arbitrary checkpoint info).
|
|
1276
|
+
*/
|
|
1277
|
+
markCheckpoint(obj) {
|
|
1278
|
+
try {
|
|
1279
|
+
this.checkpoint = obj;
|
|
1280
|
+
return true;
|
|
1281
|
+
} catch (_) {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
clearCheckpoint() {
|
|
1287
|
+
this.checkpoint = null;
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Ensure session file has safe permissions (owner read/write only)
|
|
1293
|
+
*/
|
|
1294
|
+
ensureFilePermissions(filePath = SESSION_FILE) {
|
|
1295
|
+
try {
|
|
1296
|
+
if (fs.existsSync(filePath)) {
|
|
1297
|
+
fs.chmodSync(filePath, 0o600);
|
|
1298
|
+
}
|
|
1299
|
+
return true;
|
|
1300
|
+
} catch (e) {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Watch the session file for external changes and emit 'session_file_changed'.
|
|
1307
|
+
* Note: uses fs.watchFile which is more portable; call stopWatchingSessionFile() to stop.
|
|
1308
|
+
*/
|
|
1309
|
+
watchSessionFile(filePath = SESSION_FILE, intervalMs = 1000) {
|
|
1310
|
+
try {
|
|
1311
|
+
if (this._sessionFileWatcher) {
|
|
1312
|
+
// already watching
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
// use watchFile (polling) for reliability across platforms
|
|
1316
|
+
fs.watchFile(filePath, { interval: intervalMs }, (curr, prev) => {
|
|
1317
|
+
// ignore if size/time identical
|
|
1318
|
+
if (curr.mtimeMs !== prev.mtimeMs || curr.size !== prev.size) {
|
|
1319
|
+
this._emitter.emit('session_file_changed', { filePath, curr, prev });
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
this._sessionFileWatcher = true;
|
|
1323
|
+
return true;
|
|
1324
|
+
} catch (e) {
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
stopWatchingSessionFile(filePath = SESSION_FILE) {
|
|
1330
|
+
try {
|
|
1331
|
+
if (!this._sessionFileWatcher) return true;
|
|
1332
|
+
fs.unwatchFile(filePath);
|
|
1333
|
+
this._sessionFileWatcher = null;
|
|
1334
|
+
return true;
|
|
1335
|
+
} catch (e) {
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* If device looks missing or invalid, regenerate device fields.
|
|
1342
|
+
* Condition: deviceId missing or doesn't start with expected prefix.
|
|
1343
|
+
*/
|
|
1344
|
+
refreshDeviceIfMissingOrOld(seed = 'instagram-private-api') {
|
|
1345
|
+
try {
|
|
1346
|
+
if (!this.deviceId || typeof this.deviceId !== 'string' || !this.deviceId.startsWith('android-')) {
|
|
1347
|
+
this.generateDevice(seed);
|
|
1348
|
+
this._emitter.emit('device_regenerated');
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
return false;
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Refresh authorization state from cookies (useful after merging cookies).
|
|
1359
|
+
*/
|
|
1360
|
+
refreshAuthFromCookies() {
|
|
1361
|
+
try {
|
|
1362
|
+
this.updateAuthorization();
|
|
1363
|
+
return true;
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Safe load helper that tries to load, validate, and optionally attempt fallback to backup copies.
|
|
1371
|
+
* If `validate` is true, will call validateSession() and throw if invalid.
|
|
1372
|
+
*/
|
|
1373
|
+
async loadAndValidateSession(filePath = SESSION_FILE, backupPath = SESSION_BACKUP, opts = {}) {
|
|
1374
|
+
const validate = opts.validate !== false; // default true
|
|
1375
|
+
try {
|
|
1376
|
+
const ok = await this.safeLoadSessionFromFile(filePath, backupPath, opts);
|
|
1377
|
+
if (!ok) throw new Error('load failed');
|
|
1378
|
+
if (validate && !this.validateSession()) {
|
|
1379
|
+
// try backup load
|
|
1380
|
+
const triedBackups = await this._tryLoadRotatedBackups(backupPath, opts);
|
|
1381
|
+
if (!triedBackups) throw new Error('session invalid and backups failed');
|
|
1382
|
+
}
|
|
1383
|
+
this._emitter.emit('session_loaded', filePath);
|
|
1384
|
+
return true;
|
|
1385
|
+
} catch (e) {
|
|
1386
|
+
this._emitter.emit('session_load_failed', e);
|
|
1387
|
+
throw e;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// helper: try load rotated backups .1 .. .N
|
|
1392
|
+
async _tryLoadRotatedBackups(backupPath, opts = {}) {
|
|
1393
|
+
try {
|
|
1394
|
+
const max = typeof opts.maxBackups === 'number' ? opts.maxBackups : this._maxBackupCopies;
|
|
1395
|
+
for (let i = 1; i <= max; i++) {
|
|
1396
|
+
const p = `${backupPath}.${i}`;
|
|
1397
|
+
if (!fs.existsSync(p)) continue;
|
|
1398
|
+
try {
|
|
1399
|
+
const raw = await fs.promises.readFile(p, 'utf8');
|
|
1400
|
+
const obj = JSON.parse(raw);
|
|
1401
|
+
await this.deserialize(obj);
|
|
1402
|
+
if (this.validateSession()) return true;
|
|
1403
|
+
} catch (_) {
|
|
1404
|
+
// continue trying next
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
} catch (_) {}
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Clear all session data (cookies + auth + device optional).
|
|
1413
|
+
* If `preserveDevice` is true, device fields are kept.
|
|
1414
|
+
*/
|
|
1415
|
+
clearAllSession(preserveDevice = true) {
|
|
1416
|
+
try {
|
|
1417
|
+
this.clearCookies();
|
|
1418
|
+
this.authorization = undefined;
|
|
1419
|
+
this.parsedAuthorization = undefined;
|
|
1420
|
+
this.igWWWClaim = undefined;
|
|
1421
|
+
this.passwordEncryptionKeyId = undefined;
|
|
1422
|
+
this.passwordEncryptionPubKey = undefined;
|
|
1423
|
+
if (!preserveDevice) {
|
|
1424
|
+
this.generateDevice('instagram-private-api');
|
|
1425
|
+
}
|
|
1426
|
+
this._emitter.emit('cookies_cleared');
|
|
1427
|
+
return true;
|
|
1428
|
+
} catch (e) {
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Quick helper: get value of cookie if exists, or null (non-throwing).
|
|
1435
|
+
*/
|
|
1436
|
+
getCookieValueSafe(key) {
|
|
1437
|
+
try {
|
|
1438
|
+
const c = this.extractCookie(key);
|
|
1439
|
+
return c ? c.value : null;
|
|
1440
|
+
} catch (_) {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* Convenience: set default save/retry options
|
|
1447
|
+
*/
|
|
1448
|
+
setSaveRetryOptions({ retries, delayMs, maxBackups } = {}) {
|
|
1449
|
+
if (typeof retries === 'number') this._saveRetries = retries;
|
|
1450
|
+
if (typeof delayMs === 'number') this._saveRetryDelayMs = delayMs;
|
|
1451
|
+
if (typeof maxBackups === 'number') this._maxBackupCopies = maxBackups;
|
|
1452
|
+
return { retries: this._saveRetries, delayMs: this._saveRetryDelayMs, maxBackups: this._maxBackupCopies };
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
module.exports = State;
|