instauto 9.2.0 → 9.4.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 +9 -0
- package/dist/db.d.ts +34 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +99 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1108 -0
- package/package.json +40 -14
- package/src/db.js +0 -122
- package/src/index.js +0 -1212
package/src/index.js
DELETED
|
@@ -1,1212 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const assert = require('assert');
|
|
4
|
-
const fs = require('fs-extra');
|
|
5
|
-
const { join } = require('path');
|
|
6
|
-
const UserAgent = require('user-agents');
|
|
7
|
-
const JSONDB = require('./db');
|
|
8
|
-
|
|
9
|
-
// NOTE duplicated inside puppeteer page
|
|
10
|
-
function shuffleArray(arrayIn) {
|
|
11
|
-
const array = [...arrayIn];
|
|
12
|
-
for (let i = array.length - 1; i > 0; i -= 1) {
|
|
13
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
14
|
-
[array[i], array[j]] = [array[j], array[i]]; // eslint-disable-line no-param-reassign
|
|
15
|
-
}
|
|
16
|
-
return array;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// https://stackoverflow.com/questions/14822153/escape-single-quote-in-xpath-with-nokogiri
|
|
20
|
-
// example str: "That's mine", he said.
|
|
21
|
-
function escapeXpathStr(str) {
|
|
22
|
-
const parts = str.split("'").map((token) => `'${token}'`);
|
|
23
|
-
if (parts.length === 1) return `${parts[0]}`;
|
|
24
|
-
const str2 = parts.join(', "\'", ');
|
|
25
|
-
return `concat(${str2})`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const botWorkShiftHours = 16;
|
|
29
|
-
|
|
30
|
-
const dayMs = 24 * 60 * 60 * 1000;
|
|
31
|
-
const hourMs = 60 * 60 * 1000;
|
|
32
|
-
|
|
33
|
-
const Instauto = async (db, browser, options) => {
|
|
34
|
-
const {
|
|
35
|
-
instagramBaseUrl = 'https://www.instagram.com',
|
|
36
|
-
cookiesPath,
|
|
37
|
-
|
|
38
|
-
username: myUsernameIn,
|
|
39
|
-
password,
|
|
40
|
-
enableCookies = true,
|
|
41
|
-
|
|
42
|
-
randomizeUserAgent = true,
|
|
43
|
-
userAgent,
|
|
44
|
-
|
|
45
|
-
maxFollowsPerHour = 20,
|
|
46
|
-
maxFollowsPerDay = 150,
|
|
47
|
-
|
|
48
|
-
maxLikesPerDay = 50,
|
|
49
|
-
|
|
50
|
-
followUserRatioMin = 0.2,
|
|
51
|
-
followUserRatioMax = 4.0,
|
|
52
|
-
followUserMaxFollowers = null,
|
|
53
|
-
followUserMaxFollowing = null,
|
|
54
|
-
followUserMinFollowers = null,
|
|
55
|
-
followUserMinFollowing = null,
|
|
56
|
-
|
|
57
|
-
shouldFollowUser = null,
|
|
58
|
-
shouldLikeMedia = null,
|
|
59
|
-
|
|
60
|
-
dontUnfollowUntilTimeElapsed = 3 * 24 * 60 * 60 * 1000,
|
|
61
|
-
|
|
62
|
-
excludeUsers = [],
|
|
63
|
-
|
|
64
|
-
dryRun = true,
|
|
65
|
-
|
|
66
|
-
screenshotOnError = false,
|
|
67
|
-
screenshotsPath = '.',
|
|
68
|
-
|
|
69
|
-
logger = console,
|
|
70
|
-
} = options;
|
|
71
|
-
|
|
72
|
-
let myUsername = myUsernameIn;
|
|
73
|
-
const userDataCache = {};
|
|
74
|
-
|
|
75
|
-
assert(cookiesPath);
|
|
76
|
-
assert(db);
|
|
77
|
-
|
|
78
|
-
assert(maxFollowsPerHour * botWorkShiftHours >= maxFollowsPerDay, 'Max follows per hour too low compared to max follows per day');
|
|
79
|
-
|
|
80
|
-
const {
|
|
81
|
-
addPrevFollowedUser, getPrevFollowedUser, addPrevUnfollowedUser, getLikedPhotosLastTimeUnit,
|
|
82
|
-
getPrevUnfollowedUsers, getPrevFollowedUsers, addLikedPhoto,
|
|
83
|
-
} = db;
|
|
84
|
-
|
|
85
|
-
const getNumLikesThisTimeUnit = (time) => getLikedPhotosLastTimeUnit(time).length;
|
|
86
|
-
|
|
87
|
-
// State
|
|
88
|
-
let page;
|
|
89
|
-
|
|
90
|
-
async function takeScreenshot() {
|
|
91
|
-
if (!screenshotOnError) return;
|
|
92
|
-
try {
|
|
93
|
-
const fileName = `${new Date().getTime()}.jpg`;
|
|
94
|
-
logger.log('Taking screenshot', fileName);
|
|
95
|
-
await page.screenshot({ path: join(screenshotsPath, fileName), type: 'jpeg', quality: 30 });
|
|
96
|
-
} catch (err) {
|
|
97
|
-
logger.error('Failed to take screenshot', err);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function tryLoadCookies() {
|
|
102
|
-
try {
|
|
103
|
-
const cookies = JSON.parse(await fs.readFile(cookiesPath));
|
|
104
|
-
for (const cookie of cookies) {
|
|
105
|
-
if (cookie.name !== 'ig_lang') await page.setCookie(cookie);
|
|
106
|
-
}
|
|
107
|
-
} catch (err) {
|
|
108
|
-
logger.error('No cookies found');
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function trySaveCookies() {
|
|
113
|
-
try {
|
|
114
|
-
logger.log('Saving cookies');
|
|
115
|
-
const cookies = await page.cookies();
|
|
116
|
-
|
|
117
|
-
await fs.writeFile(cookiesPath, JSON.stringify(cookies, null, 2));
|
|
118
|
-
} catch (err) {
|
|
119
|
-
logger.error('Failed to save cookies');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function tryDeleteCookies() {
|
|
124
|
-
try {
|
|
125
|
-
logger.log('Deleting cookies');
|
|
126
|
-
await fs.unlink(cookiesPath);
|
|
127
|
-
} catch (err) {
|
|
128
|
-
logger.error('No cookies to delete');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const sleep = (ms, deviation = 1) => {
|
|
133
|
-
let msWithDev = ((Math.random() * deviation) + 1) * ms;
|
|
134
|
-
if (dryRun) msWithDev = Math.min(3000, msWithDev); // for dryRun, no need to wait so long
|
|
135
|
-
logger.log('Waiting', (msWithDev / 1000).toFixed(2), 'sec');
|
|
136
|
-
return new Promise(resolve => setTimeout(resolve, msWithDev));
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
async function onImageLiked({ username, href }) {
|
|
140
|
-
await addLikedPhoto({ username, href, time: new Date().getTime() });
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function getNumFollowedUsersThisTimeUnit(timeUnit) {
|
|
144
|
-
const now = new Date().getTime();
|
|
145
|
-
|
|
146
|
-
return getPrevFollowedUsers().filter(u => now - u.time < timeUnit).length
|
|
147
|
-
+ getPrevUnfollowedUsers().filter(u => !u.noActionTaken && now - u.time < timeUnit).length;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function checkReachedFollowedUserDayLimit() {
|
|
151
|
-
if (getNumFollowedUsersThisTimeUnit(dayMs) >= maxFollowsPerDay) {
|
|
152
|
-
logger.log('Have reached daily follow/unfollow limit, waiting 10 min');
|
|
153
|
-
await sleep(10 * 60 * 1000);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function checkReachedFollowedUserHourLimit() {
|
|
158
|
-
if (getNumFollowedUsersThisTimeUnit(hourMs) >= maxFollowsPerHour) {
|
|
159
|
-
logger.log('Have reached hourly follow rate limit, pausing 10 min');
|
|
160
|
-
await sleep(10 * 60 * 1000);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function checkReachedLikedUserDayLimit() {
|
|
165
|
-
if (getNumLikesThisTimeUnit(dayMs) >= maxLikesPerDay) {
|
|
166
|
-
logger.log('Have reached daily like rate limit, pausing 10 min');
|
|
167
|
-
await sleep(10 * 60 * 1000);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function throttle() {
|
|
172
|
-
await checkReachedFollowedUserDayLimit();
|
|
173
|
-
await checkReachedFollowedUserHourLimit();
|
|
174
|
-
await checkReachedLikedUserDayLimit();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function haveRecentlyFollowedUser(username) {
|
|
178
|
-
const followedUserEntry = getPrevFollowedUser(username);
|
|
179
|
-
if (!followedUserEntry) return false; // We did not previously follow this user, so don't know
|
|
180
|
-
return new Date().getTime() - followedUserEntry.time < dontUnfollowUntilTimeElapsed;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// See https://github.com/mifi/SimpleInstaBot/issues/140#issuecomment-1149105387
|
|
184
|
-
const gotoUrl = async (url) => page.goto(url, { waitUntil: ['load', 'domcontentloaded', 'networkidle0'] });
|
|
185
|
-
|
|
186
|
-
async function gotoWithRetry(url) {
|
|
187
|
-
const maxAttempts = 3;
|
|
188
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
189
|
-
logger.log(`Goto ${url}`);
|
|
190
|
-
const response = await gotoUrl(url);
|
|
191
|
-
const status = response.status();
|
|
192
|
-
logger.log('Page loaded');
|
|
193
|
-
await sleep(2000);
|
|
194
|
-
|
|
195
|
-
// https://www.reddit.com/r/Instagram/comments/kwrt0s/error_560/
|
|
196
|
-
// https://github.com/mifi/instauto/issues/60
|
|
197
|
-
if (![560, 429].includes(status)) return status;
|
|
198
|
-
|
|
199
|
-
if (attempt > maxAttempts) {
|
|
200
|
-
throw new Error(`Navigate to user failed after ${maxAttempts} attempts, last status: ${status}`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
logger.info(`Got ${status} - Retrying request later...`);
|
|
204
|
-
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');
|
|
205
|
-
await sleep((attempt + 1) * 30 * 60 * 1000);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const getUserPageUrl = (username) => `${instagramBaseUrl}/${encodeURIComponent(username)}`;
|
|
210
|
-
|
|
211
|
-
function isAlreadyOnUserPage(username) {
|
|
212
|
-
const url = getUserPageUrl(username);
|
|
213
|
-
// optimization: already on URL? (ignore trailing slash)
|
|
214
|
-
return (page.url().replace(/\/$/, '') === url.replace(/\/$/, ''));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function navigateToUser(username) {
|
|
218
|
-
if (isAlreadyOnUserPage(username)) return true;
|
|
219
|
-
|
|
220
|
-
// logger.log('navigating from', page.url(), 'to', url);
|
|
221
|
-
logger.log(`Navigating to user ${username}`);
|
|
222
|
-
|
|
223
|
-
const url = getUserPageUrl(username);
|
|
224
|
-
const status = await gotoWithRetry(url);
|
|
225
|
-
if (status === 404) {
|
|
226
|
-
logger.warn('User page returned 404');
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (status === 200) {
|
|
231
|
-
// logger.log('Page returned 200 ☑️');
|
|
232
|
-
// some pages return 200 but nothing there (I think deleted accounts)
|
|
233
|
-
// https://github.com/mifi/SimpleInstaBot/issues/48
|
|
234
|
-
// example: https://www.instagram.com/victorialarson__/
|
|
235
|
-
// so we check if the page has the user's name on it
|
|
236
|
-
const elementHandles = await page.$x(`//body//main//*[contains(text(),${escapeXpathStr(username)})]`);
|
|
237
|
-
const foundUsernameOnPage = elementHandles.length > 0;
|
|
238
|
-
if (!foundUsernameOnPage) logger.warn(`Cannot find text "${username}" on page`);
|
|
239
|
-
return foundUsernameOnPage;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
throw new Error(`Navigate to user failed with status ${status}`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function navigateToUserWithCheck(username) {
|
|
246
|
-
if (!(await navigateToUser(username))) throw new Error('User not found');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function navigateToUserAndGetData(username) {
|
|
250
|
-
const cachedUserData = userDataCache[username];
|
|
251
|
-
|
|
252
|
-
if (isAlreadyOnUserPage(username)) {
|
|
253
|
-
// assume we have data
|
|
254
|
-
return cachedUserData;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (cachedUserData != null) {
|
|
258
|
-
// if we already have userData, just navigate
|
|
259
|
-
await navigateToUserWithCheck(username);
|
|
260
|
-
return cachedUserData;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function getUserDataFromPage() {
|
|
264
|
-
// https://github.com/mifi/instauto/issues/115#issuecomment-1199335650
|
|
265
|
-
// to test in browser: document.getElementsByTagName('html')[0].innerHTML.split('\n');
|
|
266
|
-
try {
|
|
267
|
-
const body = await page.content();
|
|
268
|
-
for (let q of body.split(/\r?\n/)) {
|
|
269
|
-
if (q.includes('edge_followed_by')) {
|
|
270
|
-
// eslint-disable-next-line prefer-destructuring
|
|
271
|
-
q = q.split(',[],[')[1];
|
|
272
|
-
// eslint-disable-next-line prefer-destructuring
|
|
273
|
-
q = q.split(']]]')[0];
|
|
274
|
-
q = JSON.parse(q);
|
|
275
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
276
|
-
q = q.data.__bbox.result.response;
|
|
277
|
-
q = q.replace(/\\/g, '');
|
|
278
|
-
q = JSON.parse(q);
|
|
279
|
-
return q.data.user;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
} catch (err) {
|
|
283
|
-
logger.warn(`Unable to get user data from page (${err.name}) - This is normal`);
|
|
284
|
-
}
|
|
285
|
-
return undefined;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// intercept special XHR network request that fetches user's data and store it in a cache
|
|
289
|
-
// TODO fallback to DOM to get user ID if this request fails?
|
|
290
|
-
// https://github.com/mifi/SimpleInstaBot/issues/125#issuecomment-1145354294
|
|
291
|
-
async function getUserDataFromInterceptedRequest() {
|
|
292
|
-
const t = setTimeout(async () => {
|
|
293
|
-
logger.log('Unable to intercept request, will send manually');
|
|
294
|
-
try {
|
|
295
|
-
await page.evaluate(async (username2) => {
|
|
296
|
-
const response = await window.fetch(`https://i.instagram.com/api/v1/users/web_profile_info/?username=${encodeURIComponent(username2.toLowerCase())}`, { mode: 'cors', credentials: 'include', headers: { 'x-ig-app-id': '936619743392459' } });
|
|
297
|
-
await response.json(); // else it will not finish the request
|
|
298
|
-
}, username);
|
|
299
|
-
// todo `https://i.instagram.com/api/v1/users/${userId}/info/`
|
|
300
|
-
// https://www.javafixing.com/2022/07/fixed-can-get-instagram-profile-picture.html?m=1
|
|
301
|
-
} catch (err) {
|
|
302
|
-
logger.error('Failed to manually send request', err);
|
|
303
|
-
}
|
|
304
|
-
}, 5000);
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const [foundResponse] = await Promise.all([
|
|
308
|
-
page.waitForResponse((response) => {
|
|
309
|
-
const request = response.request();
|
|
310
|
-
return request.method() === 'GET' && new RegExp(`https:\\/\\/i\\.instagram\\.com\\/api\\/v1\\/users\\/web_profile_info\\/\\?username=${encodeURIComponent(username.toLowerCase())}`).test(request.url());
|
|
311
|
-
}, { timeout: 30000 }),
|
|
312
|
-
navigateToUserWithCheck(username),
|
|
313
|
-
// page.waitForNavigation({ waitUntil: 'networkidle0' }),
|
|
314
|
-
]);
|
|
315
|
-
|
|
316
|
-
const json = JSON.parse(await foundResponse.text());
|
|
317
|
-
return json.data.user;
|
|
318
|
-
} finally {
|
|
319
|
-
clearTimeout(t);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
logger.log('Trying to get user data from HTML');
|
|
324
|
-
|
|
325
|
-
await navigateToUserWithCheck(username);
|
|
326
|
-
let userData = await getUserDataFromPage();
|
|
327
|
-
if (userData) {
|
|
328
|
-
userDataCache[username] = userData;
|
|
329
|
-
return userData;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
logger.log('Need to intercept network request to get user data');
|
|
333
|
-
|
|
334
|
-
// works for old accounts only:
|
|
335
|
-
userData = await getUserDataFromInterceptedRequest();
|
|
336
|
-
if (userData) {
|
|
337
|
-
userDataCache[username] = userData;
|
|
338
|
-
return userData;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return undefined;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async function getPageJson() {
|
|
345
|
-
return JSON.parse(await (await (await page.$('pre')).getProperty('textContent')).jsonValue());
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async function isActionBlocked() {
|
|
349
|
-
if ((await page.$x('//*[contains(text(), "Action Blocked")]')).length > 0) return true;
|
|
350
|
-
if ((await page.$x('//*[contains(text(), "Try Again Later")]')).length > 0) return true;
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
async function checkActionBlocked() {
|
|
355
|
-
if (await isActionBlocked()) {
|
|
356
|
-
const hours = 3;
|
|
357
|
-
logger.error(`Action Blocked, waiting ${hours} hours...`);
|
|
358
|
-
await tryDeleteCookies();
|
|
359
|
-
await sleep(hours * 60 * 60 * 1000);
|
|
360
|
-
throw new Error('Aborted operation due to action blocked');
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// How to test xpaths in the browser:
|
|
365
|
-
// document.evaluate("your xpath", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue
|
|
366
|
-
async function findButtonWithText(text) {
|
|
367
|
-
// todo escape text?
|
|
368
|
-
|
|
369
|
-
// button seems to look like this now:
|
|
370
|
-
// <button class="..."><div class="...">Follow</div></button>
|
|
371
|
-
// https://sqa.stackexchange.com/questions/36918/xpath-text-buy-now-is-working-but-not-containstext-buy-now
|
|
372
|
-
// https://github.com/mifi/SimpleInstaBot/issues/106
|
|
373
|
-
let elementHandles = await page.$x(`//header//button[contains(.,'${text}')]`);
|
|
374
|
-
if (elementHandles.length > 0) return elementHandles[0];
|
|
375
|
-
|
|
376
|
-
// old button:
|
|
377
|
-
elementHandles = await page.$x(`//header//button[text()='${text}']`);
|
|
378
|
-
if (elementHandles.length > 0) return elementHandles[0];
|
|
379
|
-
|
|
380
|
-
return undefined;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async function findFollowButton() {
|
|
384
|
-
let button = await findButtonWithText('Follow');
|
|
385
|
-
if (button) return button;
|
|
386
|
-
|
|
387
|
-
button = await findButtonWithText('Follow Back');
|
|
388
|
-
if (button) return button;
|
|
389
|
-
|
|
390
|
-
return undefined;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function findUnfollowButton() {
|
|
394
|
-
let button = await findButtonWithText('Following');
|
|
395
|
-
if (button) return button;
|
|
396
|
-
|
|
397
|
-
button = await findButtonWithText('Requested');
|
|
398
|
-
if (button) return button;
|
|
399
|
-
|
|
400
|
-
let elementHandles = await page.$x("//header//button[*//span[@aria-label='Following']]");
|
|
401
|
-
if (elementHandles.length > 0) return elementHandles[0];
|
|
402
|
-
|
|
403
|
-
elementHandles = await page.$x("//header//button[*//span[@aria-label='Requested']]");
|
|
404
|
-
if (elementHandles.length > 0) return elementHandles[0];
|
|
405
|
-
|
|
406
|
-
elementHandles = await page.$x("//header//button[*//*[name()='svg'][@aria-label='Following']]");
|
|
407
|
-
if (elementHandles.length > 0) return elementHandles[0];
|
|
408
|
-
|
|
409
|
-
elementHandles = await page.$x("//header//button[*//*[name()='svg'][@aria-label='Requested']]");
|
|
410
|
-
if (elementHandles.length > 0) return elementHandles[0];
|
|
411
|
-
|
|
412
|
-
return undefined;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
async function findUnfollowConfirmButton() {
|
|
416
|
-
const elementHandles = await page.$x("//button[text()='Unfollow']");
|
|
417
|
-
return elementHandles[0];
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
async function followUser(username) {
|
|
421
|
-
await navigateToUserAndGetData(username);
|
|
422
|
-
const elementHandle = await findFollowButton();
|
|
423
|
-
|
|
424
|
-
if (!elementHandle) {
|
|
425
|
-
if (await findUnfollowButton()) {
|
|
426
|
-
logger.log('We are already following this user');
|
|
427
|
-
await sleep(5000);
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
throw new Error('Follow button not found');
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
logger.log(`Following user ${username}`);
|
|
435
|
-
|
|
436
|
-
if (!dryRun) {
|
|
437
|
-
await elementHandle.click();
|
|
438
|
-
await sleep(5000);
|
|
439
|
-
|
|
440
|
-
await checkActionBlocked();
|
|
441
|
-
|
|
442
|
-
const elementHandle2 = await findUnfollowButton();
|
|
443
|
-
|
|
444
|
-
// Don't want to retry this user over and over in case there is an issue https://github.com/mifi/instauto/issues/33#issuecomment-723217177
|
|
445
|
-
const entry = { username, time: new Date().getTime() };
|
|
446
|
-
if (!elementHandle2) entry.failed = true;
|
|
447
|
-
|
|
448
|
-
await addPrevFollowedUser(entry);
|
|
449
|
-
|
|
450
|
-
if (!elementHandle2) {
|
|
451
|
-
logger.log('Button did not change state - Sleeping 1 min');
|
|
452
|
-
await sleep(60000);
|
|
453
|
-
throw new Error('Button did not change state');
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
await sleep(1000);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// See https://github.com/timgrossmann/InstaPy/pull/2345
|
|
461
|
-
// https://github.com/timgrossmann/InstaPy/issues/2355
|
|
462
|
-
async function unfollowUser(username) {
|
|
463
|
-
await navigateToUserAndGetData(username);
|
|
464
|
-
logger.log(`Unfollowing user ${username}`);
|
|
465
|
-
|
|
466
|
-
const res = { username, time: new Date().getTime() };
|
|
467
|
-
|
|
468
|
-
const elementHandle = await findUnfollowButton();
|
|
469
|
-
if (!elementHandle) {
|
|
470
|
-
const elementHandle2 = await findFollowButton();
|
|
471
|
-
if (elementHandle2) {
|
|
472
|
-
logger.log('User has been unfollowed already');
|
|
473
|
-
res.noActionTaken = true;
|
|
474
|
-
} else {
|
|
475
|
-
logger.log('Failed to find unfollow button');
|
|
476
|
-
res.noActionTaken = true;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (!dryRun) {
|
|
481
|
-
if (elementHandle) {
|
|
482
|
-
await elementHandle.click();
|
|
483
|
-
await sleep(1000);
|
|
484
|
-
const confirmHandle = await findUnfollowConfirmButton();
|
|
485
|
-
if (confirmHandle) await confirmHandle.click();
|
|
486
|
-
|
|
487
|
-
await sleep(5000);
|
|
488
|
-
|
|
489
|
-
await checkActionBlocked();
|
|
490
|
-
|
|
491
|
-
const elementHandle2 = await findFollowButton();
|
|
492
|
-
if (!elementHandle2) throw new Error('Unfollow button did not change state');
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
await addPrevUnfollowedUser(res);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
await sleep(1000);
|
|
499
|
-
|
|
500
|
-
return res;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const isLoggedIn = async () => (await page.$x('//*[@aria-label="Home"]')).length === 1;
|
|
504
|
-
|
|
505
|
-
async function* graphqlQueryUsers({ queryHash, getResponseProp, graphqlVariables: graphqlVariablesIn }) {
|
|
506
|
-
const graphqlUrl = `${instagramBaseUrl}/graphql/query/?query_hash=${queryHash}`;
|
|
507
|
-
|
|
508
|
-
const graphqlVariables = {
|
|
509
|
-
first: 50,
|
|
510
|
-
...graphqlVariablesIn,
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
const outUsers = [];
|
|
514
|
-
|
|
515
|
-
let hasNextPage = true;
|
|
516
|
-
let i = 0;
|
|
517
|
-
|
|
518
|
-
while (hasNextPage) {
|
|
519
|
-
const url = `${graphqlUrl}&variables=${JSON.stringify(graphqlVariables)}`;
|
|
520
|
-
// logger.log(url);
|
|
521
|
-
await page.goto(url);
|
|
522
|
-
const json = await getPageJson();
|
|
523
|
-
|
|
524
|
-
const subProp = getResponseProp(json);
|
|
525
|
-
const pageInfo = subProp.page_info;
|
|
526
|
-
const { edges } = subProp;
|
|
527
|
-
|
|
528
|
-
const ret = [];
|
|
529
|
-
edges.forEach(e => ret.push(e.node.username));
|
|
530
|
-
|
|
531
|
-
graphqlVariables.after = pageInfo.end_cursor;
|
|
532
|
-
hasNextPage = pageInfo.has_next_page;
|
|
533
|
-
i += 1;
|
|
534
|
-
|
|
535
|
-
if (hasNextPage) {
|
|
536
|
-
logger.log(`Has more pages (current ${i})`);
|
|
537
|
-
// await sleep(300);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
yield ret;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return outUsers;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function getFollowersOrFollowingGenerator({ userId, getFollowers = false }) {
|
|
547
|
-
return graphqlQueryUsers({
|
|
548
|
-
getResponseProp: (json) => json.data.user[getFollowers ? 'edge_followed_by' : 'edge_follow'],
|
|
549
|
-
graphqlVariables: { id: userId },
|
|
550
|
-
queryHash: getFollowers ? '37479f2b8209594dde7facb0d904896a' : '58712303d941c6855d4e888c5f0cd22f',
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
async function getFollowersOrFollowing({ userId, getFollowers = false }) {
|
|
555
|
-
let users = [];
|
|
556
|
-
for await (const usersBatch of getFollowersOrFollowingGenerator({ userId, getFollowers })) {
|
|
557
|
-
users = [...users, ...usersBatch];
|
|
558
|
-
}
|
|
559
|
-
return users;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function getUsersWhoLikedContent({ contentId }) {
|
|
563
|
-
return graphqlQueryUsers({
|
|
564
|
-
getResponseProp: (json) => json.data.shortcode_media.edge_liked_by,
|
|
565
|
-
graphqlVariables: {
|
|
566
|
-
shortcode: contentId,
|
|
567
|
-
include_reel: true,
|
|
568
|
-
},
|
|
569
|
-
queryHash: 'd5d763b1e2acf209d62d22d184488e57',
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/* eslint-disable no-undef */
|
|
574
|
-
async function likeCurrentUserImagesPageCode({ dryRun: dryRunIn, likeImagesMin, likeImagesMax, shouldLikeMedia: shouldLikeMediaIn }) {
|
|
575
|
-
const allImages = Array.from(document.getElementsByTagName('a')).filter(el => /instagram.com\/p\//.test(el.href));
|
|
576
|
-
|
|
577
|
-
// eslint-disable-next-line no-shadow
|
|
578
|
-
function shuffleArray(arrayIn) {
|
|
579
|
-
const array = [...arrayIn];
|
|
580
|
-
for (let i = array.length - 1; i > 0; i -= 1) {
|
|
581
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
582
|
-
[array[i], array[j]] = [array[j], array[i]]; // eslint-disable-line no-param-reassign
|
|
583
|
-
}
|
|
584
|
-
return array;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const imagesShuffled = shuffleArray(allImages);
|
|
588
|
-
|
|
589
|
-
const numImagesToLike = Math.floor((Math.random() * ((likeImagesMax + 1) - likeImagesMin)) + likeImagesMin);
|
|
590
|
-
|
|
591
|
-
instautoLog(`Liking ${numImagesToLike} image(s)`);
|
|
592
|
-
|
|
593
|
-
const images = imagesShuffled.slice(0, numImagesToLike);
|
|
594
|
-
|
|
595
|
-
if (images.length < 1) {
|
|
596
|
-
instautoLog('No images to like');
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
for (const image of images) {
|
|
601
|
-
image.click();
|
|
602
|
-
|
|
603
|
-
await window.instautoSleep(3000);
|
|
604
|
-
|
|
605
|
-
const dialog = document.querySelector('*[role=dialog]');
|
|
606
|
-
|
|
607
|
-
if (!dialog) throw new Error('Dialog not found');
|
|
608
|
-
|
|
609
|
-
const section = Array.from(dialog.querySelectorAll('section')).find(s => s.querySelectorAll('*[aria-label="Like"]')[0] && s.querySelectorAll('*[aria-label="Comment"]')[0]);
|
|
610
|
-
|
|
611
|
-
if (!section) throw new Error('Like button section not found');
|
|
612
|
-
|
|
613
|
-
const likeButtonChild = section.querySelectorAll('*[aria-label="Like"]')[0];
|
|
614
|
-
|
|
615
|
-
if (!likeButtonChild) throw new Error('Like button not found (aria-label)');
|
|
616
|
-
|
|
617
|
-
// eslint-disable-next-line no-inner-declarations
|
|
618
|
-
function findClickableParent(el) {
|
|
619
|
-
let elAt = el;
|
|
620
|
-
while (elAt) {
|
|
621
|
-
if (elAt.click) {
|
|
622
|
-
return elAt;
|
|
623
|
-
}
|
|
624
|
-
elAt = elAt.parentElement;
|
|
625
|
-
}
|
|
626
|
-
return undefined;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const foundClickable = findClickableParent(likeButtonChild);
|
|
630
|
-
|
|
631
|
-
if (!foundClickable) throw new Error('Like button not found');
|
|
632
|
-
|
|
633
|
-
const instautoLog2 = instautoLog;
|
|
634
|
-
|
|
635
|
-
// eslint-disable-next-line no-inner-declarations
|
|
636
|
-
function likeImage() {
|
|
637
|
-
if (shouldLikeMediaIn !== null && (typeof shouldLikeMediaIn === 'function')) {
|
|
638
|
-
const presentation = dialog.querySelector('article[role=presentation]');
|
|
639
|
-
const img = presentation.querySelector('img[alt^="Photo by "]');
|
|
640
|
-
const video = presentation.querySelector('video[type="video/mp4"]');
|
|
641
|
-
const mediaDesc = presentation.querySelector('[role=menuitem] h2 ~ div').textContent;
|
|
642
|
-
let mediaType; let src; let alt; let poster;
|
|
643
|
-
if (img) {
|
|
644
|
-
mediaType = 'image';
|
|
645
|
-
({ src } = img);
|
|
646
|
-
({ alt } = img);
|
|
647
|
-
} else if (video) {
|
|
648
|
-
mediaType = 'video';
|
|
649
|
-
({ poster } = video);
|
|
650
|
-
({ src } = video);
|
|
651
|
-
} else {
|
|
652
|
-
instautoLog2('Could not determin mediaType');
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (!shouldLikeMediaIn({ mediaType, mediaDesc, src, alt, poster })) {
|
|
656
|
-
instautoLog2(`shouldLikeMedia returned false for ${image.href}, skipping`);
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
foundClickable.click();
|
|
662
|
-
window.instautoOnImageLiked(image.href);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (!dryRunIn) {
|
|
666
|
-
likeImage();
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
await window.instautoSleep(3000);
|
|
670
|
-
|
|
671
|
-
const closeButtonChild = document.querySelector('svg[aria-label="Close"]');
|
|
672
|
-
|
|
673
|
-
if (!closeButtonChild) throw new Error('Close button not found (aria-label)');
|
|
674
|
-
|
|
675
|
-
const closeButton = findClickableParent(closeButtonChild);
|
|
676
|
-
|
|
677
|
-
if (!closeButton) throw new Error('Close button not found');
|
|
678
|
-
|
|
679
|
-
closeButton.click();
|
|
680
|
-
|
|
681
|
-
await window.instautoSleep(5000);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
instautoLog('Done liking images');
|
|
685
|
-
}
|
|
686
|
-
/* eslint-enable no-undef */
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
async function likeUserImages({ username, likeImagesMin, likeImagesMax } = {}) {
|
|
690
|
-
if (!likeImagesMin || !likeImagesMax || likeImagesMax < likeImagesMin || likeImagesMin < 1) throw new Error('Invalid arguments');
|
|
691
|
-
|
|
692
|
-
await navigateToUserAndGetData(username);
|
|
693
|
-
|
|
694
|
-
logger.log(`Liking ${likeImagesMin}-${likeImagesMax} user images`);
|
|
695
|
-
try {
|
|
696
|
-
await page.exposeFunction('instautoSleep', sleep);
|
|
697
|
-
await page.exposeFunction('instautoLog', (...args) => console.log(...args));
|
|
698
|
-
await page.exposeFunction('instautoOnImageLiked', (href) => onImageLiked({ username, href }));
|
|
699
|
-
} catch (err) {
|
|
700
|
-
// Ignore already exists error
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
await page.evaluate(likeCurrentUserImagesPageCode, { dryRun, likeImagesMin, likeImagesMax, shouldLikeMedia });
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
async function followUserRespectingRestrictions({ username, skipPrivate = false }) {
|
|
707
|
-
if (getPrevFollowedUser(username)) {
|
|
708
|
-
logger.log('Skipping already followed user', username);
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const graphqlUser = await navigateToUserAndGetData(username);
|
|
713
|
-
|
|
714
|
-
const { edge_followed_by: { count: followedByCount }, edge_follow: { count: followsCount }, is_private: isPrivate, is_verified: isVerified, is_business_account: isBusinessAccount, is_professional_account: isProfessionalAccount, full_name: fullName, biography, profile_pic_url_hd: profilePicUrlHd, external_url: externalUrl, business_category_name: businessCategoryName, category_name: categoryName } = graphqlUser;
|
|
715
|
-
|
|
716
|
-
// logger.log('followedByCount:', followedByCount, 'followsCount:', followsCount);
|
|
717
|
-
|
|
718
|
-
const ratio = followedByCount / (followsCount || 1);
|
|
719
|
-
|
|
720
|
-
if (isPrivate && skipPrivate) {
|
|
721
|
-
logger.log('User is private, skipping');
|
|
722
|
-
return false;
|
|
723
|
-
}
|
|
724
|
-
if (
|
|
725
|
-
(followUserMaxFollowers != null && followedByCount > followUserMaxFollowers) ||
|
|
726
|
-
(followUserMaxFollowing != null && followsCount > followUserMaxFollowing) ||
|
|
727
|
-
(followUserMinFollowers != null && followedByCount < followUserMinFollowers) ||
|
|
728
|
-
(followUserMinFollowing != null && followsCount < followUserMinFollowing)
|
|
729
|
-
) {
|
|
730
|
-
logger.log('User has too many or too few followers or following, skipping.', 'followedByCount:', followedByCount, 'followsCount:', followsCount);
|
|
731
|
-
return false;
|
|
732
|
-
}
|
|
733
|
-
if (
|
|
734
|
-
(followUserRatioMax != null && ratio > followUserRatioMax) ||
|
|
735
|
-
(followUserRatioMin != null && ratio < followUserRatioMin)
|
|
736
|
-
) {
|
|
737
|
-
logger.log('User has too many followers compared to follows or opposite, skipping');
|
|
738
|
-
return false;
|
|
739
|
-
}
|
|
740
|
-
if (shouldFollowUser !== null && (typeof shouldFollowUser === 'function' && !shouldFollowUser({ username, isVerified, isBusinessAccount, isProfessionalAccount, fullName, biography, profilePicUrlHd, externalUrl, businessCategoryName, categoryName }) === true)) {
|
|
741
|
-
logger.log(`Custom follow logic returned false for ${username}, skipping`);
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
await followUser(username);
|
|
746
|
-
|
|
747
|
-
await sleep(30000);
|
|
748
|
-
await throttle();
|
|
749
|
-
|
|
750
|
-
return true;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
async function processUserFollowers(username, {
|
|
754
|
-
maxFollowsPerUser = 5, skipPrivate = false, enableLikeImages, likeImagesMin, likeImagesMax,
|
|
755
|
-
} = {}) {
|
|
756
|
-
const enableFollow = maxFollowsPerUser > 0;
|
|
757
|
-
|
|
758
|
-
if (enableFollow) logger.log(`Following up to ${maxFollowsPerUser} followers of ${username}`);
|
|
759
|
-
if (enableLikeImages) logger.log(`Liking images of up to ${likeImagesMax} followers of ${username}`);
|
|
760
|
-
|
|
761
|
-
await throttle();
|
|
762
|
-
|
|
763
|
-
let numFollowedForThisUser = 0;
|
|
764
|
-
|
|
765
|
-
const { id: userId } = await navigateToUserAndGetData(username);
|
|
766
|
-
|
|
767
|
-
for await (const followersBatch of getFollowersOrFollowingGenerator({ userId, getFollowers: true })) {
|
|
768
|
-
logger.log('User followers batch', followersBatch);
|
|
769
|
-
|
|
770
|
-
for (const follower of followersBatch) {
|
|
771
|
-
await throttle();
|
|
772
|
-
|
|
773
|
-
try {
|
|
774
|
-
if (enableFollow && numFollowedForThisUser >= maxFollowsPerUser) {
|
|
775
|
-
logger.log('Have reached followed limit for this user, stopping');
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
let didActuallyFollow = false;
|
|
780
|
-
if (enableFollow) didActuallyFollow = await followUserRespectingRestrictions({ username: follower, skipPrivate });
|
|
781
|
-
if (didActuallyFollow) numFollowedForThisUser += 1;
|
|
782
|
-
|
|
783
|
-
const didFailToFollow = enableFollow && !didActuallyFollow;
|
|
784
|
-
|
|
785
|
-
if (enableLikeImages && !didFailToFollow) {
|
|
786
|
-
// Note: throws error if user isPrivate
|
|
787
|
-
await likeUserImages({ username: follower, likeImagesMin, likeImagesMax });
|
|
788
|
-
}
|
|
789
|
-
} catch (err) {
|
|
790
|
-
logger.error(`Failed to process follower ${follower}`, err);
|
|
791
|
-
await takeScreenshot();
|
|
792
|
-
await sleep(20000);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
async function processUsersFollowers({ usersToFollowFollowersOf, maxFollowsTotal = 150, skipPrivate, enableFollow = true, enableLikeImages = false, likeImagesMin = 1, likeImagesMax = 2 }) {
|
|
799
|
-
// If maxFollowsTotal turns out to be lower than the user list size, slice off the user list
|
|
800
|
-
const usersToFollowFollowersOfSliced = shuffleArray(usersToFollowFollowersOf).slice(0, maxFollowsTotal);
|
|
801
|
-
|
|
802
|
-
const maxFollowsPerUser = enableFollow && usersToFollowFollowersOfSliced.length > 0 ? Math.floor(maxFollowsTotal / usersToFollowFollowersOfSliced.length) : 0;
|
|
803
|
-
|
|
804
|
-
if (maxFollowsPerUser === 0 && (!enableLikeImages || likeImagesMin < 1 || likeImagesMax < 1)) {
|
|
805
|
-
logger.warn('Nothing to follow or like');
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
for (const username of usersToFollowFollowersOfSliced) {
|
|
810
|
-
try {
|
|
811
|
-
await processUserFollowers(username, { maxFollowsPerUser, skipPrivate, enableLikeImages, likeImagesMin, likeImagesMax });
|
|
812
|
-
|
|
813
|
-
await sleep(10 * 60 * 1000);
|
|
814
|
-
await throttle();
|
|
815
|
-
} catch (err) {
|
|
816
|
-
logger.error('Failed to process user followers, continuing', username, err);
|
|
817
|
-
await takeScreenshot();
|
|
818
|
-
await sleep(60 * 1000);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
async function safelyUnfollowUserList(usersToUnfollow, limit, condition = () => true) {
|
|
824
|
-
logger.log('Unfollowing users, up to limit', limit);
|
|
825
|
-
|
|
826
|
-
let i = 0; // Number of people processed
|
|
827
|
-
let j = 0; // Number of people actually unfollowed (button pressed)
|
|
828
|
-
|
|
829
|
-
for await (const listOrUsername of usersToUnfollow) {
|
|
830
|
-
// backward compatible:
|
|
831
|
-
const list = Array.isArray(listOrUsername) ? listOrUsername : [listOrUsername];
|
|
832
|
-
|
|
833
|
-
for (const username of list) {
|
|
834
|
-
if (await condition(username)) {
|
|
835
|
-
try {
|
|
836
|
-
const userFound = await navigateToUser(username);
|
|
837
|
-
|
|
838
|
-
if (!userFound) {
|
|
839
|
-
// to avoid repeatedly unfollowing failed users, flag them as already unfollowed
|
|
840
|
-
logger.log('User not found for unfollow');
|
|
841
|
-
await addPrevUnfollowedUser({ username, time: new Date().getTime(), noActionTaken: true });
|
|
842
|
-
await sleep(3000);
|
|
843
|
-
} else {
|
|
844
|
-
const { noActionTaken } = await unfollowUser(username);
|
|
845
|
-
|
|
846
|
-
if (noActionTaken) {
|
|
847
|
-
await sleep(3000);
|
|
848
|
-
} else {
|
|
849
|
-
await sleep(15000);
|
|
850
|
-
j += 1;
|
|
851
|
-
|
|
852
|
-
if (j % 10 === 0) {
|
|
853
|
-
logger.log('Have unfollowed 10 users since last break. Taking a break');
|
|
854
|
-
await sleep(10 * 60 * 1000, 0.1);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
i += 1;
|
|
860
|
-
logger.log(`Have now unfollowed (or tried to unfollow) ${i} users`);
|
|
861
|
-
|
|
862
|
-
if (limit && j >= limit) {
|
|
863
|
-
logger.log(`Have unfollowed limit of ${limit}, stopping`);
|
|
864
|
-
return j;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
await throttle();
|
|
868
|
-
} catch (err) {
|
|
869
|
-
logger.error('Failed to unfollow, continuing with next', err);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
logger.log('Done with unfollowing', i, j);
|
|
876
|
-
|
|
877
|
-
return j;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
async function safelyFollowUserList({ users, skipPrivate, limit }) {
|
|
881
|
-
logger.log('Following users, up to limit', limit);
|
|
882
|
-
|
|
883
|
-
for (const username of users) {
|
|
884
|
-
await throttle();
|
|
885
|
-
|
|
886
|
-
try {
|
|
887
|
-
await followUserRespectingRestrictions({ username, skipPrivate });
|
|
888
|
-
} catch (err) {
|
|
889
|
-
logger.error(`Failed to follow user ${username}, continuing`, err);
|
|
890
|
-
await takeScreenshot();
|
|
891
|
-
await sleep(20000);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function getPage() {
|
|
897
|
-
return page;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
page = await browser.newPage();
|
|
901
|
-
|
|
902
|
-
// https://github.com/mifi/SimpleInstaBot/issues/118#issuecomment-1067883091
|
|
903
|
-
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en' });
|
|
904
|
-
|
|
905
|
-
if (randomizeUserAgent) {
|
|
906
|
-
const userAgentGenerated = new UserAgent({ deviceCategory: 'desktop' });
|
|
907
|
-
await page.setUserAgent(userAgentGenerated.toString());
|
|
908
|
-
}
|
|
909
|
-
if (userAgent) await page.setUserAgent(userAgent);
|
|
910
|
-
|
|
911
|
-
if (enableCookies) await tryLoadCookies();
|
|
912
|
-
|
|
913
|
-
const goHome = async () => gotoUrl(`${instagramBaseUrl}/?hl=en`);
|
|
914
|
-
|
|
915
|
-
// https://github.com/mifi/SimpleInstaBot/issues/28
|
|
916
|
-
async function setLang(short, long, assumeLoggedIn = false) {
|
|
917
|
-
logger.log(`Setting language to ${long} (${short})`);
|
|
918
|
-
|
|
919
|
-
try {
|
|
920
|
-
await sleep(1000);
|
|
921
|
-
|
|
922
|
-
// when logged in, we need to go to account in order to be able to check/set language
|
|
923
|
-
// (need to see the footer)
|
|
924
|
-
if (assumeLoggedIn) {
|
|
925
|
-
await gotoUrl(`${instagramBaseUrl}/accounts/edit/`);
|
|
926
|
-
} else {
|
|
927
|
-
await goHome();
|
|
928
|
-
}
|
|
929
|
-
await sleep(3000);
|
|
930
|
-
const elementHandles = await page.$x(`//select[//option[@value='${short}' and text()='${long}']]`);
|
|
931
|
-
if (elementHandles.length < 1) throw new Error('Language selector not found');
|
|
932
|
-
logger.log('Found language selector');
|
|
933
|
-
|
|
934
|
-
// https://stackoverflow.com/questions/45864516/how-to-select-an-option-from-dropdown-select
|
|
935
|
-
const alreadyEnglish = await page.evaluate((selectElem, short2) => {
|
|
936
|
-
const optionElem = selectElem.querySelector(`option[value='${short2}']`);
|
|
937
|
-
if (optionElem.selected) return true; // already selected?
|
|
938
|
-
optionElem.selected = true;
|
|
939
|
-
// eslint-disable-next-line no-undef
|
|
940
|
-
const event = new Event('change', { bubbles: true });
|
|
941
|
-
selectElem.dispatchEvent(event);
|
|
942
|
-
return false;
|
|
943
|
-
}, elementHandles[0], short);
|
|
944
|
-
|
|
945
|
-
if (alreadyEnglish) {
|
|
946
|
-
logger.log('Already English language');
|
|
947
|
-
if (!assumeLoggedIn) {
|
|
948
|
-
await goHome(); // because we were on the settings page
|
|
949
|
-
await sleep(1000);
|
|
950
|
-
}
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
logger.log('Selected language');
|
|
955
|
-
await sleep(3000);
|
|
956
|
-
await goHome();
|
|
957
|
-
await sleep(1000);
|
|
958
|
-
} catch (err) {
|
|
959
|
-
logger.error('Failed to set language, trying fallback (cookie)', err);
|
|
960
|
-
// This doesn't seem to always work, hence why it's just a fallback now
|
|
961
|
-
await goHome();
|
|
962
|
-
await sleep(1000);
|
|
963
|
-
|
|
964
|
-
await page.setCookie({
|
|
965
|
-
name: 'ig_lang',
|
|
966
|
-
value: short,
|
|
967
|
-
path: '/',
|
|
968
|
-
});
|
|
969
|
-
await sleep(1000);
|
|
970
|
-
await goHome();
|
|
971
|
-
await sleep(3000);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
const setEnglishLang = async (assumeLoggedIn) => setLang('en', 'English', assumeLoggedIn);
|
|
976
|
-
// const setEnglishLang = async (assumeLoggedIn) => setLang('de', 'Deutsch', assumeLoggedIn);
|
|
977
|
-
|
|
978
|
-
async function tryPressButton(elementHandles, name, sleepMs = 3000) {
|
|
979
|
-
try {
|
|
980
|
-
if (elementHandles.length === 1) {
|
|
981
|
-
logger.log(`Pressing button: ${name}`);
|
|
982
|
-
elementHandles[0].click();
|
|
983
|
-
await sleep(sleepMs);
|
|
984
|
-
}
|
|
985
|
-
} catch (err) {
|
|
986
|
-
logger.warn(`Failed to press button: ${name}`);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
async function tryClickLogin() {
|
|
991
|
-
async function tryClickButton(xpath) {
|
|
992
|
-
const btn = (await page.$x(xpath))[0];
|
|
993
|
-
if (!btn) return false;
|
|
994
|
-
await btn.click();
|
|
995
|
-
return true;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
if (await tryClickButton("//button[.//text() = 'Log In']")) return true;
|
|
999
|
-
if (await tryClickButton("//button[.//text() = 'Log in']")) return true; // https://github.com/mifi/instauto/pull/110 https://github.com/mifi/instauto/issues/109
|
|
1000
|
-
return false;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
await setEnglishLang(false);
|
|
1004
|
-
|
|
1005
|
-
await tryPressButton(await page.$x('//button[contains(text(), "Accept")]'), 'Accept cookies dialog');
|
|
1006
|
-
await tryPressButton(await page.$x('//button[contains(text(), "Only allow essential cookies")]'), 'Accept cookies dialog 2 button 1', 10000);
|
|
1007
|
-
await tryPressButton(await page.$x('//button[contains(text(), "Allow essential and optional cookies")]'), 'Accept cookies dialog 2 button 2', 10000);
|
|
1008
|
-
|
|
1009
|
-
if (!(await isLoggedIn())) {
|
|
1010
|
-
if (!myUsername || !password) {
|
|
1011
|
-
await tryDeleteCookies();
|
|
1012
|
-
throw new Error('No longer logged in. Deleting cookies and aborting. Need to provide username/password');
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
try {
|
|
1016
|
-
await page.click('a[href="/accounts/login/?source=auth_switcher"]');
|
|
1017
|
-
await sleep(1000);
|
|
1018
|
-
} catch (err) {
|
|
1019
|
-
logger.info('No login page button, assuming we are on login form');
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Mobile version https://github.com/mifi/SimpleInstaBot/issues/7
|
|
1023
|
-
await tryPressButton(await page.$x('//button[contains(text(), "Log In")]'), 'Login form button');
|
|
1024
|
-
|
|
1025
|
-
await page.type('input[name="username"]', myUsername, { delay: 50 });
|
|
1026
|
-
await sleep(1000);
|
|
1027
|
-
await page.type('input[name="password"]', password, { delay: 50 });
|
|
1028
|
-
await sleep(1000);
|
|
1029
|
-
|
|
1030
|
-
for (;;) {
|
|
1031
|
-
const didClickLogin = await tryClickLogin();
|
|
1032
|
-
if (didClickLogin) break;
|
|
1033
|
-
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 :)');
|
|
1034
|
-
await sleep(6000);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
await sleep(6000);
|
|
1038
|
-
|
|
1039
|
-
// Sometimes login button gets stuck with a spinner
|
|
1040
|
-
// https://github.com/mifi/SimpleInstaBot/issues/25
|
|
1041
|
-
if (!(await isLoggedIn())) {
|
|
1042
|
-
logger.log('Still not logged in, trying to reload loading page');
|
|
1043
|
-
await page.reload();
|
|
1044
|
-
await sleep(5000);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
let warnedAboutLoginFail = false;
|
|
1048
|
-
while (!(await isLoggedIn())) {
|
|
1049
|
-
if (!warnedAboutLoginFail) logger.warn('WARNING: Login has not succeeded. This could be because of an incorrect username/password, or a "suspicious login attempt"-message. You need to manually complete the process, or if really logged in, click the Instagram logo in the top left to go to the Home page.');
|
|
1050
|
-
warnedAboutLoginFail = true;
|
|
1051
|
-
await sleep(5000);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// In case language gets reset after logging in
|
|
1055
|
-
// https://github.com/mifi/SimpleInstaBot/issues/118
|
|
1056
|
-
await setEnglishLang(true);
|
|
1057
|
-
|
|
1058
|
-
// Mobile version https://github.com/mifi/SimpleInstaBot/issues/7
|
|
1059
|
-
await tryPressButton(await page.$x('//button[contains(text(), "Save Info")]'), 'Login info dialog: Save Info');
|
|
1060
|
-
// May sometimes be "Save info" too? https://github.com/mifi/instauto/pull/70
|
|
1061
|
-
await tryPressButton(await page.$x('//button[contains(text(), "Save info")]'), 'Login info dialog: Save info');
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
await tryPressButton(await page.$x('//button[contains(text(), "Not Now")]'), 'Turn on Notifications dialog');
|
|
1065
|
-
|
|
1066
|
-
await trySaveCookies();
|
|
1067
|
-
|
|
1068
|
-
logger.log(`Have followed/unfollowed ${getNumFollowedUsersThisTimeUnit(hourMs)} in the last hour`);
|
|
1069
|
-
logger.log(`Have followed/unfollowed ${getNumFollowedUsersThisTimeUnit(dayMs)} in the last 24 hours`);
|
|
1070
|
-
logger.log(`Have liked ${getNumLikesThisTimeUnit(dayMs)} images in the last 24 hours`);
|
|
1071
|
-
|
|
1072
|
-
try {
|
|
1073
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
1074
|
-
const detectedUsername = await page.evaluate(() => window._sharedData.config.viewer.username);
|
|
1075
|
-
if (detectedUsername) myUsername = detectedUsername;
|
|
1076
|
-
} catch (err) {
|
|
1077
|
-
logger.error('Failed to detect username', err);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
if (!myUsername) {
|
|
1081
|
-
throw new Error('Don\'t know what\'s my username');
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const { id: myUserId } = await navigateToUserAndGetData(myUsername);
|
|
1085
|
-
|
|
1086
|
-
// --- END OF INITIALIZATION
|
|
1087
|
-
|
|
1088
|
-
async function doesUserFollowMe(username) {
|
|
1089
|
-
try {
|
|
1090
|
-
logger.info('Checking if user', username, 'follows us');
|
|
1091
|
-
const { id: userId } = await navigateToUserAndGetData(username);
|
|
1092
|
-
|
|
1093
|
-
const elementHandles = await page.$x("//a[contains(.,' following')][contains(@href,'/following')]");
|
|
1094
|
-
if (elementHandles.length === 0) throw new Error('Following button not found');
|
|
1095
|
-
|
|
1096
|
-
const [foundResponse] = await Promise.all([
|
|
1097
|
-
page.waitForResponse((response) => {
|
|
1098
|
-
const request = response.request();
|
|
1099
|
-
return request.method() === 'GET' && new RegExp(`instagram.com/api/v1/friendships/${userId}/following/`).test(request.url());
|
|
1100
|
-
}),
|
|
1101
|
-
elementHandles[0].click(),
|
|
1102
|
-
// page.waitForNavigation({ waitUntil: 'networkidle0' }),
|
|
1103
|
-
]);
|
|
1104
|
-
|
|
1105
|
-
const { users } = JSON.parse(await foundResponse.text());
|
|
1106
|
-
if (users.length < 2) throw new Error('Unable to find user follows list');
|
|
1107
|
-
// console.log(users, myUserId);
|
|
1108
|
-
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
|
|
1109
|
-
} catch (err) {
|
|
1110
|
-
logger.error('Failed to check if user follows us', err);
|
|
1111
|
-
return undefined;
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
async function unfollowNonMutualFollowers({ limit } = {}) {
|
|
1116
|
-
logger.log(`Unfollowing non-mutual followers (limit ${limit})...`);
|
|
1117
|
-
|
|
1118
|
-
/* const allFollowers = await getFollowersOrFollowing({
|
|
1119
|
-
userId: myUserId,
|
|
1120
|
-
getFollowers: true,
|
|
1121
|
-
}); */
|
|
1122
|
-
const allFollowingGenerator = getFollowersOrFollowingGenerator({
|
|
1123
|
-
userId: myUserId,
|
|
1124
|
-
getFollowers: false,
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
async function condition(username) {
|
|
1128
|
-
// if (allFollowers.includes(u)) return false; // Follows us
|
|
1129
|
-
if (excludeUsers.includes(username)) return false; // User is excluded by exclude list
|
|
1130
|
-
if (haveRecentlyFollowedUser(username)) {
|
|
1131
|
-
logger.log(`Have recently followed user ${username}, skipping`);
|
|
1132
|
-
return false;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
const followsMe = await doesUserFollowMe(username);
|
|
1136
|
-
logger.info('User follows us?', followsMe);
|
|
1137
|
-
return followsMe === false;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
return safelyUnfollowUserList(allFollowingGenerator, limit, condition);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
async function unfollowAllUnknown({ limit } = {}) {
|
|
1144
|
-
logger.log('Unfollowing all except excludes and auto followed');
|
|
1145
|
-
|
|
1146
|
-
const unfollowUsersGenerator = getFollowersOrFollowingGenerator({
|
|
1147
|
-
userId: myUserId,
|
|
1148
|
-
getFollowers: false,
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
function condition(username) {
|
|
1152
|
-
if (getPrevFollowedUser(username)) return false; // we followed this user, so it's not unknown
|
|
1153
|
-
if (excludeUsers.includes(username)) return false; // User is excluded by exclude list
|
|
1154
|
-
return true;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
return safelyUnfollowUserList(unfollowUsersGenerator, limit, condition);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
async function unfollowOldFollowed({ ageInDays, limit } = {}) {
|
|
1161
|
-
assert(ageInDays);
|
|
1162
|
-
|
|
1163
|
-
logger.log(`Unfollowing currently followed users who were auto-followed more than ${ageInDays} days ago (limit ${limit})...`);
|
|
1164
|
-
|
|
1165
|
-
const followingUsersGenerator = getFollowersOrFollowingGenerator({
|
|
1166
|
-
userId: myUserId,
|
|
1167
|
-
getFollowers: false,
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
function condition(username) {
|
|
1171
|
-
return getPrevFollowedUser(username) &&
|
|
1172
|
-
!excludeUsers.includes(username) &&
|
|
1173
|
-
(new Date().getTime() - getPrevFollowedUser(username).time) / (1000 * 60 * 60 * 24) > ageInDays;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
return safelyUnfollowUserList(followingUsersGenerator, limit, condition);
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
async function listManuallyFollowedUsers() {
|
|
1180
|
-
const allFollowing = await getFollowersOrFollowing({
|
|
1181
|
-
userId: myUserId,
|
|
1182
|
-
getFollowers: false,
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
return allFollowing.filter(u =>
|
|
1186
|
-
!getPrevFollowedUser(u) && !excludeUsers.includes(u));
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
return {
|
|
1190
|
-
followUserFollowers: processUserFollowers,
|
|
1191
|
-
unfollowNonMutualFollowers,
|
|
1192
|
-
unfollowAllUnknown,
|
|
1193
|
-
unfollowOldFollowed,
|
|
1194
|
-
followUser,
|
|
1195
|
-
unfollowUser,
|
|
1196
|
-
likeUserImages,
|
|
1197
|
-
sleep,
|
|
1198
|
-
listManuallyFollowedUsers,
|
|
1199
|
-
getFollowersOrFollowing,
|
|
1200
|
-
getUsersWhoLikedContent,
|
|
1201
|
-
safelyUnfollowUserList,
|
|
1202
|
-
safelyFollowUserList,
|
|
1203
|
-
getPage,
|
|
1204
|
-
followUsersFollowers: processUsersFollowers,
|
|
1205
|
-
doesUserFollowMe,
|
|
1206
|
-
navigateToUserAndGetData,
|
|
1207
|
-
};
|
|
1208
|
-
};
|
|
1209
|
-
|
|
1210
|
-
Instauto.JSONDB = JSONDB;
|
|
1211
|
-
|
|
1212
|
-
module.exports = Instauto;
|