instauto 9.1.9 → 9.1.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.
Files changed (3) hide show
  1. package/README.md +2 -3
  2. package/package.json +1 -1
  3. package/src/index.js +94 -19
package/README.md CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  [![Discord](https://img.shields.io/discord/986052713425027072)](https://discord.gg/Rh3KT9zyhj) [![PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/mifino/usd)
4
4
 
5
- instauto is an Instagram automation/bot library written in modern, clean javascript using Google's Puppeteer. Goal is to be very easy to set up, use, and extend, and obey instagram's limits. Heavily inspired by [InstaPy](https://github.com/timgrossmann/InstaPy), but I thought it was way too heavy and hard to setup.
5
+ instauto is an Instagram automation/bot library (API) written in modern, clean javascript using Google's Puppeteer. Goal is to be very easy to set up, use, and extend, and obey instagram's limits. Heavily inspired by [InstaPy](https://github.com/timgrossmann/InstaPy), but I thought it was way too heavy and hard to setup.
6
6
 
7
- **NEW! 🎉**
8
- Now there is a GUI application for those who don't want to code: [SimpleInstaBot](https://mifi.github.io/SimpleInstaBot/)
7
+ There is also a GUI application for those who don't want to code: [SimpleInstaBot](https://mifi.github.io/SimpleInstaBot/)
9
8
 
10
9
 
11
10
  ## Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instauto",
3
- "version": "9.1.9",
3
+ "version": "9.1.10",
4
4
  "description": "Instagram automation library written in Node.js",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -16,6 +16,15 @@ function shuffleArray(arrayIn) {
16
16
  return array;
17
17
  }
18
18
 
19
+ // https://stackoverflow.com/questions/14822153/escape-single-quote-in-xpath-with-nokogiri
20
+ // example str: "That's mine", he said.
21
+ function escapeXpathStr(str) {
22
+ const parts = str.split("'").map((token) => `'${token}'`);
23
+ if (parts.length === 1) return `${parts[0]}`;
24
+ const str2 = parts.join(', "\'", ');
25
+ return `concat(${str2})`;
26
+ }
27
+
19
28
  const botWorkShiftHours = 16;
20
29
 
21
30
  const dayMs = 24 * 60 * 60 * 1000;
@@ -122,7 +131,7 @@ const Instauto = async (db, browser, options) => {
122
131
  const sleep = (ms, deviation = 1) => {
123
132
  let msWithDev = ((Math.random() * deviation) + 1) * ms;
124
133
  if (dryRun) msWithDev = Math.min(3000, msWithDev); // for dryRun, no need to wait so long
125
- logger.log('Waiting', Math.round(msWithDev / 1000), 'sec');
134
+ logger.log('Waiting', (msWithDev / 1000).toFixed(2), 'sec');
126
135
  return new Promise(resolve => setTimeout(resolve, msWithDev));
127
136
  };
128
137
 
@@ -178,8 +187,9 @@ const Instauto = async (db, browser, options) => {
178
187
  for (let attempt = 0; ; attempt += 1) {
179
188
  logger.log(`Goto ${url}`);
180
189
  const response = await gotoUrl(url);
181
- await sleep(2000);
182
190
  const status = response.status();
191
+ logger.log('Page loaded');
192
+ await sleep(2000);
183
193
 
184
194
  // https://www.reddit.com/r/Instagram/comments/kwrt0s/error_560/
185
195
  // https://github.com/mifi/instauto/issues/60
@@ -217,12 +227,14 @@ const Instauto = async (db, browser, options) => {
217
227
  }
218
228
 
219
229
  if (status === 200) {
230
+ // logger.log('Page returned 200 ☑️');
220
231
  // some pages return 200 but nothing there (I think deleted accounts)
221
232
  // https://github.com/mifi/SimpleInstaBot/issues/48
222
233
  // example: https://www.instagram.com/victorialarson__/
223
234
  // so we check if the page has the user's name on it
224
- const foundUsernameOnPage = await page.evaluate((u) => window.find(u), username);
225
- if (!foundUsernameOnPage) logger.warn(`Cannot find "${username}" on page`);
235
+ const elementHandles = await page.$x(`//body//main//*[contains(text(),${escapeXpathStr(username)})]`);
236
+ const foundUsernameOnPage = elementHandles.length > 0;
237
+ if (!foundUsernameOnPage) logger.warn(`Cannot find text "${username}" on page`);
226
238
  return foundUsernameOnPage;
227
239
  }
228
240
 
@@ -247,22 +259,85 @@ const Instauto = async (db, browser, options) => {
247
259
  return cachedUserData;
248
260
  }
249
261
 
262
+ async function getUserDataFromPage() {
263
+ // https://github.com/mifi/instauto/issues/115#issuecomment-1199335650
264
+ // to test in browser: document.getElementsByTagName('html')[0].innerHTML.split('\n');
265
+ try {
266
+ const body = await page.content();
267
+ for (let q of body.split(/\r?\n/)) {
268
+ if (q.includes('edge_followed_by')) {
269
+ // eslint-disable-next-line prefer-destructuring
270
+ q = q.split(',[],[')[1];
271
+ // eslint-disable-next-line prefer-destructuring
272
+ q = q.split(']]]')[0];
273
+ q = JSON.parse(q);
274
+ // eslint-disable-next-line no-underscore-dangle
275
+ q = q.data.__bbox.result.response;
276
+ q = q.replace(/\\/g, '');
277
+ q = JSON.parse(q);
278
+ return q.data.user;
279
+ }
280
+ }
281
+ } catch (err) {
282
+ logger.warn('Failed to get user data from page', err);
283
+ }
284
+ return undefined;
285
+ }
286
+
250
287
  // intercept special XHR network request that fetches user's data and store it in a cache
251
288
  // TODO fallback to DOM to get user ID if this request fails?
252
289
  // https://github.com/mifi/SimpleInstaBot/issues/125#issuecomment-1145354294
253
- const [foundResponse] = await Promise.all([
254
- page.waitForResponse((response) => {
255
- const request = response.request();
256
- return request.method() === 'GET' && new RegExp(`https:\\/\\/i\\.instagram\\.com\\/api\\/v1\\/users\\/web_profile_info\\/\\?username=${encodeURIComponent(username.toLowerCase())}`).test(request.url());
257
- }),
258
- navigateToUserWithCheck(username),
259
- // page.waitForNavigation({ waitUntil: 'networkidle0' }),
260
- ]);
261
-
262
- const json = JSON.parse(await foundResponse.text());
263
- const userData = json.data.user;
264
- userDataCache[username] = userData;
265
- return userData;
290
+ async function getUserDataFromInterceptedRequest() {
291
+ const t = setTimeout(async () => {
292
+ logger.log('Unable to intercept request, will send manually');
293
+ try {
294
+ await page.evaluate(async (username2) => {
295
+ const response = await window.fetch(`https://i.instagram.com/api/v1/users/web_profile_info/?username=${encodeURIComponent(username2.toLowerCase())}`, { mode: 'cors', credentials: 'include', headers: { 'x-ig-app-id': '936619743392459' } });
296
+ await response.json(); // else it will not finish the request
297
+ }, username);
298
+ // todo `https://i.instagram.com/api/v1/users/${userId}/info/`
299
+ // https://www.javafixing.com/2022/07/fixed-can-get-instagram-profile-picture.html?m=1
300
+ } catch (err) {
301
+ logger.error('Failed to manually send request', err);
302
+ }
303
+ }, 5000);
304
+
305
+ try {
306
+ const [foundResponse] = await Promise.all([
307
+ page.waitForResponse((response) => {
308
+ const request = response.request();
309
+ return request.method() === 'GET' && new RegExp(`https:\\/\\/i\\.instagram\\.com\\/api\\/v1\\/users\\/web_profile_info\\/\\?username=${encodeURIComponent(username.toLowerCase())}`).test(request.url());
310
+ }, { timeout: 30000 }),
311
+ navigateToUserWithCheck(username),
312
+ // page.waitForNavigation({ waitUntil: 'networkidle0' }),
313
+ ]);
314
+
315
+ const json = JSON.parse(await foundResponse.text());
316
+ return json.data.user;
317
+ } finally {
318
+ clearTimeout(t);
319
+ }
320
+ }
321
+
322
+ logger.log('Trying to get user data from HTML');
323
+
324
+ await navigateToUserWithCheck(username);
325
+ let userData = await getUserDataFromPage();
326
+ if (userData) {
327
+ userDataCache[username] = userData;
328
+ return userData;
329
+ }
330
+
331
+ logger.log('Need to intercept network request to get user data');
332
+
333
+ // works for old accounts only:
334
+ userData = await getUserDataFromInterceptedRequest();
335
+ if (userData) {
336
+ userDataCache[username] = userData;
337
+ return userData;
338
+ }
339
+
340
+ return undefined;
266
341
  }
267
342
 
268
343
  async function getPageJson() {
@@ -1031,7 +1106,7 @@ const Instauto = async (db, browser, options) => {
1031
1106
  return followsMe === false;
1032
1107
  }
1033
1108
 
1034
- await safelyUnfollowUserList(allFollowingGenerator, limit, condition);
1109
+ return safelyUnfollowUserList(allFollowingGenerator, limit, condition);
1035
1110
  }
1036
1111
 
1037
1112
  async function unfollowAllUnknown({ limit } = {}) {
@@ -1048,7 +1123,7 @@ const Instauto = async (db, browser, options) => {
1048
1123
  return true;
1049
1124
  }
1050
1125
 
1051
- await safelyUnfollowUserList(unfollowUsersGenerator, limit, condition);
1126
+ return safelyUnfollowUserList(unfollowUsersGenerator, limit, condition);
1052
1127
  }
1053
1128
 
1054
1129
  async function unfollowOldFollowed({ ageInDays, limit } = {}) {