diff --git a/commands/departures/index.js b/commands/departures/index.js index 427cc9a..2e191bf 100644 --- a/commands/departures/index.js +++ b/commands/departures/index.js @@ -1,11 +1,10 @@ 'use strict' -const searchStations = require('vbb-stations-autocomplete') -const allStations = require('vbb-stations/simple') const parseTime = require('parse-messy-time') const linesAt = require('vbb-lines-at') const hafas = require('vbb-hafas') +const findStation = require('../../lib/find-station') const commandKeys = require('../../lib/commands-keyboard') const whenKeys = require('../../lib/when-keyboard') const getFrequentStationsKeys = require('../../lib/frequent-stations-keyboard') @@ -25,11 +24,7 @@ I don't know about this station, please double-check for typos. If you're sure it's my fault, please let my creator @derhuerst know.` const parseWhere = async (where, ctx) => { - await ctx.replyWithChatAction('typing') - let [station] = searchStations(where, 1, false, false) // non-fuzzy - if (!station) [station] = searchStations(where, 1, true, false) // fuzzy - if (station) station = allStations.find(s => s.id === station.id) // get details - + const station = findStation(where) if (station) await ctx.replyWithMarkdown(`I found ${station.name}.`) else await ctx.replyWithMarkdown(unknownStation) return station diff --git a/commands/journeys.js b/commands/journeys.js deleted file mode 100644 index c8fe7d5..0000000 --- a/commands/journeys.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -const commandsKeyboard = require('../lib/commands-keyboard') - -const journeys = async (ctx, next) => { - await ctx.replyWithMarkdown('`journeys` command', commandsKeyboard) - // todo - next() -} - -module.exports = journeys diff --git a/commands/journeys/index.js b/commands/journeys/index.js new file mode 100644 index 0000000..204bccd --- /dev/null +++ b/commands/journeys/index.js @@ -0,0 +1,106 @@ +'use strict' + +const parseTime = require('parse-messy-time') +const Markup = require('telegraf/markup') +const hafas = require('vbb-hafas') + +const findStation = require('../../lib/find-station') +const commandKeys = require('../../lib/commands-keyboard') +const whenKeys = require('../../lib/when-keyboard') +const getFrequentStationsKeys = require('../../lib/frequent-stations-keyboard') +const renderJourney = require('./render') + +const promptWhen = `\ +*When?* +e.g. "now", "in 10 minutes" or "tomorrow 17:20"` +const unknownTime = `\ +Hm, don't understand this format.` + +const promptOrigin = `\ +*Where do you start?* +Enter a location like "U mehringdamm", "Kaiserdamm 26" or send your location.` +const promptDest = `\ +*Where do you want to go?*` +// const unknownStation = `\ +// I don't know about this station, please double-check for typos. +// If you're sure it's my fault, please let my creator @derhuerst know.` + +const parseWhere = async (where, ctx) => { + // todo +} + +const parseWhen = async (when, ctx) => { + when = +parseTime(when, {now: new Date()}) + if (Number.isNaN(when)) { + await ctx.replyWithMarkdown(unknownTime) + return null + } + return when +} + +const getFrequentStationKeys = async (ctx) => { + const ids = await ctx.storage.getTopLocations() + return getFrequentStationsKeys(ids, [ + // Markup.locationRequestButton('use current location') // todo + ]) +} + +const journeys = async (ctx, next) => { + // `/a spichernstr` shorthand + if (ctx.args && ctx.args[0]) { + const station = await parseWhere(ctx.args[0], ctx) + if (station) await ctx.storage.putData('origin', station) + return next() + } + if (ctx.command) { + await ctx.replyWithMarkdown(promptOrigin, getFrequentStationKeys(ctx)) + return next() // await next message + } + + let origin = await ctx.storage.getData('origin') + if (!origin) { + origin = await parseWhere(ctx.message.text, ctx) + if (!origin) return next() // await next message + await ctx.storage.putData('origin', origin) + await ctx.storage.incLocation(origin.id) + + await ctx.replyWithMarkdown(promptDest, getFrequentStationKeys(ctx)) + return next() // await next message + } + + let destination = await ctx.storage.getData('destination') + if (!destination) { + destination = await parseWhere(ctx.message.text, ctx) + if (!destination) return next() // await next message + await ctx.storage.putData('destination', destination) + await ctx.storage.incLocation(destination.id) + + await ctx.replyWithMarkdown(promptWhen, whenKeys) + return next() // await next message + } + + let when = await ctx.storage.getData('when') + if (!when) { + when = await parseWhen(ctx.message.text, ctx) + if (!when) return next() // await next message + await ctx.storage.putData('when', when) + } + + // clear session data + await Promise.all([ + ctx.storage.putData('origin', null), + ctx.storage.putData('destination', null), + ctx.storage.putData('when', null) + ]) + + // fetch & render journeys + await ctx.replyWithChatAction('typing') + const journeys = await hafas.journeys(origin.id, destination.id, {when}) + for (let j of journeys.length) { + await ctx.replyWithMarkdown(renderJourney(j), commandKeys) + } + + next() +} + +module.exports = journeys diff --git a/commands/journeys/render.js b/commands/journeys/render.js new file mode 100644 index 0000000..902433c --- /dev/null +++ b/commands/journeys/render.js @@ -0,0 +1,57 @@ +'use strict' + +const {DateTime} = require('luxon') + +const TIMEZONE = process.env.TIMEZONE +if (!TIMEZONE) { + console.error('Missing TIMEZONE env var.') + process.exit(1) +} +const LOCALE = process.env.LOCALE +if (!LOCALE) { + console.error('Missing LOCALE env var.') + process.exit(1) +} + +const renderCoords = l => '`' + l.latitude + '`|`' + l.latitude + '`' + +const renderTime = (when) => { + return DateTime.fromMillis(+new Date(when), { + locale: LOCALE, + zone: TIMEZONE + }).toLocaleString(DateTime.TIME_SIMPLE) +} + +const renderJourney = (j) => { + const dur = new Date(part.arrival) - new Date(part.departure) + let str = [ + 'From', + j.origin ? j.origin.name : renderCoords(j.origin), + 'to', + j.destination ? j.destination.name : renderCoords(j.destination), + 'in', + ms(dur) + ].join(' ') + '.\n' + + for (let i = 0; i < j.legs.length; i++) { + const leg = j.legs[i] + + // todo: emoji for line + // todo: links/commands for origin & destination + str += `\n${renderTime(part.departure)} – *${leg.origin.name}*\n` + + const dur = new Date(part.arrival) - new Date(part.departure) + if (leg.mode === 'walking') str += '*walk*' + else if (leg.line) str += `with *${leg.line.name}* to *${leg.direction}*` + else str += leg.mode + str += ' for ' + ms(dur) + + if (i === (j.legs.length - 1)) { + str += `\n${renderTime(leg.arrival)} – *${leg.destination.name}*` + } + } + + return str +} + +module.exports = renderJourney diff --git a/lib/find-station.js b/lib/find-station.js new file mode 100644 index 0000000..084c745 --- /dev/null +++ b/lib/find-station.js @@ -0,0 +1,13 @@ +'use strict' + +const searchStations = require('vbb-stations-autocomplete') +const allStations = require('vbb-stations/simple') + +const findStation = (name) => { + let [station] = searchStations(name, 1, false, false) // non-fuzzy + if (!station) [station] = searchStations(name, 1, true, false) // fuzzy + if (station) station = allStations.find(s => s.id === station.id) // get details + return station +} + +module.exports = findStation diff --git a/lib/frequent-stations-keyboard.js b/lib/frequent-stations-keyboard.js index f7b822d..bccf3ac 100644 --- a/lib/frequent-stations-keyboard.js +++ b/lib/frequent-stations-keyboard.js @@ -6,10 +6,9 @@ const Markup = require('telegraf/markup') const namesById = Object.create(null) for (let station of stations) namesById[station.id] = station.name -const getFrequentStationsKeyboard = (ids) => { - const keys = [] +const getFrequentStationsKeyboard = (ids, keys = []) => { for (let id of ids) { - if (id in namesById) keys.push(namesById[id]) + if (id in namesById) keys.push(Markup.button(namesById[id])) } return Markup.keyboard(keys).oneTime().extra() } diff --git a/package.json b/package.json index 82e08c7..236ac77 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "hifo": "^1.0.0", "js-string-escape": "^1.0.1", "level": "^3.0.0", + "luxon": "^0.5.3", "ms": "^2.1.1", "parse-messy-time": "^2.1.0", "string-width": "^2.1.1",