instauto 9.1.0 → 9.1.3
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 -1
- package/package.json +5 -2
- package/{db.js → src/db.js} +0 -0
- package/{index.js → src/index.js} +25 -44
- package/.eslintrc +0 -36
- package/.github/FUNDING.yml +0 -2
- package/example.js +0 -107
- package/instabot.yml +0 -5
- package/logo.png +0 -0
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Now there is a GUI application for those who don't want to code: [SimpleInstaBot
|
|
|
25
25
|
|
|
26
26
|
You can run this code for example once every day using cron or pm2 or similar
|
|
27
27
|
|
|
28
|
-
See [index.js](https://github.com/mifi/instauto/blob/master/index.js) for available options.
|
|
28
|
+
See [index.js](https://github.com/mifi/instauto/blob/master/src/index.js) for available options.
|
|
29
29
|
|
|
30
30
|
## Supported functionality
|
|
31
31
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instauto",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.3",
|
|
4
4
|
"description": "Instagram automation library written in Node.js",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"lint": "eslint .",
|
|
8
8
|
"test": "exit 0"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
10
13
|
"author": "Mikael Finstad <finstaden@gmail.com>",
|
|
11
14
|
"repository": "mifi/instauto",
|
|
12
15
|
"license": "MIT",
|
package/{db.js → src/db.js}
RENAMED
|
File without changes
|
|
@@ -73,7 +73,6 @@ const Instauto = async (db, browser, options) => {
|
|
|
73
73
|
|
|
74
74
|
// State
|
|
75
75
|
let page;
|
|
76
|
-
let graphqlUserMissing = false;
|
|
77
76
|
|
|
78
77
|
async function takeScreenshot() {
|
|
79
78
|
if (!screenshotOnError) return;
|
|
@@ -221,37 +220,17 @@ const Instauto = async (db, browser, options) => {
|
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
async function navigateToUserAndGetData(username) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
await navigateToUserWithCheck(username);
|
|
236
|
-
return user;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
await navigateToUserWithCheck(username);
|
|
240
|
-
|
|
241
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
242
|
-
const sharedData = await page.evaluate(() => window._sharedData);
|
|
243
|
-
try {
|
|
244
|
-
// eslint-disable-next-line prefer-destructuring
|
|
245
|
-
return sharedData.entry_data.ProfilePage[0].graphql.user;
|
|
246
|
-
|
|
247
|
-
// JSON.parse(Array.from(document.getElementsByTagName('script')).find(el => el.innerHTML.startsWith('window.__additionalDataLoaded(\'feed\',')).innerHTML.replace(/^window.__additionalDataLoaded\('feed',({.*})\);$/, '$1'));
|
|
248
|
-
// JSON.parse(Array.from(document.getElementsByTagName('script')).find(el => el.innerHTML.startsWith('window._sharedData')).innerHTML.replace(/^window._sharedData ?= ?({.*});$/, '$1'));
|
|
249
|
-
// Array.from(document.getElementsByTagName('a')).find(el => el.attributes?.href?.value.includes(`${username}/followers`)).innerText
|
|
250
|
-
} catch (err) {
|
|
251
|
-
logger.warn('Missing graphql in page, falling back to alternative method...');
|
|
252
|
-
graphqlUserMissing = true; // Store as state so we don't have to do this every time from now on.
|
|
253
|
-
return navigateToUserAndGetData(username); // Now try again with alternative method
|
|
254
|
-
}
|
|
223
|
+
const [foundResponse] = await Promise.all([
|
|
224
|
+
page.waitForResponse((response) => {
|
|
225
|
+
const request = response.request();
|
|
226
|
+
return request.method() === 'GET' && new RegExp(`https:\\/\\/i\\.instagram\\.com\\/api\\/v1\\/users\\/web_profile_info\\/\\?username=${encodeURIComponent(username.toLowerCase())}`).test(request.url());
|
|
227
|
+
}),
|
|
228
|
+
navigateToUserWithCheck(username),
|
|
229
|
+
// page.waitForNavigation({ waitUntil: 'networkidle0' }),
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
const json = JSON.parse(await foundResponse.text());
|
|
233
|
+
return json.data.user;
|
|
255
234
|
}
|
|
256
235
|
|
|
257
236
|
async function isActionBlocked() {
|
|
@@ -309,6 +288,9 @@ const Instauto = async (db, browser, options) => {
|
|
|
309
288
|
const elementHandles3 = await page.$x("//header//button[*//span[@aria-label='Following']]");
|
|
310
289
|
if (elementHandles3.length > 0) return elementHandles3[0];
|
|
311
290
|
|
|
291
|
+
const elementHandles4 = await page.$x("//header//button[*//*[name()='svg'][@aria-label='Following']]");
|
|
292
|
+
if (elementHandles4.length > 0) return elementHandles4[0];
|
|
293
|
+
|
|
312
294
|
return undefined;
|
|
313
295
|
}
|
|
314
296
|
|
|
@@ -578,6 +560,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
578
560
|
logger.log('Skipping already followed user', username);
|
|
579
561
|
return false;
|
|
580
562
|
}
|
|
563
|
+
|
|
581
564
|
const graphqlUser = await navigateToUserAndGetData(username);
|
|
582
565
|
|
|
583
566
|
const followedByCount = graphqlUser.edge_followed_by.count;
|
|
@@ -629,9 +612,9 @@ const Instauto = async (db, browser, options) => {
|
|
|
629
612
|
|
|
630
613
|
let numFollowedForThisUser = 0;
|
|
631
614
|
|
|
632
|
-
const
|
|
615
|
+
const { id: userId } = await navigateToUserAndGetData(username);
|
|
633
616
|
|
|
634
|
-
for await (const followersBatch of getFollowersOrFollowingGenerator({ userId
|
|
617
|
+
for await (const followersBatch of getFollowersOrFollowingGenerator({ userId, getFollowers: true })) {
|
|
635
618
|
logger.log('User followers batch', followersBatch);
|
|
636
619
|
|
|
637
620
|
for (const follower of followersBatch) {
|
|
@@ -643,13 +626,13 @@ const Instauto = async (db, browser, options) => {
|
|
|
643
626
|
return;
|
|
644
627
|
}
|
|
645
628
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
629
|
+
let didActuallyFollow = false;
|
|
630
|
+
if (enableFollow) didActuallyFollow = await followUserRespectingRestrictions({ username: follower, skipPrivate });
|
|
631
|
+
if (didActuallyFollow) numFollowedForThisUser += 1;
|
|
632
|
+
|
|
633
|
+
const didFailToFollow = enableFollow && !didActuallyFollow;
|
|
651
634
|
|
|
652
|
-
if (enableLikeImages) {
|
|
635
|
+
if (enableLikeImages && !didFailToFollow) {
|
|
653
636
|
// Note: throws error if user isPrivate
|
|
654
637
|
await likeUserImages({ username: follower, likeImagesMin, likeImagesMax });
|
|
655
638
|
}
|
|
@@ -934,16 +917,14 @@ const Instauto = async (db, browser, options) => {
|
|
|
934
917
|
throw new Error('Don\'t know what\'s my username');
|
|
935
918
|
}
|
|
936
919
|
|
|
937
|
-
const
|
|
938
|
-
const myUserId = myUserData.id;
|
|
920
|
+
const { id: myUserId } = await navigateToUserAndGetData(myUsername);
|
|
939
921
|
|
|
940
922
|
// --- END OF INITIALIZATION
|
|
941
923
|
|
|
942
924
|
async function doesUserFollowMe(username) {
|
|
943
925
|
try {
|
|
944
926
|
logger.info('Checking if user', username, 'follows us');
|
|
945
|
-
const
|
|
946
|
-
const userId = userData.id;
|
|
927
|
+
const { id: userId } = await navigateToUserAndGetData(username);
|
|
947
928
|
|
|
948
929
|
const elementHandles = await page.$x("//a[contains(.,' following')][contains(@href,'/following')]");
|
|
949
930
|
if (elementHandles.length === 0) throw new Error('Following button not found');
|
package/.eslintrc
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"env": {
|
|
3
|
-
"node": true
|
|
4
|
-
},
|
|
5
|
-
"extends": "airbnb/base",
|
|
6
|
-
"parserOptions": {
|
|
7
|
-
"sourceType": "script",
|
|
8
|
-
"ecmaVersion": 2022
|
|
9
|
-
},
|
|
10
|
-
"globals": {
|
|
11
|
-
"window": true,
|
|
12
|
-
"document": true
|
|
13
|
-
},
|
|
14
|
-
"rules": {
|
|
15
|
-
"max-len": 0,
|
|
16
|
-
"arrow-parens": 0,
|
|
17
|
-
"no-console": 0,
|
|
18
|
-
"no-await-in-loop": 0,
|
|
19
|
-
"object-curly-newline": 0,
|
|
20
|
-
'no-restricted-syntax': [
|
|
21
|
-
'error',
|
|
22
|
-
{
|
|
23
|
-
selector: 'ForInStatement',
|
|
24
|
-
message: 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
selector: 'LabeledStatement',
|
|
28
|
-
message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
selector: 'WithStatement',
|
|
32
|
-
message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
}
|
|
36
|
-
}
|
package/.github/FUNDING.yml
DELETED
package/example.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const puppeteer = require('puppeteer'); // eslint-disable-line import/no-extraneous-dependencies
|
|
4
|
-
|
|
5
|
-
const Instauto = require('instauto'); // eslint-disable-line import/no-unresolved
|
|
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
|
-
|
|
11
|
-
const options = {
|
|
12
|
-
cookiesPath: './cookies.json',
|
|
13
|
-
|
|
14
|
-
username: 'your-ig-username',
|
|
15
|
-
password: 'your-ig-password',
|
|
16
|
-
|
|
17
|
-
// Global limit that prevents follow or unfollows (total) to exceed this number over a sliding window of one hour:
|
|
18
|
-
maxFollowsPerHour: 20,
|
|
19
|
-
// Global limit that prevents follow or unfollows (total) to exceed this number over a sliding window of one day:
|
|
20
|
-
maxFollowsPerDay: 150,
|
|
21
|
-
// (NOTE setting the above parameters too high will cause temp ban/throttle)
|
|
22
|
-
|
|
23
|
-
maxLikesPerDay: 50,
|
|
24
|
-
|
|
25
|
-
// Don't follow users that have a followers / following ratio less than this:
|
|
26
|
-
followUserRatioMin: 0.2,
|
|
27
|
-
// Don't follow users that have a followers / following ratio higher than this:
|
|
28
|
-
followUserRatioMax: 4.0,
|
|
29
|
-
// Don't follow users who have more followers than this:
|
|
30
|
-
followUserMaxFollowers: null,
|
|
31
|
-
// Don't follow users who have more people following them than this:
|
|
32
|
-
followUserMaxFollowing: null,
|
|
33
|
-
// Don't follow users who have less followers than this:
|
|
34
|
-
followUserMinFollowers: null,
|
|
35
|
-
// Don't follow users who have more people following them than this:
|
|
36
|
-
followUserMinFollowing: null,
|
|
37
|
-
|
|
38
|
-
// NOTE: The dontUnfollowUntilTimeElapsed option is ONLY for the unfollowNonMutualFollowers function
|
|
39
|
-
// This specifies the time during which the bot should not touch users that it has previously followed (in milliseconds)
|
|
40
|
-
// After this time has passed, it will be able to unfollow them again.
|
|
41
|
-
// TODO should remove this option from here
|
|
42
|
-
dontUnfollowUntilTimeElapsed: 3 * 24 * 60 * 60 * 1000,
|
|
43
|
-
|
|
44
|
-
// Usernames that we should not touch, e.g. your friends and actual followings
|
|
45
|
-
excludeUsers: [],
|
|
46
|
-
|
|
47
|
-
// If true, will not do any actions (defaults to true)
|
|
48
|
-
dryRun: false,
|
|
49
|
-
|
|
50
|
-
logger,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
(async () => {
|
|
54
|
-
let browser;
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
browser = await puppeteer.launch({ headless: false });
|
|
58
|
-
|
|
59
|
-
// Create a database where state will be loaded/saved to
|
|
60
|
-
const instautoDb = await Instauto.JSONDB({
|
|
61
|
-
// Will store a list of all users that have been followed before, to prevent future re-following.
|
|
62
|
-
followedDbPath: './followed.json',
|
|
63
|
-
// Will store all unfollowed users here
|
|
64
|
-
unfollowedDbPath: './unfollowed.json',
|
|
65
|
-
// Will store all likes here
|
|
66
|
-
likedPhotosDbPath: './liked-photos.json',
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const instauto = await Instauto(instautoDb, browser, options);
|
|
70
|
-
|
|
71
|
-
// This can be used to unfollow people:
|
|
72
|
-
// Will unfollow auto-followed AND manually followed accounts who are not following us back, after some time has passed
|
|
73
|
-
// The time is specified by config option dontUnfollowUntilTimeElapsed
|
|
74
|
-
// await instauto.unfollowNonMutualFollowers();
|
|
75
|
-
// await instauto.sleep(10 * 60 * 1000);
|
|
76
|
-
|
|
77
|
-
// Unfollow previously auto-followed users (regardless of whether or not they are following us back)
|
|
78
|
-
// after a certain amount of days (2 weeks)
|
|
79
|
-
// Leave room to do following after this too (unfollow 2/3 of maxFollowsPerDay)
|
|
80
|
-
const unfollowedCount = await instauto.unfollowOldFollowed({ ageInDays: 14, limit: options.maxFollowsPerDay * (2 / 3) });
|
|
81
|
-
|
|
82
|
-
if (unfollowedCount > 0) await instauto.sleep(10 * 60 * 1000);
|
|
83
|
-
|
|
84
|
-
// List of usernames that we should follow the followers of, can be celebrities etc.
|
|
85
|
-
const usersToFollowFollowersOf = ['lostleblanc', 'sam_kolder'];
|
|
86
|
-
|
|
87
|
-
// Now go through each of these and follow a certain amount of their followers
|
|
88
|
-
await instauto.followUsersFollowers({
|
|
89
|
-
usersToFollowFollowersOf,
|
|
90
|
-
maxFollowsTotal: options.maxFollowsPerDay - unfollowedCount,
|
|
91
|
-
skipPrivate: true,
|
|
92
|
-
enableLikeImages: true,
|
|
93
|
-
likeImagesMax: 3,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
await instauto.sleep(10 * 60 * 1000);
|
|
97
|
-
|
|
98
|
-
console.log('Done running');
|
|
99
|
-
|
|
100
|
-
await instauto.sleep(30000);
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.error(err);
|
|
103
|
-
} finally {
|
|
104
|
-
console.log('Closing browser');
|
|
105
|
-
if (browser) await browser.close();
|
|
106
|
-
}
|
|
107
|
-
})();
|
package/instabot.yml
DELETED
package/logo.png
DELETED
|
Binary file
|