instauto 7.2.0 → 7.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +113 -48
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -165,27 +165,45 @@ const Instauto = async (db, browser, options) => {
165
165
  return new Date().getTime() - followedUserEntry.time < dontUnfollowUntilTimeElapsed;
166
166
  }
167
167
 
168
- async function safeGoto(url) {
169
- logger.log(`Goto ${url}`);
170
- const response = await page.goto(url);
171
- await sleep(1000);
172
- const status = response.status();
168
+ async function gotoWithRetry(url) {
169
+ for (let attempt = 0; ; attempt += 1) {
170
+ logger.log(`Goto ${url}`);
171
+ const response = await page.goto(url);
172
+ await sleep(1000);
173
+ const status = response.status();
174
+
175
+ // https://www.reddit.com/r/Instagram/comments/kwrt0s/error_560/
176
+ // https://github.com/mifi/instauto/issues/60
177
+ if (![560, 429].includes(status) || attempt > 3) return status;
178
+
179
+ logger.info(`Got ${status} - Retrying request later...`);
180
+ 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');
181
+ await sleep((attempt + 1) * 30 * 60 * 1000);
182
+ }
183
+ }
184
+
185
+ async function safeGotoUser(url, checkPageForUsername) {
186
+ const status = await gotoWithRetry(url);
173
187
  if (status === 200) {
188
+ if (checkPageForUsername != null) {
189
+ // some pages return 200 but nothing there (I think deleted accounts)
190
+ // https://github.com/mifi/SimpleInstaBot/issues/48
191
+ // example: https://www.instagram.com/victorialarson__/
192
+ // so we check if the page has the user's name on it
193
+ return page.evaluate((username) => window.find(username), checkPageForUsername);
194
+ }
174
195
  return true;
175
- } else if (status === 404) {
196
+ }
197
+ if (status === 404) {
176
198
  logger.log('User not found');
177
199
  return false;
178
- } else if (status === 429) {
179
- logger.error('Got 429 Too Many Requests, waiting...');
180
- await sleep(60 * 60 * 1000);
181
- throw new Error('Aborted operation due to too many requests'); // TODO retry instead
182
200
  }
183
- throw new Error(`Navigate to user returned status ${response.status()}`);
201
+ throw new Error(`Navigate to user failed with status ${status}`);
184
202
  }
185
203
 
186
204
  async function navigateToUser(username) {
187
205
  logger.log(`Navigating to user ${username}`);
188
- return safeGoto(`${instagramBaseUrl}/${encodeURIComponent(username)}`);
206
+ return safeGotoUser(`${instagramBaseUrl}/${encodeURIComponent(username)}`, username);
189
207
  }
190
208
 
191
209
  async function getPageJson() {
@@ -197,7 +215,7 @@ const Instauto = async (db, browser, options) => {
197
215
  if (graphqlUserMissing) {
198
216
  // https://stackoverflow.com/questions/37593025/instagram-api-get-the-userid
199
217
  // 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`);
218
+ const found = await safeGotoUser(`${instagramBaseUrl}/${encodeURIComponent(username)}?__a=1`);
201
219
  if (!found) throw new Error('User not found');
202
220
 
203
221
  const json = await getPageJson();
@@ -242,12 +260,31 @@ const Instauto = async (db, browser, options) => {
242
260
  }
243
261
  }
244
262
 
245
- async function findFollowButton() {
246
- const elementHandles = await page.$x("//header//button[text()='Follow']");
263
+ // How to test xpaths in the browser:
264
+ // document.evaluate("your xpath", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue
265
+ async function findButtonWithText(text) {
266
+ // todo escape text?
267
+
268
+ // button seems to look like this now:
269
+ // <button class="..."><div class="...">Follow</div></button>
270
+ // https://sqa.stackexchange.com/questions/36918/xpath-text-buy-now-is-working-but-not-containstext-buy-now
271
+ // https://github.com/mifi/SimpleInstaBot/issues/106
272
+ let elementHandles = await page.$x(`//header//button[contains(.,'${text}')]`);
247
273
  if (elementHandles.length > 0) return elementHandles[0];
248
274
 
249
- const elementHandles2 = await page.$x("//header//button[text()='Follow Back']");
250
- if (elementHandles2.length > 0) return elementHandles2[0];
275
+ // old button:
276
+ elementHandles = await page.$x(`//header//button[text()='${text}']`);
277
+ if (elementHandles.length > 0) return elementHandles[0];
278
+
279
+ return undefined;
280
+ }
281
+
282
+ async function findFollowButton() {
283
+ let button = await findButtonWithText('Follow');
284
+ if (button) return button;
285
+
286
+ button = await findButtonWithText('Follow Back');
287
+ if (button) return button;
251
288
 
252
289
  return undefined;
253
290
  }
@@ -301,7 +338,7 @@ const Instauto = async (db, browser, options) => {
301
338
  await addPrevFollowedUser(entry);
302
339
 
303
340
  if (!elementHandle2) {
304
- logger.log('Button did not change state - Sleeping');
341
+ logger.log('Button did not change state - Sleeping 1 min');
305
342
  await sleep(60000);
306
343
  throw new Error('Button did not change state');
307
344
  }
@@ -772,6 +809,10 @@ const Instauto = async (db, browser, options) => {
772
809
  }
773
810
 
774
811
  page = await browser.newPage();
812
+
813
+ // https://github.com/mifi/SimpleInstaBot/issues/118#issuecomment-1067883091
814
+ await page.setExtraHTTPHeaders({ 'Accept-Language': 'en' });
815
+
775
816
  if (randomizeUserAgent) {
776
817
  const userAgentGenerated = new UserAgent({ deviceCategory: 'desktop' });
777
818
  await page.setUserAgent(userAgentGenerated.toString());
@@ -780,72 +821,88 @@ const Instauto = async (db, browser, options) => {
780
821
 
781
822
  if (enableCookies) await tryLoadCookies();
782
823
 
783
- const goHome = async () => page.goto(`${instagramBaseUrl}/`);
824
+ const goHome = async () => page.goto(`${instagramBaseUrl}/?hl=en`);
784
825
 
785
826
  // https://github.com/mifi/SimpleInstaBot/issues/28
786
- async function setLang(short, long) {
827
+ async function setLang(short, long, assumeLoggedIn = false) {
787
828
  logger.log(`Setting language to ${long} (${short})`);
788
829
 
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
830
  try {
805
831
  await sleep(1000);
806
- await goHome();
832
+
833
+ // when logged in, we need to go to account in order to be able to check/set language
834
+ // (need to see the footer)
835
+ if (assumeLoggedIn) {
836
+ await page.goto(`${instagramBaseUrl}/accounts/edit/`);
837
+ } else {
838
+ await goHome();
839
+ }
807
840
  await sleep(3000);
808
841
  const elementHandles = await page.$x(`//select[//option[@value='${short}' and text()='${long}']]`);
809
842
  if (elementHandles.length < 1) throw new Error('Language selector not found');
810
843
  logger.log('Found language selector');
811
844
 
812
845
  // https://stackoverflow.com/questions/45864516/how-to-select-an-option-from-dropdown-select
813
- await page.evaluate((selectElem, short2) => {
846
+ const alreadyEnglish = await page.evaluate((selectElem, short2) => {
814
847
  const optionElem = selectElem.querySelector(`option[value='${short2}']`);
848
+ if (optionElem.selected) return true; // already selected?
815
849
  optionElem.selected = true;
816
850
  // eslint-disable-next-line no-undef
817
851
  const event = new Event('change', { bubbles: true });
818
852
  selectElem.dispatchEvent(event);
853
+ return false;
819
854
  }, elementHandles[0], short);
820
- logger.log('Selected language');
821
855
 
856
+ if (alreadyEnglish) {
857
+ logger.log('Already English language');
858
+ if (!assumeLoggedIn) {
859
+ await goHome(); // because we were on the settings page
860
+ await sleep(1000);
861
+ }
862
+ return;
863
+ }
864
+
865
+ logger.log('Selected language');
822
866
  await sleep(3000);
823
867
  await goHome();
824
868
  await sleep(1000);
825
869
  } catch (err) {
826
870
  logger.error('Failed to set language, trying fallback (cookie)', err);
827
- await fallbackSetLang();
871
+ // This doesn't seem to always work, hence why it's just a fallback now
872
+ await goHome();
873
+ await sleep(1000);
874
+
875
+ await page.setCookie({
876
+ name: 'ig_lang',
877
+ value: short,
878
+ path: '/',
879
+ });
880
+ await sleep(1000);
881
+ await goHome();
882
+ await sleep(3000);
828
883
  }
829
884
  }
830
885
 
831
- const setEnglishLang = async () => setLang('en', 'English');
832
- // const setEnglishLang = async () => setLang('de', 'Deutsch');
886
+ const setEnglishLang = async (assumeLoggedIn) => setLang('en', 'English', assumeLoggedIn);
887
+ // const setEnglishLang = async (assumeLoggedIn) => setLang('de', 'Deutsch', assumeLoggedIn);
833
888
 
834
- async function tryPressButton(elementHandles, name) {
889
+ async function tryPressButton(elementHandles, name, sleepMs = 3000) {
835
890
  try {
836
891
  if (elementHandles.length === 1) {
837
892
  logger.log(`Pressing button: ${name}`);
838
893
  elementHandles[0].click();
839
- await sleep(3000);
894
+ await sleep(sleepMs);
840
895
  }
841
896
  } catch (err) {
842
897
  logger.warn(`Failed to press button: ${name}`);
843
898
  }
844
899
  }
845
900
 
846
- await setEnglishLang();
901
+ await setEnglishLang(false);
847
902
 
848
903
  await tryPressButton(await page.$x('//button[contains(text(), "Accept")]'), 'Accept cookies dialog');
904
+ await tryPressButton(await page.$x('//button[contains(text(), "Only allow essential cookies")]'), 'Accept cookies dialog 2 button 1', 10000);
905
+ await tryPressButton(await page.$x('//button[contains(text(), "Allow essential and optional cookies")]'), 'Accept cookies dialog 2 button 2', 10000);
849
906
 
850
907
  if (!(await isLoggedIn())) {
851
908
  if (!myUsername || !password) {
@@ -857,7 +914,7 @@ const Instauto = async (db, browser, options) => {
857
914
  await page.click('a[href="/accounts/login/?source=auth_switcher"]');
858
915
  await sleep(1000);
859
916
  } catch (err) {
860
- logger.warn('Login page button not found, assuming we have login form');
917
+ logger.info('No login page button, assuming we are on login form');
861
918
  }
862
919
 
863
920
  // Mobile version https://github.com/mifi/SimpleInstaBot/issues/7
@@ -868,8 +925,15 @@ const Instauto = async (db, browser, options) => {
868
925
  await page.type('input[name="password"]', password, { delay: 50 });
869
926
  await sleep(1000);
870
927
 
871
- const loginButton = (await page.$x("//button[.//text() = 'Log In']"))[0];
872
- await loginButton.click();
928
+ for (;;) {
929
+ const loginButton = (await page.$x("//button[.//text() = 'Log In']"))[0];
930
+ if (loginButton) {
931
+ await loginButton.click();
932
+ break;
933
+ }
934
+ 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 :)');
935
+ await sleep(6000);
936
+ }
873
937
 
874
938
  await sleep(6000);
875
939
 
@@ -889,7 +953,8 @@ const Instauto = async (db, browser, options) => {
889
953
  }
890
954
 
891
955
  // In case language gets reset after logging in
892
- await setEnglishLang();
956
+ // https://github.com/mifi/SimpleInstaBot/issues/118
957
+ await setEnglishLang(true);
893
958
 
894
959
  // Mobile version https://github.com/mifi/SimpleInstaBot/issues/7
895
960
  await tryPressButton(await page.$x('//button[contains(text(), "Save Info")]'), 'Login info dialog: Save Info');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instauto",
3
- "version": "7.2.0",
3
+ "version": "7.2.3",
4
4
  "description": "Instagram automation library written in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {