diff --git a/example/naver/reactivelyPrintPaymentHistory.ts b/example/naver/reactivelyPrintPaymentHistory.ts index b9a7d5d..4f42b33 100644 --- a/example/naver/reactivelyPrintPaymentHistory.ts +++ b/example/naver/reactivelyPrintPaymentHistory.ts @@ -1,8 +1,9 @@ import puppeteer from "puppeteer"; -import { NaverApp } from "trackpurchase"; +import { NaverApp } from "."; import readline from "readline"; import { concat, defer, filter, from, tap } from "rxjs"; +import { CaptchaStatus } from "app/naver"; const printNaverPayHistory = async (id: string, password: string) => { const MOBILE_UA = @@ -38,6 +39,22 @@ const printNaverPayHistory = async (id: string, password: string) => { }); } }), + tap((event) => { + function instanceOfCaptchaStatus(object: any): object is CaptchaStatus { + if (object) { + return "imageData" in object && "question" in object; + } + return false; + } + + if (instanceOfCaptchaStatus(event)) { + console.log(`encodedImage: ${event.imageData}`); + console.log(`question: ${event.question}`); + rl.question("captcha code: ", (code) => { + module.pageInteractor.fillCaptchaInput(code, password); + }); + } + }), filter((event) => event instanceof Array) ); final$.subscribe((event) => { diff --git a/package.json b/package.json index 55f9c17..6643b74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trackpurchase", - "version": "1.0.0", + "version": "1.1.0", "main": "dist/index.js", "license": "MIT", "repository": { diff --git a/src/app/naver/elementParser.ts b/src/app/naver/elementParser.ts index 1a8d5b8..11ca512 100644 --- a/src/app/naver/elementParser.ts +++ b/src/app/naver/elementParser.ts @@ -117,4 +117,8 @@ export default class ElementParser { async parseManualOTPInputElement() { return await this.page.$("#otp"); } + + async parseCaptchaInputElement() { + return await this.page.$("#captcha"); + } } diff --git a/src/app/naver/index.ts b/src/app/naver/index.ts index cdb6f49..62f647f 100644 --- a/src/app/naver/index.ts +++ b/src/app/naver/index.ts @@ -1,7 +1,7 @@ import Module from "./module"; import ModuleFactory from "./moduleFactory"; import URLChanger from "./urlChanger"; -import PageInteractor, { LoginEvent } from "./pageInteractor"; +import PageInteractor, { LoginEvent, CaptchaStatus } from "./pageInteractor"; import ElementParser from "./elementParser"; import Service from "./service"; @@ -13,4 +13,5 @@ export { ElementParser, Service, LoginEvent, + CaptchaStatus, }; diff --git a/src/app/naver/pageInteractor.test.ts b/src/app/naver/pageInteractor.test.ts index 04b6252..4f1257c 100644 --- a/src/app/naver/pageInteractor.test.ts +++ b/src/app/naver/pageInteractor.test.ts @@ -509,6 +509,235 @@ viewLayer(2); `; +const captchaLoginHTML = ` + + + + + + + + + + + Naver Sign in + + + +
+
본문 바로가기
+ + +
+ +
+ + +
+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + +`; + describe("Login", () => { jest.setTimeout(8000); it("Should publish `manual-otp-required` when manual otp is required", async () => { @@ -542,6 +771,29 @@ describe("Login", () => { // when const status = await pageInteractor.getLoginStatus(); + + // then expect(status).toBe("manual-otp-required"); }); + + describe("Captcha Status", () => { + it("Should return current imageData and question", async () => { + // given + const module = ModuleFactory.create(page); + const pageInteractor = module.pageInteractor; + + module.urlChanger.loginURL = "https://example.com"; + await module.urlChanger.moveToLoginURL(); + await page.setContent(captchaLoginHTML); + + // when + const status = await pageInteractor.getCaptchaStatus(); + + // then + expect(status).not.toBeNull(); + expect(status?.question).toBe( + "What is the first number of the store's phone number?" + ); + }); + }); }); diff --git a/src/app/naver/pageInteractor.ts b/src/app/naver/pageInteractor.ts index c989ac7..d8d5c61 100644 --- a/src/app/naver/pageInteractor.ts +++ b/src/app/naver/pageInteractor.ts @@ -7,6 +7,11 @@ export type LoginEvent = | "manual-otp-required" | "unexpected"; +export interface CaptchaStatus { + readonly imageData: string; + readonly question: string; +} + export default class PageInteractor { private _fullyLoaded = false; @@ -27,9 +32,9 @@ export default class PageInteractor { private async typeLoginInfo(id: string, password: string, delay: number) { await this.page.focus("#id"); - await this.page.keyboard.type(id, { delay: delay || 200 }); + await this.page.keyboard.type(id, { delay: delay }); await this.page.focus("#pw"); - await this.page.keyboard.type(password, { delay: delay || 200 }); + await this.page.keyboard.type(password, { delay: delay }); await this.clickLoginButton(); } @@ -83,6 +88,38 @@ export default class PageInteractor { await manualOTPElement.press("Enter"); } + async getCaptchaStatus(): Promise { + const data = await this.page.evaluate(() => { + const captchaImage = document.querySelector( + "#captchaimg" + ) as HTMLElement | null; + const captchaText = document.querySelector( + "#captcha_info" + ) as HTMLElement | null; + + if (!captchaImage || !captchaText) { + return; + } + + const imageData = captchaImage.getAttribute("src") as string; + const question = captchaText.innerText; + + return { imageData, question }; + }); + + return data || null; + } + + async fillCaptchaInput(answer: string, password: string) { + const captchaElement = await this.elementParser.parseCaptchaInputElement(); + if (!captchaElement) { + throw new Error("captcha input element not found"); + } + await captchaElement.type(answer); + + await this.typeLoginInfo("", password, 200); + } + async loadMoreHistory() { if (this._fullyLoaded) { return; diff --git a/src/app/naver/service.ts b/src/app/naver/service.ts index d7e05be..1d07b82 100644 --- a/src/app/naver/service.ts +++ b/src/app/naver/service.ts @@ -14,21 +14,27 @@ export default class Service { this.module = module; } - async normalLogin(id: string, password: string) { + async normalLogin(id: string, password: string, delay?: number) { await this.module.urlChanger.moveToLoginURL(); - await this.module.pageInteractor.login(id, password); + await this.module.pageInteractor.login(id, password, delay); } - interactiveLogin(id: string, password: string) { - const login$ = defer(() => from(this.normalLogin(id, password))); + interactiveLogin(id: string, password: string, delay?: number) { + const login$ = defer(() => from(this.normalLogin(id, password, delay))); const loginStatus$ = interval(500) .pipe(mergeMap(() => this.module.pageInteractor.getLoginStatus())) .pipe( distinctUntilChanged(), takeWhile((loginStatus) => loginStatus !== "success") ); + const captchaStatus$ = interval(500) + .pipe(mergeMap(() => this.module.pageInteractor.getCaptchaStatus())) + .pipe( + distinctUntilChanged((a, b) => a?.question === b?.question), + takeWhile((captchaStatus) => captchaStatus !== null) + ); - const result$ = concat(login$, loginStatus$); + const result$ = concat(login$, captchaStatus$, loginStatus$); return result$; }