instauto 9.1.1 → 9.1.4
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 +1 -1
- package/src/index.js +80 -67
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
package/src/index.js
CHANGED
|
@@ -58,6 +58,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
58
58
|
} = options;
|
|
59
59
|
|
|
60
60
|
let myUsername = myUsernameIn;
|
|
61
|
+
const userDataCache = {};
|
|
61
62
|
|
|
62
63
|
assert(cookiesPath);
|
|
63
64
|
assert(db);
|
|
@@ -73,7 +74,6 @@ const Instauto = async (db, browser, options) => {
|
|
|
73
74
|
|
|
74
75
|
// State
|
|
75
76
|
let page;
|
|
76
|
-
let graphqlUserMissing = false;
|
|
77
77
|
|
|
78
78
|
async function takeScreenshot() {
|
|
79
79
|
if (!screenshotOnError) return;
|
|
@@ -169,15 +169,20 @@ const Instauto = async (db, browser, options) => {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
async function gotoWithRetry(url) {
|
|
172
|
+
const maxAttempts = 3;
|
|
172
173
|
for (let attempt = 0; ; attempt += 1) {
|
|
173
174
|
logger.log(`Goto ${url}`);
|
|
174
175
|
const response = await page.goto(url);
|
|
175
|
-
await sleep(
|
|
176
|
+
await sleep(2000);
|
|
176
177
|
const status = response.status();
|
|
177
178
|
|
|
178
179
|
// https://www.reddit.com/r/Instagram/comments/kwrt0s/error_560/
|
|
179
180
|
// https://github.com/mifi/instauto/issues/60
|
|
180
|
-
if (![560, 429].includes(status)
|
|
181
|
+
if (![560, 429].includes(status)) return status;
|
|
182
|
+
|
|
183
|
+
if (attempt > maxAttempts) {
|
|
184
|
+
throw new Error(`Navigate to user failed after ${maxAttempts} attempts, last status: ${status}`);
|
|
185
|
+
}
|
|
181
186
|
|
|
182
187
|
logger.info(`Got ${status} - Retrying request later...`);
|
|
183
188
|
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');
|
|
@@ -185,73 +190,78 @@ const Instauto = async (db, browser, options) => {
|
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// example: https://www.instagram.com/victorialarson__/
|
|
195
|
-
// so we check if the page has the user's name on it
|
|
196
|
-
return page.evaluate((username) => window.find(username), checkPageForUsername);
|
|
197
|
-
}
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
if (status === 404) {
|
|
201
|
-
logger.log('User not found');
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
throw new Error(`Navigate to user failed with status ${status}`);
|
|
193
|
+
const getUserPageUrl = (username) => `${instagramBaseUrl}/${encodeURIComponent(username)}`;
|
|
194
|
+
|
|
195
|
+
function isAlreadyOnUserPage(username) {
|
|
196
|
+
const url = getUserPageUrl(username);
|
|
197
|
+
// optimization: already on URL? (ignore trailing slash)
|
|
198
|
+
return (page.url().replace(/\/$/, '') === url.replace(/\/$/, ''));
|
|
205
199
|
}
|
|
206
200
|
|
|
207
201
|
async function navigateToUser(username) {
|
|
208
|
-
|
|
209
|
-
|
|
202
|
+
if (isAlreadyOnUserPage(username)) return true;
|
|
203
|
+
|
|
210
204
|
// logger.log('navigating from', page.url(), 'to', url);
|
|
211
205
|
logger.log(`Navigating to user ${username}`);
|
|
212
|
-
|
|
206
|
+
|
|
207
|
+
const url = getUserPageUrl(username);
|
|
208
|
+
const status = await gotoWithRetry(url);
|
|
209
|
+
if (status === 404) {
|
|
210
|
+
logger.warn('User page returned 404');
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (status === 200) {
|
|
215
|
+
// some pages return 200 but nothing there (I think deleted accounts)
|
|
216
|
+
// https://github.com/mifi/SimpleInstaBot/issues/48
|
|
217
|
+
// example: https://www.instagram.com/victorialarson__/
|
|
218
|
+
// so we check if the page has the user's name on it
|
|
219
|
+
const foundUsernameOnPage = await page.evaluate((u) => window.find(u), username);
|
|
220
|
+
if (!foundUsernameOnPage) logger.warn(`Cannot find "${username}" on page`);
|
|
221
|
+
return foundUsernameOnPage;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
throw new Error(`Navigate to user failed with status ${status}`);
|
|
213
225
|
}
|
|
214
226
|
|
|
215
227
|
async function navigateToUserWithCheck(username) {
|
|
216
228
|
if (!(await navigateToUser(username))) throw new Error('User not found');
|
|
217
229
|
}
|
|
218
230
|
|
|
219
|
-
async function getPageJson() {
|
|
220
|
-
return JSON.parse(await (await (await page.$('pre')).getProperty('textContent')).jsonValue());
|
|
221
|
-
}
|
|
222
|
-
|
|
223
231
|
async function navigateToUserAndGetData(username) {
|
|
224
|
-
|
|
225
|
-
if (graphqlUserMissing) {
|
|
226
|
-
// https://stackoverflow.com/questions/37593025/instagram-api-get-the-userid
|
|
227
|
-
// https://stackoverflow.com/questions/17373886/how-can-i-get-a-users-media-from-instagram-without-authenticating-as-a-user
|
|
228
|
-
const found = await safeGotoUser(`${instagramBaseUrl}/${encodeURIComponent(username)}?__a=1`);
|
|
229
|
-
if (!found) throw new Error('User not found');
|
|
232
|
+
const cachedUserData = userDataCache[username];
|
|
230
233
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
if (isAlreadyOnUserPage(username)) {
|
|
235
|
+
// assume we have data
|
|
236
|
+
return cachedUserData;
|
|
237
|
+
}
|
|
234
238
|
|
|
239
|
+
if (cachedUserData != null) {
|
|
240
|
+
// if we already have userData, just navigate
|
|
235
241
|
await navigateToUserWithCheck(username);
|
|
236
|
-
return
|
|
242
|
+
return cachedUserData;
|
|
237
243
|
}
|
|
238
244
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
// intercept special XHR network request that fetches user's data and store it in a cache
|
|
246
|
+
// TODO fallback to DOM to get user ID if this request fails?
|
|
247
|
+
// https://github.com/mifi/SimpleInstaBot/issues/125#issuecomment-1145354294
|
|
248
|
+
const [foundResponse] = await Promise.all([
|
|
249
|
+
page.waitForResponse((response) => {
|
|
250
|
+
const request = response.request();
|
|
251
|
+
return request.method() === 'GET' && new RegExp(`https:\\/\\/i\\.instagram\\.com\\/api\\/v1\\/users\\/web_profile_info\\/\\?username=${encodeURIComponent(username.toLowerCase())}`).test(request.url());
|
|
252
|
+
}),
|
|
253
|
+
navigateToUserWithCheck(username),
|
|
254
|
+
// page.waitForNavigation({ waitUntil: 'networkidle0' }),
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
const json = JSON.parse(await foundResponse.text());
|
|
258
|
+
const userData = json.data.user;
|
|
259
|
+
userDataCache[username] = userData;
|
|
260
|
+
return userData;
|
|
261
|
+
}
|
|
246
262
|
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
}
|
|
263
|
+
async function getPageJson() {
|
|
264
|
+
return JSON.parse(await (await (await page.$('pre')).getProperty('textContent')).jsonValue());
|
|
255
265
|
}
|
|
256
266
|
|
|
257
267
|
async function isActionBlocked() {
|
|
@@ -321,7 +331,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
321
331
|
}
|
|
322
332
|
|
|
323
333
|
async function followUser(username) {
|
|
324
|
-
await
|
|
334
|
+
await navigateToUserAndGetData(username);
|
|
325
335
|
const elementHandle = await findFollowButton();
|
|
326
336
|
|
|
327
337
|
if (!elementHandle) {
|
|
@@ -363,7 +373,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
363
373
|
// See https://github.com/timgrossmann/InstaPy/pull/2345
|
|
364
374
|
// https://github.com/timgrossmann/InstaPy/issues/2355
|
|
365
375
|
async function unfollowUser(username) {
|
|
366
|
-
await
|
|
376
|
+
await navigateToUserAndGetData(username);
|
|
367
377
|
logger.log(`Unfollowing user ${username}`);
|
|
368
378
|
|
|
369
379
|
const res = { username, time: new Date().getTime() };
|
|
@@ -562,7 +572,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
562
572
|
async function likeUserImages({ username, likeImagesMin, likeImagesMax } = {}) {
|
|
563
573
|
if (!likeImagesMin || !likeImagesMax || likeImagesMax < likeImagesMin || likeImagesMin < 1) throw new Error('Invalid arguments');
|
|
564
574
|
|
|
565
|
-
await
|
|
575
|
+
await navigateToUserAndGetData(username);
|
|
566
576
|
|
|
567
577
|
logger.log(`Liking ${likeImagesMin}-${likeImagesMax} user images`);
|
|
568
578
|
try {
|
|
@@ -581,6 +591,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
581
591
|
logger.log('Skipping already followed user', username);
|
|
582
592
|
return false;
|
|
583
593
|
}
|
|
594
|
+
|
|
584
595
|
const graphqlUser = await navigateToUserAndGetData(username);
|
|
585
596
|
|
|
586
597
|
const followedByCount = graphqlUser.edge_followed_by.count;
|
|
@@ -632,9 +643,9 @@ const Instauto = async (db, browser, options) => {
|
|
|
632
643
|
|
|
633
644
|
let numFollowedForThisUser = 0;
|
|
634
645
|
|
|
635
|
-
const
|
|
646
|
+
const { id: userId } = await navigateToUserAndGetData(username);
|
|
636
647
|
|
|
637
|
-
for await (const followersBatch of getFollowersOrFollowingGenerator({ userId
|
|
648
|
+
for await (const followersBatch of getFollowersOrFollowingGenerator({ userId, getFollowers: true })) {
|
|
638
649
|
logger.log('User followers batch', followersBatch);
|
|
639
650
|
|
|
640
651
|
for (const follower of followersBatch) {
|
|
@@ -646,13 +657,13 @@ const Instauto = async (db, browser, options) => {
|
|
|
646
657
|
return;
|
|
647
658
|
}
|
|
648
659
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
}
|
|
660
|
+
let didActuallyFollow = false;
|
|
661
|
+
if (enableFollow) didActuallyFollow = await followUserRespectingRestrictions({ username: follower, skipPrivate });
|
|
662
|
+
if (didActuallyFollow) numFollowedForThisUser += 1;
|
|
654
663
|
|
|
655
|
-
|
|
664
|
+
const didFailToFollow = enableFollow && !didActuallyFollow;
|
|
665
|
+
|
|
666
|
+
if (enableLikeImages && !didFailToFollow) {
|
|
656
667
|
// Note: throws error if user isPrivate
|
|
657
668
|
await likeUserImages({ username: follower, likeImagesMin, likeImagesMax });
|
|
658
669
|
}
|
|
@@ -706,6 +717,8 @@ const Instauto = async (db, browser, options) => {
|
|
|
706
717
|
const userFound = await navigateToUser(username);
|
|
707
718
|
|
|
708
719
|
if (!userFound) {
|
|
720
|
+
// to avoid repeatedly unfollowing failed users, flag them as already unfollowed
|
|
721
|
+
logger.log('User not found for unfollow');
|
|
709
722
|
await addPrevUnfollowedUser({ username, time: new Date().getTime(), noActionTaken: true });
|
|
710
723
|
await sleep(3000);
|
|
711
724
|
} else {
|
|
@@ -740,6 +753,8 @@ const Instauto = async (db, browser, options) => {
|
|
|
740
753
|
}
|
|
741
754
|
}
|
|
742
755
|
|
|
756
|
+
logger.log('Done with unfollowing', i, j);
|
|
757
|
+
|
|
743
758
|
return j;
|
|
744
759
|
}
|
|
745
760
|
|
|
@@ -937,16 +952,14 @@ const Instauto = async (db, browser, options) => {
|
|
|
937
952
|
throw new Error('Don\'t know what\'s my username');
|
|
938
953
|
}
|
|
939
954
|
|
|
940
|
-
const
|
|
941
|
-
const myUserId = myUserData.id;
|
|
955
|
+
const { id: myUserId } = await navigateToUserAndGetData(myUsername);
|
|
942
956
|
|
|
943
957
|
// --- END OF INITIALIZATION
|
|
944
958
|
|
|
945
959
|
async function doesUserFollowMe(username) {
|
|
946
960
|
try {
|
|
947
961
|
logger.info('Checking if user', username, 'follows us');
|
|
948
|
-
const
|
|
949
|
-
const userId = userData.id;
|
|
962
|
+
const { id: userId } = await navigateToUserAndGetData(username);
|
|
950
963
|
|
|
951
964
|
const elementHandles = await page.$x("//a[contains(.,' following')][contains(@href,'/following')]");
|
|
952
965
|
if (elementHandles.length === 0) throw new Error('Following button not found');
|