@eryxenx/insta-chat 2.0.3 → 2.0.5
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 +30 -0
- package/package.json +2 -2
- package/src/methods/media.js +17 -11
- package/src/methods/message.js +9 -0
- package/src/utils/antiSuspension.js +18 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ 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.5] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Empty text send logged the account out.** `MessageMethods.send()` now drops
|
|
12
|
+
an empty (or whitespace-only) body instead of firing an
|
|
13
|
+
`IGDirectTextSendMutation` with an empty `sensitive_string_value`. Instagram
|
|
14
|
+
answers that with a 302 to the login page, which the HTTP layer classifies as
|
|
15
|
+
`IG_SESSION_INVALID`, so a command that built an empty reply (for example
|
|
16
|
+
`adduser` handed a username it could not resolve) made the poller stop and
|
|
17
|
+
report the session as dead. The dropped send returns
|
|
18
|
+
`{ messageID: null, dropped: "empty" }`.
|
|
19
|
+
|
|
20
|
+
## [2.0.4] - 2026-10-10
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Captioned media lagged behind its text.** A message carrying both text and
|
|
25
|
+
an attachment uploads the media first, then commits the caption and the media
|
|
26
|
+
back-to-back, so the text and the image land together instead of the image
|
|
27
|
+
trailing by the whole upload time. Instagram's DM media mutation has no caption
|
|
28
|
+
field, so caption + attachment is still two messages; this only removes the
|
|
29
|
+
perceived gap after the caption.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `antiSuspension.waitForTurn(threadID, { immediate })`: an immediate turn that
|
|
34
|
+
still counts against the volume caps but skips the inter-message gap, so a
|
|
35
|
+
captioned attachment can follow its caption at once.
|
|
36
|
+
|
|
7
37
|
## [1.0.12] - 2026-10-10
|
|
8
38
|
|
|
9
39
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eryxenx/insta-chat",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": " Unofficial Instagram Chat API for Node.js | Build Instagram DM bots on personal accounts \u2014 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",
|
|
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",
|
|
27
27
|
"build": "node scripts/build.js"
|
|
28
28
|
},
|
|
29
29
|
"keywords": [
|
package/src/methods/media.js
CHANGED
|
@@ -95,22 +95,28 @@ class MediaMethods {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
async _send(threadID, uploadOptions, options) {
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
await globalAntiSuspension.waitForTurn(threadID);
|
|
98
|
+
// Instagram's DM media mutation has no caption field, so an attachment with
|
|
99
|
+
// text is necessarily two messages. The recipient-visible contract is that
|
|
100
|
+
// the caption reaches Instagram before the media (so the text renders on top
|
|
101
|
+
// and the image underneath — what GoatBot-Pro commands intend).
|
|
102
|
+
//
|
|
103
|
+
// The upload is by far the slow part of a media send. If the caption is
|
|
104
|
+
// committed while the upload is still in flight, the text appears at once
|
|
105
|
+
// and the image arrives a couple of seconds later on its own. Doing the
|
|
106
|
+
// upload first and then committing caption + media back-to-back makes the
|
|
107
|
+
// two land together, which is what a person perceives as "one message".
|
|
109
108
|
const metadata = await this._upload(threadID, uploadOptions);
|
|
110
109
|
const fbid = metadata.fbid || metadata.video_id || metadata.audio_id;
|
|
111
110
|
if (!fbid) throw new Error(`Upload returned no media id: ${JSON.stringify(metadata)}`);
|
|
112
111
|
let result;
|
|
113
112
|
try {
|
|
113
|
+
if (options.text) {
|
|
114
|
+
await globalAntiSuspension.waitForTurn(threadID);
|
|
115
|
+
await this._sendCaption(threadID, options.text);
|
|
116
|
+
}
|
|
117
|
+
// Count the media against the same pacing/volume budget, but with no gap
|
|
118
|
+
// after a caption so the image immediately follows the text.
|
|
119
|
+
await globalAntiSuspension.waitForTurn(threadID, { immediate: !!options.text });
|
|
114
120
|
result = await this._commitMedia(threadID, fbid, options);
|
|
115
121
|
} catch (error) {
|
|
116
122
|
globalAntiSuspension.noteSendError(error);
|
package/src/methods/message.js
CHANGED
|
@@ -27,6 +27,15 @@ class MessageMethods {
|
|
|
27
27
|
// (doc_id 26911679871773184). Passing it anywhere else sets a pointer without
|
|
28
28
|
// rendering the quote bubble.
|
|
29
29
|
async send(text, threadID, replyToItemId = null) {
|
|
30
|
+
// Instagram rejects an empty text send (IGDirectTextSendMutation with an
|
|
31
|
+
// empty `sensitive_string_value`) with a 302 to the login page. The HTTP
|
|
32
|
+
// layer classifies any 3xx as an auth failure, so emitting one was enough to
|
|
33
|
+
// make the poller declare the session dead. Commands that build a reply from
|
|
34
|
+
// resolvable input (e.g. adduser given an unresolvable username) can end up
|
|
35
|
+
// with an empty string; drop it rather than send it.
|
|
36
|
+
if (text === undefined || text === null || String(text).trim() === "") {
|
|
37
|
+
return { messageID: null, threadID: String(threadID), dropped: "empty" };
|
|
38
|
+
}
|
|
30
39
|
// Pace outbound traffic so a personal account does not look scripted.
|
|
31
40
|
await globalAntiSuspension.waitForTurn(threadID);
|
|
32
41
|
const replyId =
|
|
@@ -204,19 +204,34 @@ class SendPacer extends EventEmitter {
|
|
|
204
204
|
return true;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
waitForTurn(threadID) {
|
|
207
|
+
waitForTurn(threadID, opts) {
|
|
208
208
|
if (!this.enabled) return Promise.resolve();
|
|
209
|
-
const run = this._queue.then(() => this._turn(threadID));
|
|
209
|
+
const run = this._queue.then(() => this._turn(threadID, opts));
|
|
210
210
|
this._queue = run.catch(() => {});
|
|
211
211
|
return run.catch(() => {});
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
async _turn(threadID) {
|
|
214
|
+
async _turn(threadID, opts = {}) {
|
|
215
215
|
this._rollWindows();
|
|
216
216
|
this._decayStrikes();
|
|
217
217
|
this._prune();
|
|
218
218
|
|
|
219
219
|
const key = String(threadID);
|
|
220
|
+
|
|
221
|
+
// A captioned media is one logical message. Instagram's own client sends the
|
|
222
|
+
// caption text and the attachment back-to-back, not seconds apart, so the
|
|
223
|
+
// media pass asks for an immediate turn: still counted against the volume
|
|
224
|
+
// caps (so the hourly/daily budget stays accurate) but without the
|
|
225
|
+
// inter-message gap that made the image trail the text.
|
|
226
|
+
if (opts && opts.immediate) {
|
|
227
|
+
const stamp = Date.now();
|
|
228
|
+
this.lastSendAt = stamp;
|
|
229
|
+
this.perThreadLastSendAt.set(key, stamp);
|
|
230
|
+
this.hourSendCount++;
|
|
231
|
+
this.daySendCount++;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
220
235
|
const cooling = this.isCoolingDown();
|
|
221
236
|
const overCap = this.hourSendCount >= this._hourlyCap() || this.daySendCount >= this.dailyCap;
|
|
222
237
|
|