bots/telegram.ts (view raw)
1import { Telegram, getUpdates } from "@gramio/wrappergram";
2import { findStops } from "@api/locationInformationRequest.ts";
3import { InlineKeyboard } from "@gramio/keyboards";
4import { getNextDepartures } from "@api/stopEvent.ts";
5import logger from "@lib/logger.ts";
6import * as kvdb from "../lib/kvdb.ts";
7
8const telegram = new Telegram(Deno.env.get("TELEGRAM_TOKEN") as string);
9
10const formatContent = (content?: string) =>
11 content
12 ?.replaceAll("-", "\\-")
13 .replaceAll(".", "\\.")
14 .replaceAll("(", "\\(")
15 .replaceAll(")", "\\)");
16
17const sendMessage = async (message: any) => {
18 await telegram.api.sendMessage(message);
19};
20
21const getUsername = (from: From) => {
22 let username = `id:${from.id}`;
23 if (from.username) username = `user:${from.username}`;
24 if (from.first_name && from.last_name)
25 return `${from.first_name} ${from.last_name} (${username})`;
26 else return username;
27};
28
29for await (const update of getUpdates(telegram)) {
30 logger.debug(JSON.stringify(update, null, 4));
31 // On new message
32 if (update.message) {
33 if (!update.message?.from) {
34 console.error("No 'from' in message");
35 continue;
36 }
37
38 const { from, text, location } = update.message;
39
40 logger.info(
41 `New message from ${getUsername(from)}: ${
42 text || JSON.stringify(location)
43 }`
44 );
45
46 telegram.api.setMyCommands({
47 commands: [
48 { command: "aide", description: "Explique comment utiliser le bot" },
49 {
50 command: "favoris",
51 description: "Affiche la liste de vos arrêts favoris",
52 },
53 ],
54 });
55
56 if (text === "/aide" || text === "/start") {
57 const inlineKeyboard = new InlineKeyboard().url(
58 "Plus d'infos",
59 "https://5ika.ch/posts/un-bot-pour-les-transports-en-commun/"
60 );
61 await sendMessage({
62 chat_id: from.id,
63 reply_markup: inlineKeyboard,
64 text: `Hey ✋ !
65Je suis un bot qui te permet d'obtenir rapidement des informations sur les transports en commun dans toute la Suisse 🇨🇭
66Entre le nom d'un arrêt et laisse-toi guider !
67
68📍 Tu peux aussi envoyer ta position GPS et je liste les arrêts à proximité.
69
70🧑💻 Ce bot est en cours de développement actif. Il se peut qu'il y ait encore quelques instabilités.
71
72/aide - Affiche ce message
73/favoris - Affiche vos favoris`,
74 });
75 } else if (text === "/favoris") {
76 const userId = `telegram:${from.id}`;
77 const favorites = kvdb.getUserFavorites(userId);
78
79 let inlineKeyboard = new InlineKeyboard();
80 for await (const favorite of favorites)
81 inlineKeyboard = inlineKeyboard
82 .text(favorite.value.name, {
83 cmd: "nextDepartures",
84 stopRef: favorite.value.stopRef,
85 })
86 .row();
87
88 await sendMessage({
89 chat_id: from.id,
90 text: "Voici vos favoris",
91 reply_markup: inlineKeyboard,
92 });
93 } else {
94 telegram.api.sendChatAction({
95 chat_id: from.id,
96 action: "find_location",
97 });
98 const searchInput = location ? location : text;
99 const stopLists = await findStops(searchInput);
100 if (!stopLists?.length) {
101 await sendMessage({
102 chat_id: from.id,
103 text: "Désolé, je n'ai pas réussi à récupérer une liste d'arrêts correspondants.",
104 });
105 } else {
106 kvdb.saveStops(stopLists);
107 let keyboard = new InlineKeyboard();
108 for (const stop of stopLists)
109 keyboard = keyboard
110 .text(stop.name, { cmd: "nextDepartures", stopRef: stop.stopRef })
111 .row();
112
113 await sendMessage({
114 chat_id: from.id,
115 text: "Quel arrêt correspond ?",
116 reply_markup: keyboard,
117 });
118 }
119 }
120 }
121
122 // On keyboard event (callback query)
123 else if (update.callback_query) {
124 const { from, data, message } = update.callback_query;
125 const userId = `telegram:${from.id}`;
126 const payload = JSON.parse(data);
127
128 logger.info(`New request from ${getUsername(from)}: ${data}`);
129
130 if (payload?.cmd === "nextDepartures") {
131 telegram.api.sendChatAction({
132 chat_id: from.id,
133 action: "find_location",
134 });
135 const nextDepartures = await getNextDepartures(payload.stopRef);
136 const stop = await kvdb.getStopByRef(payload.stopRef);
137 const text = formatContent(`Prochains départs depuis *${
138 stop.value?.name || "n.c."
139 }*:\n
140${nextDepartures
141 .map(
142 item =>
143 `*${item.serviceName}* ${item.serviceTypeIcon} *${item.departure}* _${item.departureIn} min_ \n${item.to}\n`
144 )
145 .join("\n")}`);
146
147 let keyboard = new InlineKeyboard()
148 .text("Rafraîchir", {
149 cmd: "nextDepartures",
150 stopRef: payload.stopRef,
151 refresh: message.message_id,
152 })
153 .text("Localiser", {
154 cmd: "localizeStop",
155 stopRef: payload.stopRef,
156 });
157
158 const existingFavorite = await kvdb.getUserFavorite(
159 userId,
160 payload.stopRef
161 );
162 if (!existingFavorite?.value)
163 keyboard = keyboard.row().text("Enregistrer comme favoris", {
164 cmd: "saveFavorite",
165 stopRef: payload.stopRef,
166 });
167
168 await sendMessage({
169 chat_id: from.id,
170 parse_mode: "MarkdownV2",
171 text,
172 reply_markup: keyboard,
173 });
174 } else if (payload?.cmd === "localizeStop") {
175 const stop = await kvdb.getStopByRef(payload.stopRef);
176 if (stop.value?.geoPosition)
177 await telegram.api.sendLocation({
178 chat_id: from.id,
179 longitude: stop.value.geoPosition.longitude,
180 latitude: stop.value.geoPosition.latitude,
181 });
182 else
183 sendMessage({
184 chat_id: from.id,
185 text: "Pas de coordonnées associées à cet arrêt.",
186 });
187 } else if (payload?.cmd === "saveFavorite") {
188 const stop = await kvdb.getStopByRef(payload.stopRef);
189 if (stop?.value) await kvdb.saveUserFavorite(userId, stop.value);
190 await sendMessage({
191 chat_id: from.id,
192 parse_mode: "MarkdownV2",
193 text: `L'arrêt *${formatContent(
194 stop.value?.name
195 )}* a été enregistré dans vos /favoris`,
196 });
197 }
198 }
199}