@eryxenx/insta-chat 2.0.9 → 2.0.10
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 +32 -0
- package/package.json +3 -3
- package/src/app/InstaClient.js +52 -4
- package/src/events/normalizeEvent.js +8 -0
- package/src/methods/inbox.js +14 -0
- package/src/methods/user.js +21 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ 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
|
+
## [2.0.10] - 2026-10-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`event.participantIDs` is populated on Instagram message events.** Facebook
|
|
12
|
+
MQTT events carried the thread's member list, so commands ported from
|
|
13
|
+
GoatBot-Pro read `event.participantIDs` (`all.js` tags every member, `ban.js`
|
|
14
|
+
checks membership). Instagram inbox events carry none, so the normalizer now
|
|
15
|
+
fills it from the participants the client learned from thread introspection
|
|
16
|
+
(`InstaClient.getThreadParticipantIDs`, fed by the native inbox poller's
|
|
17
|
+
per-poll snapshot). It stays `undefined` when unknown — the same shape as
|
|
18
|
+
upstream, so `|| []` guards keep working.
|
|
19
|
+
- **`addUserToGroup` / `removeUserFromGroup` resolve a username or `@handle` to
|
|
20
|
+
a numeric igid** before calling the endpoint, against the offline user index
|
|
21
|
+
first and a live lookup second, and return `false` rather than send a
|
|
22
|
+
non-numeric id Instagram would 4xx on. The frozen `adduser.js` may hand over a
|
|
23
|
+
handle it could not resolve itself.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **A username lookup can no longer masquerade as a dead session.** Both
|
|
28
|
+
`usernameinfo` and `users/search` are `/api/v1` REST paths that Instagram now
|
|
29
|
+
answers with a 302 to the login page for a web session; the HTTP layer
|
|
30
|
+
classifies any 3xx as an auth failure, so `getInfoByUsername` (and `search`)
|
|
31
|
+
used to leak an auth-classified error. They now return `null` / `[]`, letting
|
|
32
|
+
callers fall back to the users learned from thread introspection.
|
|
33
|
+
- **A command that builds an empty reply no longer risks the session.** An empty
|
|
34
|
+
text send already returns `{ messageID: null, dropped: "empty" }`; the ported
|
|
35
|
+
`adduser.js` (now byte-identical to GoatBot-Pro) can produce an empty string
|
|
36
|
+
for a handle it cannot resolve, and the drop guard keeps that from reaching
|
|
37
|
+
Instagram's login-page redirect.
|
|
38
|
+
|
|
7
39
|
## [2.0.9] - 2026-10-11
|
|
8
40
|
|
|
9
41
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eryxenx/insta-chat",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": " Unofficial Instagram Chat API for Node.js | Build Instagram DM bots on personal accounts
|
|
3
|
+
"version": "2.0.10",
|
|
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",
|
|
7
7
|
"exports": {
|
|
@@ -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/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/unsend.test.js && node test/unsendGraphQL.test.js",
|
|
27
27
|
"build": "node scripts/build.js"
|
|
28
28
|
},
|
|
29
29
|
"keywords": [
|
package/src/app/InstaClient.js
CHANGED
|
@@ -39,6 +39,10 @@ class InstaClient {
|
|
|
39
39
|
// lookups (usernameinfo / web_profile_info) for web sessions, so resolving a
|
|
40
40
|
// mentioned @handle must come from data the session already has.
|
|
41
41
|
this._userIndex = new Map();
|
|
42
|
+
// threadID -> participant igids, learned from thread introspection. Instagram
|
|
43
|
+
// inbox events carry no participant list, so frozen commands that read
|
|
44
|
+
// event.participantIDs (all, ban) are served from here by the normalizer.
|
|
45
|
+
this._threadMembers = new Map();
|
|
42
46
|
|
|
43
47
|
this._learnFromInbox();
|
|
44
48
|
loadApiMethods(this);
|
|
@@ -146,9 +150,25 @@ class InstaClient {
|
|
|
146
150
|
const users = (t.users || []).slice();
|
|
147
151
|
if (t.viewer) users.unshift(t.viewer);
|
|
148
152
|
for (const u of users) this._rememberUser(u);
|
|
153
|
+
const threadID = t.id != null ? String(t.id) : t.thread_id != null ? String(t.thread_id) : null;
|
|
154
|
+
if (threadID) this._threadMembers.set(threadID, users.map((u) => String(u.id || u.pk)).filter(Boolean));
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
// The igids of a thread's participants, from data already seen. Prefers the
|
|
159
|
+
// native poller's latest inbox snapshot (fresh every poll) and falls back to
|
|
160
|
+
// the thread introspection the FCA layer learned. Used to fill
|
|
161
|
+
// event.participantIDs for commands ported from Facebook, whose MQTT events
|
|
162
|
+
// carried the member list but Instagram's do not.
|
|
163
|
+
getThreadParticipantIDs(threadID) {
|
|
164
|
+
if (this.api && typeof this.api.getThreadMemberIDs === "function") {
|
|
165
|
+
const live = this.api.getThreadMemberIDs(threadID);
|
|
166
|
+
if (live.length) return live;
|
|
167
|
+
}
|
|
168
|
+
const members = this._threadMembers.get(String(threadID));
|
|
169
|
+
return Array.isArray(members) ? members.slice() : [];
|
|
170
|
+
}
|
|
171
|
+
|
|
152
172
|
// Indexes one Instagram user record (inbox/thread shape: id, username,
|
|
153
173
|
// full_name, interop_messaging_user_fbid, profile_pic_url) under every key a
|
|
154
174
|
// command may look it up by.
|
|
@@ -342,16 +362,44 @@ class InstaClient {
|
|
|
342
362
|
async changeAvatar() { return false; }
|
|
343
363
|
|
|
344
364
|
// Instagram group DMs support adding/removing participants. FCA passes
|
|
345
|
-
// (userID, threadID); the API takes the reverse.
|
|
346
|
-
//
|
|
365
|
+
// (userID, threadID); the API takes the reverse. Instagram's endpoint only
|
|
366
|
+
// accepts a numeric igid, but frozen commands (adduser.js) may hand over a
|
|
367
|
+
// username or @handle (they resolve numeric ids themselves; only a handle can
|
|
368
|
+
// reach here unresolved). Resolve it against data already learned, and refuse
|
|
369
|
+
// rather than send a non-numeric id the endpoint would 4xx on.
|
|
370
|
+
async _resolveUserKey(userID) {
|
|
371
|
+
const key = String(userID).replace(/^@/, "").trim();
|
|
372
|
+
if (/^\d+$/.test(key)) return key;
|
|
373
|
+
const known = this.getUserByUsername(key);
|
|
374
|
+
if (known) {
|
|
375
|
+
const id = known.id || known.pk || known.interop_messaging_user_fbid;
|
|
376
|
+
if (id != null) return String(id);
|
|
377
|
+
}
|
|
378
|
+
if (typeof this.api.getUserInfoByUsername === "function") {
|
|
379
|
+
try {
|
|
380
|
+
const u = await this.api.getUserInfoByUsername(key);
|
|
381
|
+
if (u) {
|
|
382
|
+
this._rememberUser(u);
|
|
383
|
+
const id = u.id || u.pk || u.interop_messaging_user_fbid;
|
|
384
|
+
if (id != null) return String(id);
|
|
385
|
+
}
|
|
386
|
+
} catch (_) {}
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
347
391
|
async addUserToGroup(userID, threadID) {
|
|
348
392
|
if (typeof this.api.addUserToGroup !== "function") return false;
|
|
349
|
-
|
|
393
|
+
const uid = await this._resolveUserKey(userID);
|
|
394
|
+
if (!uid) return false;
|
|
395
|
+
return this.api.addUserToGroup(threadID, uid);
|
|
350
396
|
}
|
|
351
397
|
|
|
352
398
|
async removeUserFromGroup(userID, threadID) {
|
|
353
399
|
if (typeof this.api.removeUserFromGroup !== "function") return false;
|
|
354
|
-
|
|
400
|
+
const uid = await this._resolveUserKey(userID);
|
|
401
|
+
if (!uid) return false;
|
|
402
|
+
return this.api.removeUserFromGroup(threadID, uid);
|
|
355
403
|
}
|
|
356
404
|
}
|
|
357
405
|
|
|
@@ -93,6 +93,14 @@ class EventNormalizer {
|
|
|
93
93
|
messageID: String(raw.messageID),
|
|
94
94
|
body,
|
|
95
95
|
senderName,
|
|
96
|
+
// Facebook MQTT events carried the member list; Instagram's do not. Frozen
|
|
97
|
+
// commands (all.js, ban.js) read event.participantIDs, so fill it from the
|
|
98
|
+
// members the client learned from thread introspection. Undefined when
|
|
99
|
+
// unknown — the same shape as upstream, so `|| []` guards behave.
|
|
100
|
+
participantIDs:
|
|
101
|
+
typeof this.client.getThreadParticipantIDs === "function"
|
|
102
|
+
? this.client.getThreadParticipantIDs(threadID)
|
|
103
|
+
: undefined,
|
|
96
104
|
mentions: body.includes("@") ? await this._resolveMentions(body) : {},
|
|
97
105
|
attachments: (raw.attachments || []).map((a) => ({
|
|
98
106
|
type: a.type,
|
package/src/methods/inbox.js
CHANGED
|
@@ -35,6 +35,10 @@ class InboxMethods {
|
|
|
35
35
|
this._seen = new Map();
|
|
36
36
|
this._pollTimer = null;
|
|
37
37
|
this._selfFbid = null;
|
|
38
|
+
// threadID -> participant igids from the latest inbox snapshot. The inbox
|
|
39
|
+
// event carries no participant list, so the FCA layer reads it from here to
|
|
40
|
+
// fill event.participantIDs for commands ported from Facebook (all, ban).
|
|
41
|
+
this._members = new Map();
|
|
38
42
|
// threadID -> { title, image, members:Set, adminIDs:Set, nicknames:{} }
|
|
39
43
|
this._threadState = new Map();
|
|
40
44
|
this._statePrimed = false;
|
|
@@ -58,9 +62,19 @@ class InboxMethods {
|
|
|
58
62
|
const fbid = t.viewer?.interop_messaging_user_fbid;
|
|
59
63
|
if (fbid && !this._selfFbid) this._selfFbid = String(fbid);
|
|
60
64
|
}
|
|
65
|
+
for (const t of threads) {
|
|
66
|
+
const tid = String(t.id || t.thread_id);
|
|
67
|
+
if (tid) this._members.set(tid, (t.users || []).map((u) => String(u.id || u.pk)).filter(Boolean));
|
|
68
|
+
}
|
|
61
69
|
return { threads };
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
// Participant igids for a thread, from the latest inbox snapshot.
|
|
73
|
+
getThreadMemberIDs(threadID) {
|
|
74
|
+
const members = this._members.get(String(threadID));
|
|
75
|
+
return Array.isArray(members) ? members.slice() : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
// The Polaris inbox returns messages under thread.slide_messages as a Relay
|
|
65
79
|
// connection ({ edges: [{ node }] }); the node carries sender_fbid,
|
|
66
80
|
// content_type, text_body and content. Older captures used thread.items, so
|
package/src/methods/user.js
CHANGED
|
@@ -18,21 +18,35 @@ class UserMethods {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async getInfoByUsername(username) {
|
|
21
|
+
// Both endpoints are mobile/web REST paths under /api/v1, which Instagram
|
|
22
|
+
// now answers with a 302 to the login page for a web session (the HTTP
|
|
23
|
+
// layer classifies any 3xx as an auth failure). A username lookup must not
|
|
24
|
+
// leak an auth-classified error — a command reading a handle would then look
|
|
25
|
+
// like a dead session. Return null instead; callers fall back to the users
|
|
26
|
+
// the client already learned from thread introspection.
|
|
21
27
|
try {
|
|
22
28
|
const res = await this.http.get(EP.USER_BY_USERNAME(username));
|
|
23
29
|
if (res?.user) return res.user;
|
|
24
30
|
} catch (_) {}
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
try {
|
|
33
|
+
const res = await this.http.get(EP.USER_SEARCH(username, 30));
|
|
34
|
+
const match = (res?.users || []).find(
|
|
35
|
+
(u) => u.username?.toLowerCase() === username.toLowerCase()
|
|
36
|
+
);
|
|
37
|
+
return match || null;
|
|
38
|
+
} catch (_) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
async search(query, limit = 20) {
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
try {
|
|
45
|
+
const res = await this.http.get(EP.USER_SEARCH(query, limit));
|
|
46
|
+
return res?.users || [];
|
|
47
|
+
} catch (_) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
|