instauto 9.1.7 → 9.1.10
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 +4 -3
- package/package.json +1 -1
- package/src/index.js +121 -36
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://discord.gg/Rh3KT9zyhj) [](https://paypal.me/mifino/usd)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
instauto is an Instagram automation/bot library (API) written in modern, clean javascript using Google's Puppeteer. Goal is to be very easy to set up, use, and extend, and obey instagram's limits. Heavily inspired by [InstaPy](https://github.com/timgrossmann/InstaPy), but I thought it was way too heavy and hard to setup.
|
|
6
|
+
|
|
7
|
+
There is also a GUI application for those who don't want to code: [SimpleInstaBot](https://mifi.github.io/SimpleInstaBot/)
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
## Setup
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -16,6 +16,15 @@ function shuffleArray(arrayIn) {
|
|
|
16
16
|
return array;
|
|
17
17
|
}
|
|
18
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
|
+
|
|
19
28
|
const botWorkShiftHours = 16;
|
|
20
29
|
|
|
21
30
|
const dayMs = 24 * 60 * 60 * 1000;
|
|
@@ -122,7 +131,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
122
131
|
const sleep = (ms, deviation = 1) => {
|
|
123
132
|
let msWithDev = ((Math.random() * deviation) + 1) * ms;
|
|
124
133
|
if (dryRun) msWithDev = Math.min(3000, msWithDev); // for dryRun, no need to wait so long
|
|
125
|
-
logger.log('Waiting',
|
|
134
|
+
logger.log('Waiting', (msWithDev / 1000).toFixed(2), 'sec');
|
|
126
135
|
return new Promise(resolve => setTimeout(resolve, msWithDev));
|
|
127
136
|
};
|
|
128
137
|
|
|
@@ -178,8 +187,9 @@ const Instauto = async (db, browser, options) => {
|
|
|
178
187
|
for (let attempt = 0; ; attempt += 1) {
|
|
179
188
|
logger.log(`Goto ${url}`);
|
|
180
189
|
const response = await gotoUrl(url);
|
|
181
|
-
await sleep(2000);
|
|
182
190
|
const status = response.status();
|
|
191
|
+
logger.log('Page loaded');
|
|
192
|
+
await sleep(2000);
|
|
183
193
|
|
|
184
194
|
// https://www.reddit.com/r/Instagram/comments/kwrt0s/error_560/
|
|
185
195
|
// https://github.com/mifi/instauto/issues/60
|
|
@@ -217,12 +227,14 @@ const Instauto = async (db, browser, options) => {
|
|
|
217
227
|
}
|
|
218
228
|
|
|
219
229
|
if (status === 200) {
|
|
230
|
+
// logger.log('Page returned 200 ☑️');
|
|
220
231
|
// some pages return 200 but nothing there (I think deleted accounts)
|
|
221
232
|
// https://github.com/mifi/SimpleInstaBot/issues/48
|
|
222
233
|
// example: https://www.instagram.com/victorialarson__/
|
|
223
234
|
// so we check if the page has the user's name on it
|
|
224
|
-
const
|
|
225
|
-
|
|
235
|
+
const elementHandles = await page.$x(`//body//main//*[contains(text(),${escapeXpathStr(username)})]`);
|
|
236
|
+
const foundUsernameOnPage = elementHandles.length > 0;
|
|
237
|
+
if (!foundUsernameOnPage) logger.warn(`Cannot find text "${username}" on page`);
|
|
226
238
|
return foundUsernameOnPage;
|
|
227
239
|
}
|
|
228
240
|
|
|
@@ -247,22 +259,85 @@ const Instauto = async (db, browser, options) => {
|
|
|
247
259
|
return cachedUserData;
|
|
248
260
|
}
|
|
249
261
|
|
|
262
|
+
async function getUserDataFromPage() {
|
|
263
|
+
// https://github.com/mifi/instauto/issues/115#issuecomment-1199335650
|
|
264
|
+
// to test in browser: document.getElementsByTagName('html')[0].innerHTML.split('\n');
|
|
265
|
+
try {
|
|
266
|
+
const body = await page.content();
|
|
267
|
+
for (let q of body.split(/\r?\n/)) {
|
|
268
|
+
if (q.includes('edge_followed_by')) {
|
|
269
|
+
// eslint-disable-next-line prefer-destructuring
|
|
270
|
+
q = q.split(',[],[')[1];
|
|
271
|
+
// eslint-disable-next-line prefer-destructuring
|
|
272
|
+
q = q.split(']]]')[0];
|
|
273
|
+
q = JSON.parse(q);
|
|
274
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
275
|
+
q = q.data.__bbox.result.response;
|
|
276
|
+
q = q.replace(/\\/g, '');
|
|
277
|
+
q = JSON.parse(q);
|
|
278
|
+
return q.data.user;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
logger.warn('Failed to get user data from page', err);
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
250
287
|
// intercept special XHR network request that fetches user's data and store it in a cache
|
|
251
288
|
// TODO fallback to DOM to get user ID if this request fails?
|
|
252
289
|
// https://github.com/mifi/SimpleInstaBot/issues/125#issuecomment-1145354294
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
290
|
+
async function getUserDataFromInterceptedRequest() {
|
|
291
|
+
const t = setTimeout(async () => {
|
|
292
|
+
logger.log('Unable to intercept request, will send manually');
|
|
293
|
+
try {
|
|
294
|
+
await page.evaluate(async (username2) => {
|
|
295
|
+
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' } });
|
|
296
|
+
await response.json(); // else it will not finish the request
|
|
297
|
+
}, username);
|
|
298
|
+
// todo `https://i.instagram.com/api/v1/users/${userId}/info/`
|
|
299
|
+
// https://www.javafixing.com/2022/07/fixed-can-get-instagram-profile-picture.html?m=1
|
|
300
|
+
} catch (err) {
|
|
301
|
+
logger.error('Failed to manually send request', err);
|
|
302
|
+
}
|
|
303
|
+
}, 5000);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const [foundResponse] = await Promise.all([
|
|
307
|
+
page.waitForResponse((response) => {
|
|
308
|
+
const request = response.request();
|
|
309
|
+
return request.method() === 'GET' && new RegExp(`https:\\/\\/i\\.instagram\\.com\\/api\\/v1\\/users\\/web_profile_info\\/\\?username=${encodeURIComponent(username.toLowerCase())}`).test(request.url());
|
|
310
|
+
}, { timeout: 30000 }),
|
|
311
|
+
navigateToUserWithCheck(username),
|
|
312
|
+
// page.waitForNavigation({ waitUntil: 'networkidle0' }),
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
const json = JSON.parse(await foundResponse.text());
|
|
316
|
+
return json.data.user;
|
|
317
|
+
} finally {
|
|
318
|
+
clearTimeout(t);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
logger.log('Trying to get user data from HTML');
|
|
323
|
+
|
|
324
|
+
await navigateToUserWithCheck(username);
|
|
325
|
+
let userData = await getUserDataFromPage();
|
|
326
|
+
if (userData) {
|
|
327
|
+
userDataCache[username] = userData;
|
|
328
|
+
return userData;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
logger.log('Need to intercept network request to get user data');
|
|
332
|
+
|
|
333
|
+
// works for old accounts only:
|
|
334
|
+
userData = await getUserDataFromInterceptedRequest();
|
|
335
|
+
if (userData) {
|
|
336
|
+
userDataCache[username] = userData;
|
|
337
|
+
return userData;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return undefined;
|
|
266
341
|
}
|
|
267
342
|
|
|
268
343
|
async function getPageJson() {
|
|
@@ -315,23 +390,23 @@ const Instauto = async (db, browser, options) => {
|
|
|
315
390
|
}
|
|
316
391
|
|
|
317
392
|
async function findUnfollowButton() {
|
|
318
|
-
|
|
319
|
-
if (
|
|
393
|
+
let button = await findButtonWithText('Following');
|
|
394
|
+
if (button) return button;
|
|
320
395
|
|
|
321
|
-
|
|
322
|
-
if (
|
|
396
|
+
button = await findButtonWithText('Requested');
|
|
397
|
+
if (button) return button;
|
|
323
398
|
|
|
324
|
-
|
|
325
|
-
if (
|
|
399
|
+
let elementHandles = await page.$x("//header//button[*//span[@aria-label='Following']]");
|
|
400
|
+
if (elementHandles.length > 0) return elementHandles[0];
|
|
326
401
|
|
|
327
|
-
|
|
328
|
-
if (
|
|
402
|
+
elementHandles = await page.$x("//header//button[*//span[@aria-label='Requested']]");
|
|
403
|
+
if (elementHandles.length > 0) return elementHandles[0];
|
|
329
404
|
|
|
330
|
-
|
|
331
|
-
if (
|
|
405
|
+
elementHandles = await page.$x("//header//button[*//*[name()='svg'][@aria-label='Following']]");
|
|
406
|
+
if (elementHandles.length > 0) return elementHandles[0];
|
|
332
407
|
|
|
333
|
-
|
|
334
|
-
if (
|
|
408
|
+
elementHandles = await page.$x("//header//button[*//*[name()='svg'][@aria-label='Requested']]");
|
|
409
|
+
if (elementHandles.length > 0) return elementHandles[0];
|
|
335
410
|
|
|
336
411
|
return undefined;
|
|
337
412
|
}
|
|
@@ -881,6 +956,19 @@ const Instauto = async (db, browser, options) => {
|
|
|
881
956
|
}
|
|
882
957
|
}
|
|
883
958
|
|
|
959
|
+
async function tryClickLogin() {
|
|
960
|
+
async function tryClickButton(xpath) {
|
|
961
|
+
const btn = (await page.$x(xpath))[0];
|
|
962
|
+
if (!btn) return false;
|
|
963
|
+
await btn.click();
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (await tryClickButton("//button[.//text() = 'Log In']")) return true;
|
|
968
|
+
if (await tryClickButton("//button[.//text() = 'Log in']")) return true; // https://github.com/mifi/instauto/pull/110 https://github.com/mifi/instauto/issues/109
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
|
|
884
972
|
await setEnglishLang(false);
|
|
885
973
|
|
|
886
974
|
await tryPressButton(await page.$x('//button[contains(text(), "Accept")]'), 'Accept cookies dialog');
|
|
@@ -909,11 +997,8 @@ const Instauto = async (db, browser, options) => {
|
|
|
909
997
|
await sleep(1000);
|
|
910
998
|
|
|
911
999
|
for (;;) {
|
|
912
|
-
const
|
|
913
|
-
if (
|
|
914
|
-
await loginButton.click();
|
|
915
|
-
break;
|
|
916
|
-
}
|
|
1000
|
+
const didClickLogin = await tryClickLogin();
|
|
1001
|
+
if (didClickLogin) break;
|
|
917
1002
|
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 :)');
|
|
918
1003
|
await sleep(6000);
|
|
919
1004
|
}
|
|
@@ -1021,7 +1106,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
1021
1106
|
return followsMe === false;
|
|
1022
1107
|
}
|
|
1023
1108
|
|
|
1024
|
-
|
|
1109
|
+
return safelyUnfollowUserList(allFollowingGenerator, limit, condition);
|
|
1025
1110
|
}
|
|
1026
1111
|
|
|
1027
1112
|
async function unfollowAllUnknown({ limit } = {}) {
|
|
@@ -1038,7 +1123,7 @@ const Instauto = async (db, browser, options) => {
|
|
|
1038
1123
|
return true;
|
|
1039
1124
|
}
|
|
1040
1125
|
|
|
1041
|
-
|
|
1126
|
+
return safelyUnfollowUserList(unfollowUsersGenerator, limit, condition);
|
|
1042
1127
|
}
|
|
1043
1128
|
|
|
1044
1129
|
async function unfollowOldFollowed({ ageInDays, limit } = {}) {
|