all repos — kokyo @ 55e9c6b23c3e1f92c5bc11756773c088bd263d12

Chatbot and CLI tool for Swiss public transports

feat: :sparkles: Add stops search by coordinates
Tim Izzo tim@5ika.ch
Thu, 22 Aug 2024 10:21:10 +0200
commit

55e9c6b23c3e1f92c5bc11756773c088bd263d12

parent

6236a1282014a4097ecb54d5a9e9ed6bd7ae796d

6 files changed, 81 insertions(+), 25 deletions(-)

jump to
M api/api.tsapi/api.ts

@@ -1,4 +1,5 @@

import { parse } from "@libs/xml"; +import logger from "@lib/logger.ts"; const TOKEN = Deno.env.get("API_TOKEN"); const RequestorRef = "Caroster.io";

@@ -15,8 +16,14 @@ method: "POST",

body: query, }); const xml = await response.text(); - const result = parse(xml); - return { response, result }; + try { + const result = parse(xml); + return { response, result }; + } catch (error) { + logger.error(xml); + logger.error(error); + return { response, result: null }; + } }; // Doc: https://opentransportdata.swiss/fr/cookbook/stopeventservice/

@@ -53,7 +60,10 @@ </OJPRequest>

</OJP>`); // Doc: https://opentransportdata.swiss/fr/cookbook/OJPLocationInformationRequest/ -export const getLocationInformationRequest = (textInput: string) => +export const getLocationInformationRequest = ( + textInput?: string, + coordinates?: Coordinates +) => apiFetch(`<OJP xmlns="http://www.vdv.de/ojp" xmlns:siri="http://www.siri.org.uk/siri" version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.vdv.de/ojp ../../../../Downloads/OJP-changes_for_v1.1%20(1)/OJP-changes_for_v1.1/OJP.xsd"> <OJPRequest> <siri:ServiceRequest>

@@ -63,7 +73,17 @@ <OJPLocationInformationRequest>

<siri:RequestTimestamp>${new Date().toISOString()}</siri:RequestTimestamp> <siri:MessageIdentifier>1220</siri:MessageIdentifier> <InitialInput> - <Name>${textInput}</Name> + ${textInput ? "<Name>${textInput}</Name>" : ""} + ${ + coordinates + ? ` + <GeoPosition> + <siri:Longitude>${coordinates.longitude}</siri:Longitude> + <siri:Latitude>${coordinates.latitude}</siri:Latitude> + </GeoPosition> + ` + : "" + } </InitialInput> <Restrictions> <Type>stop</Type>
M api/locationInformationRequest.tsapi/locationInformationRequest.ts

@@ -1,17 +1,28 @@

import { get } from "../lib/func.ts"; import { getLocationInformationRequest } from "./api.ts"; -export const findStopByName = async (textInput: string): Promise<Stop[]> => { - const { result } = await getLocationInformationRequest(textInput); +export const findStops = async ( + input: string | Coordinates +): Promise<Stop[]> => { + const textInput = typeof input === "string" ? input : undefined; + const coordinates = isCoordinates(input) ? input : undefined; + const { result } = await getLocationInformationRequest( + textInput, + coordinates + ); const placeResult = get<object[] | { Place: object }>( result, "OJP.OJPResponse.siri:ServiceDelivery.OJPLocationInformationDelivery.PlaceResult" ); if (!placeResult) return []; - else if (Array.isArray(placeResult)) return placeResult.map(formatPlace); + else if (Array.isArray(placeResult)) + return placeResult.map(formatPlace).filter(uniqStopFilter); else if (placeResult.Place) return [formatPlace(placeResult)]; else return []; }; + +const isCoordinates = (input: string | Coordinates) => + typeof input === "object" && "latitude" in input && "longitude" in input; const formatPlace = (item: object): Stop => ({ name: get(item, "Place.StopPlace.StopPlaceName.Text.#text"),

@@ -21,3 +32,8 @@ latitude: get(item, "Place.GeoPosition.siri:Latitude"),

longitude: get(item, "Place.GeoPosition.siri:Longitude"), }, }); + +const uniqStopFilter = (stop: Stop, index: number, arr: Stop[]) => { + const itemIndex = arr.findIndex(s => s.stopRef === stop.stopRef); + return itemIndex === index; +};
M bots/telegram.tsbots/telegram.ts

