all repos — kokyo @ ecad943d5b2b5dc7265aa97181856b3f6b76597e

Chatbot and CLI tool for Swiss public transports

feat: :sparkles: Improve Telegram bot
Tim Izzo tim@5ika.ch
Mon, 12 Aug 2024 11:20:32 +0200
commit

ecad943d5b2b5dc7265aa97181856b3f6b76597e

parent

d3e94c1b0d41b8bf8530ecb6d38c41166dc8f01f

A .vscode/settings.json

@@ -0,0 +1,3 @@

+{ + "deno.enable": true +}
M api/api.tsapi/api.ts

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

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

@@ -1,7 +1,7 @@

import { get } from "../lib/func.ts"; import { getLocationInformationRequest } from "./api.ts"; -export const findStopByName = async (textInput: string): Promise<Place[]> => { +export const findStopByName = async (textInput: string): Promise<Stop[]> => { const { result } = await getLocationInformationRequest(textInput); const placeResult = get<object[]>( result,
M bots/telegram.tsbots/telegram.ts

@@ -3,15 +3,21 @@ import { findStopByName } from "@api/locationInformationRequest.ts";

import { InlineKeyboard } from "@gramio/keyboards"; import { getNextDepartures } from "@api/stopEvent.ts"; import logger from "@lib/logger.ts"; +import * as kvdb from "../lib/kvdb.ts"; const telegram = new Telegram(Deno.env.get("TELEGRAM_TOKEN") as string); -const formatContent = (content: string) => +const formatContent = (content?: string) => content - .replaceAll("-", "\\-") + ?.replaceAll("-", "\\-") .replaceAll(".", "\\.") .replaceAll("(", "\\(") .replaceAll(")", "\\)"); + +const sendMessage = async (message: any) => { + const response = await telegram.api.sendMessage(message); + console.log(response); +}; for await (const update of getUpdates(telegram)) { console.log(update);

@@ -30,64 +36,110 @@

telegram.api.setMyCommands({ commands: [ { command: "aide", description: "Explique comment utiliser le bot" }, + { + command: "favoris", + description: "Affiche la liste de vos arrêts favoris", + }, ], }); - if (update.message.text === "/aide") { - telegram.api.sendMessage({ + if (update.message.text === "/aide" || update.message.text === "/start") { + 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. +Ce bot est en cours de développement actif. Les fonctionnalités sont pour le moment limitées. -/aide - Affiche ce message`, +/aide - Affiche ce message +/favoris - Affiche vos favoris`, + }); + } else if (update.message.text === "/favoris") { + const userId = `telegram:${update.message.from.id}`; + const favorites = kvdb.getUserFavorites(userId); + + let inlineKeyboard = new InlineKeyboard(); + for await (const favorite of favorites) + inlineKeyboard = inlineKeyboard + .text(favorite.value.name, { + cmd: "nextDepartures", + stopRef: favorite.value.stopRef, + }) + .row(); + + await sendMessage({ + chat_id: update.message.from.id, + text: "Voici vos favoris", + reply_markup: inlineKeyboard, }); } else { const stopLists = await findStopByName(update.message.text); + kvdb.saveStops(stopLists); let keyboard = new InlineKeyboard(); for (const stop of stopLists) keyboard = keyboard - .text(stop.name, { - stopName: stop.name?.slice(0, 20), - stopRef: stop.stopRef, - }) + .text(stop.name, { cmd: "nextDepartures", stopRef: stop.stopRef }) .row(); - const response = await telegram.api.sendMessage({ + await 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 userId = `telegram:${update.callback_query.from.id}`; const payload = JSON.parse(update.callback_query.data); logger.info( - `New request from ${update.callback_query.from.username}: ${payload.stopName} (${payload.stopRef})` + `New request from ${update.callback_query.from.username}: ${update.callback_query.data}` ); - if (payload?.stopRef) { + if (payload?.cmd === "nextDepartures") { const nextDepartures = await getNextDepartures(payload.stopRef); + const stop = await kvdb.getStopByRef(payload.stopRef); const text = formatContent(`Prochains départs depuis *${ - payload.stopName + stop.value?.name || "n.c." }*:\n ${nextDepartures .map( item => - `*${item.departure}* _${item.departureIn} min_ *${item.serviceName}* ${item.serviceTypeIcon} ${item.to}` + `*${item.serviceName}* ${item.serviceTypeIcon} *${item.departure}* _${item.departureIn} min_ \n${item.to}\n` ) .join("\n")}`); - const response = await telegram.api.sendMessage({ + + let keyboard = new InlineKeyboard().text("Rafraîchir", { + cmd: "nextDepartures", + stopRef: payload.stopRef, + }); + const existingFavorite = await kvdb.getUserFavorite( + userId, + payload.stopRef + ); + if (!existingFavorite?.value) + keyboard = keyboard.text("Enregistrer comme favoris", { + cmd: "saveFavorite", + stopRef: payload.stopRef, + }); + await sendMessage({ chat_id: update.callback_query.from.id, parse_mode: "MarkdownV2", text, + reply_markup: keyboard, }); - console.log(response); + } else if (payload?.cmd === "saveFavorite") { + const stop = await kvdb.getStopByRef(payload.stopRef); + if (stop?.value) await kvdb.saveUserFavorite(userId, stop.value); + await sendMessage({ + chat_id: update.callback_query.from.id, + parse_mode: "MarkdownV2", + text: `L'arrêt *${formatContent( + stop.value?.name + )}* a été enregistré dans vos /favoris`, + }); } } }
M cli.tscli.ts

@@ -17,7 +17,7 @@ });

const stopLists = await findStopByName(stopInput); const stopRef = await Select.prompt({ - message: "Sélectionner le stop", + message: "Sélectionner l'arrêt", options: stopLists.map(item => ({ name: item.name, value: item.stopRef,
M deno.jsondeno.json

@@ -3,6 +3,7 @@ "tasks": {

"cli": "deno run -A --env cli.ts", "telegram": "deno run -A --watch --env bots/telegram.ts" }, + "unstable": ["kv"], "imports": { "@api/": "./api/", "@cliffy/ansi": "jsr:@cliffy/ansi@^1.0.0-rc.5",
A lib/kvdb.ts

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

+const kv = await Deno.openKv(); + +export const saveStop = (stop: Stop) => kv.set([stop.stopRef], stop); + +export const saveStops = async (stops: Stop[]) => { + for (const stop of stops) await saveStop(stop); +}; + +export const getStopByRef = (stopRef: string) => kv.get<Stop>([stopRef]); + +export const saveUserFavorite = (userId: string, stop: Stop) => + kv.set([userId, "favorites", stop.stopRef], stop); + +export const getUserFavorites = (userId: string) => + kv.list<Stop>({ prefix: [userId] }); + +export const getUserFavorite = (userId: string, stopRef: string) => + kv.get<Stop>([userId, "favorites", stopRef]);
M types.d.tstypes.d.ts

@@ -10,10 +10,10 @@ serviceType: string;

serviceTypeIcon: string; } -interface Place { +interface Stop { name: string; stopRef: string; - geoPosition: { + geoPosition?: { latitude: number; longitude: number; };