commit 0cd1ef1cd438d5bd37a3cb2282b0f828f4c65355 Author: andre.borges Date: Tue Nov 23 13:40:12 2021 -0300 Scraper de CNDs do Mato Grosso criado diff --git a/.env b/.env new file mode 100644 index 0000000..c5adf93 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +APP_REDIS_HOST=redis-cnds +APP_REDIS_PORT=6380 + +CAP_MONSTER_KEY =89310c8388da93bf17133bf399f517de +TWO_CAPTCHA_KEY=0a26ea85b64ea9797cc296337001f6ab \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..f6c62be --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22f55ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fe8812 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Coverage +Discord +Backers on Open Collective +Sponsors on Open Collective + + Support us + +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Installation + +```bash +$ npm install +``` + +## Running the app + +```bash +# development +$ npm run start + +# watch mode +$ npm run start:dev + +# production mode +$ npm run start:prod +``` + +## Test + +```bash +# unit tests +$ npm run test + +# e2e tests +$ npm run test:e2e + +# test coverage +$ npm run test:cov +``` + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](LICENSE). diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..56167b3 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,4 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0080304 --- /dev/null +++ b/package.json @@ -0,0 +1,82 @@ +{ + "name": "scraper-cnd-mt", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "prebuild": "rimraf dist", + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "docker:build": "", + "docker:tag": "", + "docker:push": "", + "docker:publish": "npm run docker:tag && npm run docker:push" + }, + "dependencies": { + "@infosimples/node_two_captcha": "^1.0.2", + "@nestjs/bull": "^0.1.2", + "@nestjs/common": "^7.6.18", + "@nestjs/config": "^0.4.0", + "@nestjs/core": "^7.0.0", + "@nestjs/platform-express": "^7.0.0", + "bull": "^3.15.0", + "data-fns": "^0.1.8", + "date-fns": "^2.12.0", + "pdf-parse": "^1.1.1", + "puppeteer": "^5.0.0", + "reflect-metadata": "^0.1.13", + "rimraf": "^3.0.2", + "rxjs": "^6.5.4" + }, + "devDependencies": { + "@nestjs/cli": "^7.0.0", + "@nestjs/schematics": "^7.0.0", + "@nestjs/testing": "^7.0.0", + "@types/bull": "^3.14.0", + "@types/express": "^4.17.3", + "@types/jest": "25.1.4", + "@types/node": "^13.9.1", + "@types/pdf-parse": "^1.1.1", + "@types/puppeteer": "^3.0.1", + "@types/supertest": "^2.0.8", + "@typescript-eslint/eslint-plugin": "^2.23.0", + "@typescript-eslint/parser": "^2.23.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-import": "^2.20.1", + "jest": "^25.1.0", + "prettier": "^1.19.1", + "supertest": "^4.0.2", + "ts-jest": "25.2.1", + "ts-loader": "^6.2.1", + "ts-node": "^8.6.2", + "tsconfig-paths": "^3.9.0", + "typescript": "^3.7.4" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..626d7aa --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { ScraperCndMtModule } from './modules/scraper-cnd-mt/scraper-cnd-mt.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + ScraperCndMtModule + ], +}) +export class AppModule {} diff --git a/src/enums/situacao.enum.ts b/src/enums/situacao.enum.ts new file mode 100644 index 0000000..4de15b6 --- /dev/null +++ b/src/enums/situacao.enum.ts @@ -0,0 +1,6 @@ +export enum Situacao { + Negativa = 1, + Positiva, + FalhaConsulta, + PositivaEfeitoNegativa, + } \ No newline at end of file diff --git a/src/helpers/capMonster.ts b/src/helpers/capMonster.ts new file mode 100644 index 0000000..eee68af --- /dev/null +++ b/src/helpers/capMonster.ts @@ -0,0 +1,114 @@ +import axios from 'axios' + +import { ICaptchaResolver } from '../interfaces/ICaptchaResolver'; + +export class CapMonster implements ICaptchaResolver { + private readonly CAPTCHA_KEY: string = process.env.CAP_MONSTER_KEY + private readonly CAPTCHA_BALANCE: string = + 'https://api.capmonster.cloud/getBalance' + private readonly CAPTCHA_CREATE: string = + 'https://api.capmonster.cloud/createTask' + private readonly CAPTCHA_RESULT: string = + 'https://api.capmonster.cloud/getTaskResult' + + public async temSaldoDisponivel (): Promise { + try { + const balanceData = { + clientKey: this.CAPTCHA_KEY + } + + const { data } = await axios.post(this.CAPTCHA_BALANCE, balanceData) + + if (data.errorId > 0) { + console.log(`[consultar-saldo-captcha] - ${data.errorCode}`) + return false + } + + return data.balance > 0 + } catch (error) { + console.log(`[consultar-saldo-captcha] - ${error.message}`) + + return false + } + } + + public async resolver (imageData: string): Promise { + const taskId = await this.criar(imageData) + + if (taskId === 0) return null + + return await this.resultado(taskId) + } + + private async criar (imageData: string): Promise { + try { + const createTask = { + clientKey: this.CAPTCHA_KEY, + task: { + type: 'ImageToTextTask', + body: imageData + } + } + + const { data } = await axios.post(this.CAPTCHA_CREATE, createTask) + + if (data.errorId > 0) { + console.log(`[criar-captcha] - ${data.errorCode}`) + return 0 + } + + return data.taskId + } catch (error) { + console.log(`[criar-captcha] - ${error.message}`) + + return 0 + } + } + + private async resultado (taskId: number): Promise { + let tentativas = 0 + let error: boolean = false + let captchaData = null + + const getResultTask = { + clientKey: this.CAPTCHA_KEY, + taskId: taskId + } + + do { + try { + const { + data: { errorId, errorCode, solution, status } + } = await axios.post(this.CAPTCHA_RESULT, getResultTask) + + if (errorId > 0) { + console.log( + `[resultado-captcha] - Task: ${taskId} Erro: ${errorCode} Tentativas: ${tentativas}` + ) + error = true + break + } + + if (status === 'ready') { + captchaData = solution.text + break + } + + tentativas += 1 + await new Promise(r => setTimeout(r, 10 * 1000)) + } catch (error) { + console.log( + `[resultado-captcha] - Task: ${taskId} Erro: ${error.message} Tentativas: ${tentativas}` + ) + error = true + break + } + } while (tentativas < 20) + + if (error) return null + + return captchaData + } +} + +export default new CapMonster() diff --git a/src/helpers/twoCaptcha.ts b/src/helpers/twoCaptcha.ts new file mode 100644 index 0000000..36fca7f --- /dev/null +++ b/src/helpers/twoCaptcha.ts @@ -0,0 +1,114 @@ +import axios from 'axios' + +export class TwoCaptcha { + + private readonly CAPTCHA_KEY: string = 'TWO_CAPTCHA_KEY' + private readonly CAPTCHA_CREATE: string = 'http://2captcha.com/in.php' + private readonly CAPTCHA_RESULT: string = 'http://2captcha.com/res.php' + private readonly CAPTCHA_BALANCE: string = 'https://2captcha.com/res.php' + + async resolveCaptcha (imageData: string) { + const captchaId = await this._sendCaptcha(imageData) + const captchaResolved = await this._getResponseCaptcha(captchaId) + return captchaResolved + } + + public async temSaldoDisponivel (): Promise { + try { + const balanceData = { + key: this.CAPTCHA_KEY, + action: 'getbalance', + json: 1 + } + const { data: { request } } = await axios.get(this.CAPTCHA_BALANCE, { params: balanceData }); + + if ((request || '').includes('ERROR_')) { + console.log(`[consultar-saldo-captcha] - ${request}`); + return false; + } + + return Number(request) > 0; + } catch (error) { + console.log(`[consultar-saldo-captcha] - ${error.message}`); + + return false; + } + } + + private async _sendCaptcha (imageData: string): Promise { + try { + const captchaResponse = await axios.post( + this.CAPTCHA_CREATE, + { + method: 'base64', + key: this.CAPTCHA_KEY, + body: imageData, + json: 1 + }, + { headers: { 'Content-Type': 'multipart/form-data' } } + ) + + let response: string = '' + + if (captchaResponse.data && typeof captchaResponse.data === 'object') { + if (captchaResponse.data.status === 0) + throw new Error(captchaResponse.data.request) + response = captchaResponse.data.request + } else if ( + captchaResponse.data && + typeof captchaResponse.data === 'string' + ) { + if (captchaResponse.data.indexOf('OK') === -1) + throw new Error(captchaResponse.data) + response = captchaResponse.data.split('|')[1] + } + + return response + } catch (error) { + return new Promise((resolve, reject) => reject(error)) + } + } + + private async _getResponseCaptcha (captchaId: string): Promise { + return new Promise((resolve, reject) => { + const interval = setInterval(async () => { + try { + const serverResponse = await axios.get(this.CAPTCHA_RESULT, { + params: { + key: this.CAPTCHA_KEY, + action: 'get', + id: captchaId, + json: 1 + } + }) + + if (serverResponse.data.request !== 'CAPCHA_NOT_READY') { + clearInterval(interval) + + let response = '' + + if ( + serverResponse.data && + typeof serverResponse.data === 'object' + ) { + if (serverResponse.data.status === 0) + throw new Error(serverResponse.data.request) + response = serverResponse.data.request + } else if ( + serverResponse.data && + typeof serverResponse.data === 'string' + ) { + if (serverResponse.data.indexOf('OK') === -1) + throw new Error(serverResponse.data) + response = serverResponse.data.split('|')[1] + } + + resolve(response) + } + } catch (e) { + reject(e) + } + }, 10 * 1000) + }) + } +} diff --git a/src/interfaces/ICaptchaResolver.ts b/src/interfaces/ICaptchaResolver.ts new file mode 100644 index 0000000..6ceb1f7 --- /dev/null +++ b/src/interfaces/ICaptchaResolver.ts @@ -0,0 +1,4 @@ +export interface ICaptchaResolver { + temSaldoDisponivel(): Promise; + resolver(imageData: string): Promise; +} \ No newline at end of file diff --git a/src/interfaces/certidao-negativa.interface.ts b/src/interfaces/certidao-negativa.interface.ts new file mode 100644 index 0000000..aa9e314 --- /dev/null +++ b/src/interfaces/certidao-negativa.interface.ts @@ -0,0 +1,6 @@ +export interface CertidaoNegativa { + situacao: number; + dataEmissao: number; + dataValidade?: number; + file?: Uint8Array; + } \ No newline at end of file diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..f0e5ca1 --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,2 @@ +export * from './certidao-negativa.interface'; +export * from './scraper-data.interface'; \ No newline at end of file diff --git a/src/interfaces/scraper-data.interface.ts b/src/interfaces/scraper-data.interface.ts new file mode 100644 index 0000000..4cd016a --- /dev/null +++ b/src/interfaces/scraper-data.interface.ts @@ -0,0 +1,4 @@ +export interface ScrapeData { + inscricao: string; + antecipar?: boolean; + } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..42c2fa5 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,7 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + await NestFactory.createApplicationContext(AppModule); +} +bootstrap(); diff --git a/src/modules/scraper-cnd-mt/scraper-cnd-mt.module.ts b/src/modules/scraper-cnd-mt/scraper-cnd-mt.module.ts new file mode 100644 index 0000000..5247786 --- /dev/null +++ b/src/modules/scraper-cnd-mt/scraper-cnd-mt.module.ts @@ -0,0 +1,26 @@ +import { BullModule } from '@nestjs/bull'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; + +import { ScraperMtProcessor } from './scraper-cnd-mt.processor'; +import { ScraperCndMtServise } from './scraper-cnd-mt.service'; + +@Module({ + imports: [ + BullModule.registerQueueAsync({ + name: 'mato-grosso', + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + redis: { + host: configService.get('APP_REDIS_HOST'), + port: +configService.get('APP_REDIS_PORT'), + } + }), + inject: [ConfigService], + })], + providers: [ + ScraperCndMtServise, + ScraperMtProcessor + ], +}) +export class ScraperCndMtModule {} diff --git a/src/modules/scraper-cnd-mt/scraper-cnd-mt.processor.ts b/src/modules/scraper-cnd-mt/scraper-cnd-mt.processor.ts new file mode 100644 index 0000000..fbe80c0 --- /dev/null +++ b/src/modules/scraper-cnd-mt/scraper-cnd-mt.processor.ts @@ -0,0 +1,15 @@ +import { Process, Processor } from "@nestjs/bull"; +import { Job } from 'bull'; + +import { ScraperCndMtServise } from "./scraper-cnd-mt.service"; + +@Processor('mato-grosso') +export class ScraperMtProcessor { + constructor(private readonly scraperCndMtService: ScraperCndMtServise) {} + + @Process({ name: 'mato-grosso', concurrency: 1}) + handler(job: Job) { + const { inscricao } = job.data; + return this.scraperCndMtService.scraperCndMt(inscricao); + } +} diff --git a/src/modules/scraper-cnd-mt/scraper-cnd-mt.service.ts b/src/modules/scraper-cnd-mt/scraper-cnd-mt.service.ts new file mode 100644 index 0000000..5c4a3af --- /dev/null +++ b/src/modules/scraper-cnd-mt/scraper-cnd-mt.service.ts @@ -0,0 +1,265 @@ +import { Injectable, Logger, Scope } from '@nestjs/common'; +import puppeteer, { Browser, Page } from 'puppeteer'; +import pdfparse from 'pdf-parse'; +import { getUnixTime } from 'date-fns'; + +import { certidaoResult } from '../../utils/certidao.utils'; +import { Situacao } from 'src/enums/situacao.enum'; +import { CapMonster } from 'src/helpers/capMonster'; +import { TwoCaptcha } from 'src/helpers/twoCaptcha'; +import { CertidaoNegativa } from 'src/interfaces'; + +@Injectable({ scope: Scope.REQUEST }) +export class ScraperCndMtServise { + private _page: Page; + private browser: Browser; + private cndFile: Buffer; + private resultadoScraping: CertidaoNegativa; + private logger = new Logger('ScraperCndMtServise'); + + constructor() { } + + async scraperCndMt(inscricao: string) { + try { + await this.paginaInicial(inscricao); + if(this.cndFile) { + await this.capturandoInformacoesPdf(this.cndFile); + } else { + const selecionandoTabela = await this._page.$$('table'); + const selecionandoBodyTabela = await selecionandoTabela[2].$$('tbody > tr'); + const selecionandoConteudoDiv = await selecionandoBodyTabela[2].$$('td > div'); + + const mensagemCndPositiva = await selecionandoConteudoDiv[0].$eval( + 'font' , + elem => String(elem.textContent).trim(), + ); + if(mensagemCndPositiva.includes('não são suficientes')) { + this.logger.log('Certidão Positiva'); + return certidaoResult(Situacao.Positiva) + } + } + await this._page.waitFor(8000) + console.log(this.resultadoScraping) + + return this.resultadoScraping; + } catch (error) { + this.logger.error(error.message); + return certidaoResult(Situacao.FalhaConsulta); + } finally { + await this._page.close(); + await this.browser.close(); + } + } + + private async paginaInicial(inscricao: string): Promise { + + let tentativas = 0; + + do { + + await this.carregarNavegadorPaginaInicial(); + + this.logger.log(`Gerando nova CND para a inscrição ${inscricao}...`); + + await this.preencheFormulario(inscricao); + + await this.resolveCaptcha(); + + await this._page.waitForNavigation(); + + const validandoResolucaoDoCaptcha = await this._page.$$eval('form > font > b', mensagem => mensagem.map(texto => texto.textContent)) + + if(validandoResolucaoDoCaptcha[0]) { + if(validandoResolucaoDoCaptcha[0].includes('Código de caracteres inválido')){ + this.logger.log(`Captcha incorreto!`); + tentativas += 1; + await this.browser.close(); + } + } + + if(!validandoResolucaoDoCaptcha[0]){ + this.logger.log(`Captcha para a inscrição ${inscricao} foi resolvido`); + + await this.capturaArquivoNaRequisicao(); + + break; + } + } while (tentativas != 2); + } + + private async carregarNavegadorPaginaInicial() { + this.browser = await puppeteer.launch({ + headless: false, + defaultViewport: null, + }); + + this._page = await this.browser.newPage(); + await this._page.goto( + 'https://www.sefaz.mt.gov.br/cnd/certidao/servlet/ServletRotd?origem=60', + { + waitUntil: 'load', + } + ) + + return this._page; + } + + private async preencheFormulario(inscricao: string) { + await this._page.waitForSelector('#ModeloCertidao'); + await this._page.click('#ModeloCertidao'); + + await this._page.waitForSelector('#tipoDoct'); + const selecionandoOpcaoCnpj = await this._page.$$('#tipoDoct'); + await selecionandoOpcaoCnpj[1].click(); + + await this._page.waitForSelector('#numrDoctCNPJ'); + await this._page.type("#numrDoctCNPJ", inscricao, { delay: 250 }); + } + + private async capturaArquivoNaRequisicao (): Promise { + const client = await this._page.target().createCDPSession(); + let pdfBase64: Array = []; + let responseObj: Object; + + client.send('Fetch.enable', { + patterns: [ + { + requestStage: 'Response', + }, + ], + }) + + await client.on('Fetch.requestPaused', async (reqEvent) => { + const { requestId } = reqEvent; + this.logger.log('Requisição pausada') + + let responseHeaders = reqEvent.responseHeaders || []; + let contentType = ''; + + for (let elements of responseHeaders) { + if (elements.name.toLowerCase() === 'content-type') { + contentType = elements.value; + } + } + + if (contentType.endsWith('pdf')) { + + const foundHeaderIndex = responseHeaders.findIndex( + (h) => h.name === "content-disposition" + ); + const attachmentHeader = { + name: "content-disposition", + value: "attachment", + }; + if (foundHeaderIndex) { + responseHeaders[foundHeaderIndex] = attachmentHeader; + } else { + responseHeaders.push(attachmentHeader); + } + + responseObj = await client.send('Fetch.getResponseBody', { + requestId, + }); + + pdfBase64 = Object.values(responseObj); + + this.cndFile = new Buffer(pdfBase64[0], 'base64'); + + if(this.cndFile){ + this.logger.log('Cnd capturada') + } + + await client.send('Fetch.continueRequest', { requestId }); + + } else { + await client.send('Fetch.continueRequest', { requestId }); + } + }); + await this._page.waitFor(25000) + } + + private async capturandoInformacoesPdf(cndFile: Buffer): Promise { + let situacao = 0; + + this.logger.log('Extraindo informações do pdf') + + let data = await pdfparse(cndFile) + + if(data.text.includes('CERTIDÃO NEGATIVA')){ + situacao = 1; + } else if(data.text.includes('CERTIDÃO POSITIVA COM EFEITOS DE NEGATIVA')) { + situacao = 4; + } + + let inicioSeletor = data.text.indexOf('válida até:'); + + const posicaoInicialDataValidade = inicioSeletor + 13; + const posicaoFinalDataValidade = posicaoInicialDataValidade + 10; + + const dataValidade = new Date(data.text.slice(posicaoInicialDataValidade, posicaoFinalDataValidade)); + + let inicioSeletorDataEmissão = data.text.indexOf('Data da emissão:'); + + const posicaoInicialDataEmissao = inicioSeletorDataEmissão + 18; + const posicaoFinalDataEmissao = posicaoInicialDataEmissao + 10; + + const dataEmissao = new Date(data.text.slice(posicaoInicialDataEmissao, posicaoFinalDataEmissao)); + + if(dataEmissao && dataValidade && cndFile) { + this.logger.log('Scraping concluído'); + this.resultadoScraping = { + situacao: situacao, + dataEmissao: getUnixTime(dataEmissao), + dataValidade: getUnixTime(dataValidade), + file: cndFile + } + } + } + + private async resolveCaptcha (): Promise { + const captchaBinary = await this._page.evaluate(() => { + const img: HTMLImageElement = document.querySelector( + '[src="/cnd/certidao/geradorcaracteres"]' + ) + const canvas = document.createElement('canvas') + canvas.width = img.width + canvas.height = img.height + const ctx = canvas.getContext('2d') + ctx.drawImage(img, 0, 0) + return canvas.toDataURL('image/png') + }) + + const captchaService = new CapMonster() + const twoCaptchService = new TwoCaptcha() + + const matches = captchaBinary.match(/^data:(.+);base64,(.+)$/) + + if (matches.length !== 3) { + throw new Error('Erro ao extrair imagem do Captcha') + } + + let captcha; + + if (await captchaService.temSaldoDisponivel()) { + this.logger.log(' Usando CapMonster...') + captcha = await captchaService.resolver(matches[2]); + } else if (await twoCaptchService.temSaldoDisponivel()) { + this.logger.log(' Usando TwoCaptcha...') + captcha = await twoCaptchService.resolveCaptcha(matches[2]); + } else { + this.logger.log('Não tem saldo para quebrar o captcha.'); + + return { + sucesso: false, + mensagem: 'Não tem saldo para quebrar o captcha.' + } + } + + if (!captcha) throw new Error('Erro ao resolver o Captcha'); + + await this._page.type('[name="caracteres"]', captcha, {delay: 250}); + + const selecionandoElementoBotaoOk = await this._page.$$('#spanBotao'); + await selecionandoElementoBotaoOk[0].click(); + } +} \ No newline at end of file diff --git a/src/utils/index.d.ts b/src/utils/index.d.ts new file mode 100644 index 0000000..f3aa057 --- /dev/null +++ b/src/utils/index.d.ts @@ -0,0 +1 @@ +declare module '@infosimples/node_two_captcha'; \ No newline at end of file diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f007616 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "esModuleInterop": true + }, + "exclude": ["node_modules", "dist"] +}