@@ -1,5 +1,5 @@

import { Telegram, getUpdates } from "@gramio/wrappergram"; -import { findStopByName } from "@api/locationInformationRequest.ts"; +import { findStops } from "@api/locationInformationRequest.ts"; import { InlineKeyboard } from "@gramio/keyboards"; import { getNextDepartures } from "@api/stopEvent.ts"; import logger from "@lib/logger.ts";

@@ -35,9 +35,13 @@ console.error("No 'from' in message");

continue; } - const { from, text } = update.message; + const { from, text, location } = update.message; - logger.info(`New message from ${getUsername(from)}: ${text}`); + logger.info( + `New message from ${getUsername(from)}: ${ + text || JSON.stringify(location) + }` + ); telegram.api.setMyCommands({ commands: [

@@ -79,19 +83,27 @@ text: "Voici vos favoris",

reply_markup: inlineKeyboard, }); } else { - const stopLists = await findStopByName(text); - kvdb.saveStops(stopLists); - let keyboard = new InlineKeyboard(); - for (const stop of stopLists) - keyboard = keyboard - .text(stop.name, { cmd: "nextDepartures", stopRef: stop.stopRef }) - .row(); + const searchInput = location ? location : text; + const stopLists = await findStops(searchInput); + if (!stopLists?.length) { + await sendMessage({ + chat_id: from.id, + text: "Désolé, je n'ai pas réussi à récupérer une liste d'arrêts correspondants.", + }); + } else { + kvdb.saveStops(stopLists); + let keyboard = new InlineKeyboard(); + for (const stop of stopLists) + keyboard = keyboard + .text(stop.name, { cmd: "nextDepartures", stopRef: stop.stopRef }) + .row(); - await sendMessage({ - chat_id: from.id, - text: "Quel arrêt correspond ?", - reply_markup: keyboard, - }); + await sendMessage({ + chat_id: from.id, + text: "Quel arrêt correspond ?", + reply_markup: keyboard, + }); + } } }
M cli.tscli.ts

@@ -2,7 +2,7 @@ import { Command } from "@cliffy/command";

import { Input, Select } from "@cliffy/prompt"; import { colors } from "@cliffy/ansi/colors"; import { Table } from "@cliffy/table"; -import { findStopByName } from "./api/locationInformationRequest.ts"; +import { findStops } from "./api/locationInformationRequest.ts"; import { getNextDepartures } from "./api/stopEvent.ts"; await new Command()

@@ -15,7 +15,7 @@ stopInput = await Input.prompt({

message: "Nom de l'arrêt", }); - const stopLists = await findStopByName(stopInput); + const stopLists = await findStops(stopInput); const stopRef = await Select.prompt({ message: "Sélectionner l'arrêt", options: stopLists.map(item => ({
M lib/logger.tslib/logger.ts

@@ -1,8 +1,11 @@

import * as log from "@std/log"; +import { LevelName } from "@std/log"; + +const LOG_LEVEL = Deno.env.get("LOG_LEVEL") || "INFO"; log.setup({ handlers: { - stringFmt: new log.ConsoleHandler("DEBUG", { + stringFmt: new log.ConsoleHandler(LOG_LEVEL as LevelName, { formatter: rec => `${rec.datetime.toLocaleString()} [${rec.levelName}] ${rec.msg}`, }),
M types.d.tstypes.d.ts

@@ -18,3 +18,8 @@ latitude: number;

longitude: number; }; } + +interface Coordinates { + latitude: number; + longitude: number; +}