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.
- package/.eslintrc +4 -3
- package/index.js +319 -246
- 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,
|
|
121
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
434
|
-
|
|
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
|
-
|
|
561
|
-
|
|
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
|
-
|
|
573
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
585
|
+
try {
|
|
586
|
+
if (numFollowedForThisUser >= maxFollowsPerUser) {
|
|
587
|
+
logger.log('Have reached followed limit for this user, stopping');
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
611
590
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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(
|
|
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
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
674
|
-
|
|
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 (
|
|
680
|
-
logger.log(
|
|
681
|
-
|
|
705
|
+
if (limit && j >= limit) {
|
|
706
|
+
logger.log(`Have unfollowed limit of ${limit}, stopping`);
|
|
707
|
+
return j;
|
|
682
708
|
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
709
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
891
|
-
|
|
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
|
-
|
|
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": "
|
|
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": "^
|
|
22
|
+
"eslint": "^7.32.0 || ^8.2.0",
|
|
23
23
|
"eslint-config-airbnb": "^16.1.0",
|
|
24
|
-
"eslint-
|
|
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
|
}
|