@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 +54 -0
- package/DOCS.md +53 -11
- package/README.md +83 -3
- package/index.d.ts +103 -0
- package/index.js +6 -0
- package/package.json +2 -2
- package/src/api/realtime/listen.js +3 -1
- package/src/api/users/getProfileBio.js +33 -0
- package/src/api/users/getProfilePictureV2.js +54 -0
- package/src/api/users/getSelfInfo.js +27 -0
- package/src/api/users/getUserFriendship.js +42 -0
- package/src/api/users/getUserID.js +31 -0
- package/src/api/users/getUserInfoV2.js +62 -0
- package/src/api/users/getUserProfile.js +26 -0
- package/src/api.js +116 -2
- package/src/app/InstaClient.js +23 -0
- package/src/app/createFcaClient.js +13 -1
- package/src/constants.js +10 -1
- package/src/methods/inbox.js +82 -1
- package/src/methods/media.js +28 -14
- package/src/methods/user.js +180 -4
- package/src/profileParse.js +250 -0
- package/src/realtime/bridge.js +76 -0
- package/src/realtime/client.js +376 -0
- package/src/realtime/listener.js +86 -0
- package/src/realtime/mqttot.js +609 -0
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)`
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
- **
|
|
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
|
|
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`
|
|
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.
|
|
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
|
|
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
|
+
};
|