instauto 7.2.1 → 8.0.0

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/.eslintrc +4 -3
  2. package/index.js +319 -246
  3. package/package.json +4 -3
package/.eslintrc CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "env": {
3
- "node": true,
3
+ "node": true
4
4
  },
5
5
  "extends": "airbnb/base",
6
6
  "parserOptions": {
7
- "sourceType": "script"
7
+ "sourceType": "script",
8
+ "ecmaVersion": 2022
8
9
  },
9
10
  "globals": {
10
11
  "window": true,
11
- "document": true,
12
+ "document": true
12
13
  },
13
14
  "rules": {
14
15
  "max-len": 0,
package/index.js CHANGED
@@ -117,8 +117,9 @@ const Instauto = async (db, browser, options) => {
117
117
  }
118
118
  }
119
119
 
120
- const sleep = (ms, dev = 1) => {
121
- const msWithDev = ((Math.random() * dev) + 1) * ms;
120
+ const sleep = (ms, deviation = 1) => {
121
+ let msWithDev = ((Math.random() * deviation) + 1) * ms;
122
+ if (dryRun) msWithDev = Math.min(3000, msWithDev); // for dryRun, no need to wait so long
122
123
  logger.log('Waiting', Math.round(msWithDev / 1000), 'sec');
123
124
  return new Promise(resolve => setTimeout(resolve, msWithDev));
124
125
  };
@@ -165,27 +166,48 @@ const Instauto = async (db, browser, options) => {
165
166
  return new Date().getTime() - followedUserEntry.time < dontUnfollowUntilTimeElapsed;
166
167
  }
167
168
 
168
- async function safeGoto(url) {
169
- logger.log(`Goto ${url}`);
170
- const response = await page.goto(url);
171
- await sleep(1000);
172
- const status = response.status();
169
+ async function gotoWithRetry(url) {
170
+ for (let attempt = 0; ; attempt += 1) {
171
+ logger.log(`Goto ${url}`);
172
+ const response = await page.goto(url);
173
+ await sleep(1000);
174
+ const status = response.status();
175
+
176
+ // https://www.reddit.com/r/Instagram/comments/kwrt0s/error_560/
177
+ // https://github.com/mifi/instauto/issues/60
178
+ if (![560, 429].includes(status) || attempt > 3) return status;
179
+
180
+ logger.info(`Got ${status} - Retrying request later...`);
181
+ 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');
182
+ await sleep((attempt + 1) * 30 * 60 * 1000);
183
+ }
184
+ }
185
+
186
+ async function safeGotoUser(url, checkPageForUsername) {
187
+ const status = await gotoWithRetry(url);
173
188
  if (status === 200) {
189
+ if (checkPageForUsername != null) {
190
+ // some pages return 200 but nothing there (I think deleted accounts)
191
+ // https://github.com/mifi/SimpleInstaBot/issues/48
192
+ // example: https://www.instagram.com/victorialarson__/
193
+ // so we check if the page has the user's name on it
194
+ return page.evaluate((username) => window.find(username), checkPageForUsername);
195
+ }
174
196
  return true;
175
- } else if (status === 404) {
197
+ }
198
+ if (status === 404) {
176
199
  logger.log('User not found');
177
200
  return false;
178
- } else if (status === 429) {
179
- logger.error('Got 429 Too Many Requests, waiting...');
180
- await sleep(60 * 60 * 1000);
181
- throw new Error('Aborted operation due to too many requests'); // TODO retry instead
182
201
  }
183
- throw new Error(`Navigate to user returned status ${response.status()}`);
202
+ throw new Error(`Navigate to user failed with status ${status}`);
184
203
  }
185
204
 
186
205
  async function navigateToUser(username) {
206
+ const url = `${instagramBaseUrl}/${encodeURIComponent(username)}`;
207
+ if (page.url().replace(/\/$/, '') === url.replace(/\/$/, '')) return true; // optimization: already on URL? (ignore trailing slash)
208
+ // logger.log('navigating from', page.url(), 'to', url);
187
209
  logger.log(`Navigating to user ${username}`);
188
- return safeGoto(`${instagramBaseUrl}/${encodeURIComponent(username)}`);
210
+ return safeGotoUser(url, username);
189
211
  }
190
212
 
191
213
  async function getPageJson() {
@@ -197,7 +219,7 @@ const Instauto = async (db, browser, options) => {
197
219
  if (graphqlUserMissing) {
198
220
  // https://stackoverflow.com/questions/37593025/instagram-api-get-the-userid
199
221
  // https://stackoverflow.com/questions/17373886/how-can-i-get-a-users-media-from-instagram-without-authenticating-as-a-user
200
- const found = await safeGoto(`${instagramBaseUrl}/${encodeURIComponent(username)}?__a=1`);
222
+ const found = await safeGotoUser(`${instagramBaseUrl}/${encodeURIComponent(username)}?__a=1`);
201
223
  if (!found) throw new Error('User not found');
202
224
 
203
225
  const json = await getPageJson();
@@ -320,7 +342,7 @@ const Instauto = async (db, browser, options) => {
320
342
  await addPrevFollowedUser(entry);
321
343
 
322
344
  if (!elementHandle2) {
323
- logger.log('Button did not change state - Sleeping');
345
+ logger.log('Button did not change state - Sleeping 1 min');
324
346
  await sleep(60000);
325
347
  throw new Error('Button did not change state');
326
348
  }
@@ -373,7 +395,7 @@ const Instauto = async (db, browser, options) => {
373
395
 
374
396
  const isLoggedIn = async () => (await page.$x('//*[@aria-label="Home"]')).length === 1;
375
397
 
376
- async function graphqlQueryUsers({ queryHash, getResponseProp, maxPages, shouldProceed: shouldProceedArg, graphqlVariables: graphqlVariablesIn }) {
398
+ async function* graphqlQueryUsers({ queryHash, getResponseProp, graphqlVariables: graphqlVariablesIn }) {
377
399
  const graphqlUrl = `${instagramBaseUrl}/graphql/query/?query_hash=${queryHash}`;
378
400
 
379
401
  const graphqlVariables = {
@@ -386,14 +408,7 @@ const Instauto = async (db, browser, options) => {
386
408
  let hasNextPage = true;
387
409
  let i = 0;
388
410
 
389
- const shouldProceed = () => {
390
- if (!hasNextPage) return false;
391
- const isBelowMaxPages = maxPages == null || i < maxPages;
392
- if (shouldProceedArg) return isBelowMaxPages && shouldProceedArg(outUsers);
393
- return isBelowMaxPages;
394
- };
395
-
396
- while (shouldProceed()) {
411
+ while (hasNextPage) {
397
412
  const url = `${graphqlUrl}&variables=${JSON.stringify(graphqlVariables)}`;
398
413
  // logger.log(url);
399
414
  await page.goto(url);
@@ -403,44 +418,47 @@ const Instauto = async (db, browser, options) => {
403
418
  const pageInfo = subProp.page_info;
404
419
  const { edges } = subProp;
405
420
 
406
- edges.forEach(e => outUsers.push(e.node.username));
421
+ const ret = [];
422
+ edges.forEach(e => ret.push(e.node.username));
407
423
 
408
424
  graphqlVariables.after = pageInfo.end_cursor;
409
425
  hasNextPage = pageInfo.has_next_page;
410
426
  i += 1;
411
427
 
412
- if (shouldProceed()) {
428
+ if (hasNextPage) {
413
429
  logger.log(`Has more pages (current ${i})`);
414
430
  // await sleep(300);
415
431
  }
432
+
433
+ yield ret;
416
434
  }
417
435
 
418
436
  return outUsers;
419
437
  }
420
438
 
421
- async function getFollowersOrFollowing({
422
- userId, getFollowers = false, maxPages, shouldProceed,
423
- }) {
439
+ function getFollowersOrFollowingGenerator({ userId, getFollowers = false }) {
424
440
  return graphqlQueryUsers({
425
441
  getResponseProp: (json) => json.data.user[getFollowers ? 'edge_followed_by' : 'edge_follow'],
426
442
  graphqlVariables: { id: userId },
427
- shouldProceed,
428
- maxPages,
429
443
  queryHash: getFollowers ? '37479f2b8209594dde7facb0d904896a' : '58712303d941c6855d4e888c5f0cd22f',
430
444
  });
431
445
  }
432
446
 
433
- async function getUsersWhoLikedContent({
434
- contentId, maxPages, shouldProceed,
435
- }) {
447
+ async function getFollowersOrFollowing({ userId, getFollowers = false }) {
448
+ let users = [];
449
+ for await (const usersBatch of getFollowersOrFollowingGenerator({ userId, getFollowers })) {
450
+ users = [...users, ...usersBatch];
451
+ }
452
+ return users;
453
+ }
454
+
455
+ function getUsersWhoLikedContent({ contentId }) {
436
456
  return graphqlQueryUsers({
437
457
  getResponseProp: (json) => json.data.shortcode_media.edge_liked_by,
438
458
  graphqlVariables: {
439
459
  shortcode: contentId,
440
460
  include_reel: true,
441
461
  },
442
- shouldProceed,
443
- maxPages,
444
462
  queryHash: 'd5d763b1e2acf209d62d22d184488e57',
445
463
  });
446
464
  }
@@ -557,73 +575,66 @@ const Instauto = async (db, browser, options) => {
557
575
 
558
576
  const userData = await navigateToUserAndGetData(username);
559
577
 
560
- // Check if we have more than enough users that are not previously followed
561
- const shouldProceed = usersSoFar => (
562
- usersSoFar.filter(u => !getPrevFollowedUser(u)).length < maxFollowsPerUser + 5 // 5 is just a margin
563
- );
564
- let followers = await getFollowersOrFollowing({
565
- userId: userData.id,
566
- getFollowers: true,
567
- shouldProceed,
568
- });
569
-
570
- logger.log('Followers', followers);
578
+ for await (const followersBatch of getFollowersOrFollowingGenerator({ userId: userData.id, getFollowers: true })) {
579
+ logger.log('User followers batch', followersBatch);
571
580
 
572
- // Filter again
573
- followers = followers.filter(f => !getPrevFollowedUser(f));
574
-
575
- for (const follower of followers) {
576
- try {
577
- if (numFollowedForThisUser >= maxFollowsPerUser) {
578
- logger.log('Have reached followed limit for this user, stopping');
579
- return;
580
- }
581
-
582
- const graphqlUser = await navigateToUserAndGetData(follower);
583
-
584
- const followedByCount = graphqlUser.edge_followed_by.count;
585
- const followsCount = graphqlUser.edge_follow.count;
586
- const isPrivate = graphqlUser.is_private;
587
-
588
- // logger.log('followedByCount:', followedByCount, 'followsCount:', followsCount);
589
-
590
- const ratio = followedByCount / (followsCount || 1);
591
-
592
- if (isPrivate && skipPrivate) {
593
- logger.log('User is private, skipping');
594
- } else if (
595
- (followUserMaxFollowers != null && followedByCount > followUserMaxFollowers) ||
596
- (followUserMaxFollowing != null && followsCount > followUserMaxFollowing) ||
597
- (followUserMinFollowers != null && followedByCount < followUserMinFollowers) ||
598
- (followUserMinFollowing != null && followsCount < followUserMinFollowing)
599
- ) {
600
- logger.log('User has too many or too few followers or following, skipping.', 'followedByCount:', followedByCount, 'followsCount:', followsCount);
601
- } else if (
602
- (followUserRatioMax != null && ratio > followUserRatioMax) ||
603
- (followUserRatioMin != null && ratio < followUserRatioMin)
604
- ) {
605
- logger.log('User has too many followers compared to follows or opposite, skipping');
581
+ for (const follower of followersBatch) {
582
+ if (getPrevFollowedUser(follower)) {
583
+ logger.log('Skipping already followed user', follower);
606
584
  } else {
607
- await followCurrentUser(follower);
608
- numFollowedForThisUser += 1;
609
-
610
- await sleep(10000);
585
+ try {
586
+ if (numFollowedForThisUser >= maxFollowsPerUser) {
587
+ logger.log('Have reached followed limit for this user, stopping');
588
+ return;
589
+ }
611
590
 
612
- if (!isPrivate && enableLikeImages && !hasReachedDailyLikesLimit()) {
613
- try {
614
- await likeCurrentUserImages({ username: follower, likeImagesMin, likeImagesMax });
615
- } catch (err) {
616
- logger.error(`Failed to follow user's images ${follower}`, err);
617
- await takeScreenshot();
591
+ const graphqlUser = await navigateToUserAndGetData(follower);
592
+
593
+ const followedByCount = graphqlUser.edge_followed_by.count;
594
+ const followsCount = graphqlUser.edge_follow.count;
595
+ const isPrivate = graphqlUser.is_private;
596
+
597
+ // logger.log('followedByCount:', followedByCount, 'followsCount:', followsCount);
598
+
599
+ const ratio = followedByCount / (followsCount || 1);
600
+
601
+ if (isPrivate && skipPrivate) {
602
+ logger.log('User is private, skipping');
603
+ } else if (
604
+ (followUserMaxFollowers != null && followedByCount > followUserMaxFollowers) ||
605
+ (followUserMaxFollowing != null && followsCount > followUserMaxFollowing) ||
606
+ (followUserMinFollowers != null && followedByCount < followUserMinFollowers) ||
607
+ (followUserMinFollowing != null && followsCount < followUserMinFollowing)
608
+ ) {
609
+ logger.log('User has too many or too few followers or following, skipping.', 'followedByCount:', followedByCount, 'followsCount:', followsCount);
610
+ } else if (
611
+ (followUserRatioMax != null && ratio > followUserRatioMax) ||
612
+ (followUserRatioMin != null && ratio < followUserRatioMin)
613
+ ) {
614
+ logger.log('User has too many followers compared to follows or opposite, skipping');
615
+ } else {
616
+ await followCurrentUser(follower);
617
+ numFollowedForThisUser += 1;
618
+
619
+ await sleep(10000);
620
+
621
+ if (!isPrivate && enableLikeImages && !hasReachedDailyLikesLimit()) {
622
+ try {
623
+ await likeCurrentUserImages({ username: follower, likeImagesMin, likeImagesMax });
624
+ } catch (err) {
625
+ logger.error(`Failed to follow user's images ${follower}`, err);
626
+ await takeScreenshot();
627
+ }
628
+ }
629
+
630
+ await sleep(20000);
631
+ await throttle();
618
632
  }
633
+ } catch (err) {
634
+ logger.error(`Failed to process follower ${follower}`, err);
635
+ await sleep(20000);
619
636
  }
620
-
621
- await sleep(20000);
622
- await throttle();
623
637
  }
624
- } catch (err) {
625
- logger.error(`Failed to process follower ${follower}`, err);
626
- await sleep(20000);
627
638
  }
628
639
  }
629
640
  }
@@ -654,136 +665,57 @@ const Instauto = async (db, browser, options) => {
654
665
  }
655
666
  }
656
667
 
657
- async function safelyUnfollowUserList(usersToUnfollow, limit) {
658
- logger.log(`Unfollowing ${usersToUnfollow.length} users`);
668
+ async function safelyUnfollowUserList(usersToUnfollow, limit, condition = () => true) {
669
+ logger.log('Unfollowing users, up to limit', limit);
659
670
 
660
671
  let i = 0; // Number of people processed
661
672
  let j = 0; // Number of people actually unfollowed (button pressed)
662
673
 
663
- for (const username of usersToUnfollow) {
664
- try {
665
- const userFound = await navigateToUser(username);
666
-
667
- if (!userFound) {
668
- await addPrevUnfollowedUser({ username, time: new Date().getTime(), noActionTaken: true });
669
- await sleep(3000);
670
- } else {
671
- const { noActionTaken } = await unfollowCurrentUser(username);
674
+ for await (const listOrUsername of usersToUnfollow) {
675
+ // backward compatible:
676
+ const list = Array.isArray(listOrUsername) ? listOrUsername : [listOrUsername];
677
+
678
+ for (const username of list) {
679
+ if (await condition(username)) {
680
+ try {
681
+ const userFound = await navigateToUser(username);
682
+
683
+ if (!userFound) {
684
+ await addPrevUnfollowedUser({ username, time: new Date().getTime(), noActionTaken: true });
685
+ await sleep(3000);
686
+ } else {
687
+ const { noActionTaken } = await unfollowCurrentUser(username);
688
+
689
+ if (noActionTaken) {
690
+ await sleep(3000);
691
+ } else {
692
+ await sleep(15000);
693
+ j += 1;
694
+
695
+ if (j % 10 === 0) {
696
+ logger.log('Have unfollowed 10 users since last break. Taking a break');
697
+ await sleep(10 * 60 * 1000, 0.1);
698
+ }
699
+ }
700
+ }
672
701
 
673
- if (noActionTaken) {
674
- await sleep(3000);
675
- } else {
676
- await sleep(15000);
677
- j += 1;
702
+ i += 1;
703
+ logger.log(`Have now unfollowed (or tried to unfollow) ${i} users`);
678
704
 
679
- if (j % 10 === 0) {
680
- logger.log('Have unfollowed 10 users since last break. Taking a break');
681
- await sleep(10 * 60 * 1000, 0.1);
705
+ if (limit && j >= limit) {
706
+ logger.log(`Have unfollowed limit of ${limit}, stopping`);
707
+ return j;
682
708
  }
683
- }
684
- }
685
709
 
686
- i += 1;
687
- logger.log(`Have now unfollowed ${i} users of total ${usersToUnfollow.length}`);
688
-
689
- if (limit && j >= limit) {
690
- logger.log(`Have unfollowed limit of ${limit}, stopping`);
691
- return;
710
+ await throttle();
711
+ } catch (err) {
712
+ logger.error('Failed to unfollow, continuing with next', err);
713
+ }
692
714
  }
693
-
694
- await throttle();
695
- } catch (err) {
696
- logger.error('Failed to unfollow, continuing with next', err);
697
715
  }
698
716
  }
699
- }
700
-
701
- async function unfollowNonMutualFollowers({ limit } = {}) {
702
- logger.log('Unfollowing non-mutual followers...');
703
- const userData = await navigateToUserAndGetData(myUsername);
704
-
705
- const allFollowers = await getFollowersOrFollowing({
706
- userId: userData.id,
707
- getFollowers: true,
708
- });
709
- const allFollowing = await getFollowersOrFollowing({
710
- userId: userData.id,
711
- getFollowers: false,
712
- });
713
- // logger.log('allFollowers:', allFollowers, 'allFollowing:', allFollowing);
714
717
 
715
- const usersToUnfollow = allFollowing.filter((u) => {
716
- if (allFollowers.includes(u)) return false; // Follows us
717
- if (excludeUsers.includes(u)) return false; // User is excluded by exclude list
718
- if (haveRecentlyFollowedUser(u)) {
719
- logger.log(`Have recently followed user ${u}, skipping`);
720
- return false;
721
- }
722
- return true;
723
- });
724
-
725
- logger.log('usersToUnfollow', JSON.stringify(usersToUnfollow));
726
-
727
- await safelyUnfollowUserList(usersToUnfollow, limit);
728
- }
729
-
730
- async function unfollowAllUnknown({ limit } = {}) {
731
- logger.log('Unfollowing all except excludes and auto followed');
732
- const userData = await navigateToUserAndGetData(myUsername);
733
-
734
- const allFollowing = await getFollowersOrFollowing({
735
- userId: userData.id,
736
- getFollowers: false,
737
- });
738
- // logger.log('allFollowing', allFollowing);
739
-
740
- const usersToUnfollow = allFollowing.filter((u) => {
741
- if (getPrevFollowedUser(u)) return false;
742
- if (excludeUsers.includes(u)) return false; // User is excluded by exclude list
743
- return true;
744
- });
745
-
746
- logger.log('usersToUnfollow', JSON.stringify(usersToUnfollow));
747
-
748
- await safelyUnfollowUserList(usersToUnfollow, limit);
749
- }
750
-
751
- async function unfollowOldFollowed({ ageInDays, limit } = {}) {
752
- assert(ageInDays);
753
-
754
- logger.log(`Unfollowing currently followed users who were auto-followed more than ${ageInDays} days ago...`);
755
-
756
- const userData = await navigateToUserAndGetData(myUsername);
757
-
758
- const allFollowing = await getFollowersOrFollowing({
759
- userId: userData.id,
760
- getFollowers: false,
761
- });
762
- // logger.log('allFollowing', allFollowing);
763
-
764
- const usersToUnfollow = allFollowing.filter(u =>
765
- getPrevFollowedUser(u) &&
766
- !excludeUsers.includes(u) &&
767
- (new Date().getTime() - getPrevFollowedUser(u).time) / (1000 * 60 * 60 * 24) > ageInDays)
768
- .slice(0, limit);
769
-
770
- logger.log('usersToUnfollow', JSON.stringify(usersToUnfollow));
771
-
772
- await safelyUnfollowUserList(usersToUnfollow, limit);
773
-
774
- return usersToUnfollow.length;
775
- }
776
-
777
- async function listManuallyFollowedUsers() {
778
- const userData = await navigateToUserAndGetData(myUsername);
779
-
780
- const allFollowing = await getFollowersOrFollowing({
781
- userId: userData.id,
782
- getFollowers: false,
783
- });
784
-
785
- return allFollowing.filter(u =>
786
- !getPrevFollowedUser(u) && !excludeUsers.includes(u));
718
+ return j;
787
719
  }
788
720
 
789
721
  function getPage() {
@@ -791,6 +723,10 @@ const Instauto = async (db, browser, options) => {
791
723
  }
792
724
 
793
725
  page = await browser.newPage();
726
+
727
+ // https://github.com/mifi/SimpleInstaBot/issues/118#issuecomment-1067883091
728
+ await page.setExtraHTTPHeaders({ 'Accept-Language': 'en' });
729
+
794
730
  if (randomizeUserAgent) {
795
731
  const userAgentGenerated = new UserAgent({ deviceCategory: 'desktop' });
796
732
  await page.setUserAgent(userAgentGenerated.toString());
@@ -799,72 +735,88 @@ const Instauto = async (db, browser, options) => {
799
735
 
800
736
  if (enableCookies) await tryLoadCookies();
801
737
 
802
- const goHome = async () => page.goto(`${instagramBaseUrl}/`);
738
+ const goHome = async () => page.goto(`${instagramBaseUrl}/?hl=en`);
803
739
 
804
740
  // https://github.com/mifi/SimpleInstaBot/issues/28
805
- async function setLang(short, long) {
741
+ async function setLang(short, long, assumeLoggedIn = false) {
806
742
  logger.log(`Setting language to ${long} (${short})`);
807
743
 
808
- // This doesn't seem to always work, hence why it's just a fallback now
809
- async function fallbackSetLang() {
810
- await goHome();
811
- await sleep(1000);
812
-
813
- await page.setCookie({
814
- name: 'ig_lang',
815
- value: short,
816
- path: '/',
817
- });
818
- await sleep(1000);
819
- await goHome();
820
- await sleep(3000);
821
- }
822
-
823
744
  try {
824
745
  await sleep(1000);
825
- await goHome();
746
+
747
+ // when logged in, we need to go to account in order to be able to check/set language
748
+ // (need to see the footer)
749
+ if (assumeLoggedIn) {
750
+ await page.goto(`${instagramBaseUrl}/accounts/edit/`);
751
+ } else {
752
+ await goHome();
753
+ }
826
754
  await sleep(3000);
827
755
  const elementHandles = await page.$x(`//select[//option[@value='${short}' and text()='${long}']]`);
828
756
  if (elementHandles.length < 1) throw new Error('Language selector not found');
829
757
  logger.log('Found language selector');
830
758
 
831
759
  // https://stackoverflow.com/questions/45864516/how-to-select-an-option-from-dropdown-select
832
- await page.evaluate((selectElem, short2) => {
760
+ const alreadyEnglish = await page.evaluate((selectElem, short2) => {
833
761
  const optionElem = selectElem.querySelector(`option[value='${short2}']`);
762
+ if (optionElem.selected) return true; // already selected?
834
763
  optionElem.selected = true;
835
764
  // eslint-disable-next-line no-undef
836
765
  const event = new Event('change', { bubbles: true });
837
766
  selectElem.dispatchEvent(event);
767
+ return false;
838
768
  }, elementHandles[0], short);
839
- logger.log('Selected language');
840
769
 
770
+ if (alreadyEnglish) {
771
+ logger.log('Already English language');
772
+ if (!assumeLoggedIn) {
773
+ await goHome(); // because we were on the settings page
774
+ await sleep(1000);
775
+ }
776
+ return;
777
+ }
778
+
779
+ logger.log('Selected language');
841
780
  await sleep(3000);
842
781
  await goHome();
843
782
  await sleep(1000);
844
783
  } catch (err) {
845
784
  logger.error('Failed to set language, trying fallback (cookie)', err);
846
- await fallbackSetLang();
785
+ // This doesn't seem to always work, hence why it's just a fallback now
786
+ await goHome();
787
+ await sleep(1000);
788
+
789
+ await page.setCookie({
790
+ name: 'ig_lang',
791
+ value: short,
792
+ path: '/',
793
+ });
794
+ await sleep(1000);
795
+ await goHome();
796
+ await sleep(3000);
847
797
  }
848
798
  }
849
799
 
850
- const setEnglishLang = async () => setLang('en', 'English');
851
- // const setEnglishLang = async () => setLang('de', 'Deutsch');
800
+ const setEnglishLang = async (assumeLoggedIn) => setLang('en', 'English', assumeLoggedIn);
801
+ // const setEnglishLang = async (assumeLoggedIn) => setLang('de', 'Deutsch', assumeLoggedIn);
852
802
 
853
- async function tryPressButton(elementHandles, name) {
803
+ async function tryPressButton(elementHandles, name, sleepMs = 3000) {
854
804
  try {
855
805
  if (elementHandles.length === 1) {
856
806
  logger.log(`Pressing button: ${name}`);
857
807
  elementHandles[0].click();
858
- await sleep(3000);
808
+ await sleep(sleepMs);
859
809
  }
860
810
  } catch (err) {
861
811
  logger.warn(`Failed to press button: ${name}`);
862
812
  }
863
813
  }
864
814
 
865
- await setEnglishLang();
815
+ await setEnglishLang(false);
866
816
 
867
817
  await tryPressButton(await page.$x('//button[contains(text(), "Accept")]'), 'Accept cookies dialog');
818
+ await tryPressButton(await page.$x('//button[contains(text(), "Only allow essential cookies")]'), 'Accept cookies dialog 2 button 1', 10000);
819
+ await tryPressButton(await page.$x('//button[contains(text(), "Allow essential and optional cookies")]'), 'Accept cookies dialog 2 button 2', 10000);
868
820
 
869
821
  if (!(await isLoggedIn())) {
870
822
  if (!myUsername || !password) {
@@ -876,7 +828,7 @@ const Instauto = async (db, browser, options) => {
876
828
  await page.click('a[href="/accounts/login/?source=auth_switcher"]');
877
829
  await sleep(1000);
878
830
  } catch (err) {
879
- logger.warn('Login page button not found, assuming we have login form');
831
+ logger.info('No login page button, assuming we are on login form');
880
832
  }
881
833
 
882
834
  // Mobile version https://github.com/mifi/SimpleInstaBot/issues/7
@@ -887,8 +839,15 @@ const Instauto = async (db, browser, options) => {
887
839
  await page.type('input[name="password"]', password, { delay: 50 });
888
840
  await sleep(1000);
889
841
 
890
- const loginButton = (await page.$x("//button[.//text() = 'Log In']"))[0];
891
- await loginButton.click();
842
+ for (;;) {
843
+ const loginButton = (await page.$x("//button[.//text() = 'Log In']"))[0];
844
+ if (loginButton) {
845
+ await loginButton.click();
846
+ break;
847
+ }
848
+ logger.warn('Login button not found. Maybe you can help me click it? And also report an issue on github with a screenshot of what you\'re seeing :)');
849
+ await sleep(6000);
850
+ }
892
851
 
893
852
  await sleep(6000);
894
853
 
@@ -908,7 +867,8 @@ const Instauto = async (db, browser, options) => {
908
867
  }
909
868
 
910
869
  // In case language gets reset after logging in
911
- await setEnglishLang();
870
+ // https://github.com/mifi/SimpleInstaBot/issues/118
871
+ await setEnglishLang(true);
912
872
 
913
873
  // Mobile version https://github.com/mifi/SimpleInstaBot/issues/7
914
874
  await tryPressButton(await page.$x('//button[contains(text(), "Save Info")]'), 'Login info dialog: Save Info');
@@ -932,6 +892,119 @@ const Instauto = async (db, browser, options) => {
932
892
  logger.error('Failed to detect username', err);
933
893
  }
934
894
 
895
+ if (!myUsername) {
896
+ throw new Error('Don\'t know what\'s my username');
897
+ }
898
+
899
+ const myUserData = await navigateToUserAndGetData(myUsername);
900
+ const myUserId = myUserData.id;
901
+
902
+ // --- END OF INITIALIZATION
903
+
904
+
905
+ async function doesUserFollowMe(username) {
906
+ try {
907
+ logger.info('Checking if user', username, 'follows us');
908
+ const userData = await navigateToUserAndGetData(username);
909
+ const userId = userData.id;
910
+
911
+ if (!(await navigateToUser(username))) throw new Error('User not found');
912
+
913
+ const elementHandles = await page.$x("//a[contains(.,' following')][contains(@href,'/following')]");
914
+ if (elementHandles.length === 0) throw new Error('Following button not found');
915
+
916
+ const [foundResponse] = await Promise.all([
917
+ page.waitForResponse((response) => {
918
+ const request = response.request();
919
+ return request.method() === 'GET' && new RegExp(`instagram.com/api/v1/friendships/${userId}/following/`).test(request.url());
920
+ }),
921
+ elementHandles[0].click(),
922
+ // page.waitForNavigation({ waitUntil: 'networkidle0' }),
923
+ ]);
924
+
925
+ const { users } = JSON.parse(await foundResponse.text());
926
+ if (users.length < 2) throw new Error('Unable to find user follows list');
927
+ return users.some((user) => user.pk === myUserId); // If they follow us, we will show at the top of the list
928
+ } catch (err) {
929
+ logger.error('Failed to check if user follows us', err);
930
+ return undefined;
931
+ }
932
+ }
933
+
934
+ async function unfollowNonMutualFollowers({ limit } = {}) {
935
+ logger.log(`Unfollowing non-mutual followers (limit ${limit})...`);
936
+
937
+ /* const allFollowers = await getFollowersOrFollowing({
938
+ userId: myUserId,
939
+ getFollowers: true,
940
+ }); */
941
+ const allFollowingGenerator = getFollowersOrFollowingGenerator({
942
+ userId: myUserId,
943
+ getFollowers: false,
944
+ });
945
+
946
+ async function condition(username) {
947
+ // if (allFollowers.includes(u)) return false; // Follows us
948
+ if (excludeUsers.includes(username)) return false; // User is excluded by exclude list
949
+ if (haveRecentlyFollowedUser(username)) {
950
+ logger.log(`Have recently followed user ${username}, skipping`);
951
+ return false;
952
+ }
953
+
954
+ const followsMe = await doesUserFollowMe(username);
955
+ logger.info('User follows us?', followsMe);
956
+ return followsMe === false;
957
+ }
958
+
959
+ await safelyUnfollowUserList(allFollowingGenerator, limit, condition);
960
+ }
961
+
962
+ async function unfollowAllUnknown({ limit } = {}) {
963
+ logger.log('Unfollowing all except excludes and auto followed');
964
+
965
+ const unfollowUsersGenerator = getFollowersOrFollowingGenerator({
966
+ userId: myUserId,
967
+ getFollowers: false,
968
+ });
969
+
970
+ function condition(username) {
971
+ if (getPrevFollowedUser(username)) return false; // we followed this user, so it's not unknown
972
+ if (excludeUsers.includes(username)) return false; // User is excluded by exclude list
973
+ return true;
974
+ }
975
+
976
+ await safelyUnfollowUserList(unfollowUsersGenerator, limit, condition);
977
+ }
978
+
979
+ async function unfollowOldFollowed({ ageInDays, limit } = {}) {
980
+ assert(ageInDays);
981
+
982
+ logger.log(`Unfollowing currently followed users who were auto-followed more than ${ageInDays} days ago (limit ${limit})...`);
983
+
984
+ const followingUsersGenerator = getFollowersOrFollowingGenerator({
985
+ userId: myUserId,
986
+ getFollowers: false,
987
+ });
988
+
989
+ function condition(username) {
990
+ return getPrevFollowedUser(username) &&
991
+ !excludeUsers.includes(username) &&
992
+ (new Date().getTime() - getPrevFollowedUser(username).time) / (1000 * 60 * 60 * 24) > ageInDays;
993
+ }
994
+
995
+ return safelyUnfollowUserList(followingUsersGenerator, limit, condition);
996
+ }
997
+
998
+ async function listManuallyFollowedUsers() {
999
+ const allFollowing = await getFollowersOrFollowing({
1000
+ userId: myUserId,
1001
+ getFollowers: false,
1002
+ });
1003
+
1004
+ return allFollowing.filter(u =>
1005
+ !getPrevFollowedUser(u) && !excludeUsers.includes(u));
1006
+ }
1007
+
935
1008
  return {
936
1009
  followUserFollowers,
937
1010
  unfollowNonMutualFollowers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instauto",
3
- "version": "7.2.1",
3
+ "version": "8.0.0",
4
4
  "description": "Instagram automation library written in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -19,9 +19,10 @@
19
19
  "user-agents": "^1.0.559"
20
20
  },
21
21
  "devDependencies": {
22
- "eslint": "^4.19.1",
22
+ "eslint": "^7.32.0 || ^8.2.0",
23
23
  "eslint-config-airbnb": "^16.1.0",
24
- "eslint-plugin-import": "^2.9.0",
24
+ "eslint-config-airbnb-base": "^15.0.0",
25
+ "eslint-plugin-import": "^2.25.2",
25
26
  "puppeteer": "^1.19.0"
26
27
  }
27
28
  }