mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-02-22 22:59:35 +02:00
merge new-vbb-protocol into master
This commit is contained in:
commit
95833edd21
10 changed files with 128 additions and 64 deletions
|
@ -7,7 +7,7 @@ const formatLocationIdentifier = (data) => {
|
|||
for (let key in data) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data, key)) continue
|
||||
|
||||
str += key + '=' + data[key] + sep // todo: escape
|
||||
str += key + '=' + data[key] + sep // todo: escape, but how?
|
||||
}
|
||||
|
||||
return str
|
||||
|
|
|
@ -13,9 +13,9 @@ const formatPoi = (p) => {
|
|||
lid: formatLocationIdentifier({
|
||||
A: '4', // POI
|
||||
O: p.name,
|
||||
L: p.id,
|
||||
X: formatCoord(p.longitude),
|
||||
Y: formatCoord(p.latitude),
|
||||
L: p.id
|
||||
Y: formatCoord(p.latitude)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
'use strict'
|
||||
|
||||
const formatStation = id => ({type: 'S', lid: 'L=' + id})
|
||||
const formatLocationIdentifier = require('./location-identifier')
|
||||
|
||||
const formatStation = (id) => {
|
||||
return {
|
||||
// todo: name necessary?
|
||||
lid: formatLocationIdentifier({
|
||||
A: '1', // station
|
||||
L: id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = formatStation
|
||||
|
|
87
index.js
87
index.js
|
@ -101,41 +101,64 @@ const createClient = (profile, request = _request) => {
|
|||
filters.push(profile.filters.accessibility[opt.accessibility])
|
||||
}
|
||||
|
||||
const query = profile.transformJourneysQuery({
|
||||
outDate: profile.formatDate(profile, opt.when),
|
||||
outTime: profile.formatTime(profile, opt.when),
|
||||
ctxScr: journeysRef,
|
||||
numF: opt.results,
|
||||
getPasslist: !!opt.passedStations,
|
||||
maxChg: opt.transfers,
|
||||
minChgTime: opt.transferTime,
|
||||
depLocL: [from],
|
||||
viaLocL: opt.via ? [{loc: opt.via}] : null,
|
||||
arrLocL: [to],
|
||||
jnyFltrL: filters,
|
||||
getTariff: !!opt.tickets,
|
||||
// With protocol version `1.16`, the VBB endpoint fails with
|
||||
// `CGI_READ_FAILED` if you pass `numF`, the parameter for the number
|
||||
// of results. To circumvent this, we loop here, collecting journeys
|
||||
// until we have enough.
|
||||
// see https://github.com/derhuerst/hafas-client/pull/23#issuecomment-370246163
|
||||
// todo: check if `numF` is supported again, revert this change
|
||||
const journeys = []
|
||||
const more = (when, journeysRef) => {
|
||||
const query = {
|
||||
outDate: profile.formatDate(profile, when),
|
||||
outTime: profile.formatTime(profile, when),
|
||||
ctxScr: journeysRef,
|
||||
// numF: opt.results,
|
||||
getPasslist: !!opt.passedStations,
|
||||
maxChg: opt.transfers,
|
||||
minChgTime: opt.transferTime,
|
||||
depLocL: [from],
|
||||
viaLocL: opt.via ? [{loc: opt.via}] : null,
|
||||
arrLocL: [to],
|
||||
jnyFltrL: filters,
|
||||
getTariff: !!opt.tickets,
|
||||
|
||||
// todo: what is req.gisFltrL?
|
||||
getPT: true, // todo: what is this?
|
||||
outFrwd: true, // todo: what is this?
|
||||
getIV: false, // todo: walk & bike as alternatives?
|
||||
getPolyline: false // todo: shape for displaying on a map?
|
||||
}, opt)
|
||||
// todo: what is req.gisFltrL?
|
||||
getPT: true, // todo: what is this?
|
||||
outFrwd: true, // todo: what is this?
|
||||
getIV: false, // todo: walk & bike as alternatives?
|
||||
getPolyline: false // todo: shape for displaying on a map?
|
||||
}
|
||||
|
||||
return request(profile, {
|
||||
cfg: {polyEnc: 'GPA'},
|
||||
meth: 'TripSearch',
|
||||
req: query
|
||||
})
|
||||
.then((d) => {
|
||||
if (!Array.isArray(d.outConL)) return []
|
||||
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks)
|
||||
const res = d.outConL.map(parse)
|
||||
return request(profile, {
|
||||
cfg: {polyEnc: 'GPA'},
|
||||
meth: 'TripSearch',
|
||||
req: profile.transformJourneysQuery(query, opt)
|
||||
})
|
||||
.then((d) => {
|
||||
if (!Array.isArray(d.outConL)) return []
|
||||
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks)
|
||||
if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB
|
||||
|
||||
if (d.outCtxScrB) res.earlierRef = d.outCtxScrB
|
||||
if (d.outCtxScrF) res.laterRef = d.outCtxScrF
|
||||
return res
|
||||
})
|
||||
let latestDep = -Infinity
|
||||
for (let j of d.outConL) {
|
||||
j = parse(j)
|
||||
journeys.push(j)
|
||||
|
||||
if (journeys.length === opt.results) { // collected enough
|
||||
journeys.laterRef = d.outCtxScrF
|
||||
return journeys
|
||||
}
|
||||
const dep = +new Date(j.departure)
|
||||
if (dep > latestDep) latestDep = dep
|
||||
}
|
||||
|
||||
const when = new Date(latestDep)
|
||||
return more(when, d.outCtxScrF) // otherwise continue
|
||||
})
|
||||
}
|
||||
|
||||
return more(opt.when, journeysRef)
|
||||
}
|
||||
|
||||
const locations = (query, opt = {}) => {
|
||||
|
|
|
@ -26,6 +26,10 @@ const filters = require('../format/filters')
|
|||
const id = x => x
|
||||
|
||||
const defaultProfile = {
|
||||
salt: null,
|
||||
addChecksum: false,
|
||||
addMicMac: false,
|
||||
|
||||
transformReqBody: id,
|
||||
transformReq: id,
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const crypto = require('crypto')
|
||||
let captureStackTrace = () => {}
|
||||
if (process.env.NODE_ENV === 'dev') {
|
||||
captureStackTrace = require('capture-stack-trace')
|
||||
|
@ -8,6 +9,8 @@ const {stringify} = require('query-string')
|
|||
const Promise = require('pinkie-promise')
|
||||
const {fetch} = require('fetch-ponyfill')({Promise})
|
||||
|
||||
const md5 = input => crypto.createHash('md5').update(input).digest()
|
||||
|
||||
const request = (profile, data) => {
|
||||
const body = profile.transformReqBody({lang: 'en', svcReqL: [data]})
|
||||
const req = profile.transformReq({
|
||||
|
@ -19,14 +22,37 @@ const request = (profile, data) => {
|
|||
'Accept-Encoding': 'gzip, deflate',
|
||||
'user-agent': 'https://github.com/derhuerst/hafas-client'
|
||||
},
|
||||
query: null
|
||||
query: {}
|
||||
})
|
||||
const url = profile.endpoint + (req.query ? '?' + stringify(req.query) : '')
|
||||
|
||||
if (profile.addChecksum || profile.addMicMac) {
|
||||
if (!Buffer.isBuffer(profile.salt)) {
|
||||
throw new Error('profile.salt must be a Buffer.')
|
||||
}
|
||||
if (profile.addChecksum) {
|
||||
const checksum = md5(Buffer.concat([
|
||||
Buffer.from(req.body, 'utf8'),
|
||||
profile.salt
|
||||
]))
|
||||
req.query.checksum = checksum.toString('hex')
|
||||
}
|
||||
if (profile.addMicMac) {
|
||||
const mic = md5(Buffer.from(req.body, 'utf8'))
|
||||
req.query.mic = mic.toString('hex')
|
||||
|
||||
const micAsHex = Buffer.from(mic.toString('hex'), 'utf8')
|
||||
const mac = md5(Buffer.concat([micAsHex, profile.salt]))
|
||||
req.query.mac = mac.toString('hex')
|
||||
}
|
||||
}
|
||||
|
||||
const url = profile.endpoint + '?' + stringify(req.query)
|
||||
|
||||
// Async stack traces are not supported everywhere yet, so we create our own.
|
||||
const err = new Error()
|
||||
err.isHafasError = true
|
||||
err.request = body
|
||||
err.url = url
|
||||
captureStackTrace(err)
|
||||
|
||||
return fetch(url, req)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
'use strict'
|
||||
|
||||
const crypto = require('crypto')
|
||||
|
||||
const _createParseLine = require('../../parse/line')
|
||||
const _createParseJourney = require('../../parse/journey')
|
||||
const _formatStation = require('../../format/station')
|
||||
|
@ -23,17 +21,6 @@ const transformReqBody = (body) => {
|
|||
return body
|
||||
}
|
||||
|
||||
const salt = 'bdI8UVj40K5fvxwf'
|
||||
const transformReq = (req) => {
|
||||
const hash = crypto.createHash('md5')
|
||||
hash.update(req.body + salt)
|
||||
|
||||
if (!req.query) req.query = {}
|
||||
req.query.checksum = hash.digest('hex')
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
const transformJourneysQuery = (query, opt) => {
|
||||
const filters = query.jnyFltrL
|
||||
if (opt.bike) filters.push(bike)
|
||||
|
@ -142,8 +129,11 @@ const dbProfile = {
|
|||
locale: 'de-DE',
|
||||
timezone: 'Europe/Berlin',
|
||||
endpoint: 'https://reiseauskunft.bahn.de/bin/mgate.exe',
|
||||
|
||||
salt: Buffer.from('bdI8UVj40K5fvxwf', 'utf8'),
|
||||
addChecksum: true,
|
||||
|
||||
transformReqBody,
|
||||
transformReq,
|
||||
transformJourneysQuery,
|
||||
|
||||
products: modes.allProducts,
|
||||
|
|
|
@ -20,10 +20,10 @@ const modes = require('./modes')
|
|||
const formatBitmask = createFormatBitmask(modes)
|
||||
|
||||
const transformReqBody = (body) => {
|
||||
body.client = {type: 'IPA', id: 'BVG', name: 'FahrInfo', v: '4070700'}
|
||||
body.ext = 'BVG.1'
|
||||
body.ver = '1.15' // todo: 1.16 with `mic` and `mac` query params
|
||||
body.auth = {type: 'AID', aid: '1Rxs112shyHLatUX4fofnmdxK'}
|
||||
body.client = {type: 'IPA', id: 'VBB', name: 'vbbPROD', v: '4010300'}
|
||||
body.ext = 'VBB.1'
|
||||
body.ver = '1.16'
|
||||
body.auth = {type: 'AID', aid: 'hafas-vbb-apps'}
|
||||
|
||||
return body
|
||||
}
|
||||
|
@ -167,7 +167,13 @@ const formatProducts = (products) => {
|
|||
const vbbProfile = {
|
||||
locale: 'de-DE',
|
||||
timezone: 'Europe/Berlin',
|
||||
endpoint: 'https://bvg-apps.hafas.de/bin/mgate.exe',
|
||||
endpoint: 'https://fahrinfo.vbb.de/bin/mgate.exe',
|
||||
|
||||
// https://gist.github.com/derhuerst/a8d94a433358abc015ff77df4481070c#file-haf_config_base-properties-L39
|
||||
// https://runkit.com/derhuerst/hafas-decrypt-encrypted-mac-salt
|
||||
salt: Buffer.from('5243544a4d3266467846667878516649', 'hex'),
|
||||
addMicMac: true,
|
||||
|
||||
transformReqBody,
|
||||
|
||||
products: modes.allProducts,
|
||||
|
|
|
@ -175,6 +175,7 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript
|
|||
- [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas#vbb-hafas) – JavaScript client for Berlin & Brandenburg public transport HAFAS API.
|
||||
- [`hafas-departures-in-direction`](https://github.com/derhuerst/hafas-departures-in-direction#hafas-departures-in-direction) – Pass in a HAFAS client, get departures in a certain direction.
|
||||
- [`hafas-collect-departures-at`](https://github.com/derhuerst/hafas-collect-departures-at#hafas-collect-departures-at) – Utility to collect departures, using any HAFAS client.
|
||||
- [`hafas-discover-stations`](https://github.com/derhuerst/hafas-discover-stations#hafas-discover-stations) – Pass in a HAFAS client, discover stations by querying departures.
|
||||
- [`hafas-rest-api`](https://github.com/derhuerst/hafas-rest-api#hafas-rest-api) – Expose a HAFAS client via an HTTP REST API.
|
||||
- [List of european long-distance transport operators, available API endpoints, GTFS feeds and client modules.](https://github.com/public-transport/european-transport-operators)
|
||||
- [Collection of european transport JavaScript modules.](https://github.com/public-transport/european-transport-modules)
|
||||
|
|
18
test/vbb.js
18
test/vbb.js
|
@ -246,8 +246,9 @@ test('journey leg details', co(function* (t) {
|
|||
|
||||
test('journeys – station to address', co(function* (t) {
|
||||
const journeys = yield client.journeys(spichernstr, {
|
||||
type: 'location', address: 'Torfstraße 17',
|
||||
latitude: 52.5416823, longitude: 13.3491223
|
||||
type: 'location',
|
||||
address: 'Torfstr. 17, Berlin',
|
||||
latitude: 52.541797, longitude: 13.350042
|
||||
}, {results: 1, when})
|
||||
|
||||
t.ok(Array.isArray(journeys))
|
||||
|
@ -261,9 +262,9 @@ test('journeys – station to address', co(function* (t) {
|
|||
|
||||
const dest = leg.destination
|
||||
assertValidAddress(t, dest)
|
||||
t.strictEqual(dest.address, 'Torfstraße 17')
|
||||
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.5416823))
|
||||
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.3491223))
|
||||
t.strictEqual(dest.address, '13353 Berlin-Wedding, Torfstr. 17')
|
||||
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.541797))
|
||||
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.350042))
|
||||
assertValidWhen(t, leg.arrival, when)
|
||||
|
||||
t.end()
|
||||
|
@ -273,7 +274,9 @@ test('journeys – station to address', co(function* (t) {
|
|||
|
||||
test('journeys – station to POI', co(function* (t) {
|
||||
const journeys = yield client.journeys(spichernstr, {
|
||||
type: 'location', id: '9980720', name: 'ATZE Musiktheater',
|
||||
type: 'location',
|
||||
id: '900980720',
|
||||
name: 'Berlin, Atze Musiktheater für Kinder',
|
||||
latitude: 52.543333, longitude: 13.351686
|
||||
}, {results: 1, when})
|
||||
|
||||
|
@ -288,7 +291,8 @@ test('journeys – station to POI', co(function* (t) {
|
|||
|
||||
const dest = leg.destination
|
||||
assertValidPoi(t, dest)
|
||||
t.strictEqual(dest.name, 'ATZE Musiktheater')
|
||||
t.strictEqual(dest.id, '900980720')
|
||||
t.strictEqual(dest.name, 'Berlin, Atze Musiktheater für Kinder')
|
||||
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.543333))
|
||||
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.351686))
|
||||
assertValidWhen(t, leg.arrival, when)
|
||||
|
|
Loading…
Add table
Reference in a new issue