Browse Source

auto resume api queries after logging in

Mike L 3 years ago
parent
commit
7e9852c9cd
4 changed files with 95 additions and 57 deletions
  1. 49 30
      dist/twitter.js
  2. 1 3
      dist/utils.js
  3. 45 22
      src/twitter.ts
  4. 0 2
      src/utils.ts

+ 49 - 30
dist/twitter.js

@@ -147,8 +147,26 @@ class ScreenNameNormalizer {
 exports.ScreenNameNormalizer = ScreenNameNormalizer;
 ScreenNameNormalizer.normalize = (username) => `${username.toLowerCase().replace(/^@/, '')}:`;
 let browserLogin = (page) => Promise.resolve();
-let browserSaveCookies = browserLogin;
+let browserSaveCookies = browserLogin, browserAwaitLogin = browserLogin;
 let isWaitingForLogin = false;
+const LoginAwaiter = Object.seal({
+    awaiter: Promise.resolve(),
+    resolver: () => logger.warn('already logged in'),
+    new: () => {
+        isWaitingForLogin = true;
+        LoginAwaiter.awaiter = new Promise(resolve => {
+            const getNew = LoginAwaiter.new;
+            const resolver = LoginAwaiter.resolver;
+            LoginAwaiter.resolver = () => {
+                resolve();
+                isWaitingForLogin = false;
+                LoginAwaiter.resolver = resolver;
+                LoginAwaiter.new = getNew;
+            };
+        });
+        LoginAwaiter.new = () => logger.warn('awaiting login...');
+    },
+});
 const acceptCookieConsent = (page) => page.click('button:text-matches("すべて.*許可")', { timeout: 5000 })
     .then(() => logger.info('accepted cookie consent'))
     .catch((err) => { if (err.name !== 'TimeoutError')
@@ -216,20 +234,23 @@ class default_1 {
                         const startTime = new Date().getTime();
                         const getTimerTime = () => new Date().getTime() - startTime;
                         const getTimeout = () => isWaitingForLogin ? 0 : Math.max(5000, timeout - getTimerTime());
+                        const nextPageDelay = this.webshotDelay * (0.4 + Math.random() * 0.1);
                         return page.context().addCookies(this.webshotCookies)
                             .then(() => page.goto(url, { waitUntil: 'load', timeout: getTimeout() }))
                             .then(response => {
                             const nodes = [];
                             const redirectionHandler = () => acceptCookieConsent(page)
-                                .then(() => browserLogin(page))
-                                .catch((err) => {
-                                if (err.name === 'TimeoutError') {
-                                    logger.warn('navigation timed out, assuming login has failed');
-                                    isWaitingForLogin = false;
-                                }
-                                throw err;
-                            })
-                                .then(() => browserSaveCookies(page))
+                                .then(() => isWaitingForLogin ?
+                                browserAwaitLogin(page).then(() => (0, util_1.promisify)(setTimeout)(nextPageDelay)) :
+                                browserLogin(page)
+                                    .catch((err) => {
+                                    if (err.name === 'TimeoutError') {
+                                        logger.warn('navigation timed out, assuming login has failed');
+                                        LoginAwaiter.resolver();
+                                    }
+                                    throw err;
+                                })
+                                    .then(() => browserSaveCookies(page)))
                                 .then(() => page.goto(url, { waitUntil: 'load', timeout: getTimeout() }))
                                 .then(responseHandler);
                             const responseHandler = (res) => {
@@ -281,7 +302,6 @@ class default_1 {
                                     return nodes;
                                 logger.info('unable to find a smaller id than target, trying on next page...');
                                 url = graphqlLinkBuilder({ userId, after: pageInfo.end_cursor });
-                                const nextPageDelay = this.webshotDelay * (0.4 + Math.random() * 0.1);
                                 timeout += nextPageDelay;
                                 return (0, util_1.promisify)(setTimeout)(nextPageDelay)
                                     .then(() => page.goto(url, { waitUntil: 'load', timeout: getTimeout() }))
@@ -400,7 +420,7 @@ class default_1 {
             const queuedFeeds = lock.feed.slice(0, (lock.workon + 1) || undefined).reverse();
             (0, utils_1.chainPromises)(utils_1.Arr.chunk(queuedFeeds, 5).map((arr, i) => () => Promise.all(arr.map((currentFeed, j) => {
                 const promiseDelay = this.workInterval * (Math.random() + j + 10 - arr.length) * 125 / lock.feed.length;
-                const wait = (ms) => isWaitingForLogin ? (0, utils_1.neverResolves)() : (0, util_1.promisify)(setTimeout)(ms);
+                const wait = (ms) => isWaitingForLogin ? LoginAwaiter.awaiter : (0, util_1.promisify)(setTimeout)(ms);
                 const startTime = new Date().getTime();
                 const getTimerTime = () => new Date().getTime() - startTime;
                 const workon = (queuedFeeds.length - 1) - (i * 5 + j);
@@ -463,13 +483,7 @@ class default_1 {
             logger.warn('cookies will be saved to this file when needed');
         }
         browserLogin = page => page.fill('input[name="username"]', opt.credentials[0], { timeout: 0 })
-            .then(() => {
-            if (isWaitingForLogin !== true)
-                return;
-            logger.warn('still waiting for login, pausing execution...');
-            return (0, utils_1.neverResolves)();
-        })
-            .then(() => { isWaitingForLogin = true; logger.warn('blocked by login dialog, trying to log in manually...'); })
+            .then(() => { LoginAwaiter.new(); logger.warn('blocked by login dialog, trying to log in manually...'); })
             .then(() => page.fill('input[name="password"]', opt.credentials[1], { timeout: 0 }))
             .then(() => page.click('button[type="submit"]', { timeout: 0 }))
             .then(() => (next => Promise.race([
@@ -480,23 +494,28 @@ class default_1 {
                     .then(next);
             }),
             next(),
-        ]))(() => page.click('button:has-text("情報を保存")', { timeout: 0 }).then(() => { isWaitingForLogin = false; })));
+        ]))(() => page.click('button:has-text("情報を保存")', { timeout: 0 }).then(LoginAwaiter.resolver)));
+        browserAwaitLogin = page => LoginAwaiter.awaiter
+            .then(() => { logger.warn('still waiting for login, pausing execution...'); return page.context(); })
+            .then(ctx => ctx.clearCookies().then(() => ctx.addCookies(this.webshotCookies)))
+            .then();
         browserSaveCookies = page => page.context().cookies()
             .then(cookies => {
             this.webshotCookies = cookies;
             logger.info('successfully logged in, saving cookies to file...');
             fs.writeFileSync(path.resolve(this.webshotCookiesLockfile), JSON.stringify(cookies, null, 2), 'utf-8');
         });
-        exports.WebshotHelpers.handleLogin = page => browserLogin(page)
-            .then(() => page.waitForSelector('img[data-testid="user-avatar"]', { timeout: this.webshotDelay }))
-            .then(() => browserSaveCookies(page))
-            .catch((err) => {
-            if (err.name === 'TimeoutError') {
-                logger.warn('navigation timed out, assuming login has failed');
-                isWaitingForLogin = false;
-            }
-            throw err;
-        });
+        exports.WebshotHelpers.handleLogin = page => isWaitingForLogin ? browserAwaitLogin(page) :
+            browserLogin(page)
+                .then(() => page.waitForSelector('img[data-testid="user-avatar"]', { timeout: this.webshotDelay }))
+                .then(() => browserSaveCookies(page))
+                .catch((err) => {
+                if (err.name === 'TimeoutError') {
+                    logger.warn('navigation timed out, assuming login has failed');
+                    LoginAwaiter.resolver();
+                }
+                throw err;
+            });
         ScreenNameNormalizer._queryUser = this.queryUser;
         const parseMediaError = (err) => {
             if (!(err instanceof instagram_private_api_1.IgResponseError && err.text === 'Media not found or unavailable')) {

+ 1 - 3
dist/utils.js

@@ -1,11 +1,9 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.Arr = exports.BigNumOps = exports.rawRegExp = exports.customError = exports.CustomError = exports.neverResolves = exports.chainPromises = void 0;
+exports.Arr = exports.BigNumOps = exports.rawRegExp = exports.customError = exports.CustomError = exports.chainPromises = void 0;
 const CallableInstance = require("callable-instance");
 const chainPromises = (lazyPromises, reducer = (lp1, lp2) => (p) => lp1(p).then(lp2), initialValue) => lazyPromises.reduce(reducer, p => Promise.resolve(p))(initialValue);
 exports.chainPromises = chainPromises;
-const neverResolves = () => new Promise(() => undefined);
-exports.neverResolves = neverResolves;
 class CustomErrorConstructor extends CallableInstance {
     constructor(name) {
         super('getError');

+ 45 - 22
src/twitter.ts

@@ -18,7 +18,7 @@ import { SocksProxyAgent } from 'socks-proxy-agent';
 
 import { getLogger } from './loggers';
 import QQBot from './koishi';
-import { Arr, BigNumOps, chainPromises, customError, neverResolves } from './utils';
+import { Arr, BigNumOps, chainPromises, customError } from './utils';
 import Webshot, { Cookies, Page } from './webshot';
 
 const parseLink = (link: string): { userName?: string, postUrlSegment?: string } => {
@@ -177,10 +177,29 @@ export class ScreenNameNormalizer {
 
 let browserLogin = (page: Page): Promise<void> => Promise.resolve();
 
-let browserSaveCookies = browserLogin;
+let browserSaveCookies = browserLogin, browserAwaitLogin = browserLogin;
 
 let isWaitingForLogin = false;
 
+const LoginAwaiter = Object.seal({
+  awaiter: Promise.resolve(),
+  resolver: () => logger.warn('already logged in'),
+  new: () => {
+    isWaitingForLogin = true;
+    LoginAwaiter.awaiter = new Promise(resolve => {
+      const getNew = LoginAwaiter.new;
+      const resolver = LoginAwaiter.resolver;
+      LoginAwaiter.resolver = () => {
+        resolve();
+        isWaitingForLogin = false;
+        LoginAwaiter.resolver = resolver;
+        LoginAwaiter.new = getNew;
+      };
+    });
+    LoginAwaiter.new = () => logger.warn('awaiting login...');
+  },
+});
+
 const acceptCookieConsent = (page: Page) =>
   page.click('button:text-matches("すべて.*許可")', { timeout: 5000 })
     .then(() => logger.info('accepted cookie consent'))
@@ -365,11 +384,7 @@ export default class {
 
     browserLogin = page =>
       page.fill('input[name="username"]', opt.credentials[0], {timeout: 0})
-        .then(() => {
-          if (isWaitingForLogin !== true) return;
-          logger.warn('still waiting for login, pausing execution...'); return neverResolves();
-        })
-        .then(() => { isWaitingForLogin = true; logger.warn('blocked by login dialog, trying to log in manually...'); })
+        .then(() => { LoginAwaiter.new(); logger.warn('blocked by login dialog, trying to log in manually...'); })
         .then(() => page.fill('input[name="password"]', opt.credentials[1], {timeout: 0}))
         .then(() => page.click('button[type="submit"]', {timeout: 0}))
         .then(() =>
@@ -381,8 +396,13 @@ export default class {
                 .then(next);
             }),
             next(),
-          ]))(() => page.click('button:has-text("情報を保存")', {timeout: 0}).then(() => { isWaitingForLogin = false; }))
+          ]))(() => page.click('button:has-text("情報を保存")', {timeout: 0}).then(LoginAwaiter.resolver))
         );
+    browserAwaitLogin = page =>
+      LoginAwaiter.awaiter
+        .then(() => { logger.warn('still waiting for login, pausing execution...'); return page.context(); })
+        .then(ctx => ctx.clearCookies().then(() => ctx.addCookies(this.webshotCookies)))
+        .then();
     browserSaveCookies = page =>
       page.context().cookies()
         .then(cookies => {
@@ -390,14 +410,14 @@ export default class {
           logger.info('successfully logged in, saving cookies to file...');
           fs.writeFileSync(path.resolve(this.webshotCookiesLockfile), JSON.stringify(cookies, null, 2), 'utf-8');
         });
-    WebshotHelpers.handleLogin = page =>
+    WebshotHelpers.handleLogin = page => isWaitingForLogin ? browserAwaitLogin(page) :
       browserLogin(page)
         .then(() => page.waitForSelector('img[data-testid="user-avatar"]', { timeout: this.webshotDelay }))
         .then(() => browserSaveCookies(page))
         .catch((err: Error) => {
           if (err.name === 'TimeoutError') {
             logger.warn('navigation timed out, assuming login has failed');
-            isWaitingForLogin = false;
+            LoginAwaiter.resolver();
           }
           throw err;
         });
@@ -461,21 +481,25 @@ export default class {
             const startTime = new Date().getTime();
             const getTimerTime = () => new Date().getTime() - startTime;
             const getTimeout = () => isWaitingForLogin ? 0 : Math.max(5000, timeout - getTimerTime());
+            const nextPageDelay = this.webshotDelay * (0.4 + Math.random() * 0.1);
             return page.context().addCookies(this.webshotCookies)
               .then(() => page.goto(url, {waitUntil: 'load', timeout: getTimeout()}))
               .then(response => {
                 const nodes: IgGraphQLTimelineMediaNode[] = [];
                 const redirectionHandler = () =>
                   acceptCookieConsent(page)
-                    .then(() => browserLogin(page))
-                    .catch((err: Error) => {
-                      if (err.name === 'TimeoutError') {
-                        logger.warn('navigation timed out, assuming login has failed');
-                        isWaitingForLogin = false;
-                      }
-                      throw err;
-                    })
-                    .then(() => browserSaveCookies(page))
+                    .then(() => isWaitingForLogin ?
+                      browserAwaitLogin(page).then(() => promisify(setTimeout)(nextPageDelay)) :
+                      browserLogin(page)
+                        .catch((err: Error) => {
+                          if (err.name === 'TimeoutError') {
+                            logger.warn('navigation timed out, assuming login has failed');
+                            LoginAwaiter.resolver();
+                          }
+                          throw err;
+                        })
+                        .then(() => browserSaveCookies(page))
+                    )
                     .then(() => page.goto(url, {waitUntil: 'load', timeout: getTimeout()}))
                     .then(responseHandler);
                 const responseHandler = (res: typeof response): ReturnType<typeof response.json> => {
@@ -530,7 +554,6 @@ export default class {
                   // else, fetch next page using end_cursor
                   logger.info('unable to find a smaller id than target, trying on next page...');
                   url = graphqlLinkBuilder({userId, after: pageInfo.end_cursor});
-                  const nextPageDelay = this.webshotDelay * (0.4 + Math.random() * 0.1);
                   timeout += nextPageDelay;
                   return promisify(setTimeout)(nextPageDelay)
                     .then(() => page.goto(url, {waitUntil: 'load', timeout: getTimeout()}))
@@ -665,12 +688,12 @@ export default class {
         fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
       }
     });
-    
+
     const queuedFeeds = lock.feed.slice(0, (lock.workon + 1) || undefined).reverse();
     chainPromises(Arr.chunk(queuedFeeds, 5).map((arr, i) =>
       () => Promise.all(arr.map((currentFeed, j) => {
         const promiseDelay = this.workInterval * (Math.random() + j + 10 - arr.length) * 125 / lock.feed.length;
-        const wait = (ms: number) => isWaitingForLogin ? neverResolves() : promisify(setTimeout)(ms);
+        const wait = (ms: number) => isWaitingForLogin ? LoginAwaiter.awaiter : promisify(setTimeout)(ms);
         const startTime = new Date().getTime();
         const getTimerTime = () => new Date().getTime() - startTime;
 

+ 0 - 2
src/utils.ts

@@ -6,8 +6,6 @@ export const chainPromises = <T>(
   initialValue?: T
 ) => lazyPromises.reduce(reducer, p => Promise.resolve(p))(initialValue);
 
-export const neverResolves = () => new Promise<never>(() => undefined);
-
 class CustomErrorConstructor<Name extends string> extends CallableInstance<[string], CustomError<Name>> {
   constructor(name: Name) {
     super('getError');