|
@@ -10,8 +10,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
};
|
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
exports.sendPost = exports.getPostOwner = exports.WebshotHelpers = exports.ScreenNameNormalizer = exports.SessionManager = exports.urlSegmentToId = exports.idToUrlSegment = exports.isValidUrlSegment = exports.parseLink = exports.linkBuilder = void 0;
|
|
|
+const crypto = require("crypto");
|
|
|
const fs = require("fs");
|
|
|
+const http = require("http");
|
|
|
const path = require("path");
|
|
|
+const url_1 = require("url");
|
|
|
const util_1 = require("util");
|
|
|
const instagram_id_to_url_segment_1 = require("instagram-id-to-url-segment");
|
|
|
Object.defineProperty(exports, "idToUrlSegment", { enumerable: true, get: function () { return instagram_id_to_url_segment_1.instagramIdToUrlSegment; } });
|
|
@@ -44,7 +47,7 @@ const linkBuilder = (config) => {
|
|
|
};
|
|
|
exports.linkBuilder = linkBuilder;
|
|
|
class SessionManager {
|
|
|
- constructor(client, file, credentials) {
|
|
|
+ constructor(client, file, credentials, codeServicePort) {
|
|
|
this.init = () => {
|
|
|
this.ig.state.generateDevice(this.username);
|
|
|
this.ig.request.end$.subscribe(() => { this.save(); });
|
|
@@ -61,14 +64,55 @@ class SessionManager {
|
|
|
return Promise.resolve();
|
|
|
}
|
|
|
}
|
|
|
- else
|
|
|
- return this.login();
|
|
|
+ else {
|
|
|
+ return this.login().catch((err) => {
|
|
|
+ logger.error(`error while trying to log in as user ${this.username}, error: ${err}`);
|
|
|
+ logger.warn('attempting to retry after 1 minute...');
|
|
|
+ if (fs.existsSync(filePath))
|
|
|
+ fs.unlinkSync(filePath);
|
|
|
+ util_1.promisify(setTimeout)(60000).then(this.init);
|
|
|
+ });
|
|
|
+ }
|
|
|
};
|
|
|
+ this.handle2FA = (submitter) => new Promise((resolve, reject) => {
|
|
|
+ const token = crypto.randomBytes(20).toString('hex');
|
|
|
+ logger.info('please submit the code with a one-time token from your browser with this path:');
|
|
|
+ logger.info(`/confirm-2fa?code=<the code you received>&token=${token}`);
|
|
|
+ let working;
|
|
|
+ const server = http.createServer((req, res) => {
|
|
|
+ const { pathname, query } = url_1.parse(req.url, true);
|
|
|
+ if (!working && pathname === '/confirm-2fa' && query.token === token &&
|
|
|
+ typeof (query.code) === 'string' && /^\d{6}$/.test(query.code)) {
|
|
|
+ const code = query.code;
|
|
|
+ logger.debug(`received code: ${code}`);
|
|
|
+ working = true;
|
|
|
+ submitter(code)
|
|
|
+ .then(response => { res.write('OK'); res.end(); server.close(() => resolve(response)); })
|
|
|
+ .catch(err => { res.write('Error'); res.end(); reject(err); })
|
|
|
+ .finally(() => { working = false; });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ server.listen(this.codeServicePort);
|
|
|
+ });
|
|
|
this.login = () => this.ig.simulate.preLoginFlow()
|
|
|
.then(() => this.ig.account.login(this.username, this.password))
|
|
|
- .then(() => new Promise(resolve => {
|
|
|
+ .catch((err) => {
|
|
|
+ if (err instanceof instagram_private_api_1.IgLoginTwoFactorRequiredError) {
|
|
|
+ const { two_factor_identifier, totp_two_factor_on } = err.response.body.two_factor_info;
|
|
|
+ logger.debug(`2FA info: ${JSON.stringify(err.response.body.two_factor_info)}`);
|
|
|
+ logger.info(`login is requesting two-factor authentication via ${totp_two_factor_on ? 'TOTP' : 'SMS'}`);
|
|
|
+ return this.handle2FA(code => this.ig.account.twoFactorLogin({
|
|
|
+ username: this.username,
|
|
|
+ verificationCode: code,
|
|
|
+ twoFactorIdentifier: two_factor_identifier,
|
|
|
+ verificationMethod: totp_two_factor_on ? '0' : '1',
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ throw err;
|
|
|
+ })
|
|
|
+ .then(user => new Promise(resolve => {
|
|
|
logger.info(`successfully logged in as ${this.username}`);
|
|
|
- process.nextTick(() => resolve(this.ig.simulate.postLoginFlow()));
|
|
|
+ process.nextTick(() => resolve(this.ig.simulate.postLoginFlow().then(() => user)));
|
|
|
}));
|
|
|
this.save = () => this.ig.state.serialize()
|
|
|
.then((serialized) => {
|
|
@@ -78,6 +122,7 @@ class SessionManager {
|
|
|
this.ig = client;
|
|
|
this.lockfile = file;
|
|
|
[this.username, this.password] = credentials;
|
|
|
+ this.codeServicePort = codeServicePort;
|
|
|
}
|
|
|
}
|
|
|
exports.SessionManager = SessionManager;
|
|
@@ -102,6 +147,7 @@ exports.ScreenNameNormalizer = ScreenNameNormalizer;
|
|
|
ScreenNameNormalizer.normalize = (username) => `${username.toLowerCase().replace(/^@/, '')}:`;
|
|
|
let browserLogin = (page) => Promise.reject();
|
|
|
let browserSaveCookies = browserLogin;
|
|
|
+let isWaitingForLogin = false;
|
|
|
const acceptCookieConsent = (page) => page.click('button:has-text("すべて許可")', { timeout: 5000 })
|
|
|
.then(() => logger.info('accepted cookie consent'))
|
|
|
.catch((err) => { if (err.name !== 'TimeoutError')
|
|
@@ -109,6 +155,7 @@ const acceptCookieConsent = (page) => page.click('button:has-text("すべて許
|
|
|
exports.WebshotHelpers = {
|
|
|
handleLogin: browserLogin,
|
|
|
handleCookieConsent: acceptCookieConsent,
|
|
|
+ get isWaitingForLogin() { return isWaitingForLogin; },
|
|
|
};
|
|
|
let getPostOwner = (segmentId) => Promise.reject();
|
|
|
exports.getPostOwner = getPostOwner;
|
|
@@ -153,10 +200,10 @@ class default_1 {
|
|
|
logger.debug(`pulling ${targetId !== '0' ? `feed ${url} up to ${targetId}` : `top of feed ${url}`}...`);
|
|
|
return doOnNewPage(newPage => {
|
|
|
page = newPage;
|
|
|
- let timeout = this.webshotDelay;
|
|
|
+ let timeout = this.workInterval * 1000;
|
|
|
const startTime = new Date().getTime();
|
|
|
const getTimerTime = () => new Date().getTime() - startTime;
|
|
|
- const getTimeout = () => Math.max(500, timeout - getTimerTime());
|
|
|
+ const getTimeout = () => isWaitingForLogin ? 0 : Math.max(90000, timeout - getTimerTime());
|
|
|
return page.context().addCookies(this.webshotCookies)
|
|
|
.then(() => page.goto(url, { waitUntil: 'load', timeout: getTimeout() }))
|
|
|
.then(response => {
|
|
@@ -170,14 +217,16 @@ class default_1 {
|
|
|
.then(() => (next => Promise.race([
|
|
|
browserLogin(page)
|
|
|
.catch((err) => {
|
|
|
- if (err.name === 'TimeoutError')
|
|
|
+ if (err.name === 'TimeoutError') {
|
|
|
logger.warn('navigation timed out, assuming login has failed');
|
|
|
+ isWaitingForLogin = false;
|
|
|
+ }
|
|
|
throw err;
|
|
|
})
|
|
|
.then(() => browserSaveCookies(page))
|
|
|
.then(() => page.goto(url)).then(next),
|
|
|
next(),
|
|
|
- ]))(() => page.waitForSelector('article', { timeout: getTimeout() }))).then(handle => {
|
|
|
+ ]))(() => util_1.promisify(setTimeout)(2000).then(() => page.waitForSelector('article', { timeout: getTimeout() })))).then(handle => {
|
|
|
const postHandler = () => {
|
|
|
const toId = (href) => { var _a; return instagram_id_to_url_segment_1.urlSegmentToInstagramId(((_a = /\/p\/(.*)\/$/.exec(href)) !== null && _a !== void 0 ? _a : [, ''])[1]); };
|
|
|
if (targetId === '0') {
|
|
@@ -200,7 +249,7 @@ class default_1 {
|
|
|
return postHandler().then(itemIds => {
|
|
|
if (itemIds)
|
|
|
return itemIds;
|
|
|
- timeout += this.webshotDelay / 2;
|
|
|
+ timeout += this.workInterval * 500;
|
|
|
return handle.$$('a')
|
|
|
.then(as => { as.pop().scrollIntoViewIfNeeded(); return as.length + 1; })
|
|
|
.then(loadedCount => page.waitForFunction(count => document.querySelectorAll('article a').length > count, loadedCount))
|
|
@@ -327,7 +376,7 @@ class default_1 {
|
|
|
logger.warn(`invalid socks proxy url: ${opt.proxyUrl}, ignoring`);
|
|
|
}
|
|
|
}
|
|
|
- this.session = new SessionManager(this.client, opt.sessionLockfile, opt.credentials);
|
|
|
+ this.session = new SessionManager(this.client, opt.sessionLockfile, opt.credentials, opt.codeServicePort);
|
|
|
this.lockfile = opt.lockfile;
|
|
|
this.webshotCookiesLockfile = opt.webshotCookiesLockfile;
|
|
|
this.lock = opt.lock;
|
|
@@ -345,11 +394,21 @@ class default_1 {
|
|
|
logger.warn(`failed to load webshot cookies from file ${this.webshotCookiesLockfile}: `, err.message);
|
|
|
logger.warn('cookies will be saved to this file when needed');
|
|
|
}
|
|
|
- browserLogin = page => page.fill('input[name="username"]', opt.credentials[0])
|
|
|
- .then(() => logger.warn('blocked by login dialog, trying to log in manually...'))
|
|
|
- .then(() => page.fill('input[name="password"]', opt.credentials[1]))
|
|
|
- .then(() => page.click('button[type="submit"]'))
|
|
|
- .then(() => page.click('button:has-text("情報を保存")'));
|
|
|
+ browserLogin = page => page.fill('input[name="username"]', opt.credentials[0], { timeout: 0 })
|
|
|
+ .then(() => { isWaitingForLogin = true; 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([
|
|
|
+ page.waitForSelector('#verificationCodeDescription', { timeout: 0 }).then(handle => handle.innerText()).then(text => {
|
|
|
+ logger.info(`login is requesting two-factor authentication via ${/認証アプリ/.test(text) ? 'TOTP' : 'SMS'}`);
|
|
|
+ return this.session.handle2FA(code => page.fill('input[name="verificationCode"]', code, { timeout: 0 }))
|
|
|
+ .then(() => page.click('button:has-text("実行")', { timeout: 0 }))
|
|
|
+ .then(next);
|
|
|
+ }),
|
|
|
+ page.waitForResponse(res => res.status() === 429, { timeout: 0 })
|
|
|
+ .then(() => { logger.error('fatal error: login restricted: code 429, exiting'); process.exit(1); }),
|
|
|
+ next(),
|
|
|
+ ]))(() => page.click('button:has-text("情報を保存")', { timeout: 0 }).then(() => { isWaitingForLogin = false; })));
|
|
|
browserSaveCookies = page => page.context().cookies()
|
|
|
.then(cookies => {
|
|
|
this.webshotCookies = cookies;
|
|
@@ -360,8 +419,10 @@ class default_1 {
|
|
|
.then(() => page.waitForSelector('img[data-testid="user-avatar"]', { timeout: this.webshotDelay }))
|
|
|
.then(() => browserSaveCookies(page))
|
|
|
.catch((err) => {
|
|
|
- if (err.name === 'TimeoutError')
|
|
|
+ if (err.name === 'TimeoutError') {
|
|
|
logger.warn('navigation timed out, assuming login has failed');
|
|
|
+ isWaitingForLogin = false;
|
|
|
+ }
|
|
|
throw err;
|
|
|
});
|
|
|
ScreenNameNormalizer._queryUser = this.queryUser;
|