all repos — kokyo @ d3e94c1b0d41b8bf8530ecb6d38c41166dc8f01f

Chatbot and CLI tool for Swiss public transports

feat: :sparkles: Add simple Telegram bot
Tim Izzo tim@octree.ch
Fri, 09 Aug 2024 21:39:56 +0200
commit

d3e94c1b0d41b8bf8530ecb6d38c41166dc8f01f

parent

1513f141d0c31ef944771e03ce8cc54e7b19bd56

9 files changed, 218 insertions(+), 8 deletions(-)

jump to
M .env.example.env.example

@@ -1,1 +1,2 @@

-API_TOKEN=+API_TOKEN= +TELEGRAM_TOKEN=
M api/api.tsapi/api.ts

@@ -67,7 +67,8 @@ <Name>${textInput}</Name>

</InitialInput> <Restrictions> <Type>stop</Type> - <NumberOfResults>5</NumberOfResults> + <NumberOfResults>8</NumberOfResults> + <TopographicPlaceRef>23009621:2</TopographicPlaceRef> </Restrictions> </OJPLocationInformationRequest> </siri:ServiceRequest>
M api/stopEvent.tsapi/stopEvent.ts

@@ -13,6 +13,8 @@ result.OJP.OJPResponse["siri:ServiceDelivery"]["OJPStopEventDelivery"][

"StopEventResult" ]; + if (!stopEventResult) return []; + return stopEventResult .map((stopEvent: any) => { const rawDatetime =

@@ -30,11 +32,21 @@ if (rawDatetime) {

datetime = new Date(rawDatetime); departureIn = difference(datetime, new Date(), { units: ["minutes"] }); } + const serviceType = get( + stopEvent, + "StopEvent.Service.ProductCategory.Name.Text.#text" + ); return { from: get(stopEvent, "StopEvent.Service.OriginText.Text.#text"), to: get(stopEvent, "StopEvent.Service.DestinationText.Text.#text"), departure: datetime && format(datetime, "HH:mm"), departureIn: departureIn?.minutes, + serviceName: get( + stopEvent, + "StopEvent.Service.PublishedServiceName.Text.#text" + ), + serviceType, + serviceTypeIcon: getServiceTypeIcon(serviceType), stopName: get( stopEvent, "StopEvent.ThisCall.CallAtStop.StopPointName.Text.#text"

@@ -50,3 +62,13 @@ (eventA: StopEvent, eventB: StopEvent) =>

eventA.departureIn - eventB.departureIn ); }; + +const getServiceTypeIcon = (serviceType: string) => { + const icon = { + Bus: "🚍", + Tram: "🚊", + Zug: "🚆", + Schiff: "🛥️", + }[serviceType]; + return icon || serviceType; +};
A bots/telegram.ts

@@ -0,0 +1,93 @@

+import { Telegram, getUpdates } from "@gramio/wrappergram"; +import { findStopByName } from "@api/locationInformationRequest.ts"; +import { InlineKeyboard } from "@gramio/keyboards"; +import { getNextDepartures } from "@api/stopEvent.ts"; +import logger from "@lib/logger.ts"; + +const telegram = new Telegram(Deno.env.get("TELEGRAM_TOKEN") as string); + +const formatContent = (content: string) => + content + .replaceAll("-", "\\-") + .replaceAll(".", "\\.") + .replaceAll("(", "\\(") + .replaceAll(")", "\\)"); + +for await (const update of getUpdates(telegram)) { + console.log(update); + + // On new message + if (update.message) { + if (!update.message?.from) { + console.error("No 'from' in message"); + continue; + } + + logger.info( + `New message from ${update.message.from.username}: ${update.message.text}` + ); + + telegram.api.setMyCommands({ + commands: [ + { command: "aide", description: "Explique comment utiliser le bot" }, + ], + }); + + if (update.message.text === "/aide") { + telegram.api.sendMessage({ + chat_id: update.message.from.id, + text: `Hey ! Je suis un bot qui te permet d'obtenir rapidement des informations sur les transports en commun dans toute la Suisse. +Entre le nom d'un arrêt et laisse-toi guider ! + +> Ce bot est en cours de développement actif. Les fonctionnalités sont pour le moment limitées. + +/aide - Affiche ce message`, + }); + } else { + const stopLists = await findStopByName(update.message.text); + let keyboard = new InlineKeyboard(); + for (const stop of stopLists) + keyboard = keyboard + .text(stop.name, { + stopName: stop.name?.slice(0, 20), + stopRef: stop.stopRef, + }) + .row(); + + const response = await telegram.api.sendMessage({ + chat_id: update.message.from.id, + text: "Quel arrêt correspond ?", + reply_markup: keyboard, + }); + console.log(response, keyboard); + } + } + + // On keyboard event (callback query) + else if (update.callback_query) { + const payload = JSON.parse(update.callback_query.data); + + logger.info( + `New request from ${update.callback_query.from.username}: ${payload.stopName} (${payload.stopRef})` + ); + + if (payload?.stopRef) { + const nextDepartures = await getNextDepartures(payload.stopRef); + const text = formatContent(`Prochains départs depuis *${ + payload.stopName + }*:\n +${nextDepartures + .map( + item => + `*${item.departure}* _${item.departureIn} min_ *${item.serviceName}* ${item.serviceTypeIcon} ${item.to}` + ) + .join("\n")}`); + const response = await telegram.api.sendMessage({ + chat_id: update.callback_query.from.id, + parse_mode: "MarkdownV2", + text, + }); + console.log(response); + } + } +}
M cli.tscli.ts

@@ -28,12 +28,21 @@

const tableRows = nextDepartures.map(item => [ item.departure, `${item.departureIn} min`, + item.serviceName, + item.serviceType, item.to, ]); const table: Table = new Table() - .header(["Départ", "Dans", "Direction"].map(colors.bold.blue)) + .header( + ["Départ", "Dans", "Ligne", "Type", "Direction"].map(colors.bold.blue) + ) .body(tableRows) - .columns([{ minWidth: 10 }, { minWidth: 10 }]); + .columns([ + { minWidth: 10 }, + { minWidth: 10 }, + { minWidth: 10 }, + { minWidth: 10 }, + ]); console.log(""); table.render(); })
M deno.jsondeno.json

@@ -1,13 +1,20 @@

{ "tasks": { - "cli": "deno run -A --env cli.ts" + "cli": "deno run -A --env cli.ts", + "telegram": "deno run -A --watch --env bots/telegram.ts" }, "imports": { + "@api/": "./api/", + "@cliffy/ansi": "jsr:@cliffy/ansi@^1.0.0-rc.5", "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.5", "@cliffy/prompt": "jsr:@cliffy/prompt@^1.0.0-rc.5", "@cliffy/table": "jsr:@cliffy/table@^1.0.0-rc.5", - "@cliffy/ansi": "jsr:@cliffy/ansi@^1.0.0-rc.5", + "@gramio/format": "jsr:@gramio/format@^0.1.3", + "@gramio/keyboards": "jsr:@gramio/keyboards@^0.3.3", + "@gramio/wrappergram": "jsr:@gramio/wrappergram@^1.0.0", + "@lib/": "./lib/", "@libs/xml": "jsr:@libs/xml@^5.4.13", - "@std/datetime": "jsr:@std/datetime@^0.224.5" + "@std/datetime": "jsr:@std/datetime@^0.224.5", + "@std/log": "jsr:@std/log@^0.224.5" } }
M deno.lockdeno.lock

@@ -11,14 +11,24 @@ "jsr:@cliffy/keycode@1.0.0-rc.5": "jsr:@cliffy/keycode@1.0.0-rc.5",

"jsr:@cliffy/prompt@^1.0.0-rc.5": "jsr:@cliffy/prompt@1.0.0-rc.5", "jsr:@cliffy/table@1.0.0-rc.5": "jsr:@cliffy/table@1.0.0-rc.5", "jsr:@cliffy/table@^1.0.0-rc.5": "jsr:@cliffy/table@1.0.0-rc.5", + "jsr:@gramio/files@^0.0.12": "jsr:@gramio/files@0.0.12", + "jsr:@gramio/format@^0.1.3": "jsr:@gramio/format@0.1.3", + "jsr:@gramio/keyboards@^0.3.3": "jsr:@gramio/keyboards@0.3.3", + "jsr:@gramio/types@^7.3.4": "jsr:@gramio/types@7.8.0", + "jsr:@gramio/types@^7.7.2": "jsr:@gramio/types@7.8.0", + "jsr:@gramio/wrappergram@^1.0.0": "jsr:@gramio/wrappergram@1.0.0", "jsr:@libs/typing@2": "jsr:@libs/typing@2.8.1", "jsr:@libs/xml@^5.4.13": "jsr:@libs/xml@5.4.13", "jsr:@std/assert@1.0.0-rc.2": "jsr:@std/assert@1.0.0-rc.2", "jsr:@std/cli@1.0.0-rc.2": "jsr:@std/cli@1.0.0-rc.2", "jsr:@std/datetime@^0.224.5": "jsr:@std/datetime@0.224.5", "jsr:@std/encoding@1.0.0-rc.2": "jsr:@std/encoding@1.0.0-rc.2", + "jsr:@std/fmt@^1.0.0-rc.1": "jsr:@std/fmt@1.0.0", "jsr:@std/fmt@~0.225.4": "jsr:@std/fmt@0.225.6", + "jsr:@std/fs@^1.0.0-rc.5": "jsr:@std/fs@1.0.1", + "jsr:@std/io@^0.224.3": "jsr:@std/io@0.224.4", "jsr:@std/io@~0.224.2": "jsr:@std/io@0.224.4", + "jsr:@std/log@^0.224.5": "jsr:@std/log@0.224.5", "jsr:@std/path@1.0.0-rc.2": "jsr:@std/path@1.0.0-rc.2", "jsr:@std/text@1.0.0-rc.1": "jsr:@std/text@1.0.0-rc.1", "npm:@types/node": "npm:@types/node@18.16.19"

@@ -75,6 +85,34 @@ "jsr:@std/cli@1.0.0-rc.2",

"jsr:@std/fmt@~0.225.4" ] }, + "@gramio/files@0.0.12": { + "integrity": "d24c2d9b62deaed02903727f7895f58ab7a5b0cd62802dc233c3f9e947694914", + "dependencies": [ + "jsr:@gramio/types@^7.3.4" + ] + }, + "@gramio/format@0.1.3": { + "integrity": "6ba4a319f8db0c650a96a2139e9907054c735257d29b888dcdf6b1279ae86b3c", + "dependencies": [ + "jsr:@gramio/types@^7.3.4" + ] + }, + "@gramio/keyboards@0.3.3": { + "integrity": "4393bd03edadb453f8c570cc6fb561f83c0745a20a45fa4b3a1acf296e6af43c", + "dependencies": [ + "jsr:@gramio/types@^7.3.4" + ] + }, + "@gramio/types@7.8.0": { + "integrity": "c080dd37f84912d6ab87c34304aa7bd25e2612339a34c8f4173ab5db73b7049d" + }, + "@gramio/wrappergram@1.0.0": { + "integrity": "795958fec511e9f1a750c79f7c28fe073dde61f034bf8515ba2a6375e85dd3cc", + "dependencies": [ + "jsr:@gramio/files@^0.0.12", + "jsr:@gramio/types@^7.7.2" + ] + }, "@libs/typing@2.8.1": { "integrity": "08437a01ec51f74a20a5ab5d683475025f93a2ad641d2394a97e87f7b5194d78" },

