instauto 8.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) {
@@ -210,6 +212,10 @@ const Instauto = async (db, browser, options) => {
210
212
  return safeGotoUser(url, username);
211
213
  }
212
214
 
215
+ async function navigateToUserWithCheck(username) {
216
+ if (!(await navigateToUser(username))) throw new Error('User not found');
217
+ }
218
+
213
219
  async function getPageJson() {
214
220
  return JSON.parse(await (await (await page.$('pre')).getProperty('textContent')).jsonValue());
215
221
  }
@@ -226,11 +232,11 @@ const Instauto = async (db, browser, options) => {
226
232
 
227
233
  const { user } = json.graphql;
228
234
 
229
- await navigateToUser(username);
235
+ await navigateToUserWithCheck(username);
230
236
  return user;
231
237
  }
232
238
 
233
- await navigateToUser(username);
239
+ await navigateToUserWithCheck(username);
234
240
 
235
241
  // eslint-disable-next-line no-underscore-dangle
236
242
  const sharedData = await page.evaluate(() => window._sharedData);
@@ -311,8 +317,8 @@ const Instauto = async (db, browser, options) => {
311
317
  return elementHandles[0];
312
318
  }
313
319
 
314
- // NOTE: assumes we are on this page
315
- async function followCurrentUser(username) {
320
+ async function followUser(username) {
321
+ await navigateToUserWithCheck(username);
316
322
  const elementHandle = await findFollowButton();
317
323
 
318
324
  if (!elementHandle) {
@@ -353,7 +359,8 @@ const Instauto = async (db, browser, options) => {
353
359
 
354
360
  // See https://github.com/timgrossmann/InstaPy/pull/2345
355
361
  // https://github.com/timgrossmann/InstaPy/issues/2355
356
- async function unfollowCurrentUser(username) {
362
+ async function unfollowUser(username) {
363
+ await navigateToUserWithCheck(username);
357
364
  logger.log(`Unfollowing user ${username}`);
358
365
 
359
366
  const res = { username, time: new Date().getTime() };
@@ -549,9 +556,11 @@ const Instauto = async (db, browser, options) => {
549
556
  /* eslint-enable no-undef */
550
557
 
551
558
 
552
- async function likeCurrentUserImages({ username, likeImagesMin, likeImagesMax } = {}) {
559
+ async function likeUserImages({ username, likeImagesMin, likeImagesMax } = {}) {
553
560
  if (!likeImagesMin || !likeImagesMax || likeImagesMax < likeImagesMin || likeImagesMin < 1) throw new Error('Invalid arguments');
554
561
 
562
+ await navigateToUserWithCheck(username);
563
+
555
564
  logger.log(`Liking ${likeImagesMin}-${likeImagesMax} user images`);
556
565
  try {
557
566
  await page.exposeFunction('instautoSleep', sleep);
@@ -564,10 +573,57 @@ const Instauto = async (db, browser, options) => {
564
573
  await page.evaluate(likeCurrentUserImagesPageCode, { dryRun, likeImagesMin, likeImagesMax });
565
574
  }
566
575
 
567
- 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, {
568
621
  maxFollowsPerUser = 5, skipPrivate = false, enableLikeImages, likeImagesMin, likeImagesMax,
569
622
  } = {}) {
570
- 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}`);
571
627
 
572
628
  await throttle();
573
629
 
@@ -579,86 +635,52 @@ const Instauto = async (db, browser, options) => {
579
635
  logger.log('User followers batch', followersBatch);
580
636
 
581
637
  for (const follower of followersBatch) {
582
- if (getPrevFollowedUser(follower)) {
583
- logger.log('Skipping already followed user', follower);
584
- } else {
585
- try {
586
- if (numFollowedForThisUser >= maxFollowsPerUser) {
587
- logger.log('Have reached followed limit for this user, stopping');
588
- return;
589
- }
590
-
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);
638
+ await throttle();
620
639
 
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
- }
640
+ try {
641
+ if (enableFollow && numFollowedForThisUser >= maxFollowsPerUser) {
642
+ logger.log('Have reached followed limit for this user, stopping');
643
+ return;
644
+ }
629
645
 
630
- await sleep(20000);
631
- await throttle();
646
+ if (enableFollow) {
647
+ if (await followUserRespectingRestrictions({ username: follower, skipPrivate })) {
648
+ numFollowedForThisUser += 1;
632
649
  }
633
- } catch (err) {
634
- logger.error(`Failed to process follower ${follower}`, err);
635
- await sleep(20000);
636
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);
637
660
  }
638
661
  }
639
662
  }
640
663
  }
641
664
 
642
- async function followUsersFollowers({ usersToFollowFollowersOf, maxFollowsTotal = 150, skipPrivate, enableLikeImages = false, likeImagesMin = 1, likeImagesMax = 2 }) {
643
- if (!maxFollowsTotal || maxFollowsTotal <= 2) {
644
- throw new Error(`Invalid parameter maxFollowsTotal ${maxFollowsTotal}`);
645
- }
646
-
647
-
665
+ async function processUsersFollowers({ usersToFollowFollowersOf, maxFollowsTotal = 150, skipPrivate, enableFollow = true, enableLikeImages = false, likeImagesMin = 1, likeImagesMax = 2 }) {
648
666
  // If maxFollowsTotal turns out to be lower than the user list size, slice off the user list
649
667
  const usersToFollowFollowersOfSliced = shuffleArray(usersToFollowFollowersOf).slice(0, maxFollowsTotal);
650
668
 
651
- // Round up or we risk following none
652
- 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
+ }
653
675
 
654
676
  for (const username of usersToFollowFollowersOfSliced) {
655
677
  try {
656
- await followUserFollowers(username, { maxFollowsPerUser, skipPrivate, enableLikeImages, likeImagesMin, likeImagesMax });
678
+ await processUserFollowers(username, { maxFollowsPerUser, skipPrivate, enableLikeImages, likeImagesMin, likeImagesMax });
657
679
 
658
680
  await sleep(10 * 60 * 1000);
659
681
  await throttle();
660
682
  } catch (err) {
661
- console.error('Failed to follow user followers, continuing', err);
683
+ logger.error('Failed to process user followers, continuing', username, err);
662
684
  await takeScreenshot();
663
685
  await sleep(60 * 1000);
664
686
  }
@@ -684,7 +706,7 @@ const Instauto = async (db, browser, options) => {
684
706
  await addPrevUnfollowedUser({ username, time: new Date().getTime(), noActionTaken: true });
685
707
  await sleep(3000);
686
708
  } else {
687
- const { noActionTaken } = await unfollowCurrentUser(username);
709
+ const { noActionTaken } = await unfollowUser(username);
688
710
 
689
711
  if (noActionTaken) {
690
712
  await sleep(3000);
@@ -718,6 +740,22 @@ const Instauto = async (db, browser, options) => {
718
740
  return j;
719
741
  }
720
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
+
721
759
  function getPage() {
722
760
  return page;
723
761
  }
@@ -901,15 +939,12 @@ const Instauto = async (db, browser, options) => {
901
939
 
902
940
  // --- END OF INITIALIZATION
903
941
 
904
-
905
942
  async function doesUserFollowMe(username) {
906
943
  try {
907
944
  logger.info('Checking if user', username, 'follows us');
908
945
  const userData = await navigateToUserAndGetData(username);
909
946
  const userId = userData.id;
910
947
 
911
- if (!(await navigateToUser(username))) throw new Error('User not found');
912
-
913
948
  const elementHandles = await page.$x("//a[contains(.,' following')][contains(@href,'/following')]");
914
949
  if (elementHandles.length === 0) throw new Error('Following button not found');
915
950
 
@@ -924,7 +959,8 @@ const Instauto = async (db, browser, options) => {
924
959
 
925
960
  const { users } = JSON.parse(await foundResponse.text());
926
961
  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
962
+ // console.log(users, myUserId);
963
+ return users.some((user) => String(user.pk) === String(myUserId) || user.username === myUsername); // If they follow us, we will show at the top of the list
928
964
  } catch (err) {
929
965
  logger.error('Failed to check if user follows us', err);
930
966
  return undefined;
@@ -1006,19 +1042,22 @@ const Instauto = async (db, browser, options) => {
1006
1042
  }
1007
1043
 
1008
1044
  return {
1009
- followUserFollowers,
1045
+ followUserFollowers: processUserFollowers,
1010
1046
  unfollowNonMutualFollowers,
1011
1047
  unfollowAllUnknown,
1012
1048
  unfollowOldFollowed,
1013
- followCurrentUser,
1014
- unfollowCurrentUser,
1049
+ followUser,
1050
+ unfollowUser,
1051
+ likeUserImages,
1015
1052
  sleep,
1016
1053
  listManuallyFollowedUsers,
1017
1054
  getFollowersOrFollowing,
1018
1055
  getUsersWhoLikedContent,
1019
1056
  safelyUnfollowUserList,
1057
+ safelyFollowUserList,
1020
1058
  getPage,
1021
- followUsersFollowers,
1059
+ followUsersFollowers: processUsersFollowers,
1060
+ doesUserFollowMe,
1022
1061
  };
1023
1062
  };
1024
1063
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instauto",
3
- "version": "8.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": {