@eryxenx/insta-chat 2.0.11 → 2.1.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,60 @@ All notable changes to `@eryxenx/insta-chat` are documented here.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/) and uses
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [2.1.0] - 2026-10-14
10
+
11
+ ### Added
12
+
13
+ - **Realtime MQTT listening.** `listen()` can now receive DMs over Instagram's
14
+ realtime broker (`edge-mqtt.facebook.com`) instead of polling the inbox, so
15
+ replies are pushed the moment Instagram delivers them. Opt in with
16
+ `login(cookie, { realtime: { enabled: true } })`; the callback and event shape
17
+ are identical to polling, and a broker that will not connect falls back to the
18
+ poller (unless `realtime.fallback` is `false`).
19
+ - `src/realtime/mqttot.js` — a dependency-free MQTToT codec: the Thrift-compact
20
+ CONNECT struct, MQTT packet framing, zlib payloads and a TLS transport.
21
+ - `src/realtime/client.js` — `RealtimeClient`: cookie-auth connect, keepalive,
22
+ and decoding of message-sync (`/direct_v2/threads/<id>/items/<id>`) and
23
+ realtime-sub payloads into `message`, `typing`, `seen` and `presence` events.
24
+ - `src/realtime/bridge.js` — turns a message-sync patch into the same raw inbox
25
+ event the poller produces, so `EventNormalizer` is unchanged.
26
+ - `src/realtime/listener.js` — `RealtimeListener`, which `listen()` drives.
27
+ - Native `InstaAPI` gains `realtimeClient`, `realtimeConnect`, `realtimeOn`,
28
+ `realtimeSubscribe`, `realtimePing`, `realtimeDisconnect`; `RealtimeClient`
29
+ and `RealtimeListener` are exported from the package root.
30
+ - `examples/realtime-bot.js` — the tiny DM bot over realtime push.
31
+ - `test/realtime.test.js` — codec, bridge, dispatch and `listen()` fallback.
32
+ - **V2 profile plugins.** `src/api/users/` gains `getUserInfoV2`,
33
+ `getProfilePictureV2`, `getUserProfile`, `getUserID`, `getProfileBio`,
34
+ `getUserFriendship` and `getSelfInfo` (FCA surface + `users` / `account`
35
+ namespaces). `getUserInfoV2` returns the *complete* profile — username, full
36
+ name, biography, follower/following/media counts, verified/private/business
37
+ flags, external url, category, business contact and the square + HD picture —
38
+ not just the fca subset V1 returned. `getProfilePictureV2` returns the picture
39
+ at any square size (derived from the CDN `stp=` param, no extra request) plus
40
+ the HD original. The native `InstaAPI` gains `getUserProfile` /
41
+ `getProfilePicture`.
42
+ - `src/profileParse.js` — normalizes the `web_profile_info` REST JSON, the
43
+ profile GraphQL `data.user`, the HTML-embedded `PolarisViewer` blob and the
44
+ inbox user record into one shape, and resizes a picture URL.
45
+ - `test/profile.test.js` — normalization, the logout-safe resolver and the
46
+ plugins.
47
+
48
+ ### Fixed
49
+
50
+ - **A profile lookup can no longer masquerade as a dead session (the logout
51
+ bug).** `getInfo` reads `/api/v1/users/<id>/info/`, which Instagram 302s to the
52
+ login page for a web session — the HTTP layer classifies any 3xx as an auth
53
+ failure, so any command that looked up a profile could trip the session guard
54
+ and log the bot out. Profile reads now go through a resolver that tries the
55
+ users already learned from the inbox/thread views, `web_profile_info`, the
56
+ profile GraphQL query and the account's own server-rendered page, and swallows
57
+ a 429 / 302 from any of them. It never throws and returns `null` when nothing
58
+ resolves. `init()` also resolves the logged-in username from the home shell
59
+ instead of the same 302'ing REST path.
60
+
7
61
  ## [2.0.10] - 2026-10-11
8
62
 
9
63
  ### Added
package/DOCS.md CHANGED
@@ -47,8 +47,9 @@ commands call). Pass `{ native: true }` to get the bare `InstaAPI` instead.
47
47
  | `sessionParams` | – | Pre-harvested browser session params. |
48
48
  | `sessionParamsFile` | `./session-params.json` | Load params from disk. |
49
49
  | `deviceFile` | `./device.json` | Persisted device identity. |
50
- | `pollInterval` | `15000` | `listen()` poll interval (ms). |
50
+ | `pollInterval` | `15000` | `listen()` poll interval (ms), used when realtime is off. |
51
51
  | `minPollIntervalMs` | `15000` | Lowest interval `listen()` will honour (floor, min 2000). |
52
+ | `realtime` | – | `{ enabled, fallback, host, keepAliveMs }` — realtime MQTT push for `listen()`. |
52
53
 
53
54
  ## `InstaClient` (FCA surface)
54
55
 
@@ -60,9 +61,9 @@ Facebook-Chat-API shape. Its methods live one-per-file under
60
61
  |-------|---------|
61
62
  | messaging | `sendMessage(form, threadID, cb?, replyToMessage?)`, `sendMessageSync`, `editMessage(text, messageID)`, `unsendMessage(messageID, cb?, threadID?)`, `setMessageReaction(emoji, messageID, cb?, threadID?)`, `sendTypingIndicator`, `markAsRead`, `shareContact`, `resolvePhotoUrl` |
62
63
  | threads | `getThreadInfo`, `getThreadList`, `getThreadHistory` |
63
- | users | `getUserInfo`, `getUID`, `getProfilePictureUrl` |
64
- | account | `getCurrentUserID`, `getAppState`, `getCookies`, `setOptions` |
65
- | realtime | `listen`, `stopListening`, `getInbox` |
64
+ | users | `getUserInfo`, `getUserInfoV2`, `getUserProfile`, `getProfilePictureV2`, `getProfileBio`, `getUserFriendship`, `getUserID`, `getUID`, `getProfilePictureUrl` |
65
+ | account | `getCurrentUserID`, `getSelfInfo`, `getAppState`, `getCookies`, `setOptions` |
66
+ | realtime | `listen`, `stopListening`, `getInbox`, `realtimeConnect`, `realtimeOn`, `realtimeSubscribe`, `realtimePing`, `realtimeDisconnect` |
66
67
  | media | `sendPhoto`, `sendPhotoFromUrl`, `sendVideoFromUrl`, `sendVoiceFromUrl` |
67
68
 
