Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions services/taskConsumer/actions/vk/action-applier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { inject, injectable } from 'inversify';
import { Page } from 'puppeteer';
import bluebird from 'bluebird';
import { CaptchaSolver } from './captcha-solver';
import { AccountException } from '../../rpc-handlers/account.exception';

export type ActionArgs = {
page: Page;
goalAction: Function;
login: string;
};

export type ClickActionArgs = ActionArgs & {
selector: string;
};

export type CallbackAction = ActionArgs & {
callback: Function;
};

@injectable()
export class ActionApplier {
constructor(@inject(CaptchaSolver) private readonly captchaSolver: CaptchaSolver) {}

async click({ page, goalAction, selector, login }: ClickActionArgs) {
return this.callback({
callback: () => {
return page.evaluate(selectorForClick => {
document.querySelector<HTMLButtonElement>(selectorForClick).click();
}, selector);
},
page,
login,
goalAction,
});
}

async callback({ callback, page, goalAction, login }: CallbackAction) {
const waitForCaptchaPromise = page.waitFor(
() => !!document.querySelector('.recaptcha iframe'),
{ timeout: 10000 },
);

const waitForPhoneConfirmation = page.waitForFunction(
() => !!document.querySelector('#validation_phone_row'),
{ timeout: 10000 },
);

const result = await callback();

await bluebird.any([waitForCaptchaPromise, goalAction(), waitForPhoneConfirmation]);
try {
await this.captchaSolver.solveIfHas(page);
} catch (error) {
error.login = login;
throw error;
}
const needPhoneConfirmation = await page.evaluate(
() => !!document.querySelector('#validation_phone_row'),
);

if (needPhoneConfirmation) {
throw new AccountException(
'Account requires phone confirmation',
'phone_required',
login,
false,
);
}

return result;
}
}
45 changes: 45 additions & 0 deletions services/taskConsumer/actions/vk/captcha-solver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { inject, injectable } from 'inversify';
import { Page } from 'puppeteer';
import { CaptchaService } from '../../../../lib/captcha.service';

@injectable()
export class CaptchaSolver {
constructor(@inject(CaptchaService) private readonly captcha: CaptchaService) {}

async solveIfHas(page: Page) {
const hasCaptcha = await page.evaluate(() => !!document.querySelector('.recaptcha iframe'));
if (!hasCaptcha) {
return;
}

try {
const captchaUrl = await page.evaluate(() =>
document.querySelector('.recaptcha iframe').getAttribute('src'),
);
const pageUrl = await page.evaluate(() => document.location.href);
const urlObject = new URL(captchaUrl);
const siteKey = urlObject.searchParams.get('k');
const result = await this.captcha.solveRecaptchaV2({
pageUrl,
siteKey,
});
const captchaNavigationPromise = page.waitForNavigation();
await page.evaluate(
token => {
document.querySelector<HTMLInputElement>(
'.recaptcha .g-recaptcha-response',
).value = token;
document.querySelector<HTMLInputElement>('#quick_recaptcha').value = token;
document.querySelector<HTMLFormElement>('#quick_login_form').submit();
},
result,
siteKey,
);
await captchaNavigationPromise;
} catch (error) {
error.code = 'captcha_failed';
error.canRetry = true;
throw error;
}
}
}
60 changes: 13 additions & 47 deletions services/taskConsumer/actions/vk/vk-authorizer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { inject, injectable } from 'inversify';
import { Page } from 'puppeteer';
import bluebird, { AggregateError } from 'bluebird';
import { AggregateError } from 'bluebird';
import { LoggerInterface } from '../../../../lib/logger.interface';
import { ProxyInterface } from '../../proxy.interface';
import { AccountException } from '../../rpc-handlers/account.exception';
import { CaptchaService } from '../../../../lib/captcha.service';
import { ActionApplier } from './action-applier';

