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 +1 -0
- package/example.js +6 -0
- package/index.js +125 -86
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
235
|
+
await navigateToUserWithCheck(username);
|
|
230
236
|
return user;
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
await
|
|
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
|
-
|
|
315
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
631
|
-
|
|
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
|
|
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
|
-
|
|
652
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
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
|
|