instauto 9.0.0 → 9.1.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.
package/README.md CHANGED
@@ -9,6 +9,7 @@ Now there is a GUI application for those who don't want to code: [SimpleInstaBot
9
9
  ## Setup
10
10
 
11
11
  - First install [Node.js](https://nodejs.org/en/) 8 or newer.
12
+ - On MacOS, it's recommended to use [homebrew](https://brew.sh/): `brew install node`
12
13
 
13
14
  - Create a new directory with a file like [example.js](https://github.com/mifi/instauto/blob/master/example.js)
14
15
 
package/example.js CHANGED
@@ -4,6 +4,10 @@ const puppeteer = require('puppeteer'); // eslint-disable-line import/no-extrane
4
4
 
5
5
  const Instauto = require('instauto'); // eslint-disable-line import/no-unresolved
6
6
 
7
+ // Optional: Custom logger with timestamps
8
+ const log = (fn, ...args) => console[fn](new Date().toISOString(), ...args);
9
+ const logger = Object.fromEntries(['log', 'info', 'debug', 'error', 'trace', 'warn'].map((fn) => [fn, (...args) => log(fn, ...args)]));
10
+
7
11
  const options = {
8
12
  cookiesPath: './cookies.json',
9
13
 
@@ -42,6 +46,8 @@ const options = {
42
46
 
43
47
  // If true, will not do any actions (defaults to true)
44
48
  dryRun: false,
49
+
50
+ logger,
45
51
  };
46
52
 
47
53
  (async () => {
package/index.js CHANGED
@@ -136,28 +136,30 @@ const Instauto = async (db, browser, options) => {
136
136
  }
137
137
 
138
138
  async function checkReachedFollowedUserDayLimit() {
139
- const reachedFollowedUserDayLimit = getNumFollowedUsersThisTimeUnit(dayMs) >= maxFollowsPerDay;
140
- if (reachedFollowedUserDayLimit) {
139
+ if (getNumFollowedUsersThisTimeUnit(dayMs) >= maxFollowsPerDay) {
141
140
  logger.log('Have reached daily follow/unfollow limit, waiting 10 min');
142
141
  await sleep(10 * 60 * 1000);
143
142
  }
144
143
  }
145
144
 
146
145
  async function checkReachedFollowedUserHourLimit() {
147
- const hasReachedFollowedUserHourLimit = getNumFollowedUsersThisTimeUnit(hourMs) >= maxFollowsPerHour;
148
- if (hasReachedFollowedUserHourLimit) {
146
+ if (getNumFollowedUsersThisTimeUnit(hourMs) >= maxFollowsPerHour) {
149
147
  logger.log('Have reached hourly follow rate limit, pausing 10 min');
150
148
  await sleep(10 * 60 * 1000);
151
149
  }
152
150
  }
153
151
 
152
+ async function checkReachedLikedUserDayLimit() {
153
+ if (getNumLikesThisTimeUnit(dayMs) >= maxLikesPerDay) {
154
+ logger.log('Have reached daily like rate limit, pausing 10 min');
155
+ await sleep(10 * 60 * 1000);
156
+ }
157
+ }
158
+
154
159
  async function throttle() {
155
160
  await checkReachedFollowedUserDayLimit();
156
161
  await checkReachedFollowedUserHourLimit();
157
- }
158
-
159
- function hasReachedDailyLikesLimit() {
160
- return getNumLikesThisTimeUnit(dayMs) >= maxLikesPerDay;
162
+ await checkReachedLikedUserDayLimit();
161
163
  }
162
164
 
163
165
  function haveRecentlyFollowedUser(username) {
@@ -571,10 +573,57 @@ const Instauto = async (db, browser, options) => {
571
573
  await page.evaluate(likeCurrentUserImagesPageCode, { dryRun, likeImagesMin, likeImagesMax });
572
574
  }
573
575
 
574
- async function followUserFollowers(username, {
576
+ async function followUserRespectingRestrictions({ username, skipPrivate = false }) {
577
+ if (getPrevFollowedUser(username)) {
578
+ logger.log('Skipping already followed user', username);
579
+ return false;
580
+ }
581
+ const graphqlUser = await navigateToUserAndGetData(username);
582
+
583
+ const followedByCount = graphqlUser.edge_followed_by.count;
584
+ const followsCount = graphqlUser.edge_follow.count;
585
+ const isPrivate = graphqlUser.is_private;
586
+
587
+ // logger.log('followedByCount:', followedByCount, 'followsCount:', followsCount);
588
+
589
+ const ratio = followedByCount / (followsCount || 1);
590
+
591
+ if (isPrivate && skipPrivate) {
592
+ logger.log('User is private, skipping');
593
+ return false;
594
+ }
595
+ if (
596
+ (followUserMaxFollowers != null && followedByCount > followUserMaxFollowers) ||
597
+ (followUserMaxFollowing != null && followsCount > followUserMaxFollowing) ||
598
+ (followUserMinFollowers != null && followedByCount < followUserMinFollowers) ||
599
+ (followUserMinFollowing != null && followsCount < followUserMinFollowing)
600
+ ) {
601
+ logger.log('User has too many or too few followers or following, skipping.', 'followedByCount:', followedByCount, 'followsCount:', followsCount);
602
+ return false;
603
+ }
604
+ if (
605
+ (followUserRatioMax != null && ratio > followUserRatioMax) ||
606
+ (followUserRatioMin != null && ratio < followUserRatioMin)
607
+ ) {
608
+ logger.log('User has too many followers compared to follows or opposite, skipping');
609
+ return false;
610
+ }
611
+
612
+ await followUser(username);
613
+
614
+ await sleep(30000);
615
+ await throttle();
616
+
617
+ return true;
618
+ }
619
+
620
+ async function processUserFollowers(username, {
575
621
  maxFollowsPerUser = 5, skipPrivate = false, enableLikeImages, likeImagesMin, likeImagesMax,
576
622
  } = {}) {
577
- logger.log(`Following up to ${maxFollowsPerUser} followers of ${username}`);
623
+ const enableFollow = maxFollowsPerUser > 0;
624
+
625
+ if (enableFollow) logger.log(`Following up to ${maxFollowsPerUser} followers of ${username}`);
626
+ if (enableLikeImages) logger.log(`Liking images of up to ${likeImagesMax} followers of ${username}`);
578
627
 
579
628
  await throttle();
580
629
 
@@ -586,86 +635,52 @@ const Instauto = async (db, browser, options) => {
586
635
  logger.log('User followers batch', followersBatch);
587
636
 
588
637
  for (const follower of followersBatch) {
589
- if (getPrevFollowedUser(follower)) {
590
- logger.log('Skipping already followed user', follower);
591
- } else {
592
- try {
593
- if (numFollowedForThisUser >= maxFollowsPerUser) {
594
- logger.log('Have reached followed limit for this user, stopping');
595
- return;
596
- }
597
-
598
- const graphqlUser = await navigateToUserAndGetData(follower);
599
-
600
- const followedByCount = graphqlUser.edge_followed_by.count;
601
- const followsCount = graphqlUser.edge_follow.count;
602
- const isPrivate = graphqlUser.is_private;
603
-
604
- // logger.log('followedByCount:', followedByCount, 'followsCount:', followsCount);
605
-
606
- const ratio = followedByCount / (followsCount || 1);
607
-
608
- if (isPrivate && skipPrivate) {
609
- logger.log('User is private, skipping');
610
- } else if (
611
- (followUserMaxFollowers != null && followedByCount > followUserMaxFollowers) ||
612
- (followUserMaxFollowing != null && followsCount > followUserMaxFollowing) ||
613
- (followUserMinFollowers != null && followedByCount < followUserMinFollowers) ||
614
- (followUserMinFollowing != null && followsCount < followUserMinFollowing)
615
- ) {
616
- logger.log('User has too many or too few followers or following, skipping.', 'followedByCount:', followedByCount, 'followsCount:', followsCount);
617
- } else if (
618
- (followUserRatioMax != null && ratio > followUserRatioMax) ||
619
- (followUserRatioMin != null && ratio < followUserRatioMin)
620
- ) {
621
- logger.log('User has too many followers compared to follows or opposite, skipping');
622
- } else {
623
- await followUser(follower);
624
- numFollowedForThisUser += 1;
625
-
626
- await sleep(10000);
638
+ await throttle();
627
639
 
628
- if (!isPrivate && enableLikeImages && !hasReachedDailyLikesLimit()) {
629
- try {
630
- await likeUserImages({ username: follower, likeImagesMin, likeImagesMax });
631
- } catch (err) {
632
- logger.error(`Failed to follow user's images ${follower}`, err);
633
- await takeScreenshot();
634
- }
635
- }
640
+ try {
641
+ if (enableFollow && numFollowedForThisUser >= maxFollowsPerUser) {
642
+ logger.log('Have reached followed limit for this user, stopping');
643
+ return;
644
+ }
636
645
 
637
- await sleep(20000);
638
- await throttle();
646
+ if (enableFollow) {
647
+ if (await followUserRespectingRestrictions({ username: follower, skipPrivate })) {
648
+ numFollowedForThisUser += 1;
639
649
  }
640
- } catch (err) {
641
- logger.error(`Failed to process follower ${follower}`, err);
642
- await sleep(20000);
643
650
  }
651
+
652
+ if (enableLikeImages) {
653
+ // Note: throws error if user isPrivate
654
+ await likeUserImages({ username: follower, likeImagesMin, likeImagesMax });
655
+ }
656
+ } catch (err) {
657
+ logger.error(`Failed to process follower ${follower}`, err);
658
+ await takeScreenshot();
659
+ await sleep(20000);
644
660
  }
645
661
  }
646
662
  }
647
663
  }
648
664
 
649
- async function followUsersFollowers({ usersToFollowFollowersOf, maxFollowsTotal = 150, skipPrivate, enableLikeImages = false, likeImagesMin = 1, likeImagesMax = 2 }) {
650
- if (!maxFollowsTotal || maxFollowsTotal <= 2) {
651
- throw new Error(`Invalid parameter maxFollowsTotal ${maxFollowsTotal}`);
652
- }
653
-
654
-
665
+ async function processUsersFollowers({ usersToFollowFollowersOf, maxFollowsTotal = 150, skipPrivate, enableFollow = true, enableLikeImages = false, likeImagesMin = 1, likeImagesMax = 2 }) {
655
666
  // If maxFollowsTotal turns out to be lower than the user list size, slice off the user list
656
667
  const usersToFollowFollowersOfSliced = shuffleArray(usersToFollowFollowersOf).slice(0, maxFollowsTotal);
657
668
 
658
- // Round up or we risk following none
659
- const maxFollowsPerUser = Math.floor(maxFollowsTotal / usersToFollowFollowersOfSliced.length) + 1;
669
+ const maxFollowsPerUser = enableFollow && usersToFollowFollowersOfSliced.length > 0 ? Math.floor(maxFollowsTotal / usersToFollowFollowersOfSliced.length) : 0;
670
+
671
+ if (maxFollowsPerUser === 0 && (!enableLikeImages || likeImagesMin < 1 || likeImagesMax < 1)) {
672
+ logger.warn('Nothing to follow or like');
673
+ return;
674
+ }
660
675
 
661
676
  for (const username of usersToFollowFollowersOfSliced) {
662
677
  try {
663
- await followUserFollowers(username, { maxFollowsPerUser, skipPrivate, enableLikeImages, likeImagesMin, likeImagesMax });
678
+ await processUserFollowers(username, { maxFollowsPerUser, skipPrivate, enableLikeImages, likeImagesMin, likeImagesMax });
664
679
 
665
680
  await sleep(10 * 60 * 1000);
666
681
  await throttle();
667
682
  } catch (err) {
668
- console.error('Failed to follow user followers, continuing', err);
683
+ logger.error('Failed to process user followers, continuing', username, err);
669
684
  await takeScreenshot();
670
685
  await sleep(60 * 1000);
671
686
  }
@@ -725,6 +740,22 @@ const Instauto = async (db, browser, options) => {
725
740
  return j;
726
741
  }
727
742
 
743
+ async function safelyFollowUserList({ users, skipPrivate, limit }) {
744
+ logger.log('Following users, up to limit', limit);
745
+
746
+ for (const username of users) {
747
+ await throttle();
748
+
749
+ try {
750
+ await followUserRespectingRestrictions({ username, skipPrivate });
751
+ } catch (err) {
752
+ logger.error(`Failed to follow user ${username}, continuing`, err);
753
+ await takeScreenshot();
754
+ await sleep(20000);
755
+ }
756
+ }
757
+ }
758
+
728
759
  function getPage() {
729
760
  return page;
730
761
  }
@@ -1011,7 +1042,7 @@ const Instauto = async (db, browser, options) => {
1011
1042
  }
1012
1043
 
1013
1044
  return {
1014
- followUserFollowers,
1045
+ followUserFollowers: processUserFollowers,
1015
1046
  unfollowNonMutualFollowers,
1016
1047
  unfollowAllUnknown,
1017
1048
  unfollowOldFollowed,
@@ -1023,8 +1054,9 @@ const Instauto = async (db, browser, options) => {
1023
1054
  getFollowersOrFollowing,
1024
1055
  getUsersWhoLikedContent,
1025
1056
  safelyUnfollowUserList,
1057
+ safelyFollowUserList,
1026
1058
  getPage,
1027
- followUsersFollowers,
1059
+ followUsersFollowers: processUsersFollowers,
1028
1060
  doesUserFollowMe,
1029
1061
  };
1030
1062
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instauto",
3
- "version": "9.0.0",
3
+ "version": "9.1.0",
4
4
  "description": "Instagram automation library written in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {