Skip to content

Commit

Permalink
Merge pull request #5 from arnebr/main
Browse files Browse the repository at this point in the history
Adding support for new ticket numbers
  • Loading branch information
envake authored Nov 15, 2023
2 parents 75141c0 + 3c8a8f4 commit d4ff6ee
Show file tree
Hide file tree
Showing 11 changed files with 1,265 additions and 985 deletions.
6 changes: 2 additions & 4 deletions test.js → example.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import queryTicket from './index.js'
import util from 'util'
import process from 'process'

// request data with ticket number and lastname
const ticketNumber = process.env.TICKET_NR || 'XXXXXX'
const lastname = process.env.LAST_NAME || 'Mustermann'

console.log('testing with ' + ticketNumber + ' ' + lastname)

const data = await queryTicket(ticketNumber, lastname)
console.log(util.inspect(data, false, null))
// console.log(util.inspect(data, false, null))
console.log(JSON.stringify(data))
25 changes: 23 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,26 @@
* ISC Licensed
*/

import queryTicket from './lib/queryTicket.js'
export default queryTicket
import {queryTicket as rqorderdetails} from './lib/queryTicket.js'
import findTicket from './lib/findTicket.js'

/**
* queryTicket function, returns the ticket data based on the ticket number and last name. handles both old and new ticket system
* @param string | DB ticket number (ticket number up 12 digits or 6 characters)
* @param lastName | last name of purchaser
* @return Promise | fulfills with an object that contains the ticket data
*/
const queryTicket = async (ticketNumber, lastName) => {
//create a null kwid variable
let kwid = null
if (ticketNumber.length > 5) {
const order = await findTicket(ticketNumber, lastName)
//extract the kwid from the order object
kwid = order.kwid
}
const ticketData = await rqorderdetails(ticketNumber, lastName, kwid)
return ticketData
}


export default queryTicket
22 changes: 22 additions & 0 deletions lib/findTicket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { requestBackend } from './requestBackend.js'
/**
* findTicket function
* @param string | DB ticket number (Auftragsnummer 12 digits)
* @param lastName | last name of purchaser
* @return Promise | fulfills with an object that contains the ticket data
*/
export const findTicket = async (ticketNumber, lastName) => {
//make a request to the endpoint with the following body: <?xml version="1.0"?>
//<rqfindorder version="1.0"><rqheader tnr="xxx" ts="2023-08-11T11:36:51" l="de" v="23080000" d="xxx" os="xxx" app="NAVIGATOR"/><rqorder on="INT-UP-TO-12"/><authname tln="Lastname"/></rqfindorder>
const reqBody = '<?xml version="1.0"?><rqfindorder version="1.0">'
+ '<rqheader l="de" v="23080000" d="iPhone16.4.1" os="iOS_15.7.5" app="NAVIGATOR"/>'
+ '<rqorder on="' + ticketNumber + '"/><authname tln="' + lastName + '"/></rqfindorder>'
return requestBackend(reqBody).then(json => {
const data = json.rporderheadlist

const _order = data['orderheadlist'][0]['orderhead'][0]['$']
return _order})
}


