|
@@ -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;
|
|
|
|