instauto 9.1.1 → 9.1.4

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 +1 -1
  2. package/package.json +1 -1
  3. package/src/index.js +80 -67
package/README.md CHANGED
@@ -25,7 +25,7 @@ Now there is a GUI application for those who don't want to code: [SimpleInstaBot
25
25
 
26
26
  You can run this code for example once every day using cron or pm2 or similar
27
27
 
28
- See [index.js](https://github.com/mifi/instauto/blob/master/index.js) for available options.
28
+ See [index.js](https://github.com/mifi/instauto/blob/master/src/index.js) for available options.
29
29
 
30
30
  ## Supported functionality
31
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instauto",
3
- "version": "9.1.1",
3
+ "version": "9.1.4",
4
4
  "description": "Instagram automation library written in Node.js",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -58,6 +58,7 @@ const Instauto = async (db, browser, options) => {
58
58
  } = options;
59
59
 
60
60
  let myUsername = myUsernameIn;
61
+ const userDataCache = {};
61
62
 
62
63
  assert(cookiesPath);
63
64
  assert(db);
@@ -73,7 +74,6 @@ const Instauto = async (db, browser, options) => {
73
74
 
74
75
  // State
75
76
  let page;
76
- let graphqlUserMissing = false;
77
77
 
78
78
  async function takeScreenshot() {
79
79
  if (!screenshotOnError) return;
@@ -169,15 +169,20 @@ const Instauto = async (db, browser, options) => {
169
169
  }
170
170
 
171
171
  async function gotoWithRetry(url) {
172
+ const maxAttempts = 3;
172
173
  for (let attempt = 0; ; attempt += 1) {
173
174
  logger.log(`Goto ${url}`);
174
175
  const response = await page.goto(url);
175
- await sleep(1000);
176
+ await sleep(2000);
176
177
  const status = response.status();
177
178
 
178
179
  // https://www.reddit.com/r/Instagram/comments/kwrt0s/error_560/
179
180
  // https://github.com/mifi/instauto/issues/60
180
- if (![560, 429].includes(status) || attempt > 3) return status;
181
+ if (![560, 429].includes(status)) return status;
182
+
183
+ if (attempt > maxAttempts) {
184
+ throw new Error(`Navigate to user failed after ${maxAttempts} attempts, last status: ${status}`);
185
+ }
181
186
 
182
187
  logger.info(`Got ${status} - Retrying request later...`);
183
188
  if (status === 429) logger.warn('429 Too Many Requests could mean that Instagram suspects you\'re using a bot. You could try to use the Instagram Mobile app from the same IP for a few days first');
@@ -185,73 +190,78 @@ const Instauto = async (db, browser, options) => {
185
190
  }
186
191
  }
187
192
 
188
- async function safeGotoUser(url, checkPageForUsername) {
189
- const status = await gotoWithRetry(url);
190
- if (status === 200) {
191
- if (checkPageForUsername != null) {
192
- // some pages return 200 but nothing there (I think deleted accounts)
193
- // https://github.com/mifi/SimpleInstaBot/issues/48
194
- // example: https://www.instagram.com/victorialarson__/
195
- // so we check if the page has the user's name on it
196
- return page.evaluate((username) => window.find(username), checkPageForUsername);
197
- }
198
- return true;
199
- }
200
- if (status === 404) {
201
- logger.log('User not found');
202
- return false;
203
- }
204
- throw new Error(`Navigate to user failed with status ${status}`);
193
+ const getUserPageUrl = (username) => `${instagramBaseUrl}/${encodeURIComponent(username)}`;
194
+
195
+ function isAlreadyOnUserPage(username) {
196
+ const url = getUserPageUrl(username);
197
+ // optimization: already on URL? (ignore trailing slash)
198
+ return (page.url().replace(/\/$/, '') === url.replace(/\/$/, ''));
205
199
  }
206
200
 
207
201
  async function navigateToUser(username) {
208
- const url = `${instagramBaseUrl}/${encodeURIComponent(username)}`;
209
- if (page.url().replace(/\/$/, '') === url.replace(/\/$/, '')) return true; // optimization: already on URL? (ignore trailing slash)
202
+ if (isAlreadyOnUserPage(username)) return true;
203
+
210
204
  // logger.log('navigating from', page.url(), 'to', url);
211
205
  logger.log(`Navigating to user ${username}`);
212
- return safeGotoUser(url, username);
206
+
207
+ const url = getUserPageUrl(username);
208
+ const status = await gotoWithRetry(url);
209
+ if (status === 404) {
210
+ logger.warn('User page returned 404');
211
+ return false;
212
+ }
213
+
214
+ if (status === 200) {
215
+ // some pages return 200 but nothing there (I think deleted accounts)
216
+ // https://github.com/mifi/SimpleInstaBot/issues/48
217
+ // example: https://www.instagram.com/victorialarson__/
218
+ // so we check if the page has the user's name on it
219
+ const foundUsernameOnPage = await page.evaluate((u) => window.find(u), username);
220
+ if (!foundUsernameOnPage) logger.warn(`Cannot find "${username}" on page`);
221
+ return foundUsernameOnPage;
222
+ }
223
+
224
+ throw new Error(`Navigate to user failed with status ${status}`);
213
225
  }
214
226
 
215
227
  async function navigateToUserWithCheck(username) {
216
228
  if (!(await navigateToUser(username))) throw new Error('User not found');
217
229
  }
218
230
 
219
- async function getPageJson() {
220
- return JSON.parse(await (await (await page.$('pre')).getProperty('textContent')).jsonValue());
221
- }
222
-
223
231
  async function navigateToUserAndGetData(username) {
224
- // https://github.com/mifi/SimpleInstaBot/issues/36
225
- if (graphqlUserMissing) {
226
- // https://stackoverflow.com/questions/37593025/instagram-api-get-the-userid
227
- // https://stackoverflow.com/questions/17373886/how-can-i-get-a-users-media-from-instagram-without-authenticating-as-a-user
228
- const found = await safeGotoUser(`${instagramBaseUrl}/${encodeURIComponent(username)}?__a=1`);
229
- if (!found) throw new Error('User not found');
232
+ const cachedUserData = userDataCache[username];
230
233
 
231
- const json = await getPageJson();
232
-
233
- const { user } = json.graphql;
234
+ if (isAlreadyOnUserPage(username)) {
235
+ // assume we have data
236
+ return cachedUserData;
237
+ }
234
238
 
239
+ if (cachedUserData != null) {
240
+ // if we already have userData, just navigate
235
241
  await navigateToUserWithCheck(username);
236
- return user;
242
+ return cachedUserData;
237
243
  }
238
244
 
239
- await navigateToUserWithCheck(username);
240
-
241
- // eslint-disable-next-line no-underscore-dangle
242
- const sharedData = await page.evaluate(() => window._sharedData);
243
- try {
244
- // eslint-disable-next-line prefer-destructuring
245
- return sharedData.entry_data.ProfilePage[0].graphql.user;
245
+ // intercept special XHR network request that fetches user's data and store it in a cache
246
+ // TODO fallback to DOM to get user ID if this request fails?
247
+ // https://github.com/mifi/SimpleInstaBot/issues/125#issuecomment-1145354294
248
+ const [foundResponse] = await Promise.all([
249
+ page.waitForResponse((response) => {
250
+ const request = response.request();
251
+ return request.method() === 'GET' && new RegExp(`https:\\/\\/i\\.instagram\\.com\\/api\\/v1\\/users\\/web_profile_info\\/\\?username=${encodeURIComponent(username.toLowerCase())}`).test(request.url());
252
+ }),
253
+ navigateToUserWithCheck(username),
254
+ // page.waitForNavigation({ waitUntil: 'networkidle0' }),
255
+ ]);
256
+
257
+ const json = JSON.parse(await foundResponse.text());
258
+ const userData = json.data.user;
259
+ userDataCache[username] = userData;
260
+ return userData;
261
+ }
246
262
 
247
- // JSON.parse(Array.from(document.getElementsByTagName('script')).find(el => el.innerHTML.startsWith('window.__additionalDataLoaded(\'feed\',')).innerHTML.replace(/^window.__additionalDataLoaded\('feed',({.*})\);$/, '$1'));
248
- // JSON.parse(Array.from(document.getElementsByTagName('script')).find(el => el.innerHTML.startsWith('window._sharedData')).innerHTML.replace(/^window._sharedData ?= ?({.*});$/, '$1'));
249
- // Array.from(document.getElementsByTagName('a')).find(el => el.attributes?.href?.value.includes(`${username}/followers`)).innerText
250
- } catch (err) {
251
- logger.warn('Missing graphql in page, falling back to alternative method...');
252
- graphqlUserMissing = true; // Store as state so we don't have to do this every time from now on.
253
- return navigateToUserAndGetData(username); // Now try again with alternative method
254
- }
263
+ async function getPageJson() {
264
+ return JSON.parse(await (await (await page.$('pre')).getProperty('textContent')).jsonValue());
255
265
  }
256
266
 
257
267
  async function isActionBlocked() {
@@ -321,7 +331,7 @@ const Instauto = async (db, browser, options) => {
321
331
  }
322
332
 
323
333
  async function followUser(username) {
324
- await navigateToUserWithCheck(username);
334
+ await navigateToUserAndGetData(username);
325
335
  const elementHandle = await findFollowButton();
326
336
 
327
337
  if (!elementHandle) {
@@ -363,7 +373,7 @@ const Instauto = async (db, browser, options) => {
363
373
  // See https://github.com/timgrossmann/InstaPy/pull/2345
364
374
  // https://github.com/timgrossmann/InstaPy/issues/2355
365
375
  async function unfollowUser(username) {
366
- await navigateToUserWithCheck(username);
376
+ await navigateToUserAndGetData(username);
367
377
  logger.log(`Unfollowing user ${username}`);
368
378
 
369
379
  const res = { username, time: new Date().getTime() };
@@ -562,7 +572,7 @@ const Instauto = async (db, browser, options) => {
562
572
  async function likeUserImages({ username, likeImagesMin, likeImagesMax } = {}) {
563
573
  if (!likeImagesMin || !likeImagesMax || likeImagesMax < likeImagesMin || likeImagesMin < 1) throw new Error('Invalid arguments');
564
574
 
565
- await navigateToUserWithCheck(username);
575
+ await navigateToUserAndGetData(username);
566
576
 
567
577
  logger.log(`Liking ${likeImagesMin}-${likeImagesMax} user images`);
568
578
  try {
@@ -581,6 +591,7 @@ const Instauto = async (db, browser, options) => {
581
591
  logger.log('Skipping already followed user', username);
582
592
  return false;
583
593
  }
594
+
584
595
  const graphqlUser = await navigateToUserAndGetData(username);
585
596
 
586
597
  const followedByCount = graphqlUser.edge_followed_by.count;
@@ -632,9 +643,9 @@ const Instauto = async (db, browser, options) => {
632
643
 
633
644
  let numFollowedForThisUser = 0;
634
645
 
635
- const userData = await navigateToUserAndGetData(username);
646
+ const { id: userId } = await navigateToUserAndGetData(username);
636
647
 
637
- for await (const followersBatch of getFollowersOrFollowingGenerator({ userId: userData.id, getFollowers: true })) {
648
+ for await (const followersBatch of getFollowersOrFollowingGenerator({ userId, getFollowers: true })) {
638
649
  logger.log('User followers batch', followersBatch);
639
650
 
640
651
  for (const follower of followersBatch) {
@@ -646,13 +657,13 @@ const Instauto = async (db, browser, options) => {
646
657
  return;
647
658
  }
648
659
 
649
- if (enableFollow) {
650
- if (await followUserRespectingRestrictions({ username: follower, skipPrivate })) {
651
- numFollowedForThisUser += 1;
652
- }
653
- }
660
+ let didActuallyFollow = false;
661
+ if (enableFollow) didActuallyFollow = await followUserRespectingRestrictions({ username: follower, skipPrivate });
662
+ if (didActuallyFollow) numFollowedForThisUser += 1;
654
663
 
655
- if (enableLikeImages) {
664
+ const didFailToFollow = enableFollow && !didActuallyFollow;
665
+
666
+ if (enableLikeImages && !didFailToFollow) {
656
667
  // Note: throws error if user isPrivate
657
668
  await likeUserImages({ username: follower, likeImagesMin, likeImagesMax });
658
669
  }
@@ -706,6 +717,8 @@ const Instauto = async (db, browser, options) => {
706
717
  const userFound = await navigateToUser(username);
707
718
 
708
719
  if (!userFound) {
720
+ // to avoid repeatedly unfollowing failed users, flag them as already unfollowed
721
+ logger.log('User not found for unfollow');
709
722
  await addPrevUnfollowedUser({ username, time: new Date().getTime(), noActionTaken: true });
710
723
  await sleep(3000);
711
724
  } else {
@@ -740,6 +753,8 @@ const Instauto = async (db, browser, options) => {
740
753
  }
741
754
  }
742
755
 
756
+ logger.log('Done with unfollowing', i, j);
757
+
743
758
  return j;
744
759
  }
745
760
 
@@ -937,16 +952,14 @@ const Instauto = async (db, browser, options) => {
937
952
  throw new Error('Don\'t know what\'s my username');
938
953
  }
939
954
 
940
- const myUserData = await navigateToUserAndGetData(myUsername);
941
- const myUserId = myUserData.id;
955
+ const { id: myUserId } = await navigateToUserAndGetData(myUsername);
942
956
 
943
957
  // --- END OF INITIALIZATION
944
958
 
945
959
  async function doesUserFollowMe(username) {
946
960
  try {
947
961
  logger.info('Checking if user', username, 'follows us');
948
- const userData = await navigateToUserAndGetData(username);
949
- const userId = userData.id;
962
+ const { id: userId } = await navigateToUserAndGetData(username);
950
963
 
951
964
  const elementHandles = await page.$x("//a[contains(.,' following')][contains(@href,'/following')]");
952
965
  if (elementHandles.length === 0) throw new Error('Following button not found');