diff --git a/.gitignore b/.gitignore index d6fb75e..808662f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ Thumbs.db .nvm-version node_modules npm-debug.log +pnpm-debug.log + +package-lock.json +shrinkwrap.yaml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ea4454b..0000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM node - -WORKDIR /app -ADD . /app - -RUN npm install -g npm@latest -RUN npm install --production - -EXPOSE 3000 - -ENV TIMEZONE Europe/Berlin - -CMD ["npm", "start"] diff --git a/commands/departures.js b/commands/departures.js deleted file mode 100644 index 781e78c..0000000 --- a/commands/departures.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict' - -const time = require('parse-messy-time') -const search = require('vbb-stations-autocomplete') -const getStations = require('vbb-stations') -const splitLines = require('split-lines') - -const api = require('../lib/api') -const render = require('../lib/render') -const frequent = require('../lib/frequent') - -// https://core.telegram.org/method/messages.sendMessage#return-errors -const SIZE_LIMIT = 4096 - -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 textOnly = `Please enter text, other stuff is not supported yet.` - -const promptWhen = (s) => `\ -I found ${s.name}. *When?* -e.g. "now", "in 10 minutes" or "tomorrow 17:20"` -const whenButtons = [{text: 'now'}, {text: 'in 10 min'}, {text: 'in 1h'}] - -const promptWhere = `\ -*Which station?* -Enter a station name like "u mehringdamm" or "Kotti".` - - - -const start = async (ctx, tmp, freq, msg) => { - let stations = await freq(3) - if (stations.length === 0) await ctx.message(promptWhere) - else { - stations = stations.map((text) => ({text})) - await ctx.keyboard(promptWhere, stations) - } -} - - - -const when = async (ctx, tmp, freq, msg) => { - const when = time(msg.text) - const station = await tmp.get('station') - - ctx.typing() - const deps = await api.deps(station.id, when) - let rendered = render.deps(station, deps) - // This is a terrible fix. - // - It makes assumptions about how the rendered response looks. - // - It assumes that half of the message won't be too long. - // todo: properly fix this - if (rendered.length >= SIZE_LIMIT) { - rendered = splitLines(rendered) - const splitI = Math.ceil(rendered.length / 2) - const firstPart = rendered.slice(0, splitI).join('\n') + '\n```' - const secondPart = '```\n' + rendered.slice(splitI).join('\n') - - await ctx.message(firstPart, ctx.commands) - await ctx.keyboard(secondPart, ctx.commands) - } else { - await ctx.keyboard(rendered, ctx.commands) - } -} - - - -const where = async (ctx, tmp, freq, msg) => { - ctx.typing() - let [station] = search(msg.text, 1, true, false) - station = station && getStations(station.id)[0] - if (!station) return ctx.message(unknownStation) - - await tmp.set('station', station) - await freq.inc(station.id, station.name) - - await ctx.keyboard(promptWhen(station), whenButtons) -} - - - -const departures = async (ctx, newThread, keep, tmp, msg) => { - if (!msg.text) return ctx.message(textOnly) - const freq = frequent(keep, 'freq') - - const state = await tmp.get('state') - if (state === 'when') { - await when(ctx, tmp, freq, msg) - await tmp.clear() - } else if (state === 'where') { - await where(ctx, tmp, freq, msg) - await tmp.set('state', 'when') - } else { - const arg = msg.text.match(/\/\w+\s+(.+)/i) - if (arg && arg[1]) { // station passed directly (e.g. '/a spichernstr') - await where(ctx, tmp, freq, {text: arg[1]}) - await tmp.set('state', 'when') - } else { - await start(ctx, tmp, freq, msg) - await tmp.set('state', 'where') - } - } -} - -module.exports = departures diff --git a/commands/help.js b/commands/help.js deleted file mode 100644 index ac46038..0000000 --- a/commands/help.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' - -const help = (ctx) => ctx.keyboard(`\ -*This bot lets you use public transport in Berlin more easily.* You can do this: -\`/a(bfahrt)\` – Show departures at a station. -\`/r(oute)\` – Get routes from A to B. -\`/n(earby)\` – Show stations around. - -When specifying time, you can use the following formats: -- \`now\` -- \`in 10min\` -- \`tomorrow 17:20\` -- \`8 pm\` -- \`tuesday at 6\` - -The data behind this bot is from VBB, so departures & routing will be just as (in)accurate as in the BVG & VBB apps.`, ctx.commands) - -module.exports = help diff --git a/commands/journeys.js b/commands/journeys.js deleted file mode 100644 index fa2d9e7..0000000 --- a/commands/journeys.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict' - -const time = require('parse-messy-time') -const hash = require('shorthash').unique - -const api = require('../lib/api') -const render = require('../lib/render') -const frequent = require('../lib/frequent') - - - -const textOnly = `Please enter text.` - -const unknownLocation = `\ -I don't know about this location, please double-check for typos. -If you're sure it's my fault, please let my creator @derhuerst know.` - -const foundLocation = (l) => `\ -I found ${l.name}.` - -const textOrLocation = `\ -Please enter a location like "U mehringdamm", "Kaiserdamm 26" or send your location.` - -const promptWhen = `\ -*When?* -e.g. "now", "in 10 minutes" or "tomorrow 17:20"` -const whenButtons = [{text: 'now'}, {text: 'in 10 min'}, {text: 'in 1h'}] - -const promptDestination = `\ -*Where do you want to go?*` -const promptOrigin = `\ -*Where do you start?* -Enter a location like "U mehringdamm", "Kaiserdamm 26" or send your location.` - - - -const when = async (ctx, data, msg) => { - if (!msg.text) return ctx.message(textOnly) - const when = time(msg.text) - - const from = await data.get('origin') - const to = await data.get('destination') - ctx.typing() - const journeys = await api.journeys(from, to, when) - - await ctx.keyboard(render.journeys(journeys), ctx.commands) -} - - - -const currentLocation = (msg) => ({ - type: 'address', name: 'your location', - latitude: msg.location.latitude, - longitude: msg.location.longitude -}) - -const where = async (key, ctx, data, freq, msg) => { - ctx.typing() - let location - if (msg.text) { - location = await api.location(msg.text) - if (!location) return ctx.message(unknownLocation) - else ctx.message(foundLocation(location)) - - const id = location.id || hash(location.name) - freq.inc(id, location.name) - - } else if (msg.location) location = currentLocation(msg) - else return ctx.message(textOrLocation) - await data.set(key, location) -} - - - -const requestLocation = [{text: 'send location', request_location: true}] -const frequentLocations = async (freq) => { - let locations = await freq(3) - locations = locations.map((text) => ({text})) - return locations.concat(requestLocation) -} - -const journeys = async (ctx, newThread, keep, tmp, msg) => { - const freq = frequent(keep, 'freq') - - const state = await tmp.get('state') - if (state === 'when') { - await when(ctx, tmp, msg) - await tmp.clear() - } - else if (state === 'destination') { - await where('destination', ctx, tmp, freq, msg) - await tmp.set('state', 'when') - await ctx.keyboard(promptWhen, whenButtons) - } - else if (state === 'origin') { - await where('origin', ctx, tmp, freq, msg) - await tmp.set('state', 'destination') - await ctx.keyboard(promptDestination, await frequentLocations(freq)) - } - else { - await tmp.set('state', 'origin') - await ctx.keyboard(promptOrigin, await frequentLocations(freq)) - } -} - -module.exports = journeys diff --git a/commands/nearby.js b/commands/nearby.js deleted file mode 100644 index 5d5443b..0000000 --- a/commands/nearby.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict' - -const api = require('../lib/api') -const render = require('../lib/render') - - - -const locationOnly = `Please send a location, other stuff is not supported yet.` - -const promptLocation = `Please share your location with me.` - - - -const location = async (ctx, msg) => { - if (!msg.location) return ctx.message(locationOnly) - - const lat = msg.location.latitude - const long = msg.location.longitude - await ctx.typing() - const closest = await api.closest(lat, long, 3) - - const buttons = [ - {text: '/h\u2063 back to start'} - ].concat(closest.map((station) => ({ - text: `/a ${station.name}\u2063– departures` - }))) - - for (let station of closest) { - await ctx.location(station.location.latitude, station.location.longitude) - await ctx.keyboard(render.nearby(station), buttons) - } -} - - - -const nearby = async (ctx, newThread, keep, tmp, msg) => { - const state = await tmp.get('state') - if (state === 'location') { - await location(ctx, msg) - await tmp.clear() - } else { - await tmp.set('state', 'location') - await ctx.requestLocation(promptLocation, 'send location') - } -} - -module.exports = nearby diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 2c7b502..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,20 +0,0 @@ -version: '3' -services: - bot: - build: . - depends-on: - - redis - links: - - redis - volumes: - - .:/code - environment: - - TIMEZONE - - TOKEN - deploy: - restart_policy: - condition: 'on-failure' - redis: - image: redis:alpine - expose: - - '6379' diff --git a/index.js b/index.js index 791b221..eba3ac4 100644 --- a/index.js +++ b/index.js @@ -1,108 +1,3 @@ 'use strict' -const Api = require('node-telegram-bot-api') - -const log = require('./lib/log') -const namespace = require('./lib/namespace') -const context = require('./lib/context') -const storage = require('./lib/storage') -const state = require('./lib/state') - -const handlers = { - help: require('./commands/help') - , departures: require('./commands/departures') - , journeys: require('./commands/journeys') - , nearby: require('./commands/nearby') -} - -const TOKEN = process.env.TOKEN -if (!TOKEN) { - console.error('Missing TOKEN env var.') - process.exit(1) -} -const WEB_HOOK = process.env.WEB_HOOK -if (!WEB_HOOK) { - console.error('Missing WEB_HOOK env var.') - process.exit(1) -} -const PORT = process.env.PORT -if (!PORT) { - console.error('Missing PORT env var.') - process.exit(1) -} - -const parseCmd = (msg) => { - if ('string' !== typeof msg.text) return null - const t = msg.text.trim() - if (t[0] !== '/') return null - if (/^\/(?:a|abfahrt)/i.test(t)) return 'departures' - else if (/^\/(?:r|route)/i.test(t)) return 'journeys' - else if (/^\/(?:n|nearby)/i.test(t)) return 'nearby' - else if (/^\/(?:h|help)/i.test(t)) return 'help' -} - -const error = `\ -*Oh snap! An error occured.* -Report this to my creator @derhuerst to help making this bot better.` - -let api -if (process.env.NODE_ENV === 'production') { - console.info('using ' + WEB_HOOK + ' as web hook') - - const http = require('http') - - const server = http.createServer() - api = new Api(TOKEN, { - polling: false, - webHook: {port: PORT} - }) - api.setWebHook(WEB_HOOK) -} else { - console.info('using polling') - api = new Api(TOKEN, {polling: true}) -} - -api.on('message', async (msg) => { - log(msg) - const user = msg.from ? msg.from.id : msg.chat.id - - const ns = namespace(storage, user) - const cmd = state(ns, 'cmd') - - const previousCmd = await cmd() - const parsedCmd = parseCmd(msg) - let command, newThread = false - - if (parsedCmd) { - command = parsedCmd - if (parsedCmd !== previousCmd) await cmd.set(command) - if (parsedCmd) newThread = true - } else { - if (previousCmd) command = previousCmd - else { - command = 'help' - newThread = true - await cmd.set(command) - } - } - - const keep = namespace(ns, command + ':keep') - const tmp = namespace(ns, command + ':tmp') - if (parsedCmd) await tmp.clear() - const ctx = context(api, user) - - // remove comments - // Unforunately, Telegram keyboard buttons can only contain one value, which is used for both a caption and as the value inserted when those buttons are pressed. Two values (a value and a caption) would be a lot more flexbible. - // This bot circumvents this limitation by ignoring everything after the Unicode 'INVISIBLE SEPARATOR' character, which allows nice captions and parsability at the same time. - if (msg.text && msg.text.indexOf('\u2063') >= 0) - msg.text = msg.text.split('\u2063')[0] - - try { - await handlers[command](ctx, newThread, keep, tmp, msg) - } catch (e) { - console.error(e.stack) - await tmp.clear() - await cmd.set(null) - ctx.keyboard(error, ctx.commands) - } -}) +// todo diff --git a/lib/api.js b/lib/api.js deleted file mode 100644 index 2609c23..0000000 --- a/lib/api.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict' - -const search = require('vbb-stations-autocomplete') -const getStations = require('vbb-stations') -const client = require('vbb-client') - -const location = (query) => { - const [station1] = search(query, 1, false, false) - if (station1) { - const [s] = getStations(station1.id) - if (s) { - Object.assign(station1, s) - return Promise.resolve(station1) - } - } - - const [station2] = search(query, 1, true, false) // fuzzy - if (station2) { - const [s] = getStations(station2.id) - if (s) { - Object.assign(station2, s) - return Promise.resolve(station2) - } - } - - return client.locations(query, { - results: 1, identifier: 'vbb-telegram' - }) - .then((results) => results[0] || null) -} - -const deps = (id, when) => - client.departures(id, { - duration: 15, - when, - identifier: 'vbb-telegram' - }) - -const closest = (lat, long, n) => - client.nearby({ - latitude: lat, longitude: long, - results: n, - identifier: 'vbb-telegram' - }) - -const journeys = (from, to, when) => - client.journeys(from, to, { - results: 3, - when, - identifier: 'vbb-telegram' - }) - -module.exports = {location, deps, closest, journeys} diff --git a/lib/context.js b/lib/context.js deleted file mode 100644 index 9ae2582..0000000 --- a/lib/context.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' - -const commands = [ - {text: '/a\u2063 Show departures at a station'} - , {text: '/r\u2063 Get routes from A to B.'} - , {text: '/n\u2063 Show stations around.'} -] - - - -const context = (api, user) => { - - const message = (text, props) => - api.sendMessage(user, text, Object.assign({ - parse_mode: 'Markdown', - hide_keyboard: true - }, props || {})) - - const keyboard = (text, keys) => - message(text, { - reply_markup: JSON.stringify({ - keyboard: keys.map((k) => [k]), - one_time_keyboard: true - }) - }) - - const requestLocation = (text, caption) => - keyboard(text, [{text: caption, request_location: true}]) - - const location = (lat, long) => api.sendLocation(user, lat, long) - - const typing = () => api.sendChatAction(user, 'typing') - - return { - commands, - message, keyboard, - requestLocation, location, - typing - } -} - -module.exports = context diff --git a/lib/frequent.js b/lib/frequent.js deleted file mode 100644 index ddec255..0000000 --- a/lib/frequent.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -const frequent = (base, set) => { - - const inc = async (k, v) => { - await base.setnx(set + ':' + k, v) - await base.inc(set, k) - } - - const get = async (n) => { - let ks = await base.highest(set, n) - if (ks.length === 0) return [] - ks = ks.map((k) => set + ':' + k) - return base.mget(ks) - } - - get.inc = inc - return get -} - -module.exports = frequent diff --git a/lib/log.js b/lib/log.js deleted file mode 100644 index 25b9dd4..0000000 --- a/lib/log.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -const render = require('./render') - - - -const log = (msg) => console.info( - msg.from.id || msg.chat.id - , render.date(msg.date * 1000) - , render.time(msg.date * 1000) - , msg.text || - (msg.location ? msg.location.latitude + '|' + msg.location.latitude : '') -) - -module.exports = log diff --git a/lib/namespace.js b/lib/namespace.js deleted file mode 100644 index acfda4b..0000000 --- a/lib/namespace.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' - -const err = (e) => {throw e} - -const namespace = (base, ns) => { - - const keys = (p) => { - const r = new RegExp('^' + ns + ':') - return base.keys(ns + ':' + p) - .then((ks) => ks.map((k) => k.replace(r, '')), err) - } - const del = (ks) => { - if (Array.isArray(ks)) ks = ks.map((k) => ns + ':' + k) - else ks = [ns + ':' + ks] - return base.del(ks) - } - - const get = (k) => base.get(ns + ':' + k) - const mget = (ks) => base.mget(ks.map((k) => ns + ':' + k)) - - const set = (k, v) => base.set(ns + ':' + k, v) - const setnx = (k, v) => base.setnx(ns + ':' + k, v) - - const highest = (s, n) => base.highest(ns + ':' + s, n) - const inc = (s, k) => base.inc(ns + ':' + s, k) - - const clear = async () => { - let ks = await base.keys(ns + ':*') - if (ks.length === 0) return [] - await base.del(ks) - } - - return {keys, del, get, mget, set, setnx, highest, inc, clear} -} - -module.exports = namespace diff --git a/lib/render.js b/lib/render.js deleted file mode 100644 index e6d0989..0000000 --- a/lib/render.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict' - -const ms = require('ms') -const linesAt = require('vbb-lines-at') -const table = require('columnify') -const moment = require('moment') -const timezone = require('moment-timezone') - -const TIMEZONE = process.env.TIMEZONE -if (!TIMEZONE) { - console.error('Missing TIMEZONE env var.') - process.exit(1) -} - -const relative = (when) => { - if (when === null) return '?' - - // todo: timezone - const now = new Date() - if (new Date(when).getDate() !== now.getDate()) { - return moment(when).locale('de').format('dd, LT') - } - if (Math.abs(when - now) >= 3600 * 1000) { - return moment(when).locale('de').format('LT') - } - const d = when - now - return (d < 0 ? '-' : ' ') + ms(Math.abs(d)) -} - -const lines = (lines) => lines.map((l) => '`' + l.name + '`').join(', ') - -const dep = (dep) => ({ - line: dep.line.name - , when: relative(dep.when) - , direction: dep.direction -}) - -const deps = (station, deps) => - `${station.name} ${lines(linesAt[station.id] || [])}\n` - + '```\n' - + table(deps.map(dep), { - direction: {maxWidth: 20} - , columns: ['when', 'line', 'direction'] - , showHeaders: false - , columnSplitter: ' ' - }) - + '\n```' - -const nearby = (station) => - `${station.distance}m *${station.name}*` - -const time = when => timezone(when).tz(TIMEZONE).format('LT') -const date = when => timezone(when).tz(TIMEZONE).format('l') - -const part = (acc, part, i, legs) => { - const travel = part.arrival - part.departure - acc += `\n${time(part.departure)} – *${part.origin.name}*` - - acc += part.mode === 'walking' - ? `\n*walk* for ${ms(travel)}` - : `\nwith *${part.line.name}* to *${part.direction}* for ${ms(travel)}` - - if (i === legs.length - 1) - acc += `\n${time(part.arrival)} – *${part.destination.name}*` - return acc -} - -const coords = l => `\`${l.latitude}\`|\`${l.latitude}\`` - -const journey = (r) => { - return 'From ' + (r.origin.name ? r.origin.name : coords(r.origin)) - + ' to ' + (r.destination.name ? r.destination.name : coords(r.destination)) - + ` in ${ms(r.arrival - r.departure)}.\n` - + r.legs.reduce(part, '') -} - -const journeys = (r) => { - return r - .map((r) => journey(r)) - .join('\n\n---\n\n') -} - -module.exports = { - relative, dep, deps, - nearby, - time, date, part, journey, journeys -} diff --git a/lib/state.js b/lib/state.js deleted file mode 100644 index 8949941..0000000 --- a/lib/state.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -const state = (base, key) => { - - const get = () => base.get(key) - const set = (v) => base.set(key, v) - - get.set = set - return get -} - -module.exports = state diff --git a/lib/storage.js b/lib/storage.js deleted file mode 100644 index 7fc443e..0000000 --- a/lib/storage.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict' - -const Redis = require('ioredis') - -const redis = new Redis() -const err = (e) => {throw e} - - - -const keys = (p) => - redis.keys(p) - -const del = (ks) => - redis.del(ks) - - - -const get = (k) => - redis.get(k) - .then((v) => JSON.parse(v), err) - -const mget = (ks) => - redis.mget(ks) - .then((vs) => vs.map((v) => JSON.parse(v)), err) - - - -const set = (k, v) => - redis.set(k, JSON.stringify(v)) - -const setnx = (k, v) => - redis.setnx(k, JSON.stringify(v)) - - - -const highest = (s, n) => - redis.zrevrange(s, 0, n) - -const inc = (s, k) => - redis.zincrby(s, 1, k) - - - -module.exports = {keys, del, get, mget, set, setnx, highest, inc} diff --git a/license.md b/license.md index f6f1f94..4d4e73c 100644 --- a/license.md +++ b/license.md @@ -1,4 +1,4 @@ -Copyright (c) 2017, Jannis R +Copyright (c) 2018, Jannis R Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. diff --git a/package.json b/package.json index 754aaa0..1b1f338 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "private": true, "name": "vbb-telegram", "description": "A Telegram bot for Berlin & Brandenburg public transport.", "version": "0.1.0", @@ -9,31 +10,16 @@ "bot", "departures", "routing", - "journey" + "journeys", + "routes" ], "author": "Jannis R ", "homepage": "https://github.com/derhuerst/vbb-telegram", "repository": "git://github.com/derhuerst/vbb-telegram.git", "license": "ISC", "engines": { - "node": ">=7.6" + "node": ">=8" }, "dependencies": { - "columnify": "^1.5.4", - "ioredis": "^3.0.0", - "moment": "^2.16", - "moment-timezone": "^0.5.9", - "ms": "^2.0.0", - "node-telegram-bot-api": "^0.30.0", - "parse-messy-time": "^2.1", - "shorthash": "^0.0.2", - "split-lines": "^1.1.0", - "vbb-client": "^3.0.1", - "vbb-lines-at": "^3.2.0", - "vbb-stations": "^6.2.1", - "vbb-stations-autocomplete": "^3.1.0" - }, - "scripts": { - "start": "node index.js" } } diff --git a/readme.md b/readme.md index b72bba5..f45f38f 100644 --- a/readme.md +++ b/readme.md @@ -1,29 +1,25 @@ -# vbb-telegram 💬 +# vbb-telegram **A Telegram bot for Berlin & Brandenburg public transport.** [Try it!](https://telegram.me/public_transport_bot) -![the bot in action](screenshot.png) - [![https://telegram.me/public_transport_bot](https://img.shields.io/badge/telegram-%40public__transport__bot-blue.svg)](https://telegram.me/public_transport_bot) -[![dependency status](https://img.shields.io/david/derhuerst/vbb-telegram.svg)](https://david-dm.org/derhuerst/vbb-telegram) ![ISC-licensed](https://img.shields.io/github/license/derhuerst/vbb-telegram.svg) -[![gitter channel](https://badges.gitter.im/derhuerst/vbb-rest.svg)](https://gitter.im/derhuerst/vbb-rest) +[![chat with me on Gitter](https://img.shields.io/badge/chat%20with%20me-on%20gitter-512e92.svg)](https://gitter.im/derhuerst) +[![support me on Patreon](https://img.shields.io/badge/support%20me-on%20patreon-fa7664.svg)](https://patreon.com/derhuerst) + +![the bot in action](screenshot.png) ## Installing -[Redis](http://redis.io/) needs to be running. - ```bash -git clone https://github.com/derhuerst/vbb-telegram.git; cd vbb-telegram -npm install -export NODE_ENV=dev # or `production` -npm start +git clone https://github.com/derhuerst/vbb-telegram.git vbb-telegram +cd vbb-telegram +npm install --production +export NODE_ENV=dev TOKEN=… node index.js ``` -*Note*: [*forever*](https://github.com/foreverjs/forever#readme) actually isn't required to run `vbb-telegram`, but listed as a [peer dependency](https://docs.npmjs.com/files/package.json#peerdependencies). The `npm start` script calls *forever* for production usage, so to run `npm start`, you need to `npm install [-g] forever` before. - ## Contributing -If you **have a question**, **found a bug** or want to **propose a feature**, have a look at [the issues page](https://github.com/derhuerst/vbb-telegram/issues). +If you have a question or have difficulties using `vbb-telegram`, please double-check your code and setup first. If you think you have found a bug or want to propose a feature, refer to [the issues page](https://github.com/derhuerst/vbb-telegram/issues).