instauto 7.1.1 → 7.2.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.
@@ -1,2 +1,2 @@
1
1
  github: mifi
2
- custom: https://paypal.me/mifino
2
+ custom: https://mifi.no/thanks
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # instauto
1
+ ![](logo.png)
2
2
 
3
- instauto is an Instagram automation/bot library 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/CharlesCCC/InstaPy), but I thought it was way too heavy and hard to setup.
3
+ instauto is an Instagram automation/bot library 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.
4
4
 
5
5
  **NEW! 🎉**
6
6
  Now there is a GUI application for those who don't want to code: [SimpleInstaBot](https://mifi.github.io/SimpleInstaBot/)
@@ -76,10 +76,20 @@ pm2 startup
76
76
 
77
77
  Now it will run automatically on reboot! 🙌
78
78
 
79
+ ## Donate 🙈
80
+
81
+ This project is maintained by me alone. The project will always remain free and open source, but if it's useful for you, consider supporting me. :) It will give me extra motivation to improve it.
82
+
83
+ [Paypal](https://paypal.me/mifino/usd) | [crypto](https://mifi.no/thanks)
84
+
85
+ ## Credits
86
+
87
+ - Icons made by [smalllikeart](https://www.flaticon.com/authors/smalllikeart) & [Freepik](https://www.flaticon.com/authors/freepik) from [www.flaticon.com](https://www.flaticon.com/)
88
+
79
89
  ---
80
90
 
81
91
  Made with ❤️ in 🇳🇴
82
92
 
83
93
  [More apps by mifi.no](https://mifi.no/)
84
94
 
85
- Follow me on [GitHub](https://github.com/mifi/), [YouTube](https://www.youtube.com/channel/UC6XlvVH63g0H54HSJubURQA), [IG](https://www.instagram.com/mifi.no/), [Twitter](https://twitter.com/mifi_no) for more awesome content!
95
+ Follow me on [GitHub](https://github.com/mifi/), [YouTube](https://www.youtube.com/channel/UC6XlvVH63g0H54HSJubURQA), [IG](https://www.instagram.com/mifi.no/), [Twitter](https://twitter.com/mifi_no) for more awesome content!
package/example.js CHANGED
@@ -31,6 +31,10 @@ const options = {
31
31
  // Don't follow users who have more people following them than this:
32
32
  followUserMinFollowing: null,
33
33
 
34
+ // NOTE: The dontUnfollowUntilTimeElapsed option is ONLY for the unfollowNonMutualFollowers function
35
+ // This specifies the time during which the bot should not touch users that it has previously followed (in milliseconds)
36
+ // After this time has passed, it will be able to unfollow them again.
37
+ // TODO should remove this option from here
34
38
  dontUnfollowUntilTimeElapsed: 3 * 24 * 60 * 60 * 1000,
35
39
 
36
40
  // Usernames that we should not touch, e.g. your friends and actual followings
@@ -58,25 +62,33 @@ const options = {
58
62
 
59
63
  const instauto = await Instauto(instautoDb, browser, options);
60
64
 
65
+ // This can be used to unfollow people:
66
+ // Will unfollow auto-followed AND manually followed accounts who are not following us back, after some time has passed
67
+ // The time is specified by config option dontUnfollowUntilTimeElapsed
68
+ // await instauto.unfollowNonMutualFollowers();
69
+ // await instauto.sleep(10 * 60 * 1000);
70
+
71
+ // Unfollow previously auto-followed users (regardless of whether or not they are following us back)
72
+ // after a certain amount of days (2 weeks)
73
+ // Leave room to do following after this too (unfollow 2/3 of maxFollowsPerDay)
74
+ const unfollowedCount = await instauto.unfollowOldFollowed({ ageInDays: 14, limit: options.maxFollowsPerDay * (2 / 3) });
75
+
76
+ if (unfollowedCount > 0) await instauto.sleep(10 * 60 * 1000);
77
+
61
78
  // List of usernames that we should follow the followers of, can be celebrities etc.
62
79
  const usersToFollowFollowersOf = ['lostleblanc', 'sam_kolder'];
63
80
 
64
81
  // Now go through each of these and follow a certain amount of their followers
65
- await instauto.followUsersFollowers({ usersToFollowFollowersOf, skipPrivate: true, enableLikeImages: true });
66
-
67
- await instauto.sleep(10 * 60 * 1000);
68
-
69
- // This is used to unfollow people - auto-followed AND manually followed -
70
- // who are not following us back, after some time has passed
71
- // (config parameter dontUnfollowUntilTimeElapsed)
72
- await instauto.unfollowNonMutualFollowers();
82
+ await instauto.followUsersFollowers({
83
+ usersToFollowFollowersOf,
84
+ maxFollowsTotal: options.maxFollowsPerDay - unfollowedCount,
85
+ skipPrivate: true,
86
+ enableLikeImages: true,
87
+ likeImagesMax: 3,
88
+ });
73
89
 
74
90
  await instauto.sleep(10 * 60 * 1000);
75
91
 
76
- // Unfollow auto-followed users (regardless of whether they are following us)
77
- // after a certain amount of days
78
- await instauto.unfollowOldFollowed({ ageInDays: 60 });
79
-
80
92
  console.log('Done running');
81
93
 
82
94
  await instauto.sleep(30000);
package/index.js CHANGED
@@ -73,6 +73,7 @@ const Instauto = async (db, browser, options) => {
73
73
 
74
74
  // State
75
75
  let page;
76
+ let graphqlUserMissing = false;
76
77
 
77
78
  async function takeScreenshot() {
78
79
  if (!screenshotOnError) return;
@@ -164,9 +165,9 @@ const Instauto = async (db, browser, options) => {
164
165
  return new Date().getTime() - followedUserEntry.time < dontUnfollowUntilTimeElapsed;
165
166
  }
166
167
 
167
- async function navigateToUser(username) {
168
- logger.log(`Navigating to user ${username}`);
169
- const response = await page.goto(`${instagramBaseUrl}/${encodeURIComponent(username)}`);
168
+ async function safeGoto(url) {
169
+ logger.log(`Goto ${url}`);
170
+ const response = await page.goto(url);
170
171
  await sleep(1000);
171
172
  const status = response.status();
172
173
  if (status === 200) {
@@ -182,6 +183,49 @@ const Instauto = async (db, browser, options) => {
182
183
  throw new Error(`Navigate to user returned status ${response.status()}`);
183
184
  }
184
185
 
186
+ async function navigateToUser(username) {
187
+ logger.log(`Navigating to user ${username}`);
188
+ return safeGoto(`${instagramBaseUrl}/${encodeURIComponent(username)}`);
189
+ }
190
+
191
+ async function getPageJson() {
192
+ return JSON.parse(await (await (await page.$('pre')).getProperty('textContent')).jsonValue());
193
+ }
194
+
195
+ async function navigateToUserAndGetData(username) {
196
+ // https://github.com/mifi/SimpleInstaBot/issues/36
197
+ if (graphqlUserMissing) {
198
+ // https://stackoverflow.com/questions/37593025/instagram-api-get-the-userid
199
+ // https://stackoverflow.com/questions/17373886/how-can-i-get-a-users-media-from-instagram-without-authenticating-as-a-user
200
+ const found = await safeGoto(`${instagramBaseUrl}/${encodeURIComponent(username)}?__a=1`);
201
+ if (!found) throw new Error('User not found');
202
+
203
+ const json = await getPageJson();
204
+
205
+ const { user } = json.graphql;
206
+
207
+ await navigateToUser(username);
208
+ return user;
209
+ }
210
+
211
+ await navigateToUser(username);
212
+
213
+ // eslint-disable-next-line no-underscore-dangle
214
+ const sharedData = await page.evaluate(() => window._sharedData);
215
+ try {
216
+ // eslint-disable-next-line prefer-destructuring
217
+ return sharedData.entry_data.ProfilePage[0].graphql.user;
218
+
219
+ // JSON.parse(Array.from(document.getElementsByTagName('script')).find(el => el.innerHTML.startsWith('window.__additionalDataLoaded(\'feed\',')).innerHTML.replace(/^window.__additionalDataLoaded\('feed',({.*})\);$/, '$1'));
220
+ // JSON.parse(Array.from(document.getElementsByTagName('script')).find(el => el.innerHTML.startsWith('window._sharedData')).innerHTML.replace(/^window._sharedData ?= ?({.*});$/, '$1'));
221
+ // Array.from(document.getElementsByTagName('a')).find(el => el.attributes?.href?.value.includes(`${username}/followers`)).innerText
222
+ } catch (err) {
223
+ logger.warn('Missing graphql in page, falling back to alternative method...');
224
+ graphqlUserMissing = true; // Store as state so we don't have to do this every time from now on.
225
+ return navigateToUserAndGetData(username); // Now try again with alternative method
226
+ }
227
+ }
228
+
185
229
  async function isActionBlocked() {
186
230
  if ((await page.$x('//*[contains(text(), "Action Blocked")]')).length > 0) return true;
187
231
  if ((await page.$x('//*[contains(text(), "Try Again Later")]')).length > 0) return true;
@@ -310,29 +354,12 @@ const Instauto = async (db, browser, options) => {
310
354
 
311
355
  const isLoggedIn = async () => (await page.$x('//*[@aria-label="Home"]')).length === 1;
312
356
 
313
- async function getPageJson() {
314
- return JSON.parse(await (await (await page.$('pre')).getProperty('textContent')).jsonValue());
315
- }
316
-
317
- async function getCurrentUser() {
318
- // eslint-disable-next-line no-underscore-dangle
319
- return page.evaluate(() => window._sharedData.entry_data.ProfilePage[0].graphql.user);
320
- // eslint-disable-line no-undef,no-underscore-dangle,max-len
321
- // return JSON.parse(Array.from(document.getElementsByTagName('script')).find(el => el.innerHTML.startsWith('window.__additionalDataLoaded(\'feed\',')).innerHTML.replace(/^window.__additionalDataLoaded\('feed',({.*})\);$/, '$1'));
322
- // return JSON.parse(Array.from(document.getElementsByTagName('script')).find(el => el.innerHTML.startsWith('window._sharedData')).innerHTML.replace(/^window._sharedData ?= ?({.*});$/, '$1'));
323
- // Array.from(document.getElementsByTagName('a')).find(el => el.attributes?.href?.value.includes(`${username}/followers`)).innerText
324
- }
325
-
326
- async function getFollowersOrFollowing({
327
- userId, getFollowers = false, maxPages, shouldProceed: shouldProceedArg,
328
- }) {
329
- const graphqlUrl = `${instagramBaseUrl}/graphql/query`;
330
- const followersUrl = `${graphqlUrl}/?query_hash=37479f2b8209594dde7facb0d904896a`;
331
- const followingUrl = `${graphqlUrl}/?query_hash=58712303d941c6855d4e888c5f0cd22f`;
357
+ async function graphqlQueryUsers({ queryHash, getResponseProp, maxPages, shouldProceed: shouldProceedArg, graphqlVariables: graphqlVariablesIn }) {
358
+ const graphqlUrl = `${instagramBaseUrl}/graphql/query/?query_hash=${queryHash}`;
332
359
 
333
360
  const graphqlVariables = {
334
- id: userId,
335
361
  first: 50,
362
+ ...graphqlVariablesIn,
336
363
  };
337
364
 
338
365
  const outUsers = [];
@@ -348,15 +375,14 @@ const Instauto = async (db, browser, options) => {
348
375
  };
349
376
 
350
377
  while (shouldProceed()) {
351
- const url = `${getFollowers ? followersUrl : followingUrl}&variables=${JSON.stringify(graphqlVariables)}`;
378
+ const url = `${graphqlUrl}&variables=${JSON.stringify(graphqlVariables)}`;
352
379
  // logger.log(url);
353
380
  await page.goto(url);
354
381
  const json = await getPageJson();
355
382
 
356
- const subPropName = getFollowers ? 'edge_followed_by' : 'edge_follow';
357
-
358
- const pageInfo = json.data.user[subPropName].page_info;
359
- const { edges } = json.data.user[subPropName];
383
+ const subProp = getResponseProp(json);
384
+ const pageInfo = subProp.page_info;
385
+ const { edges } = subProp;
360
386
 
361
387
  edges.forEach(e => outUsers.push(e.node.username));
362
388
 
@@ -373,6 +399,33 @@ const Instauto = async (db, browser, options) => {
373
399
  return outUsers;
374
400
  }
375
401
 
402
+ async function getFollowersOrFollowing({
403
+ userId, getFollowers = false, maxPages, shouldProceed,
404
+ }) {
405
+ return graphqlQueryUsers({
406
+ getResponseProp: (json) => json.data.user[getFollowers ? 'edge_followed_by' : 'edge_follow'],
407
+ graphqlVariables: { id: userId },
408
+ shouldProceed,
409
+ maxPages,
410
+ queryHash: getFollowers ? '37479f2b8209594dde7facb0d904896a' : '58712303d941c6855d4e888c5f0cd22f',
411
+ });
412
+ }
413
+
414
+ async function getUsersWhoLikedContent({
415
+ contentId, maxPages, shouldProceed,
416
+ }) {
417
+ return graphqlQueryUsers({
418
+ getResponseProp: (json) => json.data.shortcode_media.edge_liked_by,
419
+ graphqlVariables: {
420
+ shortcode: contentId,
421
+ include_reel: true,
422
+ },
423
+ shouldProceed,
424
+ maxPages,
425
+ queryHash: 'd5d763b1e2acf209d62d22d184488e57',
426
+ });
427
+ }
428
+
376
429
  /* eslint-disable no-undef */
377
430
  async function likeCurrentUserImagesPageCode({ dryRun: dryRunIn, likeImagesMin, likeImagesMax }) {
378
431
  const allImages = Array.from(document.getElementsByTagName('a')).filter(el => /instagram.com\/p\//.test(el.href));
@@ -483,13 +536,12 @@ const Instauto = async (db, browser, options) => {
483
536
 
484
537
  let numFollowedForThisUser = 0;
485
538
 
486
- await navigateToUser(username);
539
+ const userData = await navigateToUserAndGetData(username);
487
540
 
488
541
  // Check if we have more than enough users that are not previously followed
489
542
  const shouldProceed = usersSoFar => (
490
543
  usersSoFar.filter(u => !getPrevFollowedUser(u)).length < maxFollowsPerUser + 5 // 5 is just a margin
491
544
  );
492
- const userData = await getCurrentUser();
493
545
  let followers = await getFollowersOrFollowing({
494
546
  userId: userData.id,
495
547
  getFollowers: true,
@@ -508,9 +560,8 @@ const Instauto = async (db, browser, options) => {
508
560
  return;
509
561
  }
510
562
 
511
- await navigateToUser(follower);
563
+ const graphqlUser = await navigateToUserAndGetData(follower);
512
564
 
513
- const graphqlUser = await getCurrentUser();
514
565
  const followedByCount = graphqlUser.edge_followed_by.count;
515
566
  const followsCount = graphqlUser.edge_follow.count;
516
567
  const isPrivate = graphqlUser.is_private;
@@ -549,6 +600,7 @@ const Instauto = async (db, browser, options) => {
549
600
  }
550
601
 
551
602
  await sleep(20000);
603
+ await throttle();
552
604
  }
553
605
  } catch (err) {
554
606
  logger.error(`Failed to process follower ${follower}`, err);
@@ -629,8 +681,7 @@ const Instauto = async (db, browser, options) => {
629
681
 
630
682
  async function unfollowNonMutualFollowers({ limit } = {}) {
631
683
  logger.log('Unfollowing non-mutual followers...');
632
- await navigateToUser(myUsername);
633
- const userData = await getCurrentUser();
684
+ const userData = await navigateToUserAndGetData(myUsername);
634
685
 
635
686
  const allFollowers = await getFollowersOrFollowing({
636
687
  userId: userData.id,
@@ -659,8 +710,7 @@ const Instauto = async (db, browser, options) => {
659
710
 
660
711
  async function unfollowAllUnknown({ limit } = {}) {
661
712
  logger.log('Unfollowing all except excludes and auto followed');
662
- await navigateToUser(myUsername);
663
- const userData = await getCurrentUser();
713
+ const userData = await navigateToUserAndGetData(myUsername);
664
714
 
665
715
  const allFollowing = await getFollowersOrFollowing({
666
716
  userId: userData.id,
@@ -684,9 +734,7 @@ const Instauto = async (db, browser, options) => {
684
734
 
685
735
  logger.log(`Unfollowing currently followed users who were auto-followed more than ${ageInDays} days ago...`);
686
736
 
687
- await navigateToUser(myUsername);
688
- // await page.goto(`${instagramBaseUrl}/${myUsername}`);
689
- const userData = await getCurrentUser();
737
+ const userData = await navigateToUserAndGetData(myUsername);
690
738
 
691
739
  const allFollowing = await getFollowersOrFollowing({
692
740
  userId: userData.id,
@@ -708,8 +756,7 @@ const Instauto = async (db, browser, options) => {
708
756
  }
709
757
 
710
758
  async function listManuallyFollowedUsers() {
711
- await navigateToUser(myUsername);
712
- const userData = await getCurrentUser();
759
+ const userData = await navigateToUserAndGetData(myUsername);
713
760
 
714
761
  const allFollowing = await getFollowersOrFollowing({
715
762
  userId: userData.id,
@@ -733,19 +780,56 @@ const Instauto = async (db, browser, options) => {
733
780
 
734
781
  if (enableCookies) await tryLoadCookies();
735
782
 
783
+ const goHome = async () => page.goto(`${instagramBaseUrl}/`);
736
784
 
737
- // Not sure if we can set cookies before having gone to a page
738
- await page.goto(`${instagramBaseUrl}/`);
739
- await sleep(1000);
740
- logger.log('Setting language to english');
741
- await page.setCookie({
742
- name: 'ig_lang',
743
- value: 'en',
744
- path: '/',
745
- });
746
- await sleep(1000);
747
- await page.goto(`${instagramBaseUrl}/`);
748
- await sleep(3000);
785
+ // https://github.com/mifi/SimpleInstaBot/issues/28
786
+ async function setLang(short, long) {
787
+ logger.log(`Setting language to ${long} (${short})`);
788
+
789
+ // This doesn't seem to always work, hence why it's just a fallback now
790
+ async function fallbackSetLang() {
791
+ await goHome();
792
+ await sleep(1000);
793
+
794
+ await page.setCookie({
795
+ name: 'ig_lang',
796
+ value: short,
797
+ path: '/',
798
+ });
799
+ await sleep(1000);
800
+ await goHome();
801
+ await sleep(3000);
802
+ }
803
+
804
+ try {
805
+ await sleep(1000);
806
+ await goHome();
807
+ await sleep(3000);
808
+ const elementHandles = await page.$x(`//select[//option[@value='${short}' and text()='${long}']]`);
809
+ if (elementHandles.length < 1) throw new Error('Language selector not found');
810
+ logger.log('Found language selector');
811
+
812
+ // https://stackoverflow.com/questions/45864516/how-to-select-an-option-from-dropdown-select
813
+ await page.evaluate((selectElem, short2) => {
814
+ const optionElem = selectElem.querySelector(`option[value='${short2}']`);
815
+ optionElem.selected = true;
816
+ // eslint-disable-next-line no-undef
817
+ const event = new Event('change', { bubbles: true });
818
+ selectElem.dispatchEvent(event);
819
+ }, elementHandles[0], short);
820
+ logger.log('Selected language');
821
+
822
+ await sleep(3000);
823
+ await goHome();
824
+ await sleep(1000);
825
+ } catch (err) {
826
+ logger.error('Failed to set language, trying fallback (cookie)', err);
827
+ await fallbackSetLang();
828
+ }
829
+ }
830
+
831
+ const setEnglishLang = async () => setLang('en', 'English');
832
+ // const setEnglishLang = async () => setLang('de', 'Deutsch');
749
833
 
750
834
  async function tryPressButton(elementHandles, name) {
751
835
  try {
@@ -759,8 +843,9 @@ const Instauto = async (db, browser, options) => {
759
843
  }
760
844
  }
761
845
 
846
+ await setEnglishLang();
762
847
 
763
- await tryPressButton(await page.$x('//button[text()="Accept"]'), 'Accept cookies dialog');
848
+ await tryPressButton(await page.$x('//button[contains(text(), "Accept")]'), 'Accept cookies dialog');
764
849
 
765
850
  if (!(await isLoggedIn())) {
766
851
  if (!myUsername || !password) {
@@ -791,7 +876,6 @@ const Instauto = async (db, browser, options) => {
791
876
  // Sometimes login button gets stuck with a spinner
792
877
  // https://github.com/mifi/SimpleInstaBot/issues/25
793
878
  if (!(await isLoggedIn())) {
794
- await sleep(5000);
795
879
  logger.log('Still not logged in, trying to reload loading page');
796
880
  await page.reload();
797
881
  await sleep(5000);
@@ -799,13 +883,18 @@ const Instauto = async (db, browser, options) => {
799
883
 
800
884
  let warnedAboutLoginFail = false;
801
885
  while (!(await isLoggedIn())) {
802
- 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.');
886
+ 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.');
803
887
  warnedAboutLoginFail = true;
804
888
  await sleep(5000);
805
889
  }
806
890
 
891
+ // In case language gets reset after logging in
892
+ await setEnglishLang();
893
+
807
894
  // Mobile version https://github.com/mifi/SimpleInstaBot/issues/7
808
- await tryPressButton(await page.$x('//button[contains(text(), "Save Info")]'), 'Save login info dialog');
895
+ await tryPressButton(await page.$x('//button[contains(text(), "Save Info")]'), 'Login info dialog: Save Info');
896
+ // May sometimes be "Save info" too? https://github.com/mifi/instauto/pull/70
897
+ await tryPressButton(await page.$x('//button[contains(text(), "Save info")]'), 'Login info dialog: Save info');
809
898
  }
810
899
 
811
900
  await tryPressButton(await page.$x('//button[contains(text(), "Not Now")]'), 'Turn on Notifications dialog');
@@ -834,6 +923,7 @@ const Instauto = async (db, browser, options) => {
834
923
  sleep,
835
924
  listManuallyFollowedUsers,
836
925
  getFollowersOrFollowing,
926
+ getUsersWhoLikedContent,
837
927
  safelyUnfollowUserList,
838
928
  getPage,
839
929
  followUsersFollowers,
package/logo.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instauto",
3
- "version": "7.1.1",
3
+ "version": "7.2.0",
4
4
  "description": "Instagram automation library written in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {