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.
- package/index.js +113 -48
- 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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
872
|
-
|
|
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
|
-
|
|
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');
|