@injectable()
export class VkAuthorizer {
constructor(
@inject('Logger') private readonly logger: LoggerInterface,
@inject(CaptchaService) private readonly captcha: CaptchaService,
@inject(ActionApplier) private readonly actionApplier: ActionApplier,
) {}

async signInWithCookie(page: Page, login: string, remixsid: string): Promise<boolean> {
Expand Down Expand Up @@ -66,53 +66,19 @@ export class VkAuthorizer {
password,
);

const loginNavigationPromise = page.waitForNavigation({ timeout: 10000 });
const waitForCaptchaPromise = page.waitFor(
() => !!document.querySelector('.recaptcha iframe'),
{ timeout: 10000 },
);
await page.click('#login_button');
await bluebird
.any([loginNavigationPromise as Promise<any>, waitForCaptchaPromise as Promise<any>])
.catch(async error => {
if (error instanceof AggregateError) {
return page.reload({ waitUntil: 'networkidle2' });
}

throw error;
try {
await this.actionApplier.click({
page,
goalAction: () => page.waitForNavigation({ timeout: 10000 }),
selector: '#login_button',
login,
});

const hasCaptcha = await page.evaluate(() => !!document.querySelector('.recaptcha iframe'));
if (hasCaptcha) {
try {
const captchaUrl = await page.evaluate(() =>
document.querySelector('.recaptcha iframe').getAttribute('src'),
);
const urlObject = new URL(captchaUrl);
const siteKey = urlObject.searchParams.get('k');
const result = await this.captcha.solveRecaptchaV2({
pageUrl: 'https://vk.com/login',
siteKey,
});
const captchaNavigationPromise = page.waitForNavigation();
await page.evaluate(
token => {
document.querySelector<HTMLInputElement>(
'.recaptcha .g-recaptcha-response',
).value = token;
document.querySelector<HTMLInputElement>('#quick_recaptcha').value = token;
document.querySelector<HTMLFormElement>('#quick_login_form').submit();
},
result,
siteKey,
);
await captchaNavigationPromise;
} catch (error) {
error.code = 'captcha_failed';
error.login = login;
error.canRetry = true;
} catch (error) {
if (!(error instanceof AggregateError)) {
throw error;
}

await page.reload({ waitUntil: 'networkidle2' });
}

await this.checkAccount(page, login);
Expand Down
70 changes: 28 additions & 42 deletions services/taskConsumer/rpc-handlers/join-group-rpc.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { VkUserCredentialsInterface } from '../../api/vk-users/vk-user-credentia
import { createBrowserPage } from '../actions/create-page';
import { hrefByGroupId } from '../../../lib/helper';
import { JoinGroupFailedException } from './join-group-failed.exception';
import { AccountException } from './account.exception';
import { ActionApplier } from '../actions/vk/action-applier';

type TaskArgsType = {
userCredentials: VkUserCredentialsInterface;
Expand All @@ -20,6 +20,8 @@ export class JoinGroupRpcHandler extends AbstractRpcHandler {

@inject(VkAuthorizer) private readonly vkAuthorizer: VkAuthorizer;

@inject(ActionApplier) private readonly actionApplier: ActionApplier;

protected readonly method = 'joinGroup';

static readonly method = 'joinGroup';
Expand Down Expand Up @@ -63,22 +65,31 @@ export class JoinGroupRpcHandler extends AbstractRpcHandler {
});
}

const subscribeClicked = await page.evaluate(() => {
const subscribeButton = document.querySelector<HTMLButtonElement>(
'#public_subscribe',
);
const joinButton = document.querySelector<HTMLButtonElement>('#join_button');
if (subscribeButton) {
subscribeButton.click();
return true;
}

if (joinButton) {
joinButton.click();
return true;
}

return false;
const subscribeClicked = await this.actionApplier.callback({
callback: () => {
return page.evaluate(() => {
const subscribeButton = document.querySelector<HTMLButtonElement>(
'#public_subscribe',
);
const joinButton = document.querySelector<HTMLButtonElement>(
'#join_button',
);
if (subscribeButton) {
subscribeButton.click();
return true;
}

if (joinButton) {
joinButton.click();
return true;
}

return false;
});
},
login: userCredentials.login,
goalAction: () => page.waitForSelector('#page_actions_btn'),
page,
});

if (!subscribeClicked) {
Expand All @@ -90,31 +101,6 @@ export class JoinGroupRpcHandler extends AbstractRpcHandler {
);
}

await page.waitForFunction(() => {
const form = document.querySelector('#validation_phone_row');
if (form) {
return true;
}

return !!document.querySelector('#page_actions_btn');
});

const needPhoneConfirmation = await page.evaluate(() => {
const form = document.querySelector('#validation_phone_row');
return !!form;
});

if (needPhoneConfirmation) {
throw new AccountException(
'Account requires phone confirmation',
'phone_required',
userCredentials.login,
false,
);
}

await page.waitForSelector('#page_actions_btn');

this.logger.info({
message: 'Подписался в группу',
credentials: userCredentials,
Expand Down