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.
Files changed (210) hide show
  1. package/README.md +1650 -0
  2. package/dist/constants/constants.js +280 -0
  3. package/dist/constants/index.js +41 -0
  4. package/dist/core/client.js +243 -0
  5. package/dist/core/repository.js +7 -0
  6. package/dist/core/request.js +212 -0
  7. package/dist/core/state.js +1456 -0
  8. package/dist/core/utils.js +786 -0
  9. package/dist/downloadMedia.js +381 -0
  10. package/dist/errors/index.d.ts +16 -0
  11. package/dist/errors/index.js +30 -0
  12. package/dist/errors/index.js.map +1 -0
  13. package/dist/fbns/fbns.client.d.ts +32 -0
  14. package/dist/fbns/fbns.client.events.d.ts +41 -0
  15. package/dist/fbns/fbns.client.events.js +3 -0
  16. package/dist/fbns/fbns.client.events.js.map +1 -0
  17. package/dist/fbns/fbns.client.js +179 -0
  18. package/dist/fbns/fbns.client.js.map +1 -0
  19. package/dist/fbns/fbns.device-auth.d.ts +17 -0
  20. package/dist/fbns/fbns.device-auth.js +54 -0
  21. package/dist/fbns/fbns.device-auth.js.map +1 -0
  22. package/dist/fbns/fbns.types.d.ts +83 -0
  23. package/dist/fbns/fbns.types.js +3 -0
  24. package/dist/fbns/fbns.types.js.map +1 -0
  25. package/dist/fbns/fbns.utilities.d.ts +2 -0
  26. package/dist/fbns/fbns.utilities.js +79 -0
  27. package/dist/fbns/fbns.utilities.js.map +1 -0
  28. package/dist/fbns/index.d.ts +4 -0
  29. package/dist/fbns/index.js +21 -0
  30. package/dist/fbns/index.js.map +1 -0
  31. package/dist/index.js +39 -0
  32. package/dist/mqttot/index.d.ts +4 -0
  33. package/dist/mqttot/index.js +21 -0
  34. package/dist/mqttot/index.js.map +1 -0
  35. package/dist/mqttot/mqttot.client.d.ts +39 -0
  36. package/dist/mqttot/mqttot.client.js +120 -0
  37. package/dist/mqttot/mqttot.client.js.map +1 -0
  38. package/dist/mqttot/mqttot.connect.request.packet.d.ts +7 -0
  39. package/dist/mqttot/mqttot.connect.request.packet.js +9 -0
  40. package/dist/mqttot/mqttot.connect.request.packet.js.map +1 -0
  41. package/dist/mqttot/mqttot.connect.response.packet.d.ts +7 -0
  42. package/dist/mqttot/mqttot.connect.response.packet.js +24 -0
  43. package/dist/mqttot/mqttot.connect.response.packet.js.map +1 -0
  44. package/dist/mqttot/mqttot.connection.d.ts +57 -0
  45. package/dist/mqttot/mqttot.connection.js +56 -0
  46. package/dist/mqttot/mqttot.connection.js.map +1 -0
  47. package/dist/package.json +59 -0
  48. package/dist/realtime/commands/commands.d.ts +15 -0
  49. package/dist/realtime/commands/commands.js +21 -0
  50. package/dist/realtime/commands/commands.js.map +1 -0
  51. package/dist/realtime/commands/direct.commands.d.ts +75 -0
  52. package/dist/realtime/commands/direct.commands.js +186 -0
  53. package/dist/realtime/commands/direct.commands.js.map +1 -0
  54. package/dist/realtime/commands/enhanced.direct.commands.js +987 -0
  55. package/dist/realtime/commands/index.d.ts +2 -0
  56. package/dist/realtime/commands/index.js +19 -0
  57. package/dist/realtime/commands/index.js.map +1 -0
  58. package/dist/realtime/delta-sync.manager.js +293 -0
  59. package/dist/realtime/features/dm-sender.js +88 -0
  60. package/dist/realtime/features/error-handler.js +73 -0
  61. package/dist/realtime/features/gap-handler.js +61 -0
  62. package/dist/realtime/features/presence.manager.js +66 -0
  63. package/dist/realtime/index.js +30 -0
  64. package/dist/realtime/messages/app-presence.event.d.ts +9 -0
  65. package/dist/realtime/messages/app-presence.event.js +3 -0
  66. package/dist/realtime/messages/app-presence.event.js.map +1 -0
  67. package/dist/realtime/messages/index.d.ts +3 -0
  68. package/dist/realtime/messages/index.js +20 -0
  69. package/dist/realtime/messages/index.js.map +1 -0
  70. package/dist/realtime/messages/message-sync.message.d.ts +222 -0
  71. package/dist/realtime/messages/message-sync.message.js +43 -0
  72. package/dist/realtime/messages/message-sync.message.js.map +1 -0
  73. package/dist/realtime/messages/realtime-sub.direct.data.d.ts +11 -0
  74. package/dist/realtime/messages/realtime-sub.direct.data.js +3 -0
  75. package/dist/realtime/messages/realtime-sub.direct.data.js.map +1 -0
  76. package/dist/realtime/messages/thread-update.message.d.ts +68 -0
  77. package/dist/realtime/messages/thread-update.message.js +3 -0
  78. package/dist/realtime/messages/thread-update.message.js.map +1 -0
  79. package/dist/realtime/mixins/index.d.ts +3 -0
  80. package/dist/realtime/mixins/index.js +20 -0
  81. package/dist/realtime/mixins/index.js.map +1 -0
  82. package/dist/realtime/mixins/message-sync.mixin.d.ts +8 -0
  83. package/dist/realtime/mixins/message-sync.mixin.js +381 -0
  84. package/dist/realtime/mixins/message-sync.mixin.js.map +1 -0
  85. package/dist/realtime/mixins/mixin.d.ts +19 -0
  86. package/dist/realtime/mixins/mixin.js +41 -0
  87. package/dist/realtime/mixins/mixin.js.map +1 -0
  88. package/dist/realtime/mixins/presence-typing.mixin.js +33 -0
  89. package/dist/realtime/mixins/realtime-sub.mixin.d.ts +8 -0
  90. package/dist/realtime/mixins/realtime-sub.mixin.js +55 -0
  91. package/dist/realtime/mixins/realtime-sub.mixin.js.map +1 -0
  92. package/dist/realtime/parsers/graphql-parser.js +43 -0
  93. package/dist/realtime/parsers/graphql.parser.d.ts +15 -0
  94. package/dist/realtime/parsers/graphql.parser.js +22 -0
  95. package/dist/realtime/parsers/graphql.parser.js.map +1 -0
  96. package/dist/realtime/parsers/index.d.ts +6 -0
  97. package/dist/realtime/parsers/index.js +23 -0
  98. package/dist/realtime/parsers/index.js.map +1 -0
  99. package/dist/realtime/parsers/iris-parser.js +43 -0
  100. package/dist/realtime/parsers/iris.parser.d.ts +17 -0
  101. package/dist/realtime/parsers/iris.parser.js +10 -0
  102. package/dist/realtime/parsers/iris.parser.js.map +1 -0
  103. package/dist/realtime/parsers/json-parser.js +43 -0
  104. package/dist/realtime/parsers/json.parser.d.ts +6 -0
  105. package/dist/realtime/parsers/json.parser.js +10 -0
  106. package/dist/realtime/parsers/json.parser.js.map +1 -0
  107. package/dist/realtime/parsers/parser.d.ts +9 -0
  108. package/dist/realtime/parsers/parser.js +3 -0
  109. package/dist/realtime/parsers/parser.js.map +1 -0
  110. package/dist/realtime/parsers/region-hint-parser.js +43 -0
  111. package/dist/realtime/parsers/region-hint.parser.d.ts +12 -0
  112. package/dist/realtime/parsers/region-hint.parser.js +15 -0
  113. package/dist/realtime/parsers/region-hint.parser.js.map +1 -0
  114. package/dist/realtime/parsers/skywalker-parser.js +43 -0
  115. package/dist/realtime/parsers/skywalker.parser.d.ts +12 -0
  116. package/dist/realtime/parsers/skywalker.parser.js +15 -0
  117. package/dist/realtime/parsers/skywalker.parser.js.map +1 -0
  118. package/dist/realtime/parsers-advanced.js +158 -0
  119. package/dist/realtime/proto/common.proto +38 -0
  120. package/dist/realtime/proto/direct.proto +65 -0
  121. package/dist/realtime/proto/ig-messages.proto +83 -0
  122. package/dist/realtime/proto/iris.proto +188 -0
  123. package/dist/realtime/proto-parser.js +195 -0
  124. package/dist/realtime/protocols/iris.handshake.js +74 -0
  125. package/dist/realtime/protocols/proto-definitions.js +80 -0
  126. package/dist/realtime/protocols/skywalker.protocol.js +91 -0
  127. package/dist/realtime/realtime.client.events.js +3 -0
  128. package/dist/realtime/realtime.client.js +449 -0
  129. package/dist/realtime/realtime.service.js +462 -0
  130. package/dist/realtime/reconnect.manager.js +94 -0
  131. package/dist/realtime/session.manager.js +121 -0
  132. package/dist/realtime/subscriptions/graphql.subscription.d.ts +47 -0
  133. package/dist/realtime/subscriptions/graphql.subscription.js +99 -0
  134. package/dist/realtime/subscriptions/graphql.subscription.js.map +1 -0
  135. package/dist/realtime/subscriptions/index.d.ts +2 -0
  136. package/dist/realtime/subscriptions/index.js +19 -0
  137. package/dist/realtime/subscriptions/index.js.map +1 -0
  138. package/dist/realtime/subscriptions/skywalker.subscription.d.ts +4 -0
  139. package/dist/realtime/subscriptions/skywalker.subscription.js +13 -0
  140. package/dist/realtime/subscriptions/skywalker.subscription.js.map +1 -0
  141. package/dist/realtime/topic-map.js +71 -0
  142. package/dist/realtime/topic.js +80 -0
  143. package/dist/repositories/account.repository.js +261 -0
  144. package/dist/repositories/direct-thread.repository.js +247 -0
  145. package/dist/repositories/direct.repository.js +153 -0
  146. package/dist/repositories/feed.repository.js +233 -0
  147. package/dist/repositories/friendship.repository.js +190 -0
  148. package/dist/repositories/hashtag.repository.js +101 -0
  149. package/dist/repositories/highlights.repository.js +127 -0
  150. package/dist/repositories/location.repository.js +84 -0
  151. package/dist/repositories/media.repository.js +165 -0
  152. package/dist/repositories/story.repository.js +156 -0
  153. package/dist/repositories/upload.repository.js +167 -0
  154. package/dist/repositories/user.repository.js +94 -0
  155. package/dist/sendmedia/index.js +11 -0
  156. package/dist/sendmedia/sendFile.js +154 -0
  157. package/dist/sendmedia/sendPhoto.js +145 -0
  158. package/dist/sendmedia/uploadPhoto.js +175 -0
  159. package/dist/sendmedia/uploadfFile.js +264 -0
  160. package/dist/services/live.service.js +147 -0
  161. package/dist/services/search.service.js +116 -0
  162. package/dist/shared/index.js +35 -0
  163. package/dist/shared/shared.js +86 -0
  164. package/dist/thrift/index.d.ts +3 -0
  165. package/dist/thrift/index.js +20 -0
  166. package/dist/thrift/index.js.map +1 -0
  167. package/dist/thrift/thrift.d.ts +59 -0
  168. package/dist/thrift/thrift.js +101 -0
  169. package/dist/thrift/thrift.js.map +1 -0
  170. package/dist/thrift/thrift.reading.d.ts +41 -0
  171. package/dist/thrift/thrift.reading.js +327 -0
  172. package/dist/thrift/thrift.reading.js.map +1 -0
  173. package/dist/thrift/thrift.writing.d.ts +44 -0
  174. package/dist/thrift/thrift.writing.js +342 -0
  175. package/dist/thrift/thrift.writing.js.map +1 -0
  176. package/dist/types/index.js +285 -0
  177. package/dist/useMultiFileAuthState.js +437 -0
  178. package/dist/utils/helper-1.js +1 -0
  179. package/dist/utils/helper-10.js +1 -0
  180. package/dist/utils/helper-11.js +1 -0
  181. package/dist/utils/helper-12.js +1 -0
  182. package/dist/utils/helper-13.js +1 -0
  183. package/dist/utils/helper-14.js +1 -0
  184. package/dist/utils/helper-15.js +1 -0
  185. package/dist/utils/helper-16.js +1 -0
  186. package/dist/utils/helper-17.js +1 -0
  187. package/dist/utils/helper-18.js +1 -0
  188. package/dist/utils/helper-19.js +1 -0
  189. package/dist/utils/helper-2.js +1 -0
  190. package/dist/utils/helper-20.js +1 -0
  191. package/dist/utils/helper-21.js +1 -0
  192. package/dist/utils/helper-22.js +1 -0
  193. package/dist/utils/helper-23.js +1 -0
  194. package/dist/utils/helper-24.js +1 -0
  195. package/dist/utils/helper-25.js +1 -0
  196. package/dist/utils/helper-26.js +1 -0
  197. package/dist/utils/helper-27.js +1 -0
  198. package/dist/utils/helper-28.js +1 -0
  199. package/dist/utils/helper-29.js +1 -0
  200. package/dist/utils/helper-3.js +1 -0
  201. package/dist/utils/helper-30.js +1 -0
  202. package/dist/utils/helper-4.js +1 -0
  203. package/dist/utils/helper-5.js +1 -0
  204. package/dist/utils/helper-6.js +1 -0
  205. package/dist/utils/helper-7.js +1 -0
  206. package/dist/utils/helper-8.js +1 -0
  207. package/dist/utils/helper-9.js +1 -0
  208. package/dist/utils/index.js +280 -0
  209. package/examples/listen-to-messages.js +86 -0
  210. 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;