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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tanvir143",
3
- "version": "1.0.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",
@@ -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: jarCookies.map(c => `${c.key}=${c.value}`).join('; '),
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
- return {
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
- const response = await this.http.postForm(
153
- 'https://www.instagram.com/api/v1/direct_v2/threads/broadcast/text/',
154
- form
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
- if (response.status === 'ok') return this.extractMessageInfo(response, threadID, clientContext);
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
- return new Error(`Instagram API Error: ${data.message}`);
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
- return new Error(`HTTP Error ${error.response.status}: ${error.response.statusText}`);
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
  }
@@ -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
- });
@@ -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
- });