sro-correios
Version:
JavaScript library for tracking orders through Correios (BR)
242 lines (200 loc) • 7.02 kB
text/typescript
import fetch from 'node-fetch'
import capitalize from 'capitalize'
import crypto from 'crypto'
import { Tracking, Correios, Event, CorreiosUnit, PostalType, CategoryType, Login, HashSign } from 'sro-correios'
interface ParsedLocation {
locality: string | null
origin: string
}
export class SroCorreios {
private readonly PARALLEL_TRACKS = 10
public async track (...codes: string[]): Promise<Tracking[]> {
const flatCodes = codes.flat()
const chunkSize = Math.ceil(flatCodes.length / this.PARALLEL_TRACKS)
const tracks: Tracking[] = []
for (let i = 0; i < chunkSize; i++) {
const results: Tracking[] = await Promise.all(
flatCodes.slice(this.PARALLEL_TRACKS * i, this.PARALLEL_TRACKS * (i + 1)).map(this.requestObject.bind(this))
)
tracks.push(...results)
}
return tracks
}
public static isValidOrderCode = (code: string): boolean => /^[A-Z]{2}[0-9]{9}[A-Z]{2}$/.test(code)
private async requestObject (code: string): Promise<Tracking> {
try {
if (!SroCorreios.isValidOrderCode(code)) {
return {
code,
isInvalid: true,
error: 'invalid_code'
}
}
const hashSign = this.generateHashSign()
const loginResponse = await fetch(this.loginUri(), {
method: 'POST',
headers: {
'User-Agent': this.userAgent,
'Content-Type': 'application/json'
},
body: JSON.stringify({
requestToken: this.loginToken(),
data: hashSign.date,
sign: hashSign.sign
})
})
const { token }: Login = await loginResponse.json()
const response = await fetch(this.uri(code), {
method: 'GET',
headers: {
'User-Agent': this.userAgent,
'Content-Type': 'application/json',
'app-check-token': token
}
})
const data: Correios = await response.json()
return this.parseResponse(data, code)
} catch (error) {
return {
code,
isInvalid: true,
error: 'service_unavailable'
}
}
}
private parseResponse (data: Correios, code: string): Tracking {
if (data.objetos[0].mensagem !== undefined || data.objetos[0].eventos === undefined) {
return {
code,
isInvalid: true,
error: 'not_found'
}
}
const events: Event[] = []
data.objetos[0].eventos.forEach(event => {
const locality: string | null = this.locationRules(event.unidade).locality
const origin: string | null = this.locationRules(event.unidade).origin
let destination: string | null = null
if (event.unidadeDestino !== undefined) {
destination = this.locationRules(event.unidadeDestino).origin
}
events.push({
locality,
status: event.descricao,
origin,
destination,
trackedAt: new Date(event.dtHrCriado)
})
})
const [lastEvent, firstEvent] = [events[0], events[events.length - 1]]
const isDelivered = lastEvent.status.includes('Objeto entregue')
const category = this.parseCategory(data.objetos[0].tipoPostal)
return {
code,
category,
isDelivered,
postedAt: firstEvent.trackedAt,
updatedAt: lastEvent.trackedAt,
events
}
}
private parseLocation (unit: CorreiosUnit): ParsedLocation {
const city: string = capitalize(unit.endereco.cidade)
const locality: string = `${city} / ${unit.endereco.uf}`
const origin: string = `${unit.tipo} - ${locality}`
return {
locality,
origin
}
}
private parseInternationalLocation (unit: CorreiosUnit): ParsedLocation {
const origin: string = capitalize.words(unit.nome)
return {
locality: null,
origin
}
}
private locationRules (unit: CorreiosUnit): ParsedLocation {
if (unit.tipo === 'País') {
return this.parseInternationalLocation(unit)
} else {
return this.parseLocation(unit)
}
}
private parseCategory (postalType: PostalType | undefined): CategoryType {
if (postalType === undefined) {
return {
name: 'Desconhecido',
description: 'Não identificado'
}
}
const name = capitalize.words(postalType?.categoria ?? 'Desconhecido')
let description = capitalize.words(postalType?.descricao ?? 'Não identificado')
if (!description.includes('identificado') && !description.includes('Internacional')) {
const postalCode = description.split(' ').filter(word => word.length === 2)[0]
if (postalCode !== undefined) {
description = description.replace(postalCode, postalCode.toUpperCase())
}
}
return {
name,
description
}
}
private generateHashSign (): HashSign {
const hash = crypto.createHash('md5')
const date = new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' }).replace(',', '')
const hashString = `requestToken${this.loginToken()}data${date}`
const sign = hash.update(hashString).digest('hex')
return {
sign,
date
}
}
private uri (code: string): string {
const baseUrl = this.decoder(`
\x61\x48\x52\x30\x63\x48\x4d\x36\x4c\x79\x39\x77\x63
\x6d\x39\x34\x65\x57\x46\x77\x63\x43\x35\x6a\x62\x33
\x4a\x79\x5a\x57\x6c\x76\x63\x79\x35\x6a\x62\x32\x30
\x75\x59\x6e\x49\x76\x64\x6a\x45\x76\x63\x33\x4a\x76
\x4c\x58\x4a\x68\x63\x33\x52\x79\x62\x79\x38\x3d
`)
return baseUrl + code
}
private loginUri (): string {
const url = this.decoder(`
\x61\x48\x52\x30\x63\x48\x4d\x36\x4c\x79\x39\x77\x63\x6d
\x39\x34\x65\x57\x46\x77\x63\x43\x35\x6a\x62\x33\x4a\x79
\x5a\x57\x6c\x76\x63\x79\x35\x6a\x62\x32\x30\x75\x59\x6e
\x49\x76\x64\x6a\x45\x76\x59\x58\x42\x77\x4c\x58\x5a\x68
\x62\x47\x6c\x6b\x59\x58\x52\x70\x62\x32\x34\x3d
`).replace('v1', 'v2')
return url
}
private loginToken (): string {
const token = this.decoder(`
\x57\x56\x63\x31\x61\x32\x4e\x74\x4f\x58\x42\x61\x52\x48
\x52\x70\x59\x32\x6b\x31\x61\x6d\x49\x79\x4d\x48\x56\x5a
\x4d\x6a\x6c\x35\x59\x32\x31\x57\x63\x47\x49\x7a\x54\x58
\x56\x6a\x53\x45\x70\x73\x57\x56\x68\x53\x62\x47\x4a\x74
\x55\x6e\x42\x69\x56\x31\x5a\x31\x5a\x45\x63\x34\x4e\x31
\x4a\x71\x54\x58\x6c\x53\x56\x45\x6b\x31\x54\x31\x52\x6a
\x4d\x6b\x35\x36\x51\x54\x56\x4e\x65\x6c\x55\x31\x54\x30
\x52\x56\x4e\x56\x4a\x55\x51\x6b\x4e\x50\x56\x47\x52\x48
\x54\x6d\x74\x5a\x4e\x46\x46\x55\x55\x54\x52\x4e\x4d\x45
\x6b\x31\x55\x57\x70\x72\x4d\x55\x31\x36\x56\x54\x4e\x50
\x51\x54\x30\x39
`)
return token
}
private get userAgent (): string {
const UAS = this.decoder(`
\x52\x47\x46\x79\x64\x43\x38\x79\x4c\x6a\x45\x34\x49\x43
\x68\x6b\x59\x58\x4a\x30\x4f\x6d\x6c\x76\x4b\x51\x3d\x3d
`)
return UAS
}
private decoder (value: string): string {
return Buffer.from(value, '\x62\x61\x73\x65\x36\x34').toString('utf-8')
};
}