Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add order(bookingNr, nameOrEmail) #43

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: test

on:
push:
branches:
- '*'
pull_request:
branches:
- '*'

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['10', '12', '14', '16', '18', '20']

steps:
- name: checkout
uses: actions/checkout@v3
- name: setup Node v${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install

- run: npm test
6 changes: 0 additions & 6 deletions .travis.yml

This file was deleted.

45 changes: 45 additions & 0 deletions example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict'

const assert = require('assert')
const flix = require('.')

const find = (name, stream) => {
return new Promise((resolve, reject) => {
stream.once('error', reject)
stream.on('data', (item) => {
if (item.name === name) resolve(item)
})
stream.once('end', () => resolve(null))
})
}

;(async () => {
const berlin = await find('Berlin', flix.regions.all())
assert.ok(berlin, 'Berlin not found')
const hamburg = await find('Hamburg', flix.regions.all())
assert.ok(hamburg, 'Hamburg not found')
const berlinAlexanderplatz = await find('Berlin Alexanderplatz', flix.stations.all())
assert.ok(berlinAlexanderplatz, 'Berlin Alexanderplatz not found')

const [journey] = await flix.journeys(berlin, hamburg, {
departureAfter: new Date('2020-06-01T08:00+02:00'),
results: 1,
})
console.error(journey)

const [depAtAlexanderplatz] = await flix.departures(berlinAlexanderplatz)
console.error(depAtAlexanderplatz)

const trip = await flix.trip(depAtAlexanderplatz.tripId)
console.error(trip)

const bookingNr = process.env.BOOKING_NR
const nameOrEmail = process.env.NAME_OR_EMAIL
if (bookingNr && nameOrEmail) {
console.log(await flix.order(bookingNr, nameOrEmail))
}
})()
.catch((err) => {
console.error(err)
process.exit(1)
})
43 changes: 37 additions & 6 deletions lib/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,54 @@

const { fetch } = require('fetch-ponyfill')()
const { stringify } = require('query-string')

const endpoint = 'https://api.flixbus.com/mobile/v1/'
const debug = require('debug')('flix:fetch')

// keys & endpoints
// https://api.meinfernbus.de/mobile/v1: uR=s7k6m=[cCS^zY86H8CNAnkC6n
// https://api.flixbus.com/mobile/v1: k8LKgcuFoHnN5x/NdDYD6QSvjB4=
// keys seem to work for both (flix also works for mfb and vice versa)

const request = async (route, headers = {}, query = {}) => {
query = stringify(query)
const res = await fetch(`${endpoint}${route}?${query}`, { headers })
const defaults = {
endpoint: 'https://api.flixbus.com/mobile/v1/',
apiKey: 'k8LKgcuFoHnN5x/NdDYD6QSvjB4=',
userAgent: 'FlixBus/3.3 (iPhone; iOS 9.3.4; Scale/2.00)',
}

const request = async (route, opt, headers = {}, query = {}) => {
opt = { ...defaults, ...opt }
if (typeof opt.apiKey !== 'string' || !opt.apiKey) {
throw new Error('`opt.apiKey` must be non-empty string')
}
if (typeof opt.userAgent !== 'string' || !opt.userAgent) {
throw new Error('`opt.userAgent` must be non-empty string')
}

const url = `${opt.endpoint}${route}?${stringify(query)}`
const fetchCfg = {
headers: {
'X-API-Authentication': opt.apiKey,
'User-Agent': opt.userAgent,
...headers,
},
}

debug('fetch', url, fetchCfg)
const res = await fetch(url, fetchCfg)
if (!res.ok) {
const error = new Error(res.statusText)
error.statusCode = res.status
error.url = url
throw error
}
return res.json()

const body = await res.text()
debug('response body', body)
return JSON.parse(body)
}

request.features = { // required by fpti
apiKey: 'Custom API key',
userAgent: 'Custom User-Agent',
}

module.exports = request
13 changes: 12 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,16 @@
const stations = require('./stations')
const regions = require('./regions')
const journeys = require('./journeys')
const { departuresToday, arrivalsToday } = require('./timetable')
const trip = require('./trip')
const order = require('./order')

module.exports = { stations, regions, journeys }
module.exports = {
stations,
regions,
journeys,
departuresToday,
arrivalsToday,
trip,
order,
}
34 changes: 9 additions & 25 deletions lib/journeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,15 @@ const uniq = require('lodash/uniq')
const sortBy = require('lodash/sortBy')
const slug = require('slugg')

const {
formatDateTime: formatInputDate,
parseDateTime: formatOutputDate,
normalizeEmpty: m,
parseSparseStation: createStation,
parseOperator: createOperator,
} = require('./util')
const fetch = require('./fetch')

const m = (a) => ((a === undefined || a === '') ? null : a)
const formatInputDate = (date) => moment(date).tz('Europe/Berlin').format('DD.MM.YYYY')
const formatOutputDate = (date) => moment.unix(+date.timestamp).utcOffset(date.tz).format()

const createOperator = (o) => ({
type: 'operator',
id: o.key,
name: o.label,
url: o.url,
address: o.address,
})

const createStation = (s) => ({
type: 'station',
id: s.id + '',
name: s.name,
importance: Number.isInteger(s.importance_order) ? s.importance_order : null,
})

const extractLegs = (rawOrigin, rawDestination, rawJourney) => {
let {
departure,
Expand Down Expand Up @@ -140,7 +128,6 @@ const defaults = () => ({
adults: 1,
children: 0,
bicycles: 0,
apiKey: 'k8LKgcuFoHnN5x/NdDYD6QSvjB4=',
})

const journeys = async (origin, destination, opt = {}) => {
Expand All @@ -165,7 +152,6 @@ const journeys = async (origin, destination, opt = {}) => {
if (!Number.isInteger(options.adults) || options.adults < 1) throw new Error('`opt.adults` must be a positive integer')
if (!Number.isInteger(options.children) || options.children < 0) throw new Error('`opt.children` must be a non-negative integer')
if (!Number.isInteger(options.bicycles) || options.bicycles < 0) throw new Error('`opt.bicycles` must be a non-negative integer')
if (typeof options.apiKey !== 'string' || options.apiKey.length < 1) throw new Error('`opt.apiKey` must be non-empty string')

const date = options.when || options.departureAfter
const endDate = moment(date).add(options.interval || 0, 'minutes').toDate()
Expand All @@ -174,9 +160,7 @@ const journeys = async (origin, destination, opt = {}) => {
let currentDate = date
let journeys = []
do {
const { trips } = await fetch('trip/search.json', {
'X-API-Authentication': options.apiKey,
'User-Agent': 'FlixBus/3.3 (iPhone; iOS 9.3.4; Scale/2.00)',
const { trips } = await fetch('trip/search.json', opt, {
'X-User-Country': 'de',
}, {
from: +origin,
Expand Down Expand Up @@ -209,6 +193,7 @@ const journeys = async (origin, destination, opt = {}) => {
return journeys
}
journeys.features = { // required by fpti
...fetch.features,
when: 'Journey date, synonym to departureAfter',
departureAfter: 'List journeys with a departure (first leg) after this date',
results: 'Max. number of results returned',
Expand All @@ -219,7 +204,6 @@ journeys.features = { // required by fpti
adults: 'Number of traveling adults',
children: 'Number of traveling children',
bicycles: 'Number of traveling bicycles',
apiKey: 'Custom API key',
}

module.exports = journeys
78 changes: 78 additions & 0 deletions lib/order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict'

const {
normalizeEmpty,
parseSparseTrip,
parseStation,
parseWhen,
} = require('./util')
const fetch = require('./fetch')

const parseNote = (note) => {
return {
type: note.name,
title: note.title,
html: note.html,
}
}

const order = async (bookingNr, nameOrEmail, opt = {}) => {
opt = {
...opt,
}

if (typeof bookingNr !== 'string' || !bookingNr) {
throw new Error('bookingNr is invalid')
}
if (typeof nameOrEmail !== 'string' || !nameOrEmail) {
throw new Error('nameOrEmail is invalid')
}

let res
try {
res = await fetch(`orders/${encodeURIComponent(bookingNr)}/search.json`, {
...opt,
endpoint: 'https://api.flixbus.com/mobile/v2/',
}, {}, {
query: nameOrEmail,
})
} catch (err) {
if (err.statusCode === 404) return null
throw err
}

return {
id: res.order.order_uid,
bookingNr: res.order.id,
downloadCode: res.download_hash,
qrCodeUrl: res.order.qr_image,
bookingConfirmationUrl: res.order.reminder_link,
// @todo res.order.invoices
// @todo res.order.attachments
notes: res.order.info_blocks.map(parseNote),
legs: res.order.trips.map((trip) => ({
...parseSparseTrip(null, trip),
tripId: trip.trip_uid,
origin: parseStation(trip.departure_station),
departure: parseWhen({ planned: 'departure' }, trip, false).when,
destination: parseStation(trip.arrival_station),
arrival: parseWhen({ planned: 'arrival' }, trip, false).when,
orderStatus: trip.order_status,
passengers: trip.passengers.map((p) => ({
type: p.type,
firstName: normalizeEmpty(p.firstname),
lastName: normalizeEmpty(p.lastname),
phone: normalizeEmpty(p.phone),
})),
appleWalletUrl: trip.passbook_url,
warnings: trip.warnings,
// @todo p.push_channel_uid, p.seats_per_relation
})),
}
}

order.features = { // require by fpti
...fetch.features,
}

module.exports = order
16 changes: 6 additions & 10 deletions lib/regions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
const fetch = require('./fetch')
const merge = require('lodash/merge')
const intoStream = require('into-stream').object

const m = (a) => ((a === undefined || a === '') ? null : a)
const {
normalizeEmpty: m,
parseCountry: formatCountry,
} = require('./util')

const defaults = {
apiKey: 'k8LKgcuFoHnN5x/NdDYD6QSvjB4=',
}

const formatCountry = (country) => (country ? { name: m(country.name), code: m(country.alpha2_code) } : null)

const createRegion = (city) => ({
type: 'region',
id: city.id + '',
Expand All @@ -30,18 +29,15 @@ const createRegion = (city) => ({

const allAsync = async (opt) => {
const options = merge({}, defaults, opt)
const results = await fetch('network.json', {
'X-API-Authentication': options.apiKey,
'User-Agent': 'FlixBus/3.3 (iPhone; iOS 9.3.4; Scale/2.00)',
})
const results = await fetch('network.json', options)
return results.cities.map(createRegion)
}

const all = (opt = {}) => {
return intoStream(allAsync(opt))
}
all.features = { // required by fpti
apiKey: 'Custom API key',
...fetch.features,
}

module.exports = { all }
Loading