instauto 7.2.2 → 8.0.1
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 +297 -238
- 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);
|
|
571
|
-
|
|
572
|
-
// Filter again
|
|
573
|
-
followers = followers.filter(f => !getPrevFollowedUser(f));
|
|
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
|
-
}
|
|
578
|
+
for await (const followersBatch of getFollowersOrFollowingGenerator({ userId: userData.id, getFollowers: true })) {
|
|
579
|
+
logger.log('User followers batch', followersBatch);
|
|
581
580
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
686
|
-
i += 1;
|
|
687
|
-
logger.log(`Have now unfollowed ${i} users of total ${usersToUnfollow.length}`);
|
|
688
709
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
715
|
-
const usersToUnfollow = allFollowing.filter((u) => {
|
|
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
717
|
|
|
785
|
-
return
|
|
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());
|
|
@@ -802,27 +738,19 @@ const Instauto = async (db, browser, options) => {
|
|
|
802
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');
|
|
@@ -841,6 +769,10 @@ const Instauto = async (db, browser, options) => {
|
|
|
841
769
|
|
|
842
770
|
if (alreadyEnglish) {
|
|
843
771
|
logger.log('Already English language');
|
|
772
|
+
if (!assumeLoggedIn) {
|
|
773
|
+
await goHome(); // because we were on the settings page
|
|
774
|
+
await sleep(1000);
|
|
775
|
+
}
|
|
844
776
|
return;
|
|
845
777
|
}
|
|
846
778
|
|
|
@@ -850,12 +782,23 @@ const Instauto = async (db, browser, options) => {
|
|
|
850
782
|
await sleep(1000);
|
|
851
783
|
} catch (err) {
|
|
852
784
|
logger.error('Failed to set language, trying fallback (cookie)', err);
|
|
853
|
-
|
|
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);
|
|
854
797
|
}
|
|
855
798
|
}
|
|
856
799
|
|
|
857
|
-
const setEnglishLang = async () => setLang('en', 'English');
|
|
858
|
-
// const setEnglishLang = async () => setLang('de', 'Deutsch');
|
|
800
|
+
const setEnglishLang = async (assumeLoggedIn) => setLang('en', 'English', assumeLoggedIn);
|
|
801
|
+
// const setEnglishLang = async (assumeLoggedIn) => setLang('de', 'Deutsch', assumeLoggedIn);
|
|
859
802
|
|
|
860
803
|
async function tryPressButton(elementHandles, name, sleepMs = 3000) {
|
|
861
804
|
try {
|
|
@@ -869,7 +812,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
869
812
|
}
|
|
870
813
|
}
|
|
871
814
|
|
|
872
|
-
await setEnglishLang();
|
|
815
|
+
await setEnglishLang(false);
|
|
873
816
|
|
|
874
817
|
await tryPressButton(await page.$x('//button[contains(text(), "Accept")]'), 'Accept cookies dialog');
|
|
875
818
|
await tryPressButton(await page.$x('//button[contains(text(), "Only allow essential cookies")]'), 'Accept cookies dialog 2 button 1', 10000);
|
|
@@ -924,7 +867,8 @@ const Instauto = async (db, browser, options) => {
|
|
|
924
867
|
}
|
|
925
868
|
|
|
926
869
|
// In case language gets reset after logging in
|
|
927
|
-
|
|
870
|
+
// https://github.com/mifi/SimpleInstaBot/issues/118
|
|
871
|
+
await setEnglishLang(true);
|
|
928
872
|
|
|
929
873
|
// Mobile version https://github.com/mifi/SimpleInstaBot/issues/7
|
|
930
874
|
await tryPressButton(await page.$x('//button[contains(text(), "Save Info")]'), 'Login info dialog: Save Info');
|
|
@@ -948,6 +892,120 @@ const Instauto = async (db, browser, options) => {
|
|
|
948
892
|
logger.error('Failed to detect username', err);
|
|
949
893
|
}
|
|
950
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
|
+
// console.log(users, myUserId);
|
|
928
|
+
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
|
|
929
|
+
} catch (err) {
|
|
930
|
+
logger.error('Failed to check if user follows us', err);
|
|
931
|
+
return undefined;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async function unfollowNonMutualFollowers({ limit } = {}) {
|
|
936
|
+
logger.log(`Unfollowing non-mutual followers (limit ${limit})...`);
|
|
937
|
+
|
|
938
|
+
/* const allFollowers = await getFollowersOrFollowing({
|
|
939
|
+
userId: myUserId,
|
|
940
|
+
getFollowers: true,
|
|
941
|
+
}); */
|
|
942
|
+
const allFollowingGenerator = getFollowersOrFollowingGenerator({
|
|
943
|
+
userId: myUserId,
|
|
944
|
+
getFollowers: false,
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
async function condition(username) {
|
|
948
|
+
// if (allFollowers.includes(u)) return false; // Follows us
|
|
949
|
+
if (excludeUsers.includes(username)) return false; // User is excluded by exclude list
|
|
950
|
+
if (haveRecentlyFollowedUser(username)) {
|
|
951
|
+
logger.log(`Have recently followed user ${username}, skipping`);
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const followsMe = await doesUserFollowMe(username);
|
|
956
|
+
logger.info('User follows us?', followsMe);
|
|
957
|
+
return followsMe === false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
await safelyUnfollowUserList(allFollowingGenerator, limit, condition);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
async function unfollowAllUnknown({ limit } = {}) {
|
|
964
|
+
logger.log('Unfollowing all except excludes and auto followed');
|
|
965
|
+
|
|
966
|
+
const unfollowUsersGenerator = getFollowersOrFollowingGenerator({
|
|
967
|
+
userId: myUserId,
|
|
968
|
+
getFollowers: false,
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
function condition(username) {
|
|
972
|
+
if (getPrevFollowedUser(username)) return false; // we followed this user, so it's not unknown
|
|
973
|
+
if (excludeUsers.includes(username)) return false; // User is excluded by exclude list
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
await safelyUnfollowUserList(unfollowUsersGenerator, limit, condition);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async function unfollowOldFollowed({ ageInDays, limit } = {}) {
|
|
981
|
+
assert(ageInDays);
|
|
982
|
+
|
|
983
|
+
logger.log(`Unfollowing currently followed users who were auto-followed more than ${ageInDays} days ago (limit ${limit})...`);
|
|
984
|
+
|
|
985
|
+
const followingUsersGenerator = getFollowersOrFollowingGenerator({
|
|
986
|
+
userId: myUserId,
|
|
987
|
+
getFollowers: false,
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
function condition(username) {
|
|
991
|
+
return getPrevFollowedUser(username) &&
|
|
992
|
+
!excludeUsers.includes(username) &&
|
|
993
|
+
(new Date().getTime() - getPrevFollowedUser(username).time) / (1000 * 60 * 60 * 24) > ageInDays;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return safelyUnfollowUserList(followingUsersGenerator, limit, condition);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function listManuallyFollowedUsers() {
|
|
1000
|
+
const allFollowing = await getFollowersOrFollowing({
|
|
1001
|
+
userId: myUserId,
|
|
1002
|
+
getFollowers: false,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
return allFollowing.filter(u =>
|
|
1006
|
+
!getPrevFollowedUser(u) && !excludeUsers.includes(u));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
951
1009
|
return {
|
|
952
1010
|
followUserFollowers,
|
|
953
1011
|
unfollowNonMutualFollowers,
|
|
@@ -962,6 +1020,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
962
1020
|
safelyUnfollowUserList,
|
|
963
1021
|
getPage,
|
|
964
1022
|
followUsersFollowers,
|
|
1023
|
+
doesUserFollowMe,
|
|
965
1024
|
};
|
|
966
1025
|
};
|
|
967
1026
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instauto",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
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
|
}
|