UNPKG

cttpie

Version:

Simple Web App for CTT service wrapping

260 lines (225 loc) 9.79 kB
const fetch = require("node-fetch"); const parser = require("node-html-parser"); const entities = require("html-entities"); const yonius = require("yonius"); const engine = require("./engine"); const config = require("../util/config"); const COLUMN_SEQUENCE = ["time", "status", "reason", "location", "person"]; const MONTHS = [ "janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro" ]; class Crawler extends engine.Engine { async init() {} async destroy() {} async track(req, res, next) { const tracking = req.query.tracking; if (!tracking) throw new yonius.OperationalError("No tracking number provided", 400); const handler = this[`_handle${this._handler}`].bind(this); const { identifier, statusDate, statusHour, status, states } = await handler(tracking); res.setHeader("Content-Type", "application/json"); res.send( JSON.stringify({ tracking: tracking, identifier: identifier, status: status, status_date: statusDate, status_hour: statusHour, states: states }) ); } async _handleNeo(tracking) { const url = "https://appserver.ctt.pt/CustomerArea/screenservices/CustomerArea/CustomerArea/PublicArea_Detail/DataActionGetObjectEventsByInputObjectCode"; const response = await fetch(url, { method: "POST", headers: { "X-CSRFToken": config.conf.CSRF_TOKEN, Cookie: config.conf.COOKIE, "Content-Type": "application/json" }, body: JSON.stringify({ versionInfo: { apiVersion: await this._getApiVersion() }, viewName: "CustomerArea.PublicArea_Detail", screenData: { variables: { ObjectsLength: -1, ObjectCodeInput: tracking, _objectCodeInputInDataFetchStatus: 1, SearchInput: tracking, _searchInputInDataFetchStatus: 1 } } }) }); const data = await response.json(); const versionInfo = data.versionInfo; if (versionInfo.hasApiVersionChanged) { throw new yonius.OperationalError( "API version information changed, requires re-config", 500 ); } const info = data.data.ObjectEventsFromQuery; if (info === undefined) { throw new yonius.OperationalError("Error obtained remote data, invalid session", 500); } const events = info.Events || {}; const eventsList = events.List || []; if (!info.Found) { throw new yonius.OperationalError("Tracking number not found", 404); } const states = []; let status = null; eventsList.forEach(event => { const state = { status: event.State || null, details: event.Event || null, date_time: event.DateTime || null }; states.push(state); }); // uses the status of the first state as the statues of the order // as a whole (to be processed by external agents) if (!status) status = states.length > 0 ? states[0].status : null; return { identifier: info.ObjectName, status: status, states: states }; } async _handleLegacy(tracking) { // gathers the information from the target CTT page using // the node's HTML parser engine const url = `http://www.ctt.pt/feapl_2/app/open/cttexpresso/objectSearch/objectSearch.jspx?objects=${tracking}`; const response = await fetch(url); const body = await response.text(); const root = parser.parse(body); // obtains the reference to the multiple elements from which // information is going to be obtained const headerTable = root.querySelector(".full-width"); const header = headerTable.querySelectorAll("tr")[1]; const details = root.querySelector("#details_0"); const table = details.querySelector("table"); // in case no header or table is found in the target page an error // must be raised (erroneous situation) if (!header) throw new yonius.OperationalError("No header found in page"); if (!table) throw new yonius.OperationalError("No table found in page"); // unpacks the multiple header elements for the tracking element // so that we can define the summary of the tracking status const identifier = header.childNodes[1].structuredText; const statusDate = header.childNodes[5].structuredText; const statusHour = header.childNodes[7].structuredText; let status = header.childNodes[9].structuredText; // builds the sequence that is going to hold the multiple states const states = []; let date = null; let dateLabel = null; // iterates over the complete st of rows in the table trying // to find the multiple states for the order for (const row of table.childNodes) { // verifies if the current node is a valid and // concrete row and in case its not continue the loop if (row.nodeType !== parser.NodeType.ELEMENT_NODE) continue; if (row.tagName.toLowerCase() !== "tr") continue; let index = 0; const state = {}; // iterates over the complete set of columns in the row to try // to find the correct label ones for (const column of row.childNodes) { if (column.nodeType !== parser.NodeType.ELEMENT_NODE) { continue; } // in case the current child is a row we assume that // this is the date field for the current state if (column.tagName.toLowerCase() === "tr") { dateLabel = entities.AllHtmlEntities.decode(column.structuredText); date = this._parseDate(dateLabel); continue; } // in case the current element is now a column skips // the current iteration (nothing useful here) if (column.tagName.toLowerCase() !== "td") { continue; } const columnName = COLUMN_SEQUENCE[index]; state[columnName] = entities.AllHtmlEntities.decode(column.structuredText); index++; } const [hours, minutes] = this._parseTime(state.time); const stateDate = new Date(date.getTime() + (hours * 3600 + minutes * 60) * 1000); const stateTimestamp = stateDate.getTime(); state.date = stateDate; state.date_label = dateLabel; state.timestamp = parseInt(stateTimestamp / 1000); states.push(state); } // uses the status of the first state as the statues of the order // as a whole (to be processed by external agents) if (!status) status = states.length > 0 ? states[0].status : null; return { identifier: identifier, statusDate: statusDate, statusHour: statusHour, status: status, states: states }; } async _getApiVersion(force = true) { if (config.API_VERSION) return config.API_VERSION; if (!force && this._apiVersion) return this._apiVersion; const configUrl = "https://appserver.ctt.pt/CustomerArea/scripts/CustomerArea.CustomerArea.PublicArea_Detail.mvc.js"; const configResponse = await fetch(configUrl); const configData = await configResponse.text(); const token = '/DataActionGetObjectEventsByInputObjectCode", '; const subConfig = configData.slice(configData.indexOf(token) + token.length); this._apiVersion = subConfig.slice(0, subConfig.indexOf(",")).trim(); this._apiVersion = this._apiVersion.slice(1, this._apiVersion.length - 1); if (!this._apiVersion) { throw new yonius.OperationalError("Not possible to obtain API version", 500); } if (this._apiVersion.length !== 22) { throw new yonius.OperationalError( `Invalid API version token length expected 22 got ${this._apiVersion.length}`, 500 ); } return this._apiVersion; } _parseDate(value) { let day, month, year; [day, month, year] = value.split(",")[1].trim().split(" "); day = parseInt(day); month = MONTHS.indexOf(month.toLowerCase()); year = parseInt(year); const date = new Date(year, month, day); return date; } _parseTime(value) { let hours, minutes; [hours, minutes] = value.split(":"); hours = parseInt(hours); minutes = parseInt(minutes); return [hours, minutes]; } get _handler() { return config.conf.HANDLER.charAt(0).toUpperCase() + config.conf.HANDLER.slice(1); } } module.exports = { Crawler: Crawler };