export default findTicket
45 changes: 34 additions & 11 deletions lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,28 @@ const line = (_train) => {

// follows FPTF Station, https://github.com/public-transport/friendly-public-transport-format/blob/master/spec/readme.md#station
const station = (_station) => {
return {
type: 'station',
id: _station['nr'][0],
name: _station['n'][0],
location: {

// the longitudes and latitudes are optional in _station
// if they are not present, the longitude and longitude should not be included in the locaton object
let location = {}
if (('x' in _station) && ('y' in _station)) {
location = {
type: 'location',
longitude: coordinate(_station['x'][0]),
latitude: coordinate(_station['y'][0])
}
} else {
location = {
type: 'location',
}
}


return {
type: 'station',
id: _station['nr'][0],
name: _station['n'][0],
location: location
}
}

Expand Down Expand Up @@ -91,14 +104,24 @@ const parseLegs = (_trainList) => {

// takes the base64 encoded httext field and extracts the ticket price
const price = (_httext) => {

let c = atob(_httext.replace(/\r?\n|\r/g, ""))

c = c.match(/Gesamtpreis:\s*\d{0,2},\d{0,2}\s*EUR/g)
c = c[0].match(/\d{0,2},\d{0,2}/g)
c = c[0].replace(',', '.')

return parseFloat(c).toPrecision(4)
let price=null;
price = c.match(/Gesamtpreis:\s*\d{0,2},\d{0,2}\s*EUR/g)
//when Gesamtpreis is not found search again but with html tags, because thats what the new ticket layout gives in the new systems
if (!price) {
const regexPrice = /Gesamtpreis: <\/span><span>([\d,]+,\d{2})<\/span>/;
const matchPrice = c.match(regexPrice);
if (matchPrice && matchPrice[1]) {
price = matchPrice[1];
}
}else{
price=price[0];
}
price = price.match(/\d{0,2},\d{0,2}/g)
price = price[0].replace(',', '.')

return parseFloat(price).toPrecision(4)
}

export { parseLegs, price }
156 changes: 63 additions & 93 deletions lib/queryTicket.js
Original file line number Diff line number Diff line change
@@ -1,110 +1,80 @@
import fetch from 'node-fetch'
import { parseString } from 'xml2js'
import { parseLegs, price } from './parse.js'

const endpoint = 'https://fahrkarten.bahn.de/mobile/dbc/xs.go?'
import requestBackend from './requestBackend.js';

/**
* queryTicket function
* parseTicket
*
* @param string | DB ticket number (Auftragsnummer)
* @param lastName | last name of purchaser
* @return Promise | fulfills with an object that contains the ticket data
* @param string | ticket API response body, JSON-decoded
* @return object | the ticket data
*/
const queryTicket = async (ticketNumber, lastName) => {
const parseTicket = (json) => {
const data = json.rporderdetails

// selectors
const _order = data['order'][0]['$']
const _tck = data['order'][0]['tcklist']?.[0]['tck'][0]
const _ticket = _tck?.['mtk'][0]
let _encoded_price = _tck['htdata'][0]['ht'][0]['_']
//if _tck['htdata'][0]['ht'][0]['_'] starts with data:image/png;base64, then it is a old ticket, we need to check for that
if(_encoded_price.startsWith('data:image/png;base64,')){
_encoded_price = _tck['htdata'][0]['ht'][1]['_'] // revert back to the old price format
}

const reqBody = '<?xml version="1.0"?><rqorderdetails version="1.0">'
+ '<rqheader tnr="61743782011" ts="2022-07-15T22:29:00" l="de" v="22060000" d="iPhone13,1" os="iOS_15.5" app="NAVIGATOR"/>'
+ '<rqorder on="' + ticketNumber + '"/><authname tln="' + lastName + '"/></rqorderdetails>'
const _price = _tck ? {
amount: price(_encoded_price),
currency: 'EUR'
}: undefined

return fetch(endpoint, {
method: 'POST',
headers: {
'User-Agent': 'github.com/envake/db-tickets',
'Accept-Language': 'de-DE,de;q=0.9'
return {
// doesn't follow a standard by now and just dumps some info
order: {
ticketNumber: _order['on'],
bookingDate: _order['cdt'],
validFrom: _order['vfrom'],
validUntil: _order['vto'],
journeyStart: _order['sdt'],
firstName: _ticket?.['reisender_vorname'][0],
lastName: _ticket?.['reisender_nachname'][0],
text: _ticket?.['txt'][0],
class: _ticket?.['nvplist'][0]['nvp'][3]['_'] // could be wrong
},
body: reqBody
})
.then(response => {
if (!response.ok) {
throw new HTTPResponseError(response)
}
return response.text()
})
.then(body => {
return new Promise((resolve, reject) => parseString(body, (err, result) => {
if (err) {
reject(err)
}
else {
resolve(result)
}
}))
})
.then(json => {
if (json['rperror']) {
throw new XMLRPCError(json['rperror']['error'])
}

const data = json.rporderdetails
console.log(JSON.stringify(data))

// selectors
const _order = data['order'][0]['$']
const _tck = data['order'][0]['tcklist']?.[0]['tck'][0]
const _ticket = _tck?.['mtk'][0]
// follows FPTF Journey, https://github.com/public-transport/friendly-public-transport-format/blob/master/spec/readme.md#journey
journey: {
type: 'journey',
id: _order['cid'], // unique, required, use cid as id here?
legs: parseLegs(data['order'][0]['schedulelist'][0]['out'][0]['trainlist'][0]['train']),
price: _price
},

const _price = _tck ? {
amount: price(_tck['htdata'][0]['ht'][1]['_']),
currency: 'EUR'
}: undefined
...(data['order'][0]['schedulelist'][0]['ret'] && { returnJourney: {
type: 'journey',
id: _order['cid'], // unique, required, ! same as outward journey?
legs: parseLegs(data['order'][0]['schedulelist'][0]['ret'][0]['trainlist'][0]['train']),
price: _price
}})
}
}

return {
// doesn't follow a standard by now and just dumps some info
order: {
ticketNumber: _order['on'],
bookingDate: _order['cdt'],
validFrom: _order['vfrom'],
validUntil: _order['vto'],
journeyStart: _order['sdt'],
firstName: _ticket?.['reisender_vorname'][0],
lastName: _ticket?.['reisender_nachname'][0],
text: _ticket?.['txt'][0],
class: _ticket?.['nvplist'][0]['nvp'][3]['_'] // could be wrong
},
/**
* queryTicket function
*
* @param string | DB ticket number (Auftragsnummer)
* @param lastName | last name of purchaser
* @param kwid | kwid of the ticket (only needed for tickets from the new system)
* @return Promise | fulfills with an object that contains the ticket data
*/
const queryTicket = async (ticketNumber, lastName,kwid=null) => {

// follows FPTF Journey, https://github.com/public-transport/friendly-public-transport-format/blob/master/spec/readme.md#journey
journey: {
type: 'journey',
id: _order['cid'], // unique, required, use cid as id here?
legs: parseLegs(data['order'][0]['schedulelist'][0]['out'][0]['trainlist'][0]['train']),
price: _price
},
let reqBody = `<?xml version="1.0"?> <rqorderdetails version="2.0"> <rqheader l="de" v="23080000" d="iPhone16.4.1" os="iOS_15.7.5" app="NAVIGATOR" /> <rqorder on="${ticketNumber}" ${kwid ? `kwid="${kwid}" ` : ''}/> <authname tln="${lastName}"/> </rqorderdetails> `

...(data['order'][0]['schedulelist'][0]['ret'] && { returnJourney: {
type: 'journey',
id: _order['cid'], // unique, required, ! same as outward journey?
legs: parseLegs(data['order'][0]['schedulelist'][0]['ret'][0]['trainlist'][0]['train']),
price: _price
}})
}
return await requestBackend(reqBody)
.then(json => {
return parseTicket(json)
})
}

class HTTPResponseError extends Error {
constructor(response, ...args) {
super(`HTTP Error Response: ${response.status} ${response.statusText}`, ...args)
this.response = response
}
}

class XMLRPCError extends Error {
constructor(response, ...args) {
super(`XML Error Response:
${response[0]['$']['nr']}
${response[0]['txt'][0]}`, ...args)
this.response = response
}
export {
parseTicket,
queryTicket,
}

export default queryTicket
58 changes: 58 additions & 0 deletions lib/requestBackend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import fetch from 'node-fetch'
import { parseString } from 'xml2js'

const endpoint = 'https://fahrkarten.bahn.de/mobile/dbc/xs.go?'

/**
* requestBackend function, that is used to make a request to the backend
* @param string | body of the request
* @return Promise | fulfills with an object that contains the data from the request
*/
export const requestBackend = async (reqBody) => {
return fetch(endpoint, {
method: 'POST',
headers: {
'User-Agent': 'github.com/public-transport/db-tickets',
'Accept-Language': 'de-DE,de;q=0.9'
},
body: reqBody
}).then(response => {
if (!response.ok) {
const err = new HTTPResponseError(response)
err.url = endpoint
err.requestBody = reqBody
throw err
}
return response.text()
}).then(body => {
return new Promise((resolve, reject) => parseString(body, (err, result) => {
if (err) {
reject(err)
}
else {
resolve(result)
}
}))
}).then(json => {
if (json['rperror']) {
throw new XMLRPCError(json['rperror']['error'])
}
return json
})
}
class HTTPResponseError extends Error {
constructor(response, ...args) {
super(`HTTP Error Response: ${response.status} ${response.statusText}`, ...args)
this.response = response
}
}

class XMLRPCError extends Error {
constructor(response, ...args) {
super(`XML Error Response:
${response[0]['$']['nr']}
${response[0]['txt'][0]}`, ...args)
this.response = response
}
}
export default requestBackend
Loading

0 comments on commit d4ff6ee

Please sign in to comment.