instauto 7.2.3 → 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 +230 -203
  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
  };
@@ -202,8 +203,11 @@ const Instauto = async (db, browser, options) => {
202
203
  }
203
204
 
204
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);
205
209
  logger.log(`Navigating to user ${username}`);
206
- return safeGotoUser(`${instagramBaseUrl}/${encodeURIComponent(username)}`, username);
210
+ return safeGotoUser(url, username);
207
211
  }
208
212
 
209
213
  async function getPageJson() {
@@ -391,7 +395,7 @@ const Instauto = async (db, browser, options) => {
391
395
 
392
396
  const isLoggedIn = async () => (await page.$x('//*[@aria-label="Home"]')).length === 1;
393
397
 
394
- async function graphqlQueryUsers({ queryHash, getResponseProp, maxPages, shouldProceed: shouldProceedArg, graphqlVariables: graphqlVariablesIn }) {
398
+ async function* graphqlQueryUsers({ queryHash, getResponseProp, graphqlVariables: graphqlVariablesIn }) {
395
399
  const graphqlUrl = `${instagramBaseUrl}/graphql/query/?query_hash=${queryHash}`;
396
400
 
397
401
  const graphqlVariables = {
@@ -404,14 +408,7 @@ const Instauto = async (db, browser, options) => {
404
408
  let hasNextPage = true;
405
409
  let i = 0;
406
410
 
407
- const shouldProceed = () => {
408
- if (!hasNextPage) return false;
409
- const isBelowMaxPages = maxPages == null || i < maxPages;
410
- if (shouldProceedArg) return isBelowMaxPages && shouldProceedArg(outUsers);
411
- return isBelowMaxPages;
412
- };
413
-
414
- while (shouldProceed()) {
411
+ while (hasNextPage) {
415
412
  const url = `${graphqlUrl}&variables=${JSON.stringify(graphqlVariables)}`;
416
413
  // logger.log(url);
417
414
  await page.goto(url);
@@ -421,44 +418,47 @@ const Instauto = async (db, browser, options) => {
421
418
  const pageInfo = subProp.page_info;
422
419
  const { edges } = subProp;
423
420
 
424
- edges.forEach(e => outUsers.push(e.node.username));
421
+ const ret = [];
422
+ edges.forEach(e => ret.push(e.node.username));
425
423
 
426
424
  graphqlVariables.after = pageInfo.end_cursor;
427
425
  hasNextPage = pageInfo.has_next_page;
428
426
  i += 1;
429
427
 
430
- if (shouldProceed()) {
428
+ if (hasNextPage) {
431
429
  logger.log(`Has more pages (current ${i})`);
432
430
  // await sleep(300);
433
431
  }
432
+
433
+ yield ret;
434
434
  }
435
435
 
436
436
  return outUsers;
437
437
  }
438
438
 
439
- async function getFollowersOrFollowing({
440
- userId, getFollowers = false, maxPages, shouldProceed,
441
- }) {
439
+ function getFollowersOrFollowingGenerator({ userId, getFollowers = false }) {
442
440
  return graphqlQueryUsers({
443
441
  getResponseProp: (json) => json.data.user[getFollowers ? 'edge_followed_by' : 'edge_follow'],
444
442
  graphqlVariables: { id: userId },
445
- shouldProceed,
446
- maxPages,
447
443
  queryHash: getFollowers ? '37479f2b8209594dde7facb0d904896a' : '58712303d941c6855d4e888c5f0cd22f',
448
444
  });
449
445
  }
450
446
 
451
- async function getUsersWhoLikedContent({
452
- contentId, maxPages, shouldProceed,
453
- }) {
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 }) {
454
456
  return graphqlQueryUsers({
455
457
  getResponseProp: (json) => json.data.shortcode_media.edge_liked_by,
456
458
  graphqlVariables: {
457
459
  shortcode: contentId,
458
460
  include_reel: true,
459
461
  },
460
- shouldProceed,
461
- maxPages,
462
462
  queryHash: 'd5d763b1e2acf209d62d22d184488e57',
463
463
  });
464
464
  }
@@ -575,73 +575,66 @@ const Instauto = async (db, browser, options) => {
575
575
 
576
576
  const userData = await navigateToUserAndGetData(username);
577
577
 
578
- // Check if we have more than enough users that are not previously followed
579
- const shouldProceed = usersSoFar => (
580
- usersSoFar.filter(u => !getPrevFollowedUser(u)).length < maxFollowsPerUser + 5 // 5 is just a margin
581
- );
582
- let followers = await getFollowersOrFollowing({
583
- userId: userData.id,
584
- getFollowers: true,
585
- shouldProceed,
586
- });
587
-
588
- logger.log('Followers', followers);
578
+ for await (const followersBatch of getFollowersOrFollowingGenerator({ userId: userData.id, getFollowers: true })) {
579
+ logger.log('User followers batch', followersBatch);
589
580
 
590
- // Filter again
591
- followers = followers.filter(f => !getPrevFollowedUser(f));
592
-
593
- for (const follower of followers) {
594
- try {
595
- if (numFollowedForThisUser >= maxFollowsPerUser) {
596
- logger.log('Have reached followed limit for this user, stopping');
597
- return;
598
- }
599
-
600
- const graphqlUser = await navigateToUserAndGetData(follower);
601
-
602
- const followedByCount = graphqlUser.edge_followed_by.count;
603
- const followsCount = graphqlUser.edge_follow.count;
604
- const isPrivate = graphqlUser.is_private;
605
-
606
- // logger.log('followedByCount:', followedByCount, 'followsCount:', followsCount);
607
-
608
- const ratio = followedByCount / (followsCount || 1);
609
-
610
- if (isPrivate && skipPrivate) {
611
- logger.log('User is private, skipping');
612
- } else if (
613
- (followUserMaxFollowers != null && followedByCount > followUserMaxFollowers) ||
614
- (followUserMaxFollowing != null && followsCount > followUserMaxFollowing) ||
615
- (followUserMinFollowers != null && followedByCount < followUserMinFollowers) ||
616
- (followUserMinFollowing != null && followsCount < followUserMinFollowing)
617
- ) {
618
- logger.log('User has too many or too few followers or following, skipping.', 'followedByCount:', followedByCount, 'followsCount:', followsCount);
619
- } else if (
620
- (followUserRatioMax != null && ratio > followUserRatioMax) ||
621
- (followUserRatioMin != null && ratio < followUserRatioMin)
622
- ) {
623
- 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);
624
584
  } else {
625
- await followCurrentUser(follower);
626
- numFollowedForThisUser += 1;
627
-
628
- await sleep(10000);
585
+ try {
586
+ if (numFollowedForThisUser >= maxFollowsPerUser) {
587
+ logger.log('Have reached followed limit for this user, stopping');
588
+ return;
589
+ }
629
590
 
630
- if (!isPrivate && enableLikeImages && !hasReachedDailyLikesLimit()) {
631
- try {
632
- await likeCurrentUserImages({ username: follower, likeImagesMin, likeImagesMax });
633
- } catch (err) {
634
- logger.error(`Failed to follow user's images ${follower}`, err);
635
- 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();
636
632
  }
633
+ } catch (err) {
634
+ logger.error(`Failed to process follower ${follower}`, err);
635
+ await sleep(20000);
637
636
  }
638
-
639
- await sleep(20000);
640
- await throttle();
641
637
  }
642
- } catch (err) {
643
- logger.error(`Failed to process follower ${follower}`, err);
644
- await sleep(20000);
645
638
  }
646
639
  }
647
640
  }
@@ -672,136 +665,57 @@ const Instauto = async (db, browser, options) => {
672
665
  }
673
666
  }
674
667
 
675
- async function safelyUnfollowUserList(usersToUnfollow, limit) {
676
- logger.log(`Unfollowing ${usersToUnfollow.length} users`);
668
+ async function safelyUnfollowUserList(usersToUnfollow, limit, condition = () => true) {
669
+ logger.log('Unfollowing users, up to limit', limit);
677
670
 
678
671
  let i = 0; // Number of people processed
679
672
  let j = 0; // Number of people actually unfollowed (button pressed)
680
673
 
681
- for (const username of usersToUnfollow) {
682
- try {
683
- const userFound = await navigateToUser(username);
684
-
685
- if (!userFound) {
686
- await addPrevUnfollowedUser({ username, time: new Date().getTime(), noActionTaken: true });
687
- await sleep(3000);
688
- } else {
689
- 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
+ }
690
701
 
691
- if (noActionTaken) {
692
- await sleep(3000);
693
- } else {
694
- await sleep(15000);
695
- j += 1;
702
+ i += 1;
703
+ logger.log(`Have now unfollowed (or tried to unfollow) ${i} users`);
696
704
 
697
- if (j % 10 === 0) {
698
- logger.log('Have unfollowed 10 users since last break. Taking a break');
699
- await sleep(10 * 60 * 1000, 0.1);
705
+ if (limit && j >= limit) {
706
+ logger.log(`Have unfollowed limit of ${limit}, stopping`);
707
+ return j;
700
708
  }
701
- }
702
- }
703
-
704
- i += 1;
705
- logger.log(`Have now unfollowed ${i} users of total ${usersToUnfollow.length}`);
706
709
 
707
- if (limit && j >= limit) {
708
- logger.log(`Have unfollowed limit of ${limit}, stopping`);
709
- return;
710
+ await throttle();
711
+ } catch (err) {
712
+ logger.error('Failed to unfollow, continuing with next', err);
713
+ }
710
714
  }
711
-
712
- await throttle();
713
- } catch (err) {
714
- logger.error('Failed to unfollow, continuing with next', err);
715
715
  }
716
716
  }
717
- }
718
-
719
- async function unfollowNonMutualFollowers({ limit } = {}) {
720
- logger.log('Unfollowing non-mutual followers...');
721
- const userData = await navigateToUserAndGetData(myUsername);
722
-
723
- const allFollowers = await getFollowersOrFollowing({
724
- userId: userData.id,
725
- getFollowers: true,
726
- });
727
- const allFollowing = await getFollowersOrFollowing({
728
- userId: userData.id,
729
- getFollowers: false,
730
- });
731
- // logger.log('allFollowers:', allFollowers, 'allFollowing:', allFollowing);
732
-
733
- const usersToUnfollow = allFollowing.filter((u) => {
734
- if (allFollowers.includes(u)) return false; // Follows us
735
- if (excludeUsers.includes(u)) return false; // User is excluded by exclude list
736
- if (haveRecentlyFollowedUser(u)) {
737
- logger.log(`Have recently followed user ${u}, skipping`);
738
- return false;
739
- }
740
- return true;
741
- });
742
-
743
- logger.log('usersToUnfollow', JSON.stringify(usersToUnfollow));
744
-
745
- await safelyUnfollowUserList(usersToUnfollow, limit);
746
- }
747
-
748
- async function unfollowAllUnknown({ limit } = {}) {
749
- logger.log('Unfollowing all except excludes and auto followed');
750
- const userData = await navigateToUserAndGetData(myUsername);
751
-
752
- const allFollowing = await getFollowersOrFollowing({
753
- userId: userData.id,
754
- getFollowers: false,
755
- });
756
- // logger.log('allFollowing', allFollowing);
757
-
758
- const usersToUnfollow = allFollowing.filter((u) => {
759
- if (getPrevFollowedUser(u)) return false;
760
- if (excludeUsers.includes(u)) return false; // User is excluded by exclude list
761
- return true;
762
- });
763
-
764
- logger.log('usersToUnfollow', JSON.stringify(usersToUnfollow));
765
-
766
- await safelyUnfollowUserList(usersToUnfollow, limit);
767
- }
768
-
769
- async function unfollowOldFollowed({ ageInDays, limit } = {}) {
770
- assert(ageInDays);
771
-
772
- logger.log(`Unfollowing currently followed users who were auto-followed more than ${ageInDays} days ago...`);
773
-
774
- const userData = await navigateToUserAndGetData(myUsername);
775
-
776
- const allFollowing = await getFollowersOrFollowing({
777
- userId: userData.id,
778
- getFollowers: false,
779
- });
780
- // logger.log('allFollowing', allFollowing);
781
717
 
782
- const usersToUnfollow = allFollowing.filter(u =>
783
- getPrevFollowedUser(u) &&
784
- !excludeUsers.includes(u) &&
785
- (new Date().getTime() - getPrevFollowedUser(u).time) / (1000 * 60 * 60 * 24) > ageInDays)
786
- .slice(0, limit);
787
-
788
- logger.log('usersToUnfollow', JSON.stringify(usersToUnfollow));
789
-
790
- await safelyUnfollowUserList(usersToUnfollow, limit);
791
-
792
- return usersToUnfollow.length;
793
- }
794
-
795
- async function listManuallyFollowedUsers() {
796
- const userData = await navigateToUserAndGetData(myUsername);
797
-
798
- const allFollowing = await getFollowersOrFollowing({
799
- userId: userData.id,
800
- getFollowers: false,
801
- });
802
-
803
- return allFollowing.filter(u =>
804
- !getPrevFollowedUser(u) && !excludeUsers.includes(u));
718
+ return j;
805
719
  }
806
720
 
807
721
  function getPage() {
@@ -978,6 +892,119 @@ const Instauto = async (db, browser, options) => {
978
892
  logger.error('Failed to detect username', err);
979
893
  }
980
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
+
981
1008
  return {
982
1009
  followUserFollowers,
983
1010
  unfollowNonMutualFollowers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instauto",
3
- "version": "7.2.3",
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
  }