|
@@ -1,10 +1,13 @@
|
|
|
+import * as crypto from 'crypto';
|
|
|
import * as fs from 'fs';
|
|
|
+import * as http from 'http';
|
|
|
import * as path from 'path';
|
|
|
+import { parse as parseUrl } from 'url';
|
|
|
+import { promisify } from 'util';
|
|
|
+
|
|
|
import {
|
|
|
IgApiClient,
|
|
|
- IgClientError, IgExactUserNotFoundError,
|
|
|
- IgLoginRequiredError,
|
|
|
- IgNetworkError,
|
|
|
+ IgClientError, IgExactUserNotFoundError, IgLoginTwoFactorRequiredError, IgLoginRequiredError, IgNetworkError,
|
|
|
ReelsMediaFeedResponseItem, UserFeedResponseUser
|
|
|
} from 'instagram-private-api';
|
|
|
import { RequestError } from 'request-promise/errors';
|
|
@@ -37,6 +40,7 @@ export {linkBuilder, parseLink};
|
|
|
interface IWorkerOption {
|
|
|
sessionLockfile: string;
|
|
|
credentials: [string, string];
|
|
|
+ codeServicePort: number;
|
|
|
proxyUrl: string;
|
|
|
lock: ILock;
|
|
|
lockfile: string;
|
|
@@ -53,11 +57,13 @@ export class SessionManager {
|
|
|
private username: string;
|
|
|
private password: string;
|
|
|
private lockfile: string;
|
|
|
-
|
|
|
- constructor(client: IgApiClient, file: string, credentials: [string, string]) {
|
|
|
+ private codeServicePort: number;
|
|
|
+
|
|
|
+ constructor(client: IgApiClient, file: string, credentials: [string, string], codeServicePort: number) {
|
|
|
this.ig = client;
|
|
|
this.lockfile = file;
|
|
|
[this.username, this.password] = credentials;
|
|
|
+ this.codeServicePort = codeServicePort;
|
|
|
}
|
|
|
|
|
|
public init = () => {
|
|
@@ -74,15 +80,57 @@ export class SessionManager {
|
|
|
logger.error(`failed to load client session cookies from file ${this.lockfile}: `, err);
|
|
|
return Promise.resolve();
|
|
|
}
|
|
|
- } else return this.login();
|
|
|
+ } else {
|
|
|
+ return this.login().catch((err: IgClientError) => {
|
|
|
+ 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);
|
|
|
+ promisify(setTimeout)(60000).then(this.init);
|
|
|
+ });
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
+ public handle2FA = <T>(submitter: (code: string) => Promise<T>) => new Promise<T>((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} = parseUrl(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);
|
|
|
+ });
|
|
|
+
|
|
|
public login = () =>
|
|
|
this.ig.simulate.preLoginFlow()
|
|
|
.then(() => this.ig.account.login(this.username, this.password))
|
|
|
- .then(() => new Promise(resolve => {
|
|
|
+ .catch((err: IgClientError) => {
|
|
|
+ if (err instanceof 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<typeof user>(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)));
|
|
|
}));
|
|
|
|
|
|
public save = () =>
|
|
@@ -181,7 +229,7 @@ export default class {
|
|
|
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.lock = opt.lock;
|
|
|
this.workInterval = opt.workInterval;
|