diff --git a/src/commands/roll.ts b/src/commands/roll.ts new file mode 100644 index 0000000..7f3c526 --- /dev/null +++ b/src/commands/roll.ts @@ -0,0 +1,107 @@ +import axios from "axios"; + +interface RollResult { + distance: number; + id: string; + title: string; +} + +const url = `https://backend-map.vault48.org/api/route/random`; +const successMessages = [ + "Вот маршрут для тебя: {link}, {dist}км", + "Ну вот есть, например, {link}, всего {dist}км", + "Нашёл {link}, всего {dist}км, надо ехать", + "Вот, {link}, всего {dist}км, катал его, нет?", +]; + +const failureMessages = [ + "Ничего не нашёл 🤷", + "Не, такого нет, попробуй снова", + "Может, что-то и было такое, но сейчас я не ничего не нашёл.", +]; + +const say = (vals: string[]) => + vals[Math.floor(Math.random() * (vals.length - 1))]; + +const parseVal = (val?: string) => { + const parsed = val && parseInt(val, 10); + + if (!parsed || !Number.isFinite(parsed) || parsed <= 0 || parsed >= 10000) { + return; + } + + return parsed; +}; + +const escape = (val?: string) => val && val.replace(/([-.\[\]])/g, "\\$1"); +const deviate = (max: number, factor: number) => [ + Math.round(max * (1 - factor)), + Math.round(max * (1 + factor)), +]; + +// begin search in range of 80%-120% of specified distance +const startDeviation = 0.2; +// add 10% each time we haven't found anything +const deviationStep = 0.1; +// perform this amount of tries, increasing deviation each time we haven't found anything +const deviationTries = 8; + +const getRoute = async ( + min?: number, + max?: number +): Promise => { + if (!min && !max) { + return axios.get(url).then((it) => it.data); + } + + if (min && max && min > max) { + return axios + .get(url, { params: { min: max, max: min } }) + .then((it) => it.data); + } + + if (min && max) { + return axios + .get(url, { params: { min, max } }) + .then((it) => it.data); + } + + if (!min) { + return; + } + + for (let i = 0; i < deviationTries; i += 1) { + const deviation = startDeviation + deviationStep * i; + + let [devMin, devMax] = deviate(min, deviation); + + try { + return await axios + .get(url, { params: { min: devMin, max: devMax } }) + .then((it) => it.data) + .catch(); + } catch (error) {} + } +}; + +export const roll = async (text: string) => { + try { + const parts = text.trim().split(" "); + + const result = await getRoute(parseVal(parts[1]), parseVal(parts[2])); + + if (!result || !result?.id) { + return say(failureMessages); + } + + const link = `https://map.vault48.org/${result.id}`; + const distance = result.distance.toFixed(0); + const message = say(successMessages); + + return message + .replace("{dist}", distance) + .replace("{link}", `[${escape(result.title ?? link)}](${escape(link)})`); + } catch (error) { + console.warn("Error, when trying to fetch random route", error); + } +}; diff --git a/src/index.ts b/src/index.ts index a6b801a..10a2ad5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { TelegramApi } from "./api/telegram"; import { HttpApi } from "./api/http"; import { PostgresDB } from "./service/db/postgres"; import { PgTransport } from "./service/db/postgres/loggerTransport"; +import { roll } from "./commands/roll"; async function main() { try { @@ -29,9 +30,11 @@ async function main() { await httpApi.listen(); await telegramApi.probe(); + telegram.hears(/\/roll(.*)/, roll); + logger.info("bot successfully started"); } catch (e) { - logger.error(`FATAL EXCEPTION ${e.message}`); + logger.error(`FATAL EXCEPTION ${e}`); } } diff --git a/src/service/telegram/index.ts b/src/service/telegram/index.ts index 2f6e0e2..143c617 100644 --- a/src/service/telegram/index.ts +++ b/src/service/telegram/index.ts @@ -8,6 +8,8 @@ import { ExtraReplyMessage } from "telegraf/typings/telegram-types"; // import SocksProxyAgent from 'socks-proxy-agent'; +const maxMessageAge = 3 * 60e3; // skip messages older than this seconds + export class TelegramService { public readonly bot: Telegraf; public readonly webhook: WebhookConfig = {}; @@ -18,7 +20,7 @@ export class TelegramService { telegram: { webhookReply: true, apiMode: "bot", - // agent, // TODO: add proxy support + // agent, // TODO: add proxy support if they block it }, }; @@ -158,4 +160,40 @@ export class TelegramService { !!username && !!this.props.owners && this.props.owners.includes(username) ); }; + + public hears = ( + what: string | RegExp, + callback: ( + text: string + ) => string | Promise | undefined | void + ) => + this.bot.hears(what, async (ctx) => { + let text: string | void | undefined = "%% not received %%"; + + try { + const age = Date.now() - ctx.message.date * 1000; + const message = ctx.update.message.text; + + if (age > maxMessageAge) { + console.warn( + `skipped message "${message}", since its age ${age / 1000} seconds` + ); + + return; + } + + text = await callback(message); + + if (!text) { + return; + } + + ctx.reply(text, { parse_mode: "MarkdownV2" }); + } catch (error) { + console.warn( + `error replying to ${what} (${ctx.update.message.text}) with message "${text}"`, + error + ); + } + }); }