68
69
  Group-management calls Instagram DMs cannot do (`changeNickname`, `setTitle`,
@@ -135,15 +136,53 @@ All take `(threadID, source, options)` where `options = { filename, text }`.
135
136
 
136
137
  ### Users
137
138
 
138
- `getUserInfo(userID)`, `getUserInfoByUsername(username)`, `searchUsers(query, limit)`.
139
+ `getUserInfo(userID)`, `getUserInfoByUsername(username)`, `searchUsers(query, limit)`,
140
+ `getUserProfile(idOrUsername, opts?)`, `getProfilePicture(idOrUsername, opts?)`.
141
+
142
+ `getUserProfile` returns the complete merged profile (see
143
+ `README.md` -> *Full profiles*): id, username, full name, biography,
144
+ follower/following/media counts, verified/private/business flags, external url,
145
+ category, business contact, `profilePicUrl` and `profilePicUrlHd`. It is
146
+ logout-safe — it merges the users learned from the inbox/thread views with
147
+ `web_profile_info`, the profile GraphQL query and the account's own page, and
148
+ swallows a 429 / 302 from any source, so a profile read never trips the session
149
+ guard. Returns `null` when nothing resolves.
150
+
151
+ `getProfilePicture(idOrUsername, { size })` returns the best picture URL at the
152
+ requested square size.
139
153
 
140
154
  ### Inbox / listening
141
155
 
142
- `getInbox()` returns `{ threads }`. `listen(callback, intervalMs)` polls and
143
- emits normalized `message` events; it returns a `stop()` function.
144
- `stopListening()` also stops polling. The interval is raised to the client's
145
- `minPollIntervalMs` floor (set via the constructor options; default 15000 ms,
146
- minimum 2000 ms), so pass that option if you want a shorter cycle than 15 s.
156
+ `getInbox()` returns `{ threads }`. `listen(callback, intervalMs)` emits
157
+ normalized `message` events and returns a `stop()` function. `stopListening()`
158
+ also stops it.
159
+
160
+ By default `listen` polls the inbox; the interval is raised to the client's
161
+ `minPollIntervalMs` floor (default 15000 ms, minimum 2000 ms). Pass
162
+ `{ realtime: { enabled: true } }` to `login` and `listen` instead subscribes to
163
+ Instagram's realtime MQTT stream, so each new DM is pushed the moment Instagram
164
+ delivers it. The callback and event shape are the same either way, and a realtime
165
+ connection that cannot be established falls back to polling unless you set
166
+ `realtime: { enabled: true, fallback: false }`.
167
+
168
+ ### Realtime (MQTT)
169
+
170
+ The realtime broker (`edge-mqtt.facebook.com`) is an MQTToT connection whose
171
+ CONNECT payload is a Thrift-compact struct and whose password is
172
+ `sessionid=<sessionid>`; the session cookie authenticates it, so there is no app
173
+ login. Message-sync publishes (`/direct_v2/threads/<id>/items/<id>`) are decoded
174
+ into the same raw inbox events the poller produces and normalized identically.
175
+
176
+ | Method | Description |
177
+ |--------|-------------|
178
+ | `realtimeClient(options?)` | Build a raw `RealtimeClient` without connecting. |
179
+ | `realtimeConnect(options?)` | Open the MQTT connection. Resolves to the client. |
180
+ | `realtimeOn(event, handler)` | Subscribe to `message`, `typing`, `seen`, `presence`, `direct`, `receive`, `error`, `disconnected`. |
181
+ | `realtimeSubscribe()` | Subscribe to the Direct message-sync stream (reads the cursor from the inbox query). |
182
+ | `realtimePing()` | Send a keepalive ping. |
183
+ | `realtimeDisconnect()` | Close the connection. |
184
+
185
+ `RealtimeClient` and `RealtimeListener` are also exported from the package root.
147
186
 
148
187
  ### Event shape
149
188
 
@@ -242,7 +281,10 @@ Instagram rejects sends whose id does not decode to a recent timestamp.
242
281
 
243
282
  ## Limitations
244
283
 
245
- - **Polling** inbox, not MQTT — there is a short delay before events arrive.
284
+ - **Realtime is best-effort.** `listen({ realtime: { enabled: true } })` subscribes
285
+ to Instagram's MQTT push stream for near-instant delivery; if the broker is
286
+ unreachable (or the process runs on a host that blocks it) it falls back to
287
+ polling. Polling has a short delay before events arrive.
246
288
  - Instagram rate-limits aggressively; keep a sane `pollInterval` and avoid
247
289
  tight send loops.
248
290
  - Only works for **personal** accounts with a valid browser session cookie.
package/README.md CHANGED
@@ -23,7 +23,7 @@ your own personal account:
23
23
 
24
24
  - ✅ **Send** text, photos, videos, voice notes
25
25
  - ✅ **React**, edit and unsend messages
26
- - ✅ **Listen** for incoming DMs (polling) and dispatch them to your own commands
26
+ - ✅ **Listen** for incoming DMs — realtime MQTT push or polling — and dispatch them to your own commands
27
27
  - ✅ **Typing** indicators, mark-seen, mute/unmute, hide
28
28
  - ✅ **Users** — lookup by id / username, search
29
29
  - ✅ **Pure HTTP at runtime** — no Puppeteer, no browser, no native build
@@ -111,7 +111,17 @@ go stale use `api.refreshSession()`.
111
111
  | `login(cookie, { native: true })` | Returns the bare `InstaAPI` instead. |
112
112
  | `createClient(cookie, [options])` | Alias of `login`. |
113
113
 
114
- `options`: `{ native, sessionParams, sessionParamsFile, deviceFile, pollInterval }`.
114
+ `options`: `{ native, sessionParams, sessionParamsFile, deviceFile, pollInterval, minPollIntervalMs, realtime }`.
115
+
116
+ To make `listen()` push instead of poll, opt into realtime MQTT:
117
+
118
+ ```javascript
119
+ const api = await login(cookie, { realtime: { enabled: true } });
120
+ ```
121
+
122
+ `realtime` is `{ enabled, fallback, host, keepAliveMs }`. With `fallback !== false`
123
+ (the default) a broker that will not connect degrades to the inbox poller, so the
124
+ bot still gets events. See [Listening](#listening).
115
125
 
116
126
  ### Two layers
117
127
 
@@ -136,6 +146,11 @@ binds a thread. `EventNormalizer` maps raw inbox events to the FCA event shape.
136
146
  | `sendTypingIndicator(threadID)` | Typing indicator. |
137
147
  | `getThreadInfo` / `getThreadList` / `getThreadHistory` | Thread reads. |
138
148
  | `getUserInfo` / `getUID` | User lookup. |
149
+ | `getUserInfoV2(idOrIds)` | Full profile (all details) for one id or an array of ids. |
150
+ | `getUserProfile(idOrHandle)` | The complete merged profile object. |
151
+ | `getProfilePictureV2(idOrHandle, opts)` | Profile picture at any size (square + HD). |
152
+ | `getProfileBio(idOrHandle)` / `getUserID(idOrHandle)` / `getUserFriendship(id)` | Bio, id, friendship state. |
153
+ | `getSelfInfo()` | The logged-in account's own profile. |
139
154
  | `getAppState` / `getCookies` | Cookie jar. |
140
155
 
141
156
  ### Messaging (native `InstaAPI`)
@@ -173,6 +188,39 @@ binds a thread. `EventNormalizer` maps raw inbox events to the FCA event shape.
173
188
  | `muteThread` / `unmuteThread` / `hideThread` | Thread state. |
174
189
  | `getUserInfo(userID)` / `getUserInfoByUsername(username)` | User lookup. |
175
190
  | `searchUsers(query, limit)` | Search users. |
191
+ | `getUserProfile(idOrUsername, [opts])` | Complete profile merged from every source (see below). |
192
+ | `getProfilePicture(idOrUsername, [opts])` | Best picture URL; `opts.size` resizes via the CDN. |
193
+
194
+ ### Full profiles (`getUserInfoV2` / `getUserProfile`)
195
+
196
+ V1 `getUserInfo` returns a small fca-shaped subset. V2 returns **every detail a
197
+ web session can read**, and it is *logout-safe*: profile lookups go through a
198
+ resolver that tries several sources and never turns a rate-limit (429) or a
199
+ login redirect (302) into a dead-session error - the bug that used to log the
200
+ bot out whenever a command looked up a profile.
201
+
202
+ Sources, merged in order (a field a source cannot supply stays `null`):
203
+
204
+ 1. **Inbox/thread record** - the users the client has already seen in the DM
205
+ views. Free, needs no request, and the only source that still answers once
206
+ the profile REST paths 429. `getInbox()` populates it automatically.
207
+ 2. **`web_profile_info` REST** - canonical counts (followers/following/media),
208
+ biography, verified/private/business flags, external url, category.
209
+ 3. **Profile GraphQL** (`PolarisProfilePageContentQuery`) - richest payload when
210
+ Instagram answers it.
211
+ 4. **The account's own page** - the home shell Instagram server-renders embeds
212
+ the logged-in account's full profile, so self always resolves.
213
+
214
+ ```javascript
215
+ const info = await api.getUserInfoV2("32715034217");
216
+ // info["32715034217"] => { id, username, name, biography, followerCount,
217
+ // followingCount, mediaCount, isVerified, isPrivate, isBusinessAccount,
218
+ // externalUrl, category, profilePicUrl, profilePicUrlHd, fbid, ... }
219
+
220
+ const profile = await api.getUserProfile("@someuser"); // the raw merged object
221
+ const pic = await api.getProfilePictureV2("someuser", { size: 640 });
222
+ // pic => { url, hd, thumb, sizes: { "150": ..., "320": ..., "640": ..., "1080": ... } }
223
+ ```
176
224
 
177
225
  ### Listening
178
226
 
@@ -187,7 +235,35 @@ const stop = api.listen((err, event) => {
187
235
  stop();
188
236
  ```
189
237
 
190
- `listen` polls the inbox (default every 15s, override with `options.pollInterval`).
238
+ `listen` uses the realtime MQTT stream when you pass
239
+ `{ realtime: { enabled: true } }` to `login` — Instagram pushes each new DM over
240
+ the broker, so replies are near-instant. Without it, `listen` polls the inbox
241
+ (default every 15s, override with `options.pollInterval`). Either way the callback
242
+ and event shape are identical, and a realtime connection that fails falls back to
243
+ polling unless you set `realtime: { enabled: true, fallback: false }`.
244
+
245
+ ### Realtime (MQTT)
246
+
247
+ Instagram's realtime broker (`edge-mqtt.facebook.com`) is an MQTToT connection
248
+ authenticated by the same `sessionid` cookie — no app login. `listen()` drives it
249
+ when realtime is enabled; you can also use it directly:
250
+
251
+ ```javascript
252
+ const api = await login(cookie, { native: true });
253
+
254
+ api.realtimeOn("message", (e) => console.log(e.message));
255
+ api.realtimeOn("typing", (e) => console.log(e.threadID, "is typing"));
256
+ api.realtimeOn("seen", (e) => console.log(e.threadID, "read"));
257
+ api.realtimeOn("presence", (e) => console.log(e.value));
258
+
259
+ await api.realtimeConnect();
260
+ await api.realtimeSubscribe(); // reads seq_id/snapshot_at_ms from the inbox query
261
+ // ...later
262
+ api.realtimeDisconnect();
263
+ ```
264
+
265
+ `RealtimeClient` (the raw client) and `RealtimeListener` (the listen() wrapper)
266
+ are exported too, for callers that want to drive the socket themselves.
191
267
 
192
268
  ---
193
269
 
@@ -230,6 +306,10 @@ const login = require("@eryxenx/insta-chat");
230
306
  | `src/session.js` | Browser session-param harvesting (`puppeteer-core`). |
231
307
  | `src/deviceIdentity.js` | Persisted device identity. |
232
308
  | `src/methods/` | Native: `message`, `media`, `reactions`, `thread`, `user`, `inbox`, `typing`. |
309
+ | `src/realtime/mqttot.js` | Dependency-free MQTToT codec (Thrift compact + MQTT packets + TLS transport). |
310
+ | `src/realtime/client.js` | Realtime MQTT client: connect, dispatch, direct events. |
311
+ | `src/realtime/bridge.js` | Message-sync patch → raw inbox event. |
312
+ | `src/realtime/listener.js` | Wires realtime into `listen()` with poller fallback. |
233
313
 
234
314
  See [DOCS.md](./DOCS.md) for the full protocol notes (GraphQL doc_ids, send /
235
315
  upload / reaction flows).
package/index.d.ts CHANGED
@@ -10,10 +10,36 @@ export interface InstaChatOptions {
10
10
  deviceFile?: string;
11
11
  /** Polling interval in ms for listen(). Defaults to 15000. */
12
12
  pollInterval?: number;
13
+ /** Lowest interval listen() will honour (ms, min 2000). Defaults to 15000. */
14
+ minPollIntervalMs?: number;
15
+ /** Opt listen() into the realtime MQTT push stream. */
16
+ realtime?: RealtimeOptions;
13
17
  /** login() resolves the native InstaAPI instead of the FCA-shaped InstaClient. */
14
18
  native?: boolean;
15
19
  }
16
20
 
21
+ export interface RealtimeOptions {
22
+ /** Turn realtime MQTT on for listen(). */
23
+ enabled?: boolean;
24
+ /** Fall back to polling when the broker is unreachable. Defaults to true. */
25
+ fallback?: boolean;
26
+ /** Broker host. Defaults to edge-mqtt.facebook.com */
27
+ host?: string;
28
+ /** Keepalive ping interval (ms). Defaults to 30000. */
29
+ keepAliveMs?: number;
30
+ /** Pre-known iris subscription cursor; otherwise read from the inbox query. */
31
+ seqId?: number;
32
+ snapshotAtMs?: number;
33
+ /** Resolve the cursor yourself: () => ({ seqId, snapshotAtMs }). */
34
+ getRealtimeState?: () => Promise<{ seqId: number; snapshotAtMs: number } | null> | { seqId: number; snapshotAtMs: number } | null;
35
+ /** Android app version sent in the CONNECT. */
36
+ appVersion?: string;
37
+ /** Android User-Agent sent in the CONNECT. */
38
+ userAgent?: string;
39
+ /** Fired when the connection + subscription are ready. */
40
+ onReady?: (listener: any) => void;
41
+ }
42
+
17
43
  export interface SendOptions {
18
44
  filename?: string;
19
45
  text?: string;
@@ -79,6 +105,52 @@ export interface InboxResult {
79
105
  threads: any[];
80
106
  }
81
107
 
108
+ // The complete profile Instagram exposes, merged from every source the session
109
+ // can read. Fields a source cannot supply stay null.
110
+ export interface ProfileInfo {
111
+ id: string | null;
112
+ pk: string | null;
113
+ username: string | null;
114
+ fullName: string | null;
115
+ biography: string;
116
+ externalUrl: string | null;
117
+ category: string | null;
118
+ isVerified: boolean;
119
+ isPrivate: boolean;
120
+ isBusinessAccount: boolean;
121
+ isProfessionalAccount: boolean;
122
+ hasProfilePic: boolean;
123
+ profilePicUrl: string | null;
124
+ profilePicUrlHd: string | null;
125
+ fbid: string | null;
126
+ followerCount: number | null;
127
+ followingCount: number | null;
128
+ mediaCount: number | null;
129
+ highlightReelCount: number | null;
130
+ businessEmail: string | null;
131
+ businessPhone: string | null;
132
+ publicEmail: string | null;
133
+ publicPhone: string | null;
134
+ raw: any;
135
+ }
136
+
137
+ export interface ProfilePictureOptions {
138
+ /** Square size for the returned `url` (default 320). */
139
+ size?: number;
140
+ /** Square sizes to populate under `sizes` (default [150, 320, 640, 1080]). */
141
+ sizes?: number[];
142
+ }
143
+
144
+ export interface ProfilePicture {
145
+ id: string | null;
146
+ userID: string | null;
147
+ username: string | null;
148
+ url: string | null;
149
+ hd: string | null;
150
+ thumb: string | null;
151
+ sizes: Record<string, string>;
152
+ }
153
+
82
154
  export interface CookieUtils {
83
155
  parse(input: string): Record<string, string>;
84
156
  }
@@ -120,8 +192,17 @@ export declare class InstaClient {
120
192
  getUID(input: string): string | null;
121
193
  getProfilePictureUrl(idOrUsername: string): Promise<string | null>;
122
194
 
195
+ // users (V2 profile surface — see ProfileInfo)
196
+ getUserInfoV2(ids: string | string[], callback?: (err: any, info: any) => void): Promise<Record<string, any>>;
197
+ getProfilePictureV2(idOrUsername: string, options?: ProfilePictureOptions, callback?: (err: any, info: any) => void): Promise<ProfilePicture | null>;
198
+ getUserProfile(idOrUsername: string, callback?: (err: any, info: ProfileInfo | null) => void): Promise<ProfileInfo | null>;
199
+ getUserID(idOrUsername: string, callback?: (err: any, id: string | null) => void): Promise<string | null>;
200
+ getProfileBio(idOrUsername: string, callback?: (err: any, info: any) => void): Promise<{ id: string | null; userID: string | null; username: string | null; name: string | null; biography: string }>;
201
+ getUserFriendship(userID: string, callback?: (err: any, info: any) => void): Promise<Record<string, boolean | string | null> | null>;
202
+
123
203
  // account
124
204
  getCurrentUserID(): string | null;
205
+ getSelfInfo(callback?: (err: any, info: any) => void): Promise<Record<string, any> | null>;
125
206
  getAppState(): Array<{ key: string; value: string; domain?: string; path?: string }>;
126
207
  getCookies(): Array<{ key: string; value: string; domain?: string; path?: string }>;
127
208
  setOptions(...args: any[]): boolean;
@@ -145,6 +226,14 @@ export declare class InstaClient {
145
226
  stopListening(): void;
146
227
  getInbox(cursor?: string): Promise<InboxResult>;
147
228
  rememberThread(messageID: string, threadID: string): void;
229
+
230
+ // realtime (MQTT) — also on the native client
231
+ realtimeClient(options?: RealtimeOptions): any;
232
+ realtimeConnect(options?: RealtimeOptions): Promise<any>;
233
+ realtimeOn(event: string, handler: (payload: any) => void): this;
234
+ realtimeSubscribe(): Promise<{ seqId: number; snapshotAtMs: number }>;
235
+ realtimePing(): Promise<boolean>;
236
+ realtimeDisconnect(): void;
148
237
  }
149
238
 
150
239
  /** Per-thread convenience view over an InstaClient. */
@@ -201,11 +290,15 @@ export interface LoginExport {
201
290
  findChromium(): string | undefined;
202
291
  constants: any;
203
292
  moduleLogin: LoginExport;
293
+ RealtimeClient: any;
294
+ RealtimeListener: any;
204
295
  }
205
296
 
206
297
  export const InstaClient: typeof InstaClient;
207
298
  export const InstaContext: typeof InstaContext;
208
299
  export const EventNormalizer: typeof EventNormalizer;
300
+ export const RealtimeClient: any;
301
+ export const RealtimeListener: any;
209
302
  export const createFcaClient: typeof createFcaClient;
210
303
  export const loadApiMethods: typeof loadApiMethods;
211
304
  export const wrap: typeof wrap;
@@ -246,6 +339,8 @@ export declare class InstaAPI {
246
339
  getUserInfo(userID: string): Promise<any>;
247
340
  getUserInfoByUsername(username: string): Promise<any>;
248
341
  searchUsers(query: string, limit?: number): Promise<any[]>;
342
+ getUserProfile(idOrUsername: string, opts?: { refresh?: boolean }): Promise<ProfileInfo | null>;
343
+ getProfilePicture(idOrUsername: string, opts?: ProfilePictureOptions): Promise<string | null>;
249
344
 
250
345
  // media
251
346
  sendPhoto(threadID: string, filePath: string, options?: SendOptions): Promise<any>;
@@ -262,6 +357,14 @@ export declare class InstaAPI {
262
357
  getInbox(cursor?: string): Promise<InboxResult>;
263
358
  listen(callback: (err: Error | null, event: IncomingEvent | null) => void, intervalMs?: number): () => void;
264
359
  stopListening(): void;
360
+
361
+ // realtime (MQTT)
362
+ realtimeClient(options?: RealtimeOptions): any;
363
+ realtimeConnect(options?: RealtimeOptions): Promise<any>;
364
+ realtimeOn(event: string, handler: (payload: any) => void): this;
365
+ realtimeSubscribe(): Promise<{ seqId: number; snapshotAtMs: number }>;
366
+ realtimePing(): Promise<boolean>;
367
+ realtimeDisconnect(): void;
265
368
  }
266
369
 
267
370
  declare const login: LoginExport;
package/index.js CHANGED
@@ -17,6 +17,8 @@ const { createFcaClient } = require("./src/app/createFcaClient");
17
17
  const EventNormalizer = require("./src/events/normalizeEvent");
18
18
  const loadApiMethods = require("./src/app/loadApiMethods");
19
19
  const moduleLogin = require("./module/login");
20
+ const { RealtimeClient } = require("./src/realtime/client");
21
+ const { RealtimeListener } = require("./src/realtime/listener");
20
22
 
21
23
  /**
22
24
  * Log in to Instagram with a browser cookie / sessionid and return a ready
@@ -88,6 +90,8 @@ login.CookieUtils = { parse: normalizeCookie };
88
90
  login.harvestSessionParams = harvestSessionParams;
89
91
  login.findChromium = findChromium;
90
92
  login.moduleLogin = moduleLogin;
93
+ login.RealtimeClient = RealtimeClient;
94
+ login.RealtimeListener = RealtimeListener;
91
95
 
92
96
  module.exports = login;
93
97
 
@@ -107,5 +111,7 @@ module.exports.harvestSessionParams = harvestSessionParams;
107
111
  module.exports.findChromium = findChromium;
108
112
  module.exports.constants = constants;
109
113
  module.exports.moduleLogin = moduleLogin;
114
+ module.exports.RealtimeClient = RealtimeClient;
115
+ module.exports.RealtimeListener = RealtimeListener;
110
116
  module.exports.antiSuspension = require("./src/utils/antiSuspension");
111
117
  module.exports.sessionGuard = require("./src/utils/sessionGuard");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eryxenx/insta-chat",
3
- "version": "2.0.11",
3
+ "version": "2.1.1",
4
4
  "description": " Unofficial Instagram Chat API for Node.js | Build Instagram DM bots on personal accounts — send messages, media, voice notes, reactions, edit & unsend, listen for incoming chats. No browser at runtime",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "CHANGELOG.md"
24
24
  ],
25
25
  "scripts": {
26
- "test": "node test/smoke.js && node test/antiSuspension.test.js && node test/replyContent.test.js && node test/edit.test.js && node test/sendReply.test.js && node test/client.test.js && node test/relayError.test.js && node test/safetyWiring.test.js && node test/mediaOrder.test.js && node test/sessionSafety.test.js && node test/emptySend.test.js && node test/pollFloor.test.js && node test/userLookup.test.js && node test/unsend.test.js && node test/unsendGraphQL.test.js",
26
+ "test": "node test/smoke.js && node test/antiSuspension.test.js && node test/replyContent.test.js && node test/edit.test.js && node test/sendReply.test.js && node test/client.test.js && node test/relayError.test.js && node test/safetyWiring.test.js && node test/mediaOrder.test.js && node test/sessionSafety.test.js && node test/emptySend.test.js && node test/pollFloor.test.js && node test/userLookup.test.js && node test/profile.test.js && node test/unsend.test.js && node test/unsendGraphQL.test.js && node test/realtime.test.js",
27
27
  "build": "node scripts/build.js"
28
28
  },
29
29
  "keywords": [
@@ -5,7 +5,9 @@
5
5
  * Original author credit must be retained in all copies and derivatives.
6
6
  */
7
7
 
8
- // fca.listen(callback) - the Instagram inbox poller.
8
+ // fca.listen(callback) - the Instagram inbox listener. Realtime MQTT push when
9
+ // options.realtime.enabled is set (see InstaChatOptions), otherwise the inbox
10
+ // poller. Both feed the same callback.
9
11
  module.exports = function (client) {
10
12
  return function listen(callback, intervalMs) {
11
13
  return client.api.listen(callback, intervalMs || client.options.pollInterval);
@@ -0,0 +1,33 @@
1
+ /*!
2
+ * insta-chat - Unofficial Instagram Chat API
3
+ * Copyright (c) 2026 EryXenX (https://github.com/EryXenX)
4
+ * Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
5
+ * Original author credit must be retained in all copies and derivatives.
6
+ */
7
+
8
+ // fca.getProfileBio(idOrUsername, callback) -> { id, username, name, biography }.
9
+ //
10
+ // The biography is the profile "about" text. Sourced from the same logout-safe
11
+ // profile resolver as getUserInfoV2, so a rate-limited profile endpoint yields
12
+ // an empty bio rather than an error.
13
+ module.exports = function (client) {
14
+ return async function getProfileBio(idOrUsername, callback) {
15
+ const cb = typeof callback === "function" ? callback : null;
16
+ const key = String(idOrUsername == null ? "" : idOrUsername).replace(/^@/, "");
17
+ try {
18
+ const profile = await client.getProfile(key).catch(() => null);
19
+ const out = {
20
+ id: profile?.id || null,
21
+ userID: profile?.id || null,
22
+ username: profile?.username || null,
23
+ name: profile?.fullName || null,
24
+ biography: profile?.biography || ""
25
+ };
26
+ if (cb) cb(null, out);
27
+ return out;
28
+ } catch (error) {
29
+ if (cb) cb(error, null);
30
+ return { id: null, userID: null, username: null, name: null, biography: "" };
31
+ }
32
+ };
33
+ };
@@ -0,0 +1,54 @@
1
+ /*!
2
+ * insta-chat - Unofficial Instagram Chat API
3
+ * Copyright (c) 2026 EryXenX (https://github.com/EryXenX)
4
+ * Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
5
+ * Original author credit must be retained in all copies and derivatives.
6
+ */
7
+
8
+ // fca.getProfilePictureV2(idOrUsername, [options], callback)
9
+ // -> { id, userID, username, url, hd, thumb, sizes: { <px>: url } }
10
+ //
11
+ // Returns the user's profile picture at every size a caller may want, resolved
12
+ // from whichever source answers (inbox-learned record, web_profile_info or the
13
+ // account's own page). Instagram's CDN resizes a picture from its URL, so the
14
+ // size variants are derived without extra requests.
15
+ //
16
+ // options: { size } -> `url` is returned at that square size (default 320).
17
+ // { sizes: [n, ...] } -> a map of square sizes under `sizes`.
18
+ module.exports = function (client) {
19
+ return async function getProfilePictureV2(idOrUsername, options, callback) {
20
+ if (typeof options === "function") { callback = options; options = {}; }
21
+ options = options || {};
22
+ const cb = typeof callback === "function" ? callback : null;
23
+ const key = String(idOrUsername == null ? "" : idOrUsername).replace(/^@/, "");
24
+ try {
25
+ const profile = await client.getProfile(key).catch(() => null);
26
+ if (!profile || (!profile.profilePicUrl && !profile.profilePicUrlHd)) {
27
+ const empty = { id: profile?.id || null, userID: profile?.id || null, username: profile?.username || null, url: null, hd: null, thumb: null, sizes: {} };
28
+ if (cb) cb(null, empty);
29
+ return empty;
30
+ }
31
+ const { profilePictureAtSize } = require("../../profileParse");
32
+ const hd = profile.profilePicUrlHd || profile.profilePicUrl;
33
+ const base = profile.profilePicUrl || hd;
34
+ const size = Number(options.size) || 320;
35
+ const wanted = Array.isArray(options.sizes) && options.sizes.length ? options.sizes : [150, 320, 640, 1080];
36
+ const sizes = {};
37
+ for (const px of wanted) sizes[String(px)] = profilePictureAtSize(hd, px);
38
+ const out = {
39
+ id: profile.id,
40
+ userID: profile.id,
41
+ username: profile.username,
42
+ url: profilePictureAtSize(base, size),
43
+ hd,
44
+ thumb: profilePictureAtSize(base, 150),
45
+ sizes
46
+ };
47
+ if (cb) cb(null, out);
48
+ return out;
49
+ } catch (error) {
50
+ if (cb) cb(error, null);
51
+ return null;
52
+ }
53
+ };
54
+ };
@@ -0,0 +1,27 @@
1
+ /*!
2
+ * insta-chat - Unofficial Instagram Chat API
3
+ * Copyright (c) 2026 EryXenX (https://github.com/EryXenX)
4
+ * Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
5
+ * Original author credit must be retained in all copies and derivatives.
6
+ */
7
+
8
+ // fca.getSelfInfo(callback) -> the logged-in account's own profile.
9
+ //
10
+ // Convenience wrapper over getUserInfoV2 for the bot's own id. Unlike another
11
+ // user's profile this always resolves: the session cookie carries the account
12
+ // id and the account's own page embeds its full PolarisViewer blob.
13
+ module.exports = function (client) {
14
+ return async function getSelfInfo(callback) {
15
+ const cb = typeof callback === "function" ? callback : null;
16
+ const selfID = typeof client.getCurrentUserID === "function" ? client.getCurrentUserID() : client.userID;
17
+ try {
18
+ const info = await client.getUserInfoV2(selfID);
19
+ const profile = info && info[String(selfID)] ? info[String(selfID)] : null;
20
+ if (cb) cb(null, profile);
21
+ return profile;
22
+ } catch (error) {
23
+ if (cb) cb(error, null);
24
+ return null;
25
+ }
26
+ };
27
+ };
@@ -0,0 +1,42 @@
1
+ /*!
2
+ * insta-chat - Unofficial Instagram Chat API
3
+ * Copyright (c) 2026 EryXenX (https://github.com/EryXenX)
4
+ * Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
5
+ * Original author credit must be retained in all copies and derivatives.
6
+ */
7
+
8
+ // fca.getUserFriendship(userID, callback) -> { id, blocking, isRestricted,
9
+ // isFollowedBy, follows }.
10
+ //
11
+ // Instagram's DM GraphQL user records carry a `friendship_status` for every
12
+ // participant ({ blocking, is_restricted, ... }). This exposes that state from
13
+ // data the client already learned, so it needs no extra request and cannot look
14
+ // like a dead session. Returns null when the user has not been observed.
15
+ module.exports = function (client) {
16
+ return async function getUserFriendship(userID, callback) {
17
+ const cb = typeof callback === "function" ? callback : null;
18
+ const key = String(userID == null ? "" : userID).replace(/^@/, "");
19
+ try {
20
+ const learned = client.api && typeof client.api._learnedUser === "function"
21
+ ? client.api._learnedUser(key)
22
+ : null;
23
+ const fs = learned && learned.friendship_status;
24
+ const out = {
25
+ id: learned ? String(learned.id || learned.pk || key) : null,
26
+ userID: learned ? String(learned.id || learned.pk || key) : null,
27
+ username: learned ? learned.username || null : null,
28
+ blocking: fs ? fs.blocking === true : null,
29
+ isRestricted: fs ? fs.is_restricted === true : null,
30
+ isFollowedBy: fs ? fs.is_followed_by === true : null,
31
+ follows: fs ? fs.following === true : null,
32
+ incomingRequest: fs ? fs.incoming_request === true : null,
33
+ outgoingRequest: fs ? fs.outgoing_request === true : null
34
+ };
35
+ if (cb) cb(null, out);
36
+ return out;
37
+ } catch (error) {
38
+ if (cb) cb(error, null);
39
+ return null;
40
+ }
41
+ };
42
+ };