UNPKG

generate-db-shop-urls

Version:
174 lines (149 loc) 5.6 kB
'use strict' const {DateTime} = require('luxon') const debug = require('debug')('generate-db-shop-urls') const debugResponse = require('debug')('generate-db-shop-urls:response') const request = require('./lib/request') const parse = require('./lib/parse') const compareJourney = require('./lib/compare-journey') const {showDetails} = require('./lib/helpers') const START_URL = 'https://reiseauskunft.bahn.de/bin/query.exe/dn' const timezone = 'Europe/Berlin' const locale = 'de-DE' const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o) const isISO8601String = str => 'string' === typeof str && !Number.isNaN(Date.parse(str)) const validateJourney = (j, name) => { if (!isObj(j)) throw new Error(name + ' must be an object.') const invalid = new Error(name + ' is invalid.') if (j.type !== 'journey' || !Array.isArray(j.legs) || !j.legs.length) throw invalid const firstLeg = j.legs[0] if (!firstLeg.origin || !isISO8601String(firstLeg.departure)) throw invalid const lastLeg = j.legs[j.legs.length - 1] if (!lastLeg.destination || !isISO8601String(lastLeg.arrival)) throw invalid const orig = firstLeg.origin if (isObj(orig) && orig.type !== 'station' && orig.type !== 'stop') { throw new Error(name + '.origin must be a station/stop.') } const dest = lastLeg.destination if (isObj(dest) && dest.type !== 'station' && dest.type !== 'stop') { throw new Error(name + '.destination must be a station/stop.') } // todo: departure, arrival } const formatDate = (d) => { return DateTime .fromISO(d, {zone: timezone, locale}) .toFormat('ccc, dd.MM.yy') } const formatTime = (d) => { return DateTime .fromISO(d, {zone: timezone, locale}) .toFormat('HH:mm') } const generateDbShopLink = async (outbound, opt) => { validateJourney(outbound, 'outbound') const options = { class: '2', // '1' or '2' // see https://gist.github.com/juliuste/202bb04f450a79f8fa12a2ec3abcd72d bahncard: '0', // no bahncard age: 40, // age of the traveller returning: null, // no return journey ...opt, } const orig = outbound.legs[0].origin const originId = orig.station?.id || orig.id || orig const lastOutboundLeg = outbound.legs[outbound.legs.length - 1] const dest = lastOutboundLeg.destination const destinationId = dest.station?.id || dest.id || dest if (options.returning) { validateJourney(options.returning, 'opt.returning') const rOrig = options.returning.legs[0].origin const rOrigId = rOrig.station?.id || rOrig.id || rOrig if (destinationId !== rOrigId) { throw new Error('origin.destination !== opt.returning.orgin.') } if (Date.parse(lastOutboundLeg.plannedArrival) > Date.parse(options.returning.legs[0].plannedDeparture)) { throw new Error('origin.destination !== opt.returning.orgin.') } } if (!['1', '2'].includes(options.class)) { throw new Error('opt.class must be `1` or `2`.') } if ( typeof options.bahncard !== 'string' || options.bahncard.length > 1 || options.bahncard.length > 2 ) { throw new Error('opt.bahncard is invalid.') } if ( typeof options.age !== 'number' || options.age < 0 || options.age > 200 ) { throw new Error('opt.age is invalid.') } const req = { // todo: https://gist.github.com/derhuerst/5abc2e1f74b9bb29a3aeffe59b503103/edit revia: 'yes', 'existOptimizePrice-deactivated': '1', country: 'DEU', // dbkanal_007: 'L01_S01_D001_qf-bahn-svb-kl2_lz03', start: '1', protocol: 'https:', // HAFAS mgate.exe uses `HYBRID` rtMode: 'DB-HYBRID', externRequest: 'yes', HWAI: showDetails(false), // WAT. Their API fails if `S` is missing, even though the ID in // `REQ0JourneyStopsSID` overrides whatever is in `S`. Same for // `Z` and `REQ0JourneyStopsZID`. // todo: support POIs and addresses REQ0JourneyStopsS0A: '1', S: 'foo', REQ0JourneyStopsSID: 'A=1@L=00' + originId, REQ0JourneyStopsZ0A: '1', Z: 'bar', REQ0JourneyStopsZID: 'A=1@L=00' + destinationId, // todo: a few minutes earlier? date: formatDate(outbound.legs[0].departure), time: formatTime(outbound.legs[0].departure), timesel: 'depart', // todo: a few minutes earlier? returnDate: options.returning ? formatDate(options.returning.legs[0].departure) : '', returnTime: options.returning ? formatTime(options.returning.legs[0].departure) : '', returnTimesel: 'depart', optimize: '0', auskunft_travelers_number: '1', // todo: make customisable 'tariffTravellerType.1': 'E', 'tariffTravellerReductionClass.1': options.bahncard, 'tariffTravellerAge.1': options.age, tariffClass: options.class, } debug('request', req) const {data, cookies} = await request(START_URL, req) debug('cookies', cookies) debugResponse(data) const results = parse(outbound, options.returning, false)(data) debug('outbound results', ...results) let result = results.find((f) => { return compareJourney(outbound, options.returning, f.journey, false) }) // todo: return `null` instead? if (!result) throw new Error('no matching outbound journey found') debug('outbound next step', result.nextStep) if (options.returning) { const {data} = await request(result.nextStep, null, cookies) debugResponse(data) const results = parse(outbound, options.returning, true)(data) debug('returning results', ...results) result = results.find((f) => { return compareJourney(outbound, options.returning, f.journey, true) }) // todo: return `null` instead? if (!result) throw new Error('no matching returning journey found') debug('returning next step', result.nextStep) } return result.nextStep } module.exports = generateDbShopLink