tanvir143 1.0.0 → 1.0.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/package.json +1 -1
- package/src/instagramChat.js +14 -1
- package/src/methods/sendMessage.js +61 -7
- package/src/utils/http.js +14 -2
- package/test/api-surface.test.js +0 -73
- package/test/index-smoke.test.js +0 -72
- package/test/static-contract.test.js +0 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tanvir143",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Bot-first Instagram Chat API with MQTT realtime messaging, session persistence, media, thread management, and InstaBot/FCA-compatible APIs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/src/instagramChat.js
CHANGED
|
@@ -143,6 +143,19 @@ class InstagramChatAPI extends EventEmitter {
|
|
|
143
143
|
const jarCookies = this.http.jar.serializeSync().cookies;
|
|
144
144
|
const find = (key) => jarCookies.find(c => c.key === key)?.value;
|
|
145
145
|
|
|
146
|
+
// Build the MQTT Cookie header from the same authenticated Instagram
|
|
147
|
+
// session used by HttpClient. Keep only Instagram cookies so a browser
|
|
148
|
+
// export containing unrelated domains cannot contaminate the handshake.
|
|
149
|
+
const instagramCookies = jarCookies.filter((cookie) => {
|
|
150
|
+
const domain = String(cookie.domain || '').toLowerCase();
|
|
151
|
+
return domain === 'instagram.com' ||
|
|
152
|
+
domain.endsWith('.instagram.com');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const cookieHeader = instagramCookies
|
|
156
|
+
.map(c => `${c.key}=${c.value}`)
|
|
157
|
+
.join('; ');
|
|
158
|
+
|
|
146
159
|
this.mqtt = new InstagramMQTTClient({
|
|
147
160
|
deviceId: this.deviceId,
|
|
148
161
|
phoneId: this.phoneId,
|
|
@@ -152,7 +165,7 @@ class InstagramChatAPI extends EventEmitter {
|
|
|
152
165
|
sessionId: find('sessionid'),
|
|
153
166
|
csrftoken: find('csrftoken'),
|
|
154
167
|
igDid: find('ig_did'),
|
|
155
|
-
cookies:
|
|
168
|
+
cookies: cookieHeader,
|
|
156
169
|
userAgent: this.http.userAgent,
|
|
157
170
|
http: this.http
|
|
158
171
|
});
|
|
@@ -112,7 +112,14 @@ class SendMessage {
|
|
|
112
112
|
if (!csrfToken) throw new Error('Missing csrftoken cookie');
|
|
113
113
|
|
|
114
114
|
const mutationToken = CryptoUtils.generateUUID();
|
|
115
|
-
|
|
115
|
+
|
|
116
|
+
// Instagram's private Direct endpoint is stricter than the inbox
|
|
117
|
+
// endpoint: keep the authenticated user id in the form whenever the
|
|
118
|
+
// ds_user_id cookie is available. This also keeps cookie-login and
|
|
119
|
+
// password-login sessions consistent.
|
|
120
|
+
const userId = this.http.getCookieValue('ds_user_id');
|
|
121
|
+
|
|
122
|
+
const result = {
|
|
116
123
|
action: 'send_item',
|
|
117
124
|
[recipientField]: recipientValue,
|
|
118
125
|
client_context: mutationToken,
|
|
@@ -121,8 +128,19 @@ class SendMessage {
|
|
|
121
128
|
device_id: this.deviceId,
|
|
122
129
|
_csrftoken: csrfToken,
|
|
123
130
|
_uuid: this.uuid,
|
|
131
|
+
use_unified_inbox: 'true',
|
|
124
132
|
...form
|
|
125
133
|
};
|
|
134
|
+
|
|
135
|
+
if (userId) result._uid = userId;
|
|
136
|
+
|
|
137
|
+
// The private endpoint historically expects a zero thread id when
|
|
138
|
+
// recipient_users is used to create/open a DM thread.
|
|
139
|
+
if (recipientField === 'recipient_users' && result.thread_ids === undefined) {
|
|
140
|
+
result.thread_ids = '["0"]';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
extractMessageInfo(response, fallbackThreadID, clientContext) {
|
|
@@ -149,13 +167,49 @@ class SendMessage {
|
|
|
149
167
|
const form = this.buildBroadcastForm(recipientType, recipient || JSON.stringify([threadID]), extra);
|
|
150
168
|
const clientContext = form.client_context;
|
|
151
169
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
170
|
+
// Direct-message POSTs are handled on Instagram's mobile API host more
|
|
171
|
+
// reliably than the web host. Keep a web-host fallback because Instagram
|
|
172
|
+
// can route accounts differently.
|
|
173
|
+
const endpoints = [
|
|
174
|
+
'https://i.instagram.com/api/v1/direct_v2/threads/broadcast/text/',
|
|
175
|
+
'https://www.instagram.com/api/v1/direct_v2/threads/broadcast/text/'
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
let lastError = null;
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
181
|
+
try {
|
|
182
|
+
const response = await this.http.postForm(endpoints[i], form);
|
|
183
|
+
|
|
184
|
+
if (response?.status === 'ok') {
|
|
185
|
+
return this.extractMessageInfo(response, threadID, clientContext);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const error = new Error(response?.message || 'Failed to send text message');
|
|
189
|
+
error.responseData = response;
|
|
190
|
+
lastError = error;
|
|
191
|
+
|
|
192
|
+
// If Instagram returned a normal API response rather than throwing,
|
|
193
|
+
// only try the fallback host for an authentication/route failure.
|
|
194
|
+
const message = String(response?.message || '').toLowerCase();
|
|
195
|
+
if (!message.includes('login_required') && i === 0) break;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
lastError = error;
|
|
198
|
+
|
|
199
|
+
const message = String(error?.message || '').toLowerCase();
|
|
200
|
+
const retryOnFallback =
|
|
201
|
+
message.includes('login_required') ||
|
|
202
|
+
message.includes('http error 401') ||
|
|
203
|
+
message.includes('http error 403') ||
|
|
204
|
+
message.includes('http error 404');
|
|
205
|
+
|
|
206
|
+
if (!retryOnFallback || i === endpoints.length - 1) {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
156
211
|
|
|
157
|
-
|
|
158
|
-
throw new Error(response.message || 'Failed to send text message');
|
|
212
|
+
throw lastError || new Error('Failed to send text message');
|
|
159
213
|
}
|
|
160
214
|
|
|
161
215
|
// Route a single attachment (local path or remote URL) to the right sendMedia method
|
package/src/utils/http.js
CHANGED
|
@@ -337,9 +337,21 @@ class HttpClient extends EventEmitter {
|
|
|
337
337
|
console.log('Response data:', JSON.stringify(data, null, 2));
|
|
338
338
|
}
|
|
339
339
|
if (data && data.message) {
|
|
340
|
-
|
|
340
|
+
const apiError = new Error(`Instagram API Error: ${data.message}`);
|
|
341
|
+
apiError.status = error.response.status;
|
|
342
|
+
apiError.code = data.error_type || data.error_code || null;
|
|
343
|
+
apiError.responseData = data;
|
|
344
|
+
apiError.url = error.response.config?.url || error.config?.url;
|
|
345
|
+
return apiError;
|
|
341
346
|
}
|
|
342
|
-
|
|
347
|
+
|
|
348
|
+
const httpError = new Error(
|
|
349
|
+
`HTTP Error ${error.response.status}: ${error.response.statusText}`
|
|
350
|
+
);
|
|
351
|
+
httpError.status = error.response.status;
|
|
352
|
+
httpError.responseData = data;
|
|
353
|
+
httpError.url = error.response.config?.url || error.config?.url;
|
|
354
|
+
return httpError;
|
|
343
355
|
}
|
|
344
356
|
return error;
|
|
345
357
|
}
|
package/test/api-surface.test.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Maintainer: Tanvir Ahmed
|
|
5
|
-
* WhatsApp: wa.me/+8801750079773
|
|
6
|
-
* GitHub: www.github.com/143tanvir
|
|
7
|
-
* Instagram: @ig.tanvir_ahmed
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const test = require('node:test');
|
|
11
|
-
const assert = require('node:assert/strict');
|
|
12
|
-
const { createCompatibilityApi } = require('../src/compatibility');
|
|
13
|
-
|
|
14
|
-
function fakeRaw() {
|
|
15
|
-
const methods = [
|
|
16
|
-
'getCurrentUserID','listen','stopListening','on','off','once','removeListener','removeAllListeners',
|
|
17
|
-
'sendMessage','sendDirectMessage','replyToMessage','unsendMessage','sendPhoto','sendVideo','sendVoice','sendGIF',
|
|
18
|
-
'sendPhotoFromUrl','sendVideoFromUrl','sendVoiceFromUrl','sendReaction','removeReaction','getInbox','getThreadInfo',
|
|
19
|
-
'getThreadHistory','getPendingRequests','searchThreads','deleteThread','approveRequest','declineRequest','muteThread',
|
|
20
|
-
'unmuteThread','changeThreadTitle','changeNickname','sendTypingIndicator','stopTypingIndicator','markAsRead','markAsUnread',
|
|
21
|
-
'getUserInfo','getUserInfoByUsername','searchUsers','searchReels','getHealth','getSession','loadSession','logout',
|
|
22
|
-
'initDatabase','saveMessageToDB','getMessagesFromDB','scheduleTask','stopTask','setOptions'
|
|
23
|
-
];
|
|
24
|
-
const raw = {};
|
|
25
|
-
for (const name of methods) raw[name] = (...args) => {
|
|
26
|
-
if (name === 'getCurrentUserID') return { userId: '123456789', username: 'test' };
|
|
27
|
-
if (name === 'getInbox') return { threads: [] };
|
|
28
|
-
if (name === 'getSession') return { cookies: [] };
|
|
29
|
-
if (name === 'getHealth') return { authenticated: true };
|
|
30
|
-
return { ok: true, name, args };
|
|
31
|
-
};
|
|
32
|
-
raw.on = raw.off = raw.once = raw.removeListener = raw.removeAllListeners = () => raw;
|
|
33
|
-
raw.stories = { getUserStories: async () => [], getFeedStories: async () => [], react: async () => ({ok:true}), reply: async () => ({ok:true}) };
|
|
34
|
-
raw.live = { getLiveFeed: async () => [], sendComment: async () => ({ok:true}), sendHeart: async () => ({ok:true}) };
|
|
35
|
-
raw.search = { hashtags: async () => [], places: async () => [], audio: async () => [] };
|
|
36
|
-
return raw;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
test('exposes InstaBot/FCA compatibility surface', async () => {
|
|
40
|
-
const api = createCompatibilityApi(fakeRaw());
|
|
41
|
-
const required = [
|
|
42
|
-
'getCurrentUserID','listen','listenMqtt','stopListening',
|
|
43
|
-
'sendMessage','sendImage','sendAudio','sendVideo','sendPhoto','sendVoice','sendGIF',
|
|
44
|
-
'sendPhotoFromUrl','sendVideoFromUrl','sendVoiceFromUrl',
|
|
45
|
-
'getThreadList','getInbox','getThreadInfo','getThreadHistory','getPendingRequests',
|
|
46
|
-
'searchThreads','approveRequest','declineRequest','setTitle','changeNickname',
|
|
47
|
-
'sendTypingIndicator','stopTypingIndicator','markAsRead','markAsUnread','markAsDelivered',
|
|
48
|
-
'getUserInfo','getUserInfoByUsername','searchUsers',
|
|
49
|
-
'sendReaction','setMessageReaction','removeReaction','unsendMessage',
|
|
50
|
-
'musicSearch','sendMusic','sendTextEffect','sendAvatarTextEffect',
|
|
51
|
-
'getUserStories','getFeedStories','reactToStory','replyToStory',
|
|
52
|
-
'getLiveFeed','sendLiveComment','sendLiveHeart',
|
|
53
|
-
'searchHashtags','searchPlaces','searchReels',
|
|
54
|
-
'getSession','getAppState','loadSession','saveSession','loadSessionFromFile','logout',
|
|
55
|
-
'getHealth','initDatabase','scheduleTask','setOptions'
|
|
56
|
-
];
|
|
57
|
-
for (const key of required) assert.equal(typeof api[key], 'function', key);
|
|
58
|
-
|
|
59
|
-
assert.equal(await api.getCurrentUserID(), '123456789');
|
|
60
|
-
const list = await api.getThreadList(10);
|
|
61
|
-
assert.deepEqual(list, []);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('supports callback style without losing Promise return', async () => {
|
|
65
|
-
const api = createCompatibilityApi(fakeRaw());
|
|
66
|
-
let called = false;
|
|
67
|
-
const promise = api.getUserInfo('42', (err, result) => {
|
|
68
|
-
assert.ifError(err);
|
|
69
|
-
called = !!result;
|
|
70
|
-
});
|
|
71
|
-
await promise;
|
|
72
|
-
assert.equal(called, true);
|
|
73
|
-
});
|
package/test/index-smoke.test.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Maintainer: Tanvir Ahmed
|
|
5
|
-
* WhatsApp: wa.me/+8801750079773
|
|
6
|
-
* GitHub: www.github.com/143tanvir
|
|
7
|
-
* Instagram: @ig.tanvir_ahmed
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const test = require('node:test');
|
|
11
|
-
const assert = require('node:assert/strict');
|
|
12
|
-
const Module = require('node:module');
|
|
13
|
-
const path = require('node:path');
|
|
14
|
-
|
|
15
|
-
function freshIndex() {
|
|
16
|
-
const target = require.resolve('../index.js');
|
|
17
|
-
delete require.cache[target];
|
|
18
|
-
|
|
19
|
-
class FakeClient {
|
|
20
|
-
constructor(options) {
|
|
21
|
-
this.options = options;
|
|
22
|
-
this.authenticated = true;
|
|
23
|
-
this.userId = '123456789';
|
|
24
|
-
this.username = 'test';
|
|
25
|
-
this.http = { jar: { serializeSync: () => ({ cookies: [] }) } };
|
|
26
|
-
}
|
|
27
|
-
async loginWithCookies(cookies) { this.cookies = cookies; return { success: true, userID: '123456789', username: 'test' }; }
|
|
28
|
-
async login() { return { success: true, userID: '123456789', username: 'test' }; }
|
|
29
|
-
getSession() { return { userId: this.userId, username: this.username, cookies: { cookies: [] } }; }
|
|
30
|
-
serialize() { return this.getSession(); }
|
|
31
|
-
async loadSession() {}
|
|
32
|
-
async deserialize() {}
|
|
33
|
-
getCurrentUserID() { return { userId: this.userId, username: this.username }; }
|
|
34
|
-
listen() {}
|
|
35
|
-
stopListening() {}
|
|
36
|
-
getHealth() { return { authenticated: true }; }
|
|
37
|
-
logout() { return Promise.resolve({ success: true }); }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const originalLoad = Module._load;
|
|
41
|
-
Module._load = function(request, parent, isMain) {
|
|
42
|
-
const resolvedParent = parent && parent.filename ? path.normalize(parent.filename) : '';
|
|
43
|
-
if (request === './src/instagramChat' && resolvedParent.endsWith(path.normalize('/index.js'))) return FakeClient;
|
|
44
|
-
if (request === './src/utils/cookies' && resolvedParent.endsWith(path.normalize('/index.js'))) {
|
|
45
|
-
return { parse: x => x, parseHeaderString: () => [], parseNetscape: () => [], parseJSON: () => [] };
|
|
46
|
-
}
|
|
47
|
-
if (request === './src/utils/setOptions' && resolvedParent.endsWith(path.normalize('/index.js'))) {
|
|
48
|
-
return { setOptions: x => x, getOptions: () => ({}), resetOptions: () => ({}), validateOptions: () => ({ valid: true, errors: [], warnings: [] }), applyOptions: (c) => c };
|
|
49
|
-
}
|
|
50
|
-
return originalLoad.call(this, request, parent, isMain);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
return require(target);
|
|
55
|
-
} finally {
|
|
56
|
-
Module._load = originalLoad;
|
|
57
|
-
delete require.cache[target];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
test('index login accepts bot-style { cookies, options } and returns compatibility API', async () => {
|
|
62
|
-
const { login } = freshIndex();
|
|
63
|
-
const api = await login({
|
|
64
|
-
cookies: [{ name: 'sessionid', value: 'x' }],
|
|
65
|
-
autoReconnect: true,
|
|
66
|
-
userAgent: 'test-agent'
|
|
67
|
-
});
|
|
68
|
-
assert.equal(typeof api.sendImage, 'function');
|
|
69
|
-
assert.equal(typeof api.listenMqtt, 'function');
|
|
70
|
-
assert.equal(typeof api.getThreadList, 'function');
|
|
71
|
-
assert.equal(api.version, '1.0.0');
|
|
72
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Maintainer: Tanvir Ahmed
|
|
5
|
-
* WhatsApp: wa.me/+8801750079773
|
|
6
|
-
* GitHub: www.github.com/143tanvir
|
|
7
|
-
* Instagram: @ig.tanvir_ahmed
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const test = require('node:test');
|
|
11
|
-
const assert = require('node:assert/strict');
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
|
|
15
|
-
const root = path.resolve(__dirname, '..');
|
|
16
|
-
const packageJson = require(path.join(root, 'package.json'));
|
|
17
|
-
|
|
18
|
-
test('package metadata is publishable and correctly renamed', () => {
|
|
19
|
-
assert.equal(packageJson.name, 'tanvir143');
|
|
20
|
-
assert.match(packageJson.version, /^\d+\.\d+\.\d+$/);
|
|
21
|
-
assert.equal(packageJson.main, 'index.js');
|
|
22
|
-
assert.equal(packageJson.types, 'index.d.ts');
|
|
23
|
-
assert.equal(packageJson.publishConfig.access, 'public');
|
|
24
|
-
assert.equal(packageJson.license, 'MIT');
|
|
25
|
-
assert.ok(packageJson.files.includes('src/'));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('required project identifiers are gone from executable/docs metadata', () => {
|
|
29
|
-
const scan = [];
|
|
30
|
-
const files = [
|
|
31
|
-
'index.js','index.d.ts','README.md','package.json',
|
|
32
|
-
...fs.readdirSync(path.join(root, 'src')).map(f => path.join('src', f))
|
|
33
|
-
].filter(f => fs.existsSync(path.join(root, f)) && fs.statSync(path.join(root, f)).isFile());
|
|
34
|
-
for (const file of files) scan.push(fs.readFileSync(path.join(root, file), 'utf8'));
|
|
35
|
-
const text = scan.join('\n');
|
|
36
|
-
assert.equal(text.includes('@cexy/wonica'), false);
|
|
37
|
-
assert.equal(text.includes('bdrakib123'), false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('all source JS files pass Node syntax validation via parser', () => {
|
|
41
|
-
const sourceFiles = [];
|
|
42
|
-
function walk(dir) {
|
|
43
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
-
const full = path.join(dir, entry.name);
|
|
45
|
-
if (entry.isDirectory()) walk(full);
|
|
46
|
-
else if (entry.name.endsWith('.js')) sourceFiles.push(full);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
walk(path.join(root, 'src'));
|
|
50
|
-
assert.ok(sourceFiles.length > 0);
|
|
51
|
-
});
|