@@ -98,10 +136,24 @@ "integrity": "160d7674a20ebfbccdf610b3801fee91cf6e42d1c106dd46bbaf46e395cd35ef"

}, "@std/fmt@0.225.6": { "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8" + }, + "@std/fmt@1.0.0": { + "integrity": "8a95c9fdbb61559418ccbc0f536080cf43341655e1444f9d375a66886ceaaa3d" + }, + "@std/fs@1.0.1": { + "integrity": "d6914ca2c21abe591f733b31dbe6331e446815e513e2451b3b9e472daddfefcb" }, "@std/io@0.224.4": { "integrity": "bce1151765e4e70e376039fd72c71672b4d4aae363878a5ee3e58361b81197ec" }, + "@std/log@0.224.5": { + "integrity": "4612a45189438441bbd923a4cad1cce5c44c6c4a039195a3e8d831ce38894eee", + "dependencies": [ + "jsr:@std/fmt@^1.0.0-rc.1", + "jsr:@std/fs@^1.0.0-rc.5", + "jsr:@std/io@^0.224.3" + ] + }, "@std/path@1.0.0-rc.2": { "integrity": "39f20d37a44d1867abac8d91c169359ea6e942237a45a99ee1e091b32b921c7d" },

@@ -123,8 +175,12 @@ "jsr:@cliffy/ansi@^1.0.0-rc.5",

"jsr:@cliffy/command@^1.0.0-rc.5", "jsr:@cliffy/prompt@^1.0.0-rc.5", "jsr:@cliffy/table@^1.0.0-rc.5", + "jsr:@gramio/format@^0.1.3", + "jsr:@gramio/keyboards@^0.3.3", + "jsr:@gramio/wrappergram@^1.0.0", "jsr:@libs/xml@^5.4.13", - "jsr:@std/datetime@^0.224.5" + "jsr:@std/datetime@^0.224.5", + "jsr:@std/log@^0.224.5" ] } }
A lib/logger.ts

@@ -0,0 +1,18 @@

+import * as log from "@std/log"; + +log.setup({ + handlers: { + stringFmt: new log.ConsoleHandler("DEBUG", { + formatter: rec => + `${rec.datetime.toLocaleString()} [${rec.levelName}] ${rec.msg}`, + }), + }, + loggers: { + default: { + level: "DEBUG", + handlers: ["stringFmt"], + }, + }, +}); + +export default log;
M types.d.tstypes.d.ts

@@ -5,6 +5,9 @@ departure: string;

departureIn?: number; stopName: string; quay: string; + serviceName: string; + serviceType: string; + serviceTypeIcon: string; } interface Place {