diff --git a/deno.jsonc b/deno.jsonc index 791aa74..5694537 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -17,8 +17,6 @@ }, "imports": { "alosaur/": "https://deno.land/x/alosaur@v0.38.0/", - "grammy/": "https://deno.land/x/grammy@v1.21.1/", - "grammy_types/": "https://deno.land/x/grammy_types@v3.5.2/", "std/": "https://deno.land/std@0.207.0/", "typeorm": "npm:typeorm@0.3.17", "sqlite3": "https://deno.land/x/sqlite3@0.10.0/mod.ts", diff --git a/telegram-bot/commands/show.command.ts b/telegram-bot/commands/show.command.ts index 8260fcc..4152a55 100644 --- a/telegram-bot/commands/show.command.ts +++ b/telegram-bot/commands/show.command.ts @@ -42,7 +42,7 @@ export class ShowCommand extends BaseCommand { } // Get the slug from the `/show slug` message text - const slug = ctx.message?.text?.split(" ")[1]; + const slug = ctx.message?.text?.split(" ")[1]; // Alternatively, use ctx.match if (!slug) { await ctx.reply("Please specify a callout slug. E.g. `/show my-callout`"); diff --git a/telegram-bot/commands/start.command.ts b/telegram-bot/commands/start.command.ts index 761cf3d..19085b6 100644 --- a/telegram-bot/commands/start.command.ts +++ b/telegram-bot/commands/start.command.ts @@ -5,7 +5,7 @@ import { CommunicationService } from "../services/communication.service.ts"; import { StateMachineService } from "../services/state-machine.service.ts"; import { MessageRenderer } from "../renderer/message.renderer.ts"; import { ChatState } from "../enums/index.ts"; -import { ListCommand } from "./index.ts"; +import { ListCommand, ResetCommand } from "./index.ts"; import type { AppContext } from "../types/index.ts"; @@ -24,6 +24,7 @@ export class StartCommand extends BaseCommand { protected readonly messageRenderer: MessageRenderer, protected readonly stateMachine: StateMachineService, protected readonly listCommand: ListCommand, + protected readonly resetCommand: ResetCommand, ) { super(); } @@ -31,9 +32,14 @@ export class StartCommand extends BaseCommand { // Handle the /start command, replay with markdown formatted text: https://grammy.dev/guide/basics#sending-message-with-formatting async action(ctx: AppContext): Promise { const session = await ctx.session; - const successful = await this.checkAction(ctx); - if (!successful) { - return false; + + // Always allow the start command, automatically reset the session if it is not on the initial state + const startCanUsed = await this.checkAction(ctx, true); + if (!startCanUsed) { + // Send the welcome message before the reset command + await this.communication.send(ctx, this.messageRenderer.welcome()); + // Execute the reset command + return await this.resetCommand.action(ctx); } await this.communication.send(ctx, this.messageRenderer.welcome()); @@ -53,6 +59,6 @@ export class StartCommand extends BaseCommand { await this.messageRenderer.help(session.state), ); - return successful; + return true; } } diff --git a/telegram-bot/constants/characters.ts b/telegram-bot/constants/characters.ts new file mode 100644 index 0000000..675dd36 --- /dev/null +++ b/telegram-bot/constants/characters.ts @@ -0,0 +1,2 @@ +export const CHECKMARK = "✓"; +export const DOT = "•"; diff --git a/telegram-bot/constants/events.ts b/telegram-bot/constants/events.ts index 51a7332..688977d 100644 --- a/telegram-bot/constants/events.ts +++ b/telegram-bot/constants/events.ts @@ -1,8 +1,21 @@ // Event related constants -export const BUTTON_CALLBACK_PREFIX = "callback_query:data"; // Do not change this prefix +export const INLINE_BUTTON_CALLBACK_PREFIX = "callback_query:data"; // Note: We choose short strings here because the length of these strings is limited in the Telegram bot API // and the slug is appended here.The max allowed size of a callback string is 64 bytes. -export const BUTTON_CALLBACK_SHOW_CALLOUT = "1"; -export const BUTTON_CALLBACK_CALLOUT_INTRO = "2"; -export const BUTTON_CALLBACK_CALLOUT_PARTICIPATE = "3"; +export const INLINE_BUTTON_CALLBACK_SHOW_CALLOUT = "1"; +export const INLINE_BUTTON_CALLBACK_CALLOUT_INTRO = "2"; +export const INLINE_BUTTON_CALLBACK_CALLOUT_PARTICIPATE = "3"; + +/** + * Prefix for the callout response interaction events, used for skip and done inline buttons, perhaps useful for others. + * @example + * ``` + * ${INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE}:skip + * ${INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE}:done + * ``` + */ +export const INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE = "ibccr"; + +export const TRUTHY_MESSAGE_KEY = "yes"; +export const FALSY_MESSAGE_KEY = "no"; diff --git a/telegram-bot/constants/index.ts b/telegram-bot/constants/index.ts index 2077d9f..ee5003a 100644 --- a/telegram-bot/constants/index.ts +++ b/telegram-bot/constants/index.ts @@ -1,3 +1,4 @@ +export * from "./characters.ts"; export * from "./events.ts"; export * from "./html.ts"; export * from "./render.ts"; diff --git a/telegram-bot/constants/render.ts b/telegram-bot/constants/render.ts index c3d93a5..c7876f5 100644 --- a/telegram-bot/constants/render.ts +++ b/telegram-bot/constants/render.ts @@ -14,7 +14,7 @@ export const EMPTY_RENDER: RenderEmpty = { skipTexts: [], }, parseType: ParsedResponseType.NONE, - removeKeyboard: true, + removeCustomKeyboard: true, forceReply: false, }; diff --git a/telegram-bot/deno.json b/telegram-bot/deno.json index ef323d4..3808d73 100644 --- a/telegram-bot/deno.json +++ b/telegram-bot/deno.json @@ -9,8 +9,6 @@ }, "imports": { "alosaur/": "https://deno.land/x/alosaur@v0.38.0/", - "grammy/": "https://deno.land/x/grammy@v1.21.1/", - "grammy_types/": "https://deno.land/x/grammy_types@v3.5.2/", "std/": "https://deno.land/std@0.207.0/", "typeorm": "npm:typeorm@0.3.17", "sqlite3": "https://deno.land/x/sqlite3@0.10.0/mod.ts", diff --git a/telegram-bot/deno.lock b/telegram-bot/deno.lock index 9fc0cce..eb9ac8c 100644 --- a/telegram-bot/deno.lock +++ b/telegram-bot/deno.lock @@ -2,17 +2,16 @@ "version": "3", "packages": { "specifiers": { - "npm:@types/markdown-it": "npm:@types/markdown-it@13.0.7", + "npm:@types/markdown-it": "npm:@types/markdown-it@14.0.0", "npm:date-fns@3.3.1": "npm:date-fns@3.3.1", "npm:googleapis@131.0.0": "npm:googleapis@131.0.0", - "npm:jsonwebtoken": "npm:jsonwebtoken@9.0.2", - "npm:markdown-it": "npm:markdown-it@14.0.0", + "npm:markdown-it": "npm:markdown-it@14.1.0", "npm:typeorm@0.3.17": "npm:typeorm@0.3.17", "npm:valtio@2.0.0-beta.1": "npm:valtio@2.0.0-beta.1" }, "npm": { - "@babel/runtime@7.24.0": { - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "@babel/runtime@7.24.4": { + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { "regenerator-runtime": "regenerator-runtime@0.14.1" } @@ -25,8 +24,8 @@ "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", "dependencies": {} }, - "@types/markdown-it@13.0.7": { - "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==", + "@types/markdown-it@14.0.0": { + "integrity": "sha512-2rStaAqMaLQNfo9mg2HNlley75jUTAkZKqlk3pxDSgaFk44zd+CAVpczpoh6/RtOzfUtwpEyD6lsHWUfKbVSDg==", "dependencies": { "@types/linkify-it": "@types/linkify-it@3.0.5", "@types/mdurl": "@types/mdurl@1.0.5" @@ -36,8 +35,8 @@ "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", "dependencies": {} }, - "agent-base@7.1.0": { - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "agent-base@7.1.1": { + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dependencies": { "debug": "debug@4.3.4" } @@ -100,7 +99,7 @@ "es-errors": "es-errors@1.3.0", "function-bind": "function-bind@1.1.2", "get-intrinsic": "get-intrinsic@1.2.4", - "set-function-length": "set-function-length@1.2.1" + "set-function-length": "set-function-length@1.2.2" } }, "chalk@4.1.2": { @@ -150,7 +149,7 @@ "date-fns@2.30.0": { "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "dependencies": { - "@babel/runtime": "@babel/runtime@7.24.0" + "@babel/runtime": "@babel/runtime@7.24.4" } }, "date-fns@3.3.1": { @@ -215,19 +214,20 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dependencies": {} }, - "gaxios@6.3.0": { - "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==", + "gaxios@6.4.0": { + "integrity": "sha512-apAloYrY4dlBGlhauDAYSZveafb5U6+L9titing1wox6BvWM0TSXBp603zTrLpyLMGkrcFgohnUN150dFN/zOA==", "dependencies": { "extend": "extend@3.0.2", "https-proxy-agent": "https-proxy-agent@7.0.4", "is-stream": "is-stream@2.0.1", - "node-fetch": "node-fetch@2.7.0" + "node-fetch": "node-fetch@2.7.0", + "uuid": "uuid@9.0.1" } }, "gcp-metadata@6.1.0": { "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", "dependencies": { - "gaxios": "gaxios@6.3.0", + "gaxios": "gaxios@6.4.0", "json-bigint": "json-bigint@1.0.0" } }, @@ -242,7 +242,7 @@ "function-bind": "function-bind@1.1.2", "has-proto": "has-proto@1.0.3", "has-symbols": "has-symbols@1.0.3", - "hasown": "hasown@2.0.1" + "hasown": "hasown@2.0.2" } }, "glob@8.1.0": { @@ -255,24 +255,24 @@ "once": "once@1.4.0" } }, - "google-auth-library@9.6.3": { - "integrity": "sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ==", + "google-auth-library@9.7.0": { + "integrity": "sha512-I/AvzBiUXDzLOy4iIZ2W+Zq33W4lcukQv1nl7C8WUA6SQwyQwUwu3waNmWNAvzds//FG8SZ+DnKnW/2k6mQS8A==", "dependencies": { "base64-js": "base64-js@1.5.1", "ecdsa-sig-formatter": "ecdsa-sig-formatter@1.0.11", - "gaxios": "gaxios@6.3.0", + "gaxios": "gaxios@6.4.0", "gcp-metadata": "gcp-metadata@6.1.0", "gtoken": "gtoken@7.1.0", "jws": "jws@4.0.0" } }, - "googleapis-common@7.0.1": { - "integrity": "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==", + "googleapis-common@7.1.0": { + "integrity": "sha512-p3KHiWDBBWJEXk6SYauBEvxw5+UmRy7k2scxGtsNv9eHsTbpopJ3/7If4OrNnzJ9XMLg3IlyQXpVp8YPQsStiw==", "dependencies": { "extend": "extend@3.0.2", - "gaxios": "gaxios@6.3.0", - "google-auth-library": "google-auth-library@9.6.3", - "qs": "qs@6.11.2", + "gaxios": "gaxios@6.4.0", + "google-auth-library": "google-auth-library@9.7.0", + "qs": "qs@6.12.0", "url-template": "url-template@2.0.8", "uuid": "uuid@9.0.1" } @@ -280,8 +280,8 @@ "googleapis@131.0.0": { "integrity": "sha512-fa4kdkY0VwHDw/04ItpQv2tlvlPIwbh6NjHDoWAVrV52GuaZbYCMOC5Y+hRmprp5HHIMRODmyb2YujlbZSRUbQ==", "dependencies": { - "google-auth-library": "google-auth-library@9.6.3", - "googleapis-common": "googleapis-common@7.0.1" + "google-auth-library": "google-auth-library@9.7.0", + "googleapis-common": "googleapis-common@7.1.0" } }, "gopd@1.0.1": { @@ -293,7 +293,7 @@ "gtoken@7.1.0": { "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", "dependencies": { - "gaxios": "gaxios@6.3.0", + "gaxios": "gaxios@6.4.0", "jws": "jws@4.0.0" } }, @@ -315,8 +315,8 @@ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dependencies": {} }, - "hasown@2.0.1": { - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "hasown@2.0.2": { + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "function-bind@1.1.2" } @@ -328,7 +328,7 @@ "https-proxy-agent@7.0.4": { "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dependencies": { - "agent-base": "agent-base@7.1.0", + "agent-base": "agent-base@7.1.1", "debug": "debug@4.3.4" } }, @@ -361,29 +361,6 @@ "bignumber.js": "bignumber.js@9.1.2" } }, - "jsonwebtoken@9.0.2": { - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dependencies": { - "jws": "jws@3.2.2", - "lodash.includes": "lodash.includes@4.3.0", - "lodash.isboolean": "lodash.isboolean@3.0.3", - "lodash.isinteger": "lodash.isinteger@4.0.4", - "lodash.isnumber": "lodash.isnumber@3.0.3", - "lodash.isplainobject": "lodash.isplainobject@4.0.6", - "lodash.isstring": "lodash.isstring@4.0.1", - "lodash.once": "lodash.once@4.1.1", - "ms": "ms@2.1.2", - "semver": "semver@7.6.0" - } - }, - "jwa@1.4.1": { - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "buffer-equal-constant-time@1.0.1", - "ecdsa-sig-formatter": "ecdsa-sig-formatter@1.0.11", - "safe-buffer": "safe-buffer@5.2.1" - } - }, "jwa@2.0.0": { "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", "dependencies": { @@ -392,13 +369,6 @@ "safe-buffer": "safe-buffer@5.2.1" } }, - "jws@3.2.2": { - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "jwa@1.4.1", - "safe-buffer": "safe-buffer@5.2.1" - } - }, "jws@4.0.0": { "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "dependencies": { @@ -409,52 +379,18 @@ "linkify-it@5.0.0": { "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dependencies": { - "uc.micro": "uc.micro@2.0.0" - } - }, - "lodash.includes@4.3.0": { - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "dependencies": {} - }, - "lodash.isboolean@3.0.3": { - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "dependencies": {} - }, - "lodash.isinteger@4.0.4": { - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "dependencies": {} - }, - "lodash.isnumber@3.0.3": { - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "dependencies": {} - }, - "lodash.isplainobject@4.0.6": { - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dependencies": {} - }, - "lodash.isstring@4.0.1": { - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dependencies": {} - }, - "lodash.once@4.1.1": { - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dependencies": {} - }, - "lru-cache@6.0.0": { - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "yallist@4.0.0" + "uc.micro": "uc.micro@2.1.0" } }, - "markdown-it@14.0.0": { - "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", + "markdown-it@14.1.0": { + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dependencies": { "argparse": "argparse@2.0.1", "entities": "entities@4.5.0", "linkify-it": "linkify-it@5.0.0", "mdurl": "mdurl@2.0.0", "punycode.js": "punycode.js@2.3.1", - "uc.micro": "uc.micro@2.0.0" + "uc.micro": "uc.micro@2.1.0" } }, "mdurl@2.0.0": { @@ -525,10 +461,10 @@ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dependencies": {} }, - "qs@6.11.2": { - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "qs@6.12.0": { + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", "dependencies": { - "side-channel": "side-channel@1.0.5" + "side-channel": "side-channel@1.0.6" } }, "reflect-metadata@0.1.14": { @@ -547,14 +483,8 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dependencies": {} }, - "semver@7.6.0": { - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "lru-cache@6.0.0" - } - }, - "set-function-length@1.2.1": { - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "set-function-length@1.2.2": { + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { "define-data-property": "define-data-property@1.1.4", "es-errors": "es-errors@1.3.0", @@ -571,8 +501,8 @@ "safe-buffer": "safe-buffer@5.2.1" } }, - "side-channel@1.0.5": { - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "side-channel@1.0.6": { + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { "call-bind": "call-bind@1.0.7", "es-errors": "es-errors@1.3.0", @@ -640,8 +570,8 @@ "yargs": "yargs@17.7.2" } }, - "uc.micro@2.0.0": { - "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==", + "uc.micro@2.1.0": { + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dependencies": {} }, "url-template@2.0.8": { @@ -685,10 +615,6 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dependencies": {} }, - "yallist@4.0.0": { - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dependencies": {} - }, "yargs-parser@20.2.9": { "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dependencies": {} @@ -724,7 +650,7 @@ } }, "redirects": { - "https://lib.deno.dev/x/grammy@^1.20/mod.ts": "https://deno.land/x/grammy@v1.21.1/mod.ts" + "https://lib.deno.dev/x/grammy@^1.20/mod.ts": "https://deno.land/x/grammy@v1.22.4/mod.ts" }, "remote": { "https://cdn.skypack.dev/-/debug@v4.3.4-o4liVvMlOnQWbLSYZMXw/dist=es2019,mode=imports/optimized/debug.js": "671100993996e39b501301a87000607916d4d2d9f8fc8e9c5200ae5ba64a1389", @@ -796,16 +722,6 @@ "https://deno.land/std@0.179.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", "https://deno.land/std@0.179.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", "https://deno.land/std@0.179.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", - "https://deno.land/std@0.202.0/path/_basename.ts": "057d420c9049821f983f784fd87fa73ac471901fb628920b67972b0f44319343", - "https://deno.land/std@0.202.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.202.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2", - "https://deno.land/std@0.202.0/path/_util.ts": "4e191b1bac6b3bf0c31aab42e5ca2e01a86ab5a0d2e08b75acf8585047a86221", - "https://deno.land/std@0.202.0/path/basename.ts": "bdfa5a624c6a45564dc6758ef2077f2822978a6dbe77b0a3514f7d1f81362930", - "https://deno.land/std@0.202.0/streams/_common.ts": "3b2c1f0287ce2ad51fff4091a7d0f48375c85b0ec341468e76d5ac13bb0014dd", - "https://deno.land/std@0.202.0/streams/iterate_reader.ts": "3b42d3056c8ccade561f1c7ac22d5e671e745933d9f9168fd3b5913588d911c3", - "https://deno.land/std@0.205.0/encoding/_util.ts": "f368920189c4fe6592ab2e93bd7ded8f3065b84f95cd3e036a4a10a75649dcba", - "https://deno.land/std@0.205.0/encoding/base64.ts": "cc03110d6518170aeaa68ec97f89c6d6e2276294b30807e7332591d7ce2e4b72", - "https://deno.land/std@0.205.0/encoding/base64url.ts": "7608862858d28a003f9d6cb78dd61e645ecd1ae1f45faf0e09a306eafe66b16e", "https://deno.land/std@0.207.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", "https://deno.land/std@0.207.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", "https://deno.land/std@0.207.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", @@ -837,7 +753,6 @@ "https://deno.land/std@0.207.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", "https://deno.land/std@0.207.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", "https://deno.land/std@0.207.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", - "https://deno.land/std@0.207.0/dotenv/mod.ts": "039468f5c87d39b69d7ca6c3d68ebca82f206ec0ff5e011d48205eea292ea5a6", "https://deno.land/std@0.207.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2", "https://deno.land/std@0.208.0/media_types/_db.ts": "7606d83e31f23ce1a7968cbaee852810c2cf477903a095696cdc62eaab7ce570", "https://deno.land/std@0.208.0/media_types/_util.ts": "0879b04cc810ff18d3dcd97d361e03c9dfb29f67d7fc4a9c6c9d387282ef5fe8", @@ -850,87 +765,133 @@ "https://deno.land/std@0.208.0/media_types/parse_media_type.ts": "31ccf2388ffab31b49500bb89fa0f5de189c8897e2ee6c9954f207637d488211", "https://deno.land/std@0.208.0/media_types/type_by_extension.ts": "a6f351c9fc2ed742393792f8c59550fa9ef940ca6b462ee16168a3cda6020c19", "https://deno.land/std@0.208.0/media_types/vendor/mime-db.v1.52.0.ts": "6925bbcae81ca37241e3f55908d0505724358cda3384eaea707773b2c7e99586", - "https://deno.land/std@0.211.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", - "https://deno.land/std@0.211.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", "https://deno.land/std@0.211.0/path/_common/assert_path.ts": "2ca275f36ac1788b2acb60fb2b79cb06027198bc2ba6fb7e163efaedde98c297", "https://deno.land/std@0.211.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", - "https://deno.land/std@0.211.0/path/_common/common.ts": "6157c7ec1f4db2b4a9a187efd6ce76dcaf1e61cfd49f87e40d4ea102818df031", "https://deno.land/std@0.211.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", - "https://deno.land/std@0.211.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", - "https://deno.land/std@0.211.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", - "https://deno.land/std@0.211.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", - "https://deno.land/std@0.211.0/path/_common/glob_to_reg_exp.ts": "2007aa87bed6eb2c8ae8381adcc3125027543d9ec347713c1ad2c68427330770", - "https://deno.land/std@0.211.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", - "https://deno.land/std@0.211.0/path/_common/normalize_string.ts": "dfdf657a1b1a7db7999f7c575ee7e6b0551d9c20f19486c6c3f5ff428384c965", - "https://deno.land/std@0.211.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", "https://deno.land/std@0.211.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", - "https://deno.land/std@0.211.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", - "https://deno.land/std@0.211.0/path/_interface.ts": "a1419fcf45c0ceb8acdccc94394e3e94f99e18cfd32d509aab514c8841799600", "https://deno.land/std@0.211.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", "https://deno.land/std@0.211.0/path/basename.ts": "5d341aadb7ada266e2280561692c165771d071c98746fcb66da928870cd47668", - "https://deno.land/std@0.211.0/path/common.ts": "973e019d3cfa6a134a13f1fda3f7efbaf400a64365d7a7b96f66afe373a09dc5", - "https://deno.land/std@0.211.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", - "https://deno.land/std@0.211.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", - "https://deno.land/std@0.211.0/path/format.ts": "98fad25f1af7b96a48efb5b67378fcc8ed77be895df8b9c733b86411632162af", - "https://deno.land/std@0.211.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", - "https://deno.land/std@0.211.0/path/glob_to_regexp.ts": "83c5fd36a8c86f5e72df9d0f45317f9546afa2ce39acaafe079d43a865aced08", - "https://deno.land/std@0.211.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", - "https://deno.land/std@0.211.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", - "https://deno.land/std@0.211.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", - "https://deno.land/std@0.211.0/path/join_globs.ts": "e9589869a33dc3982101898ee50903db918ca00ad2614dbe3934d597d7b1fbea", - "https://deno.land/std@0.211.0/path/mod.ts": "8e1ffe983557e9637184ccb84bd6b0447e319f4a28bfad7f3f41ee050579e5e6", - "https://deno.land/std@0.211.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", - "https://deno.land/std@0.211.0/path/normalize_glob.ts": "98ee8268fad271193603271c203ae973280b5abfbdd2cbca1053fd2af71869ca", - "https://deno.land/std@0.211.0/path/parse.ts": "65e8e285f1a63b714e19ef24b68f56e76934c3df0b6e65fd440d3991f4f8aefb", "https://deno.land/std@0.211.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", "https://deno.land/std@0.211.0/path/posix/basename.ts": "39ee27a29f1f35935d3603ccf01d53f3d6e0c5d4d0f84421e65bd1afeff42843", - "https://deno.land/std@0.211.0/path/posix/common.ts": "809cc86e79db8171b9a97ac397d56b9588c25a8f3062f483c8d651a2b6739daa", - "https://deno.land/std@0.211.0/path/posix/dirname.ts": "6535d2bdd566118963537b9dda8867ba9e2a361015540dc91f5afbb65c0cce8b", - "https://deno.land/std@0.211.0/path/posix/extname.ts": "8d36ae0082063c5e1191639699e6f77d3acf501600a3d87b74943f0ae5327427", - "https://deno.land/std@0.211.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", - "https://deno.land/std@0.211.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", - "https://deno.land/std@0.211.0/path/posix/glob_to_regexp.ts": "54d3ff40f309e3732ab6e5b19d7111d2d415248bcd35b67a99defcbc1972e697", - "https://deno.land/std@0.211.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", - "https://deno.land/std@0.211.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", - "https://deno.land/std@0.211.0/path/posix/join.ts": "aef88d5fa3650f7516730865dbb951594d1a955b785e2450dbee93b8e32694f3", - "https://deno.land/std@0.211.0/path/posix/join_globs.ts": "35ddd5f321d79e1fc72d2ec9a8d8863f0bb1431125e57bb2661799278d4ee9cd", - "https://deno.land/std@0.211.0/path/posix/mod.ts": "9dfff9f3618ba6990eb8495dadef13871e5756419b25079b6b905a4ebf790926", - "https://deno.land/std@0.211.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", - "https://deno.land/std@0.211.0/path/posix/normalize_glob.ts": "0f01bcfb0791144f0e901fd2cc706432baf84828c393f3c25c53583f03d0c0b7", - "https://deno.land/std@0.211.0/path/posix/parse.ts": "d5bac4eb21262ab168eead7e2196cb862940c84cee572eafedd12a0d34adc8fb", - "https://deno.land/std@0.211.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", - "https://deno.land/std@0.211.0/path/posix/resolve.ts": "bac20d9921beebbbb2b73706683b518b1d0c1b1da514140cee409e90d6b2913a", - "https://deno.land/std@0.211.0/path/posix/separator.ts": "6530f253a33d92d8f8a1d1d7fa7fad2992c739ad9886dde72e4e78793f1cfd49", - "https://deno.land/std@0.211.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", - "https://deno.land/std@0.211.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", - "https://deno.land/std@0.211.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", - "https://deno.land/std@0.211.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", - "https://deno.land/std@0.211.0/path/separator.ts": "2b5a590d4f1942e70650ee7421d161c24ec7d3b94b49981e4138ae07397fb2d2", - "https://deno.land/std@0.211.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", - "https://deno.land/std@0.211.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", "https://deno.land/std@0.211.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", "https://deno.land/std@0.211.0/path/windows/basename.ts": "e2dbf31d1d6385bfab1ce38c333aa290b6d7ae9e0ecb8234a654e583cf22f8fe", - "https://deno.land/std@0.211.0/path/windows/common.ts": "809cc86e79db8171b9a97ac397d56b9588c25a8f3062f483c8d651a2b6739daa", - "https://deno.land/std@0.211.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", - "https://deno.land/std@0.211.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", - "https://deno.land/std@0.211.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", - "https://deno.land/std@0.211.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", - "https://deno.land/std@0.211.0/path/windows/glob_to_regexp.ts": "6dcd1242bd8907aa9660cbdd7c93446e6927b201112b0cba37ca5d80f81be51b", - "https://deno.land/std@0.211.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", - "https://deno.land/std@0.211.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", - "https://deno.land/std@0.211.0/path/windows/join.ts": "e0b3356615c1a75c56ebb6a7311157911659e11fd533d80d724800126b761ac3", - "https://deno.land/std@0.211.0/path/windows/join_globs.ts": "35ddd5f321d79e1fc72d2ec9a8d8863f0bb1431125e57bb2661799278d4ee9cd", - "https://deno.land/std@0.211.0/path/windows/mod.ts": "e739f7e783b69fb7956bed055e117201ccb071a7917c09f87c5c8c2b54369d38", - "https://deno.land/std@0.211.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", - "https://deno.land/std@0.211.0/path/windows/normalize_glob.ts": "49c634af33a7c6bc738885c4b34633278b7ab47bd47bf11281b2190970b823e2", - "https://deno.land/std@0.211.0/path/windows/parse.ts": "b9239edd892a06a06625c1b58425e199f018ce5649ace024d144495c984da734", - "https://deno.land/std@0.211.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", - "https://deno.land/std@0.211.0/path/windows/resolve.ts": "75b2e3e1238d840782cee3d8864d82bfaa593c7af8b22f19c6422cf82f330ab3", - "https://deno.land/std@0.211.0/path/windows/separator.ts": "2bbcc551f64810fb43252185bd1d33d66e0477d74bd52f03b89f5dc21a3dd486", - "https://deno.land/std@0.211.0/path/windows/to_file_url.ts": "1cd63fd35ec8d1370feaa4752eccc4cc05ea5362a878be8dc7db733650995484", - "https://deno.land/std@0.211.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", "https://deno.land/std@0.211.0/streams/_common.ts": "4f9f2958d853b9a456be033631dabb7519daa68ee4d02caf53e2ecbffaf5805f", "https://deno.land/std@0.211.0/streams/iterate_reader.ts": "353e516908ce637e8b2a2e1301fa60316825667d0d880d47ea4c427a9a7758cf", + "https://deno.land/std@0.221.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376", + "https://deno.land/std@0.221.0/encoding/base64.ts": "8ccae67a1227b875340a8582ff707f37b131df435b07080d3bb58e07f5f97807", + "https://deno.land/std@0.221.0/encoding/base64url.ts": "9cc46cf510436be63ac00ebf97a7de1993e603ca58e1853b344bf90d80ea9945", + "https://deno.land/std@0.222.1/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.222.1/assert/_diff.ts": "4bf42969aa8b1a33aaf23eb8e478b011bfaa31b82d85d2ff4b5c4662d8780d2b", + "https://deno.land/std@0.222.1/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4", + "https://deno.land/std@0.222.1/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.222.1/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.222.1/assert/assert_array_includes.ts": "167b2c29997defd49a1835de52b54ae3cbb2bcba52df7c7ee45fe64b473264f1", + "https://deno.land/std@0.222.1/assert/assert_equals.ts": "cc1f4b0ff4ad511e69f965535b56a6cdbbbc0f086bf376e0243214df6039c883", + "https://deno.land/std@0.222.1/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.222.1/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.222.1/assert/assert_greater.ts": "26903fc7170a9eb37ee6c6606c772b5a0465a85e719cfe46f57de35555931419", + "https://deno.land/std@0.222.1/assert/assert_greater_or_equal.ts": "10527cf379a71a55a88b96d9b3373d0346ea2bdd8d73d2faaab1224e2cedb727", + "https://deno.land/std@0.222.1/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.222.1/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.222.1/assert/assert_less.ts": "091f0cc80f53425be22b14c9b6ae410fea08e16eca391a476303c5f4852fcd9e", + "https://deno.land/std@0.222.1/assert/assert_less_or_equal.ts": "9418b2f809023f778d58fd6834410814900eacb8a91708647970ae1085553813", + "https://deno.land/std@0.222.1/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.222.1/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.222.1/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.222.1/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.222.1/assert/assert_not_strict_equals.ts": "61e4adfd80eddaab5da5e5444431dfb19457f26b1f1e7a8be27c5d981b7f50b9", + "https://deno.land/std@0.222.1/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.222.1/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.222.1/assert/assert_strict_equals.ts": "dbcdcb5b8b74e6c06bce6a9fa43ff4d1089793e7832baff251e514954b9b266b", + "https://deno.land/std@0.222.1/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.222.1/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", + "https://deno.land/std@0.222.1/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.222.1/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.222.1/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.222.1/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.222.1/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.222.1/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", + "https://deno.land/std@0.222.1/dotenv/mod.ts": "0180eaeedaaf88647318811cdaa418cc64dc51fb08354f91f5f480d0a1309f7d", + "https://deno.land/std@0.222.1/dotenv/parse.ts": "09977ff88dfd1f24f9973a338f0f91bbdb9307eb5ff6085446e7c423e4c7ba0c", + "https://deno.land/std@0.222.1/dotenv/stringify.ts": "275da322c409170160440836342eaa7cf012a1d11a7e700d8ca4e7f2f8aa4615", + "https://deno.land/std@0.222.1/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", + "https://deno.land/std@0.222.1/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", + "https://deno.land/std@0.222.1/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.222.1/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", + "https://deno.land/std@0.222.1/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.222.1/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.222.1/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", + "https://deno.land/std@0.222.1/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.222.1/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", + "https://deno.land/std@0.222.1/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.222.1/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", + "https://deno.land/std@0.222.1/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", + "https://deno.land/std@0.222.1/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.222.1/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", + "https://deno.land/std@0.222.1/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", + "https://deno.land/std@0.222.1/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.222.1/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", + "https://deno.land/std@0.222.1/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", + "https://deno.land/std@0.222.1/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.222.1/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.222.1/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", + "https://deno.land/std@0.222.1/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", + "https://deno.land/std@0.222.1/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.222.1/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", + "https://deno.land/std@0.222.1/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.222.1/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.222.1/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.222.1/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", + "https://deno.land/std@0.222.1/path/mod.ts": "2821a1bb3a4148a0ffe79c92aa41aa9319fef73c6d6f5178f52b2c720d3eb02d", + "https://deno.land/std@0.222.1/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.222.1/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", + "https://deno.land/std@0.222.1/path/parse.ts": "3e172974e3c71025f5fbd2bd9db4307acb9cc2de14cf6f4464bf40957663cabe", + "https://deno.land/std@0.222.1/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.222.1/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", + "https://deno.land/std@0.222.1/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.222.1/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.222.1/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", + "https://deno.land/std@0.222.1/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", + "https://deno.land/std@0.222.1/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", + "https://deno.land/std@0.222.1/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.222.1/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", + "https://deno.land/std@0.222.1/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.222.1/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.222.1/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", + "https://deno.land/std@0.222.1/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.222.1/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.222.1/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.222.1/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.222.1/path/posix/parse.ts": "0b1fc4cb890dbb699ec1d2c232d274843b4a7142e1ad976b69fe51c954eb6080", + "https://deno.land/std@0.222.1/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", + "https://deno.land/std@0.222.1/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", + "https://deno.land/std@0.222.1/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", + "https://deno.land/std@0.222.1/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", + "https://deno.land/std@0.222.1/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", + "https://deno.land/std@0.222.1/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.222.1/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", + "https://deno.land/std@0.222.1/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", + "https://deno.land/std@0.222.1/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.222.1/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", + "https://deno.land/std@0.222.1/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.222.1/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.222.1/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.222.1/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", + "https://deno.land/std@0.222.1/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", + "https://deno.land/std@0.222.1/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.222.1/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", + "https://deno.land/std@0.222.1/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.222.1/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.222.1/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", + "https://deno.land/std@0.222.1/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.222.1/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.222.1/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.222.1/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.222.1/path/windows/parse.ts": "dbdfe2bc6db482d755b5f63f7207cd019240fcac02ad2efa582adf67ff10553a", + "https://deno.land/std@0.222.1/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", + "https://deno.land/std@0.222.1/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", + "https://deno.land/std@0.222.1/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", + "https://deno.land/std@0.222.1/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", "https://deno.land/x/alosaur@v0.38.0/mod.ts": "7d0397dc2a9a33f95a564e027067686d08aff3d9151d49230cbd3d0302c56973", "https://deno.land/x/alosaur@v0.38.0/src/decorator/Area.ts": "ae5de805032b12c874a342d9c1dbf03b44ee42808265538c2631c1c95a9947d7", "https://deno.land/x/alosaur@v0.38.0/src/decorator/Body.ts": "23a3ab5e7d0bdd64b795d39f12122be237aeb83b5c8fadbd9abc49593600ffe3", @@ -1051,76 +1012,46 @@ "https://deno.land/x/ammonia@0.3.1/mod.ts": "170075af1b2e2922b2f1229bac4acba5cb824b10e97de4604da55ea492db2e26", "https://deno.land/x/ammonia@0.3.1/pkg/ammonia_wasm.js": "75a90cc78b52f1f2e4e998c1b574f97097de2d2ee7f3a55dca562c4f93a618e0", "https://deno.land/x/ammonia@0.3.1/wasm.js": "60a03b400d2ff529d2d3a0a804f10abe564d5ffaa1bf8344c2c27799088f514e", - "https://deno.land/x/djwt@v3.0.1/algorithm.ts": "b1c6645f9dbd6e6c47c123a3b18c28b956f91c65ed17f5b6d5d968fc3750542b", - "https://deno.land/x/djwt@v3.0.1/deps.ts": "c8b329dc18c54f93879699ffb728bd805d548f7c64c4d2916efa5fb2c712cc47", - "https://deno.land/x/djwt@v3.0.1/mod.ts": "eca176976595654b8a536e7b8029a029c435d079ef58feff2d3bde33ea107417", - "https://deno.land/x/djwt@v3.0.1/signature.ts": "a5649368a4b433b2810e7d47f53661fe3b0f7fe1778cb49234ceae3d6e861185", - "https://deno.land/x/djwt@v3.0.1/util.ts": "5cb264d2125c553678e11446bcfa0494025d120e3f59d0a3ab38f6800def697d", - "https://deno.land/x/grammy@v1.19.2/bot.ts": "8d13cd72f1512e3f76d685131c7d0db5ba51f2c877db5ac2c0aa4b0f6f876aa8", - "https://deno.land/x/grammy@v1.19.2/composer.ts": "8660f86990f4ef2afc4854a1f2610bb8d60f88116f3a57c8e5515a77b277f82d", - "https://deno.land/x/grammy@v1.19.2/context.ts": "4cf51ed7538750edb4379f757f6b8b3c1f3987242d58393160b463c9ca13c997", - "https://deno.land/x/grammy@v1.19.2/convenience/constants.ts": "3be0f6393ab2b2995fad6bcd4c9cf8a1a615ae4543fc864c107ba0dd38f123f6", - "https://deno.land/x/grammy@v1.19.2/convenience/frameworks.ts": "77e2f9fc841ab92d4310b556126447a42f131ad976a6adfff454c016f339b28e", - "https://deno.land/x/grammy@v1.19.2/convenience/inline_query.ts": "409d1940c7670708064efa495003bcbfdf6763a756b2e6303c464489fd3394ff", - "https://deno.land/x/grammy@v1.19.2/convenience/input_media.ts": "7af72a5fdb1af0417e31b1327003f536ddfdf64e06ab8bc7f5da6b574de38658", - "https://deno.land/x/grammy@v1.19.2/convenience/keyboard.ts": "21220dc2321c40203c699fa4eb7b07ed8217956ea0477c241a551224a58a278d", - "https://deno.land/x/grammy@v1.19.2/convenience/session.ts": "f92d57b6b2b61920912cf5c44d4db2f6ca999fe4f9adef170c321889d49667c2", - "https://deno.land/x/grammy@v1.19.2/convenience/webhook.ts": "f1da7d6426171fb7b5d5f6b59633f91d3bab9a474eea821f714932650965eb9e", - "https://deno.land/x/grammy@v1.19.2/core/api.ts": "7d4d8df3567e322ab3b793360ee48da09f46ad531ef994a87b3e6aef4ec23bf2", - "https://deno.land/x/grammy@v1.19.2/core/client.ts": "39639e4f5fc3a3f9d528c6906d7e3cdc268cf5d33929eeab801bb39642a59103", - "https://deno.land/x/grammy@v1.19.2/core/error.ts": "4638b2127ebe60249c78b83011d468f5e1e1a87748d32fe11a8200d9f824ad13", - "https://deno.land/x/grammy@v1.19.2/core/payload.ts": "420e17c3c2830b5576ea187cfce77578fe09f1204b25c25ea2f220ca7c86e73b", - "https://deno.land/x/grammy@v1.19.2/filter.ts": "201ddac882ab6cd46cae2d18eb8097460dfe7cedadaab2ba16959c5286d5a5f1", - "https://deno.land/x/grammy@v1.19.2/mod.ts": "b81cccf69779667b36bef5d0373d1567684917a3b9827873f3de7a7e6af1926f", - "https://deno.land/x/grammy@v1.19.2/platform.deno.ts": "84735643c8dde2cf8af5ac2e6b8eb0768452260878da93238d673cb1b4ccea55", - "https://deno.land/x/grammy@v1.19.2/types.deno.ts": "0f47eacde6d3d65f107f2abf16ecfe726298d30263367cc82e977c801b766229", - "https://deno.land/x/grammy@v1.19.2/types.ts": "729415590dfa188dbe924dea614dff4e976babdbabb28a307b869fc25777cdf0", - "https://deno.land/x/grammy@v1.21.1/bot.ts": "bbfc31f976a27a48992ebb21bcdc137f216eb28e32cc5de0041dcc8fca53d5b8", - "https://deno.land/x/grammy@v1.21.1/composer.ts": "a86dcd6c83e91f720ceb85dab2b1c7b966fc18fc6440848b87b4897fcaa63fc8", - "https://deno.land/x/grammy@v1.21.1/context.ts": "3e9b8e277f8b75bed20b46047ad93b027a182e9d3504f1f2d2bba0852d0bb77f", - "https://deno.land/x/grammy@v1.21.1/convenience/constants.ts": "8d7e2fb9b0f5bd4c10585d8a7528dee573dea5096041b35bebadbb943318d1fc", - "https://deno.land/x/grammy@v1.21.1/convenience/frameworks.ts": "ef57a31722a7f1b393f04db83cd123e6455a7b17028d3388e7d5d12970ec8978", - "https://deno.land/x/grammy@v1.21.1/convenience/inline_query.ts": "409d1940c7670708064efa495003bcbfdf6763a756b2e6303c464489fd3394ff", - "https://deno.land/x/grammy@v1.21.1/convenience/input_media.ts": "7af72a5fdb1af0417e31b1327003f536ddfdf64e06ab8bc7f5da6b574de38658", - "https://deno.land/x/grammy@v1.21.1/convenience/keyboard.ts": "88aeab16f2aaf0b4098135b5f7a678f7d2ce288f28c7eba93eaa5553a7395152", - "https://deno.land/x/grammy@v1.21.1/convenience/session.ts": "f92d57b6b2b61920912cf5c44d4db2f6ca999fe4f9adef170c321889d49667c2", - "https://deno.land/x/grammy@v1.21.1/convenience/webhook.ts": "f1da7d6426171fb7b5d5f6b59633f91d3bab9a474eea821f714932650965eb9e", - "https://deno.land/x/grammy@v1.21.1/core/api.ts": "840c5d39ca953d5bdbf89e61836a8212b22110d142b1606127b9c1a5f7d0b96a", - "https://deno.land/x/grammy@v1.21.1/core/client.ts": "df622a135e71229ffe722406850c9c08b90dcdd4d049b46926128599c73f9dc5", - "https://deno.land/x/grammy@v1.21.1/core/error.ts": "4638b2127ebe60249c78b83011d468f5e1e1a87748d32fe11a8200d9f824ad13", - "https://deno.land/x/grammy@v1.21.1/core/payload.ts": "420e17c3c2830b5576ea187cfce77578fe09f1204b25c25ea2f220ca7c86e73b", - "https://deno.land/x/grammy@v1.21.1/filter.ts": "31b9048d543f4e280a573d2e0b47f4ebbbaead753e4c836525026af5f6af9307", - "https://deno.land/x/grammy@v1.21.1/mod.ts": "7723e08709ff7fd01df3e463503e14e4fd1a581669380eed70351e1121e8a833", - "https://deno.land/x/grammy@v1.21.1/platform.deno.ts": "68272a7e1d9a2d74d8a45342526485dbc0531dee812f675d7f8a4e7fc8393028", - "https://deno.land/x/grammy@v1.21.1/types.deno.ts": "9ef5b8524e5779b1cc6df72736b0663a103b0be549dc4d4c93df2528b27e1534", - "https://deno.land/x/grammy@v1.21.1/types.ts": "729415590dfa188dbe924dea614dff4e976babdbabb28a307b869fc25777cdf0", + "https://deno.land/x/djwt@v3.0.2/algorithm.ts": "b1c6645f9dbd6e6c47c123a3b18c28b956f91c65ed17f5b6d5d968fc3750542b", + "https://deno.land/x/djwt@v3.0.2/deps.ts": "a7954fe567f2097b4f6aca11d091b6df658e485a817ac4dee47257ed5c28fd6e", + "https://deno.land/x/djwt@v3.0.2/mod.ts": "962d8f2c4d6a4db111f45d777b152356aec31ba7db0ca664601175a422629857", + "https://deno.land/x/djwt@v3.0.2/signature.ts": "16238fbf558267c85dd6c0178045f006c8b914a7301db87149f3318326569272", + "https://deno.land/x/djwt@v3.0.2/util.ts": "5cb264d2125c553678e11446bcfa0494025d120e3f59d0a3ab38f6800def697d", + "https://deno.land/x/grammy@v1.22.4/bot.ts": "994a8cc67b6c1b6b6c1814579c263f3c5cdfda32396261bcc9fd8b8f1302984b", + "https://deno.land/x/grammy@v1.22.4/composer.ts": "a86dcd6c83e91f720ceb85dab2b1c7b966fc18fc6440848b87b4897fcaa63fc8", + "https://deno.land/x/grammy@v1.22.4/context.ts": "084cb930b59712c4682633b7a003a5029d95a02af132d81c4c50ca60fb50ccfa", + "https://deno.land/x/grammy@v1.22.4/convenience/constants.ts": "4df7992fbdac5bca4f7352b6bd7e5b24c391901ff59ee3982570a40d8d6a28a6", + "https://deno.land/x/grammy@v1.22.4/convenience/frameworks.ts": "6a535734adcef9ca3f662e761fd5ab0067213a13e85340cc932624b4062e9264", + "https://deno.land/x/grammy@v1.22.4/convenience/inline_query.ts": "409d1940c7670708064efa495003bcbfdf6763a756b2e6303c464489fd3394ff", + "https://deno.land/x/grammy@v1.22.4/convenience/input_media.ts": "7af72a5fdb1af0417e31b1327003f536ddfdf64e06ab8bc7f5da6b574de38658", + "https://deno.land/x/grammy@v1.22.4/convenience/keyboard.ts": "88aeab16f2aaf0b4098135b5f7a678f7d2ce288f28c7eba93eaa5553a7395152", + "https://deno.land/x/grammy@v1.22.4/convenience/session.ts": "4072f5067ca5d6d13a80761ae0b731cdd9f3a1b37bf549d877ac214d20bda6cc", + "https://deno.land/x/grammy@v1.22.4/convenience/webhook.ts": "f1da7d6426171fb7b5d5f6b59633f91d3bab9a474eea821f714932650965eb9e", + "https://deno.land/x/grammy@v1.22.4/core/api.ts": "178364d7717caddaa7067514be7d2a3b6a39b47f3620d8fab3752c4dead70d42", + "https://deno.land/x/grammy@v1.22.4/core/client.ts": "df622a135e71229ffe722406850c9c08b90dcdd4d049b46926128599c73f9dc5", + "https://deno.land/x/grammy@v1.22.4/core/error.ts": "4638b2127ebe60249c78b83011d468f5e1e1a87748d32fe11a8200d9f824ad13", + "https://deno.land/x/grammy@v1.22.4/core/payload.ts": "420e17c3c2830b5576ea187cfce77578fe09f1204b25c25ea2f220ca7c86e73b", + "https://deno.land/x/grammy@v1.22.4/filter.ts": "0c3138b7b9932d76ec41f1e8e8258fd691a33b7b68e48cc676689fb98b601082", + "https://deno.land/x/grammy@v1.22.4/mod.ts": "7723e08709ff7fd01df3e463503e14e4fd1a581669380eed70351e1121e8a833", + "https://deno.land/x/grammy@v1.22.4/platform.deno.ts": "68272a7e1d9a2d74d8a45342526485dbc0531dee812f675d7f8a4e7fc8393028", + "https://deno.land/x/grammy@v1.22.4/types.deno.ts": "30c5b090b08a4e3023b9de74e2db9abbe89e47a7d938991059a209f07769ccaf", + "https://deno.land/x/grammy@v1.22.4/types.ts": "729415590dfa188dbe924dea614dff4e976babdbabb28a307b869fc25777cdf0", "https://deno.land/x/grammy_parse_mode@1.9.0/deps.deno.ts": "647effb311140ce1a688371fb4dd0d525a7c7257a90b46c571595cebf4c5692d", "https://deno.land/x/grammy_parse_mode@1.9.0/format.ts": "7debb58d4af04ea86988ce5e448ec119ba7d5d7e513fbc37d7cda106f01273b0", "https://deno.land/x/grammy_parse_mode@1.9.0/hydrate.ts": "d0d872b7e0f13d58c937e03c119ea8b9a9c75a1ed1e79369ab2e02583c658339", "https://deno.land/x/grammy_parse_mode@1.9.0/mod.ts": "58b539ea91fa72c1fd66dd5415b6c4b63104301b07d9daf860bd66343db7062c", "https://deno.land/x/grammy_parse_mode@1.9.0/transformer.ts": "b08e1e9fdc421286a16de28d3c8e4b53ff6cb0af5536809a8dfad24c4b6cc20c", - "https://deno.land/x/grammy_types@v3.3.0/api.ts": "efc90a31eb6f59ae5e7a4cf5838f46529e2fa6fa7e97a51a82dbd28afad21592", - "https://deno.land/x/grammy_types@v3.3.0/inline.ts": "b5669d79f8c0c6f7d6ca856d548c1ac7d490efd54ee785d18a7c4fc12abfd73b", - "https://deno.land/x/grammy_types@v3.3.0/manage.ts": "e39ec87e74469f70f35aa51dc520b02136ea5e75f9d7a7e0e513846a00b63fd2", - "https://deno.land/x/grammy_types@v3.3.0/markup.ts": "7b547b79130a112f98fbd3f0f754c8bb926f7cab3040d244b5f597aea0e1ce09", - "https://deno.land/x/grammy_types@v3.3.0/message.ts": "e78a7797174c537bb8de80597e265121615fa36a531dd88ac5af27aa68779172", - "https://deno.land/x/grammy_types@v3.3.0/methods.ts": "7547cedfec2c2727b30b8fa38050aee6642c56673b21cfd0ac56b0e531f02795", - "https://deno.land/x/grammy_types@v3.3.0/mod.ts": "7b5f421b4fbb1761f7f0d68328eaddd515f3222ce3f3cdfbedd8d5a4781e91a7", - "https://deno.land/x/grammy_types@v3.3.0/passport.ts": "e3fb63aec96510bcc317ef48fd25b435444b8f407502d7568c00fce15f2958fd", - "https://deno.land/x/grammy_types@v3.3.0/payment.ts": "d23e9038c5b479b606e620dd84e3e67b6642ada110a962f2d5b5286e99ec7de5", - "https://deno.land/x/grammy_types@v3.3.0/settings.ts": "5e989f5bd6c587d55673bd8052293869aa2f372e9223dd7f6e28632bfe021b6e", - "https://deno.land/x/grammy_types@v3.3.0/update.ts": "6d5ec6d1f6d2acf021f807f6bbf7d541487f30672cfab4700e7f935a490c3b78", - "https://deno.land/x/grammy_types@v3.5.2/api.ts": "ae04d6628e3d25ae805bc07a19475065044fc44cde0a40877405bc3544d03a5f", - "https://deno.land/x/grammy_types@v3.5.2/inline.ts": "12b33002c4d7880b2e80aaee68ac344110360886efe48ab20d40e93b90849f04", - "https://deno.land/x/grammy_types@v3.5.2/manage.ts": "3bc9717ba157d3b0e076ff256322f9bf7ea2da28eaf25ea1dbcdc84c9f780804", - "https://deno.land/x/grammy_types@v3.5.2/markup.ts": "38f2de2c01531486d98ad17f7622af15d720ffaf5cf721af77ece49e403a09bb", - "https://deno.land/x/grammy_types@v3.5.2/message.ts": "90b9a23fc90f056ad34f71ed194f2cba30a91f3fa7b3e699a577172a2eceeb2d", - "https://deno.land/x/grammy_types@v3.5.2/methods.ts": "3429cc7f124337bb2cd2d16015272125f8e44d440d3480a87241905113fab701", - "https://deno.land/x/grammy_types@v3.5.2/mod.ts": "7b5f421b4fbb1761f7f0d68328eaddd515f3222ce3f3cdfbedd8d5a4781e91a7", - "https://deno.land/x/grammy_types@v3.5.2/passport.ts": "e3fb63aec96510bcc317ef48fd25b435444b8f407502d7568c00fce15f2958fd", - "https://deno.land/x/grammy_types@v3.5.2/payment.ts": "d23e9038c5b479b606e620dd84e3e67b6642ada110a962f2d5b5286e99ec7de5", - "https://deno.land/x/grammy_types@v3.5.2/settings.ts": "5e989f5bd6c587d55673bd8052293869aa2f372e9223dd7f6e28632bfe021b6e", - "https://deno.land/x/grammy_types@v3.5.2/update.ts": "a9fe07b677235a0d29e371f7fdc57ebf46b248ce956a2d918ed844a3c0fbe5de", + "https://deno.land/x/grammy_types@v3.6.2/api.ts": "ae04d6628e3d25ae805bc07a19475065044fc44cde0a40877405bc3544d03a5f", + "https://deno.land/x/grammy_types@v3.6.2/inline.ts": "12b33002c4d7880b2e80aaee68ac344110360886efe48ab20d40e93b90849f04", + "https://deno.land/x/grammy_types@v3.6.2/manage.ts": "71b279fdba92d98a53bfe9b725f7c5e11445094da9bd7f8ab78913a43e534064", + "https://deno.land/x/grammy_types@v3.6.2/markup.ts": "3151020d7a57d000da4ada30f48236f26ce16f522db4ecd480c2f1803423005e", + "https://deno.land/x/grammy_types@v3.6.2/message.ts": "bc4727af1923b63ffb2e8109d383ac3f60166d7f9807d6361b6005ddb3823907", + "https://deno.land/x/grammy_types@v3.6.2/methods.ts": "c44f390e687620b4d376e3a2060a47b225cd2189633b80b090d9f2be7dad2738", + "https://deno.land/x/grammy_types@v3.6.2/mod.ts": "7b5f421b4fbb1761f7f0d68328eaddd515f3222ce3f3cdfbedd8d5a4781e91a7", + "https://deno.land/x/grammy_types@v3.6.2/passport.ts": "19820e7d6c279521f8bc8912d6a378239f73d4ab525453808994b5f44ef95215", + "https://deno.land/x/grammy_types@v3.6.2/payment.ts": "d23e9038c5b479b606e620dd84e3e67b6642ada110a962f2d5b5286e99ec7de5", + "https://deno.land/x/grammy_types@v3.6.2/settings.ts": "5e989f5bd6c587d55673bd8052293869aa2f372e9223dd7f6e28632bfe021b6e", + "https://deno.land/x/grammy_types@v3.6.2/update.ts": "cfacb5558cd7d50fb90076abc16102a52e435ab88b2991c698e48928e515b50e", "https://deno.land/x/jsonc@1/impl/edit.ts": "20b85191c8936f44a98af16bc09923d9836aa9398543dcad3ce0a599ec1234e8", "https://deno.land/x/jsonc@1/impl/format.ts": "e5cefe9bb086d5041eedc7bc96b5a71d1cf01758c19db13552a3d18f89139f81", "https://deno.land/x/jsonc@1/impl/parser.ts": "38688f235ca92b1a938bf5bce700f37aace9db7f3164685b63a70ed937b83167", diff --git a/telegram-bot/deps/djwt.ts b/telegram-bot/deps/djwt.ts index 07fbe39..080050e 100644 --- a/telegram-bot/deps/djwt.ts +++ b/telegram-bot/deps/djwt.ts @@ -1,2 +1,2 @@ // JSON Web Token -export * as djwt from "https://deno.land/x/djwt@v3.0.1/mod.ts"; +export * as djwt from "https://deno.land/x/djwt@v3.0.2/mod.ts"; diff --git a/telegram-bot/deps/dotenv.ts b/telegram-bot/deps/dotenv.ts deleted file mode 100644 index e7d3708..0000000 --- a/telegram-bot/deps/dotenv.ts +++ /dev/null @@ -1 +0,0 @@ -export * as dotenv from "std/dotenv/mod.ts"; diff --git a/telegram-bot/deps/grammy.ts b/telegram-bot/deps/grammy.ts index 228d887..bf16cb0 100644 --- a/telegram-bot/deps/grammy.ts +++ b/telegram-bot/deps/grammy.ts @@ -1,5 +1,4 @@ -export * from "grammy/mod.ts"; +export * from "https://deno.land/x/grammy@v1.22.4/mod.ts"; export * from "https://deno.land/x/grammy_parse_mode@1.9.0/mod.ts"; -export type * from "grammy/types.deno.ts"; -export type * from "grammy_types/message.ts"; -export type * from "grammy_types/mod.ts"; +export type * from "https://deno.land/x/grammy@v1.22.4/types.deno.ts"; +export type * from "https://deno.land/x/grammy_types@v3.6.2/message.ts"; diff --git a/telegram-bot/deps/index.ts b/telegram-bot/deps/index.ts index 3f89a23..21dceba 100644 --- a/telegram-bot/deps/index.ts +++ b/telegram-bot/deps/index.ts @@ -3,7 +3,6 @@ export * from "./ammonia.ts"; export * from "./beabee-client.ts"; export * from "./beabee-common.ts"; export * from "./djwt.ts"; -export * from "./dotenv.ts"; export * from "./googleapis.ts"; export * from "./grammy.ts"; export * from "./jsonc.ts"; diff --git a/telegram-bot/deps/std.ts b/telegram-bot/deps/std.ts index 191f29f..2cbe496 100644 --- a/telegram-bot/deps/std.ts +++ b/telegram-bot/deps/std.ts @@ -1,4 +1,4 @@ -import { db as mediaTypeDb } from "https://deno.land/std@0.208.0/media_types/_db.ts"; +import { db as mediaTypeDb } from "https://deno.land/std@0.208.0/media_types/_db.ts"; // TODO: Update to the latest version of std import * as _mediaTypes from "https://deno.land/std@0.208.0/media_types/mod.ts"; export const mediaTypes = { ..._mediaTypes, db: mediaTypeDb }; @@ -6,4 +6,8 @@ export { dirname, fromFileUrl, join, -} from "https://deno.land/std@0.211.0/path/mod.ts"; +} from "https://deno.land/std@0.222.1/path/mod.ts"; + +export { equal } from "https://deno.land/std@0.222.1/assert/mod.ts"; + +export * as dotenv from "https://deno.land/std@0.222.1/dotenv/mod.ts"; diff --git a/telegram-bot/enums/replay-type.ts b/telegram-bot/enums/replay-type.ts index 2d3fa86..952778b 100644 --- a/telegram-bot/enums/replay-type.ts +++ b/telegram-bot/enums/replay-type.ts @@ -11,4 +11,6 @@ export enum ReplayType { CALLOUT_COMPONENT_SCHEMA = "callout-component-schema", /** No answer is needed */ NONE = "none", + /** Accept callback query */ + CALLBACK_QUERY_DATA = "callback_query", } diff --git a/telegram-bot/event-managers/callout-response.events.ts b/telegram-bot/event-managers/callout-response.events.ts index 1c2e9e3..bb82d23 100644 --- a/telegram-bot/event-managers/callout-response.events.ts +++ b/telegram-bot/event-managers/callout-response.events.ts @@ -2,6 +2,7 @@ import { Singleton } from "../deps/index.ts"; import { CalloutService } from "../services/callout.service.ts"; import { CommunicationService } from "../services/communication.service.ts"; import { EventService } from "../services/event.service.ts"; +import { BotService } from "../services/bot.service.ts"; import { TransformService } from "../services/transform.service.ts"; import { KeyboardService } from "../services/keyboard.service.ts"; import { StateMachineService } from "../services/state-machine.service.ts"; @@ -10,8 +11,11 @@ import { ResetCommand } from "../commands/reset.command.ts"; import { ListCommand } from "../commands/list.command.ts"; import { ChatState } from "../enums/index.ts"; import { - BUTTON_CALLBACK_CALLOUT_INTRO, - BUTTON_CALLBACK_CALLOUT_PARTICIPATE, + FALSY_MESSAGE_KEY, + INLINE_BUTTON_CALLBACK_CALLOUT_INTRO, + INLINE_BUTTON_CALLBACK_CALLOUT_PARTICIPATE, + INLINE_BUTTON_CALLBACK_PREFIX, + TRUTHY_MESSAGE_KEY, } from "../constants/index.ts"; import { BaseEventManager } from "../core/base.events.ts"; @@ -23,6 +27,7 @@ const SHOW_LIST_AFTER_RESPONSE = true; export class CalloutResponseEventManager extends BaseEventManager { constructor( protected readonly event: EventService, + protected readonly bot: BotService, protected readonly callout: CalloutService, protected readonly communication: CommunicationService, protected readonly messageRenderer: MessageRenderer, @@ -40,14 +45,14 @@ export class CalloutResponseEventManager extends BaseEventManager { public init() { // Listen for the callback query data event with the `callout-respond:yes` data this.event.on( - `callback_query:data:${BUTTON_CALLBACK_CALLOUT_INTRO}`, + `${INLINE_BUTTON_CALLBACK_PREFIX}:${INLINE_BUTTON_CALLBACK_CALLOUT_INTRO}`, (event) => { this.onCalloutIntroKeyboardPressed(event); }, ); this.event.on( - `callback_query:data:${BUTTON_CALLBACK_CALLOUT_PARTICIPATE}`, + `${INLINE_BUTTON_CALLBACK_PREFIX}:${INLINE_BUTTON_CALLBACK_CALLOUT_PARTICIPATE}`, (event) => { this.onCalloutParticipateKeyboardPressed(event); }, @@ -60,7 +65,6 @@ export class CalloutResponseEventManager extends BaseEventManager { const startResponse = data?.[2] as "continue" | "cancel" === "continue"; const session = await ctx.session; - // Remove the inline keyboard await this.keyboard.removeInlineKeyboard(ctx); if (!startResponse) { @@ -177,9 +181,10 @@ export class CalloutResponseEventManager extends BaseEventManager { protected async onCalloutIntroKeyboardPressed(ctx: AppContext) { const data = ctx.callbackQuery?.data?.split(":"); const shortSlug = data?.[1]; - const startIntro = data?.[2] as "yes" | "no" === "yes"; // This is the key, so it's not localized + const startIntro = + data?.[2] as typeof TRUTHY_MESSAGE_KEY | typeof FALSY_MESSAGE_KEY === + TRUTHY_MESSAGE_KEY; // This is the key, so it's not localized - // Remove the inline keyboard await this.keyboard.removeInlineKeyboard(ctx); if (!shortSlug) { diff --git a/telegram-bot/event-managers/callout.events.ts b/telegram-bot/event-managers/callout.events.ts index 1c8d853..125cb76 100644 --- a/telegram-bot/event-managers/callout.events.ts +++ b/telegram-bot/event-managers/callout.events.ts @@ -5,7 +5,10 @@ import { CalloutRenderer } from "../renderer/index.ts"; import { EventService } from "../services/event.service.ts"; import { KeyboardService } from "../services/keyboard.service.ts"; import { StateMachineService } from "../services/state-machine.service.ts"; -import { BUTTON_CALLBACK_SHOW_CALLOUT } from "../constants/index.ts"; +import { + INLINE_BUTTON_CALLBACK_PREFIX, + INLINE_BUTTON_CALLBACK_SHOW_CALLOUT, +} from "../constants/index.ts"; import { BaseEventManager } from "../core/base.events.ts"; import { ChatState } from "../enums/index.ts"; @@ -26,9 +29,9 @@ export class CalloutEventManager extends BaseEventManager { } public init() { - // Listen for the callback query data event with the `BUTTON_CALLBACK_SHOW_CALLOUT` data + // Listen for the callback query data event with the `INLINE_BUTTON_CALLBACK_SHOW_CALLOUT` data this.event.on( - `callback_query:data:${BUTTON_CALLBACK_SHOW_CALLOUT}`, + `${INLINE_BUTTON_CALLBACK_PREFIX}:${INLINE_BUTTON_CALLBACK_SHOW_CALLOUT}`, (event) => { this.onCalloutSelectionKeyboardPressed(event); }, diff --git a/telegram-bot/event-managers/telegram.events.ts b/telegram-bot/event-managers/telegram.events.ts index a6454ed..8a366a9 100644 --- a/telegram-bot/event-managers/telegram.events.ts +++ b/telegram-bot/event-managers/telegram.events.ts @@ -2,6 +2,7 @@ import { Singleton } from "../deps/index.ts"; import { EventService } from "../services/event.service.ts"; import { BotService } from "../services/bot.service.ts"; import { BaseEventManager } from "../core/base.events.ts"; +import { INLINE_BUTTON_CALLBACK_PREFIX } from "../constants/index.ts"; import type { AppContext } from "../types/index.ts"; @@ -21,7 +22,7 @@ export class TelegramEventManager extends BaseEventManager { public init() { // Forward callback query data, e.g. Telegram keyboard button presses this.bot.on( - "callback_query:data", + INLINE_BUTTON_CALLBACK_PREFIX, (ctx) => this.onCallbackQueryData(ctx), ); @@ -36,13 +37,13 @@ export class TelegramEventManager extends BaseEventManager { protected onCallbackQueryData(ctx: AppContext) { if (!ctx.callbackQuery?.data) { // Dispatch general callback event - this.event.emit("callback_query:data", ctx); + this.event.emit(INLINE_BUTTON_CALLBACK_PREFIX, ctx); return; } // Dispatch specific callback events this.event.emitDetailedEvents( - "callback_query:data:" + ctx.callbackQuery.data, + `${INLINE_BUTTON_CALLBACK_PREFIX}:${ctx.callbackQuery.data}`, ctx, ); } diff --git a/telegram-bot/locales/de.json b/telegram-bot/locales/de.json index ba09e16..7d61022 100644 --- a/telegram-bot/locales/de.json +++ b/telegram-bot/locales/de.json @@ -36,6 +36,8 @@ }, "info": { "messages": { + "answer": "Ihre Antwort:", + "answers": "Ihre Antworten:", "command": { "notUsable": "Der Befehl {command} kann hier nicht verwendet werden. Wenn Sie glauben, dass es sich um einen Fehler handelt, verwenden Sie bitte den Befehl /reset." }, @@ -55,6 +57,7 @@ "multipleNumbersAllowed": "Geben Sie eine oder mehrere Zahlen ein.", "multipleSelectionsAllowed": "Bitte geben Sie die Antwortnummer ein. Sie können mehrere Auswahlmöglichkeiten treffen, senden Sie einfach für jede eine separate Nachricht.", "multipleValuesAllowed": "Sie können mehrere Werte eingeben, indem Sie jeden Wert in einer separaten Nachricht senden.", + "noAnswerYet": "Du hast bisher keine Antwort gegeben", "onlyOneAddressAllowed": "Bitte geben Sie eine Adresse ein.", "onlyOneEmailAllowed": "Bitte geben Sie eine E-Mail-Adresse ein.", "onlyOneNumberAllowed": "Bitte geben Sie eine Zahl ein.", diff --git a/telegram-bot/locales/de@informal.json b/telegram-bot/locales/de@informal.json index aceabc2..c641566 100644 --- a/telegram-bot/locales/de@informal.json +++ b/telegram-bot/locales/de@informal.json @@ -36,6 +36,8 @@ }, "info": { "messages": { + "answer": "Deine Antwort:", + "answers": "Deine Antworten:", "command": { "notUsable": "Das Kommando {command} kann hier nicht verwendet werden. Wenn du glaubst, dass es sich um einen Fehler handelt, verwende bitte den Befehl /reset." }, @@ -55,6 +57,7 @@ "multipleNumbersAllowed": "Gib eine oder mehrere Zahlen ein.", "multipleSelectionsAllowed": "Bitte gib die Antwortnummer ein. Du kannst mehrere Optionen wählen, sende einfach für jede eine separate Nachricht.", "multipleValuesAllowed": "Du kannst mehrere Werte eingeben, indem du jeden Wert in einer separaten Nachricht sendest.", + "noAnswerYet": "Sie haben bisher keine Antwort gegeben", "onlyOneAddressAllowed": "Bitte gib eine Adresse ein.", "onlyOneEmailAllowed": "Bitte gib eine E-Mail-Adresse ein.", "onlyOneNumberAllowed": "Bitte gib eine Zahl ein.", diff --git a/telegram-bot/locales/en.json b/telegram-bot/locales/en.json index 09d8e4f..97d6fc9 100644 --- a/telegram-bot/locales/en.json +++ b/telegram-bot/locales/en.json @@ -36,11 +36,13 @@ }, "info": { "messages": { + "answer": "Your answer:", + "answers": "Your answers:", "command": { "notUsable": "You cannot use the {command} command here. If you think this is an error, please use the /reset command." }, "continueList": "Want to take part in a callout? Check out the list of open callouts and see which one piques your interest", - "done": "When you are finished with your response, just type \"{done}\".", + "done": "When you are finished with your response, just press or type \"{done}\".", "enterAmountOfMoney": "Please enter an amount of money.", "enterDate": "Please enter a date.", "enterLotsOfText": "Please reply with a single message.", @@ -55,6 +57,7 @@ "multipleNumbersAllowed": "Enter one or more numbers.", "multipleSelectionsAllowed": "Please type in the answer number.", "multipleValuesAllowed": "You can enter multiple values by sending each value in a separate message.", + "noAnswerYet": "You haven't given an answer yet", "onlyOneAddressAllowed": "Please enter an address.", "onlyOneEmailAllowed": "Please enter an e-mail address.", "onlyOneNumberAllowed": "Please enter a number.", diff --git a/telegram-bot/locales/nl.json b/telegram-bot/locales/nl.json index dae16fd..c66291d 100644 --- a/telegram-bot/locales/nl.json +++ b/telegram-bot/locales/nl.json @@ -36,6 +36,8 @@ }, "info": { "messages": { + "answer": "", + "answers": "", "command": { "notUsable": "" }, @@ -55,6 +57,7 @@ "multipleNumbersAllowed": "", "multipleSelectionsAllowed": "", "multipleValuesAllowed": "", + "noAnswerYet": "", "onlyOneAddressAllowed": "", "onlyOneEmailAllowed": "", "onlyOneNumberAllowed": "", diff --git a/telegram-bot/locales/pt.json b/telegram-bot/locales/pt.json index dae16fd..c66291d 100644 --- a/telegram-bot/locales/pt.json +++ b/telegram-bot/locales/pt.json @@ -36,6 +36,8 @@ }, "info": { "messages": { + "answer": "", + "answers": "", "command": { "notUsable": "" }, @@ -55,6 +57,7 @@ "multipleNumbersAllowed": "", "multipleSelectionsAllowed": "", "multipleValuesAllowed": "", + "noAnswerYet": "", "onlyOneAddressAllowed": "", "onlyOneEmailAllowed": "", "onlyOneNumberAllowed": "", diff --git a/telegram-bot/locales/ru.json b/telegram-bot/locales/ru.json index dae16fd..c66291d 100644 --- a/telegram-bot/locales/ru.json +++ b/telegram-bot/locales/ru.json @@ -36,6 +36,8 @@ }, "info": { "messages": { + "answer": "", + "answers": "", "command": { "notUsable": "" }, @@ -55,6 +57,7 @@ "multipleNumbersAllowed": "", "multipleSelectionsAllowed": "", "multipleValuesAllowed": "", + "noAnswerYet": "", "onlyOneAddressAllowed": "", "onlyOneEmailAllowed": "", "onlyOneNumberAllowed": "", diff --git a/telegram-bot/renderer/callout-response.renderer.ts b/telegram-bot/renderer/callout-response.renderer.ts index 42e3b6b..8f5d0d2 100644 --- a/telegram-bot/renderer/callout-response.renderer.ts +++ b/telegram-bot/renderer/callout-response.renderer.ts @@ -17,25 +17,26 @@ import { import { calloutComponentTypeToParsedResponseType, createCalloutGroupKey, - escapeHtml, escapeMd, - range, + getSelectionLabelNumberRange, sanitizeHtml, } from "../utils/index.ts"; -import { ParsedResponseType, RenderType } from "../enums/index.ts"; +import { ParsedResponseType, RenderType, ReplayType } from "../enums/index.ts"; import { KeyboardService } from "../services/keyboard.service.ts"; import { I18nService } from "../services/i18n.service.ts"; import { ConditionService } from "../services/condition.service.ts"; import { MessageRenderer } from "./message.renderer.ts"; import { - BUTTON_CALLBACK_CALLOUT_PARTICIPATE, EMPTY_RENDER, + INLINE_BUTTON_CALLBACK_CALLOUT_PARTICIPATE, + INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, } from "../constants/index.ts"; import type { GetCalloutDataWithExt, Render, RenderMarkdown, + ReplayAccepted, } from "../types/index.ts"; import { CalloutComponentInputSignatureSchema } from "../deps/index.ts"; @@ -124,40 +125,11 @@ export class CalloutResponseRenderer { const placeholder = input.placeholder as string | undefined; if (placeholder) { - result.markdown = `_${escapeMd( - this.i18n.t("bot.info.messages.placeholder", { placeholder }), - ) - }_`; - } - - return result; - } - - /** - * Render a note to the user how many answers are expected - * @param component The component to render this note for - * @param prefix The prefix, used to group the answers later (only used to group slides) - * @returns - */ - protected multipleMd(component: CalloutComponentSchema, prefix: string) { - const multiple = this.isMultiple(component); - const required = component.validate?.required || false; - const result: Render = { - key: createCalloutGroupKey(component.key, prefix), - type: RenderType.MARKDOWN, - accepted: this.condition.replayConditionNone(multiple, required), - markdown: ``, - parseType: ParsedResponseType.NONE, - forceReply: false, - }; - if (multiple) { - result.markdown += `\n\n_${escapeMd( - `${this.i18n.t("bot.info.messages.multipleValuesAllowed")}\n\n${this.messageRenderer.writeDoneMessage( - this.i18n.t("bot.reactions.messages.done"), - ).text - }`, - ) - }_`; + result.markdown = `_${ + escapeMd( + this.i18n.t("bot.info.messages.placeholder", { placeholder }), + ) + }_`; } return result; @@ -165,30 +137,20 @@ export class CalloutResponseRenderer { /** * Render a note to the user if the answer is required - * @param component The component to render this note for - * @param prefix The prefix, used to group the answers later (only used to group slides) + * @param required If the answer is required * @returns */ - protected requiredMd(component: CalloutComponentSchema, prefix: string) { - const multiple = this.isMultiple(component); - const required = component.validate?.required || false; - const result: Render = { - key: createCalloutGroupKey(component.key, prefix), - type: RenderType.MARKDOWN, - accepted: this.condition.replayConditionNone(multiple, required), - markdown: ``, - parseType: ParsedResponseType.NONE, - forceReply: false, - }; + protected requiredMd(required: boolean) { if (!required) { - result.markdown += `\n\n_${escapeMd( - this.messageRenderer.writeSkipMessage( - this.i18n.t("bot.reactions.messages.skip"), - ).text, - ) - }_`; + return `\n\n_${ + escapeMd( + this.messageRenderer.writeSkipMessage( + this.i18n.t("bot.reactions.messages.skip"), + ).text, + ) + }_`; } - return result; + return ""; } /** @@ -199,27 +161,26 @@ export class CalloutResponseRenderer { * @param component The component to render this note and keyboard for * @param prefix The prefix, used to group the answers later (only used to group slides) */ - protected answerOptionsMdKeyboard( + protected answerOptionsMdInlineKeyboard( result: RenderMarkdown, component: CalloutComponentSchema, prefix: string, - multiple = this.isMultiple(component), required = component.validate?.required || false, ) { const placeholder = component.placeholder; if (placeholder) { - result.markdown += `\n\n${this.placeholderMd(component, prefix).markdown - }`; + result.markdown += `\n\n${ + this.placeholderMd(component, prefix).markdown + }`; } - result.markdown += `${this.multipleMd(component, prefix).markdown}`; - result.markdown += `${this.requiredMd(component, prefix).markdown}`; + result.markdown += `${this.requiredMd(required)}`; - result.keyboard = this.keyboard.skipDone( - result.keyboard, + result.inlineKeyboard = this.keyboard.inlineSkip( + INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, + result.inlineKeyboard, required, - multiple, ); return result; @@ -338,12 +299,13 @@ export class CalloutResponseRenderer { * @returns The note in Markdown */ protected howManyFilesMd(multiple?: boolean): string { - return `_${escapeMd( - multiple - ? this.i18n.t("bot.info.messages.uploadFilesHere") - : this.i18n.t("bot.info.messages.uploadFileHere"), - ) - }_`; + return `_${ + escapeMd( + multiple + ? this.i18n.t("bot.info.messages.uploadFilesHere") + : this.i18n.t("bot.info.messages.uploadFileHere"), + ) + }_`; } /** @@ -352,12 +314,13 @@ export class CalloutResponseRenderer { * @returns The note in Markdown */ protected howManyAddressesMd(multiple?: boolean): string { - return `_${escapeMd( - multiple - ? this.i18n.t("bot.info.messages.multipleAddressesAllowed") - : this.i18n.t("bot.info.messages.onlyOneAddressAllowed"), - ) - }_`; + return `_${ + escapeMd( + multiple + ? this.i18n.t("bot.info.messages.multipleAddressesAllowed") + : this.i18n.t("bot.info.messages.onlyOneAddressAllowed"), + ) + }_`; } /** @@ -366,12 +329,13 @@ export class CalloutResponseRenderer { * @returns The note in Markdown */ protected howManyEmailsMd(multiple?: boolean): string { - return `_${escapeMd( - multiple - ? this.i18n.t("bot.info.messages.multipleEmailsAllowed") - : this.i18n.t("bot.info.messages.onlyOneEmailAllowed"), - ) - }_`; + return `_${ + escapeMd( + multiple + ? this.i18n.t("bot.info.messages.multipleEmailsAllowed") + : this.i18n.t("bot.info.messages.onlyOneEmailAllowed"), + ) + }_`; } /** @@ -380,21 +344,23 @@ export class CalloutResponseRenderer { * @returns The note in Markdown */ protected howManyNumbersMd(multiple?: boolean): string { - return `_${escapeMd( - multiple - ? this.i18n.t("bot.info.messages.multipleNumbersAllowed") - : this.i18n.t("bot.info.messages.onlyOneNumberAllowed"), - ) - }_`; + return `_${ + escapeMd( + multiple + ? this.i18n.t("bot.info.messages.multipleNumbersAllowed") + : this.i18n.t("bot.info.messages.onlyOneNumberAllowed"), + ) + }_`; } protected howManySelectionsMd(multiple?: boolean): string { - return `_${escapeMd( - multiple - ? this.i18n.t("bot.info.messages.multipleSelectionsAllowed") - : this.i18n.t("bot.info.messages.onlyOneSelectionAllowed"), - ) - }_`; + return `_${ + escapeMd( + multiple + ? this.i18n.t("bot.info.messages.multipleSelectionsAllowed") + : this.i18n.t("bot.info.messages.onlyOneSelectionAllowed"), + ) + }_`; } protected textTypeMd( @@ -403,15 +369,17 @@ export class CalloutResponseRenderer { | CalloutComponentType.INPUT_TEXT_AREA, ) { if (type === CalloutComponentType.INPUT_TEXT_FIELD) { - return `_${escapeMd( - this.i18n.t("bot.info.messages.enterText"), - ) - }_`; + return `_${ + escapeMd( + this.i18n.t("bot.info.messages.enterText"), + ) + }_`; } else if (type === CalloutComponentType.INPUT_TEXT_AREA) { - return `_${escapeMd( - this.i18n.t("bot.info.messages.enterLotsOfText"), - ) - }_`; + return `_${ + escapeMd( + this.i18n.t("bot.info.messages.enterLotsOfText"), + ) + }_`; } } @@ -439,7 +407,12 @@ export class CalloutResponseRenderer { file.filePattern || file.type === "signature" ? "image/*" : "", ); - this.answerOptionsMdKeyboard(result, file, prefix); + this.answerOptionsMdInlineKeyboard( + result, + file, + prefix, + required, + ); return result; } @@ -463,10 +436,11 @@ export class CalloutResponseRenderer { ) { let html = ""; - if (content.label) { - `${escapeHtml(content.label)}`; - html += `%0A%0A`; // %0A is a newline - } + // TODO: Should the label rendered? + // if (content.label.trim().length) { + // `${escapeHtml(content.label)}`; + // html += `\n\n`; + // } html += `${sanitizeHtml(content.html)}`; @@ -479,7 +453,6 @@ export class CalloutResponseRenderer { false, ), parseType: ParsedResponseType.NONE, - removeKeyboard: true, forceReply: false, }; @@ -494,20 +467,27 @@ export class CalloutResponseRenderer { result.parseType = ParsedResponseType.BOOLEAN; result.markdown += `\n\n`; - const truthyMessage = this.i18n.t("bot.reactions.messages.truthy"); - const falsyMessage = this.i18n.t("bot.reactions.messages.falsy"); - const doneMessage = this.i18n.t("bot.reactions.messages.done"); - const skipMessage = this.i18n.t("bot.reactions.messages.skip"); + const truthyMessageKey = "bot.reactions.messages.truthy"; + const falsyMessageKey = "bot.reactions.messages.falsy"; + const doneMessageKey = "bot.reactions.messages.done"; + const skipMessageKey = "bot.reactions.messages.skip"; + + const truthyMessage = this.i18n.t(truthyMessageKey); + const falsyMessage = this.i18n.t(falsyMessageKey); + const doneMessage = this.i18n.t(doneMessageKey); + const skipMessage = this.i18n.t(skipMessageKey); + const multiple = this.isMultiple(input); const required = result.accepted.required; - result.markdown += `_${escapeMd( - this.i18n.t("bot.response.messages.answerWithTruthyOrFalsy", { - truthy: truthyMessage, - falsy: falsyMessage, - }), - ) - }_`; + result.markdown += `_${ + escapeMd( + this.i18n.t("bot.response.messages.answerWithTruthyOrFalsy", { + truthy: truthyMessage, + falsy: falsyMessage, + }), + ) + }_`; result.accepted = this.condition.replayConditionText( multiple, @@ -517,13 +497,19 @@ export class CalloutResponseRenderer { !required ? [skipMessage] : [], ); - result.keyboard = this.keyboard.yesNo( - result.keyboard, + result.inlineKeyboard = this.keyboard.inlineYesNo( + INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, + result.inlineKeyboard, truthyMessage, falsyMessage, ); - this.answerOptionsMdKeyboard(result, input, prefix); + this.answerOptionsMdInlineKeyboard( + result, + input, + prefix, + required, + ); return result; } @@ -537,6 +523,7 @@ export class CalloutResponseRenderer { const result = this.baseComponent(input, prefix); result.markdown += `\n\n`; const multiple = this.isMultiple(input); + const required = result.accepted.required; switch (input.type) { case CalloutComponentType.INPUT_ADDRESS: { @@ -557,38 +544,43 @@ export class CalloutResponseRenderer { break; } case CalloutComponentType.INPUT_PHONE_NUMBER: { - result.markdown += `_${escapeMd( - this.i18n.t("bot.info.messages.enterTelephoneNumber"), - ) - }_`; + result.markdown += `_${ + escapeMd( + this.i18n.t("bot.info.messages.enterTelephoneNumber"), + ) + }_`; break; } case CalloutComponentType.INPUT_CURRENCY: { - result.markdown += `_${escapeMd( - this.i18n.t("bot.info.messages.enterAmountOfMoney"), - ) - }_`; + result.markdown += `_${ + escapeMd( + this.i18n.t("bot.info.messages.enterAmountOfMoney"), + ) + }_`; break; } case CalloutComponentType.INPUT_DATE_TIME: { - result.markdown += `_${escapeMd( - this.i18n.t("bot.info.messages.enterDate"), - ) - }_`; + result.markdown += `_${ + escapeMd( + this.i18n.t("bot.info.messages.enterDate"), + ) + }_`; break; } case CalloutComponentType.INPUT_TIME: { - result.markdown += `_${escapeMd( - this.i18n.t("bot.info.messages.enterTime"), - ) - }_`; + result.markdown += `_${ + escapeMd( + this.i18n.t("bot.info.messages.enterTime"), + ) + }_`; break; } case CalloutComponentType.INPUT_URL: { - result.markdown += `_${escapeMd( - this.i18n.t("bot.info.messages.enterUrl"), - ) - }_`; + result.markdown += `_${ + escapeMd( + this.i18n.t("bot.info.messages.enterUrl"), + ) + }_`; break; } @@ -603,7 +595,7 @@ export class CalloutResponseRenderer { } } - this.answerOptionsMdKeyboard(result, input, prefix, multiple); + this.answerOptionsMdInlineKeyboard(result, input, prefix, required); return result; } @@ -632,19 +624,26 @@ export class CalloutResponseRenderer { valueLabel, ), }; - result.markdown += `\n${this.selectValues(select, prefix, valueLabel).markdown - }`; + result.markdown += `\n${ + this.selectValues(select, prefix, valueLabel).markdown + }`; result.markdown += `\n\n`; result.markdown += this.howManySelectionsMd(multiple); - result.keyboard = this.keyboard.selection( - result.keyboard, - range(1, Object.keys(valueLabel).length).map(String), + result.inlineKeyboard = this.keyboard.inlineSelection( + INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, + result.inlineKeyboard, + getSelectionLabelNumberRange(valueLabel), ); - this.answerOptionsMdKeyboard(result, select, prefix); + this.answerOptionsMdInlineKeyboard( + result, + select, + prefix, + required, + ); return result; } @@ -673,21 +672,22 @@ export class CalloutResponseRenderer { ), }; - result.markdown += `\n${this.selectableValues(selectable, prefix, valueLabel).markdown - }`; + result.markdown += `\n${ + this.selectableValues(selectable, prefix, valueLabel).markdown + }`; result.markdown += this.howManySelectionsMd(multiple); - result.keyboard = this.keyboard.selection( - result.keyboard, - range(1, Object.keys(valueLabel).length).map(String), + result.inlineKeyboard = this.keyboard.inlineSelection( + INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, + result.inlineKeyboard, + getSelectionLabelNumberRange(valueLabel), ); - this.answerOptionsMdKeyboard( + this.answerOptionsMdInlineKeyboard( result, selectable, prefix, - multiple, required, ); @@ -799,7 +799,6 @@ export class CalloutResponseRenderer { type: (component as CalloutComponentSchema).type || "undefined", }), parseType: calloutComponentTypeToParsedResponseType(component), - removeKeyboard: true, forceReply: false, }; results.push(unknown); @@ -817,13 +816,12 @@ export class CalloutResponseRenderer { accepted: this.condition.replayConditionNone(), html: "", parseType: ParsedResponseType.NONE, - removeKeyboard: true, forceReply: false, }; result.html = `${sanitizeHtml(callout.intro)}`; const continueKeyboard = this.keyboard.inlineContinueCancel( - `${BUTTON_CALLBACK_CALLOUT_PARTICIPATE}:${callout.slug}`, + `${INLINE_BUTTON_CALLBACK_CALLOUT_PARTICIPATE}:${callout.slug}`, ); result.inlineKeyboard = continueKeyboard; @@ -840,7 +838,6 @@ export class CalloutResponseRenderer { accepted: this.condition.replayConditionNone(), html: ``, parseType: ParsedResponseType.NONE, - removeKeyboard: true, forceReply: false, afterDelay: 3000, }; @@ -877,4 +874,57 @@ export class CalloutResponseRenderer { return [...slidesRenders, thankYou]; } + + public answersGiven(answers: ReplayAccepted[], multiple: boolean) { + const tKey = answers.length === 0 + ? "bot.info.messages.no-answer-yet" + : answers.length === 1 + ? "bot.info.messages.answer" + : "bot.info.messages.answers"; + + const result: Render = { + key: "answers-given", + type: RenderType.MARKDOWN, + accepted: this.condition.replayConditionNone(), + markdown: `*${escapeMd(this.i18n.t(tKey))}*`, + parseType: ParsedResponseType.NONE, + forceReply: false, + }; + + for (const answer of answers) { + switch (answer.type) { + case ReplayType.TEXT: + result.markdown += `\n • ${escapeMd(answer.text || "")}`; + break; + case ReplayType.SELECTION: + result.markdown += `\n • ${escapeMd(answer.label || "")}`; + break; + case ReplayType.FILE: + result.markdown += `\n • ${escapeMd("")}`; + break; + case ReplayType.CALLBACK_QUERY_DATA: + result.markdown += `\n • ${escapeMd(answer.data)}`; + break; + case ReplayType.CALLOUT_COMPONENT_SCHEMA: + if (Array.isArray(answer.answer)) { + for (const a of answer.answer) { + result.markdown += `\n • ${escapeMd(a)}`; + } + } else { + result.markdown += `\n • ${ + escapeMd(answer.answer?.toString() || "") + }`; + } + break; + } + } + + if (multiple) { + result.markdown += `\n\n${ + escapeMd(this.messageRenderer.writeDoneMessage().text) + }`; + } + + return result; + } } diff --git a/telegram-bot/renderer/callout.renderer.ts b/telegram-bot/renderer/callout.renderer.ts index a39b1b6..56a8660 100644 --- a/telegram-bot/renderer/callout.renderer.ts +++ b/telegram-bot/renderer/callout.renderer.ts @@ -1,7 +1,7 @@ import { InputFile, InputMediaBuilder, Singleton } from "../deps/index.ts"; import { downloadImage, escapeMd } from "../utils/index.ts"; import { ParsedResponseType, RenderType } from "../enums/index.ts"; -import { BUTTON_CALLBACK_CALLOUT_INTRO } from "../constants/index.ts"; +import { INLINE_BUTTON_CALLBACK_CALLOUT_INTRO } from "../constants/index.ts"; import { ConditionService } from "../services/condition.service.ts"; import { KeyboardService } from "../services/keyboard.service.ts"; @@ -29,7 +29,7 @@ export class CalloutRenderer { } /** - * @fires `callback_query:data:${BUTTON_CALLBACK_CALLOUT_INTRO}` + * @fires `${INLINE_BUTTON_CALLBACK_PREFIX}:${INLINE_BUTTON_CALLBACK_CALLOUT_INTRO}` * * @param callout * @returns @@ -37,10 +37,11 @@ export class CalloutRenderer { protected startResponseKeyboard( callout: CalloutDataExt, ): Render { - const keyboardMessageMd = `_${escapeMd(this.i18n.t("bot.response.messages.calloutStartResponse")) - }_`; + const keyboardMessageMd = `_${ + escapeMd(this.i18n.t("bot.response.messages.calloutStartResponse")) + }_`; const yesNoInlineKeyboard = this.keyboard.inlineYesNo( - `${BUTTON_CALLBACK_CALLOUT_INTRO}:${callout.shortSlug}`, + `${INLINE_BUTTON_CALLBACK_CALLOUT_INTRO}:${callout.shortSlug}`, ); const result: Render = { @@ -50,7 +51,6 @@ export class CalloutRenderer { inlineKeyboard: yesNoInlineKeyboard, accepted: this.condition.replayConditionNone(), parseType: ParsedResponseType.NONE, - forceReply: true, }; return result; } @@ -70,8 +70,6 @@ export class CalloutRenderer { markdown: `${listChar} ${this.title(callout).markdown}\n`, accepted: this.condition.replayConditionNone(), parseType: ParsedResponseType.NONE, - removeKeyboard: true, - forceReply: false, }; return result; @@ -91,8 +89,6 @@ export class CalloutRenderer { markdown: "", accepted: this.condition.replayConditionNone(), parseType: ParsedResponseType.NONE, - removeKeyboard: true, - forceReply: false, }; if (callouts.items.length === 0) { @@ -102,8 +98,9 @@ export class CalloutRenderer { return [listResult]; } - listResult.markdown = `*${escapeMd(this.i18n.t("bot.render.callout.list.title")) - }*\n\n`; + listResult.markdown = `*${ + escapeMd(this.i18n.t("bot.render.callout.list.title")) + }*\n\n`; let p = 1; for (const callout of callouts.items) { listResult.markdown += `${this.listItem(callout, `${p}.`).markdown}`; @@ -111,8 +108,9 @@ export class CalloutRenderer { } const inlineKeyboard = this.keyboard.inlineCalloutSelection(callouts.items); - const keyboardMessageMd = `_${escapeMd(this.i18n.t("bot.keyboard.message.select-detail-callout")) - }_`; + const keyboardMessageMd = `_${ + escapeMd(this.i18n.t("bot.keyboard.message.select-detail-callout")) + }_`; const keyboardResult: Render = { key: "callout:list:keyboard", @@ -121,8 +119,6 @@ export class CalloutRenderer { inlineKeyboard, accepted: this.condition.replayConditionNone(), parseType: ParsedResponseType.NONE, - removeKeyboard: true, - forceReply: false, }; return [listResult, keyboardResult]; @@ -141,7 +137,6 @@ export class CalloutRenderer { markdown: "", accepted: this.condition.replayConditionNone(), parseType: ParsedResponseType.NONE, - forceReply: false, }; const title = escapeMd(callout.title); @@ -179,8 +174,6 @@ export class CalloutRenderer { photo: calloutImage, accepted: this.condition.replayConditionNone(), parseType: ParsedResponseType.NONE, - removeKeyboard: true, - forceReply: false, }; const keyboardResult = this.startResponseKeyboard(callout); diff --git a/telegram-bot/renderer/message.renderer.ts b/telegram-bot/renderer/message.renderer.ts index 6c48c46..ad96b4c 100644 --- a/telegram-bot/renderer/message.renderer.ts +++ b/telegram-bot/renderer/message.renderer.ts @@ -343,8 +343,10 @@ export class MessageRenderer { } as RenderText; } - public writeDoneMessage(doneText: string): RenderText { - const tKey = "bot.info.messages.done"; + public writeDoneMessage( + tKey = "bot.info.messages.done", + doneText = this.i18n.t("bot.reactions.messages.done"), + ): RenderText { return { type: RenderType.TEXT, text: this.i18n.t(tKey, { done: doneText }), diff --git a/telegram-bot/services/communication.service.ts b/telegram-bot/services/communication.service.ts index 5dee7cf..13e293a 100644 --- a/telegram-bot/services/communication.service.ts +++ b/telegram-bot/services/communication.service.ts @@ -1,12 +1,34 @@ import { BaseService } from "../core/index.ts"; -import { fmt, Message, ParseModeFlavor, Singleton, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply } from "../deps/index.ts"; +import { + fmt, + ForceReply, + InlineKeyboardMarkup, + Message, + ParseModeFlavor, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + Singleton, +} from "../deps/index.ts"; import { ParsedResponseType, RenderType, ReplayType } from "../enums/index.ts"; + import { EventService } from "./event.service.ts"; +import { KeyboardService } from "./keyboard.service.ts"; import { TransformService } from "./transform.service.ts"; import { ConditionService } from "./condition.service.ts"; import { ValidationService } from "./validation.service.ts"; -import { getIdentifier, sleep } from "../utils/index.ts"; -import { MessageRenderer } from "../renderer/message.renderer.ts"; +import { I18nService } from "../services/i18n.service.ts"; + +import { + getIdentifier, + getSelectionLabelNumberRange, + sleep, +} from "../utils/index.ts"; +import { CalloutResponseRenderer, MessageRenderer } from "../renderer/index.ts"; +import { + CHECKMARK, + INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, + INLINE_BUTTON_CALLBACK_PREFIX, +} from "../constants/index.ts"; import type { AppContext, @@ -15,7 +37,6 @@ import type { RenderResponseParsed, ReplayAccepted, } from "../types/index.ts"; -import { InlineKeyboard } from "../deps/grammy.ts"; /** * Service to handle the communication with the telegram bot and the telegram user. @@ -25,10 +46,13 @@ import { InlineKeyboard } from "../deps/grammy.ts"; export class CommunicationService extends BaseService { constructor( protected readonly event: EventService, + protected readonly keyboard: KeyboardService, protected readonly messageRenderer: MessageRenderer, + protected readonly calloutResponseRenderer: CalloutResponseRenderer, protected readonly transform: TransformService, protected readonly condition: ConditionService, protected readonly validation: ValidationService, + protected readonly i18n: I18nService, ) { super(); console.debug(`${this.constructor.name} created`); @@ -37,10 +61,8 @@ export class CommunicationService extends BaseService { /** * Reply to a Telegram message or action with a single render object * - * @todo: Make use of https://grammy.dev/plugins/parse-mode - * * @param ctx - * @param res + * @param render */ public async send(ctx: AppContext, render: Render) { if (render.keyboard && render.inlineKeyboard) { @@ -51,19 +73,21 @@ export class CommunicationService extends BaseService { await sleep(render.beforeDelay); } - // link previews are disabled by default + // link previews are disabled by default, define render.linkPreview to enable them if (!render.linkPreview) { render.linkPreview = { is_disabled: true, }; } - let markup: InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | undefined = render.keyboard || render.inlineKeyboard || undefined; - // (render.removeKeyboard ? { remove_keyboard: true as true } : undefined); - (render.forceReply ? { force_reply: true as true } : undefined); + let markup: + | InlineKeyboardMarkup + | ReplyKeyboardMarkup + | ReplyKeyboardRemove + | ForceReply + | undefined = render.keyboard || render.inlineKeyboard || undefined; - - if (!markup && render.removeKeyboard) { + if (!markup && render.removeCustomKeyboard) { markup = { remove_keyboard: true } as ReplyKeyboardRemove; } @@ -71,11 +95,11 @@ export class CommunicationService extends BaseService { markup = { force_reply: true } as ForceReply; } - let message: Message.TextMessage | undefined; + let message: Message | undefined; switch (render.type) { case RenderType.PHOTO: - await ctx.replyWithMediaGroup([render.photo], {}); + message = (await ctx.replyWithMediaGroup([render.photo], {}))[0]; if (render.keyboard) { message = await ctx.reply("", { link_preview_options: render.linkPreview, @@ -120,24 +144,117 @@ export class CommunicationService extends BaseService { throw new Error("Unknown render type: " + (render as Render).type); } - // Store latest sended keyboard to be able to remove it later if the keyboard is not empty - // TODO: Should we move this to the `KeyboardService` or `StateMachineService`? - if ( - message && markup && markup instanceof InlineKeyboard && - markup.inline_keyboard.entries.length > 0 - ) { - const session = await ctx.session; - session._data.latestKeyboard = { - message_id: message.message_id, - chat_id: message.chat.id, - type: "inline", - inlineKeyboard: markup, + if (message && markup) { + await this.keyboard.storeLatestInSession(ctx, markup, message); + } + + if (render.afterDelay) { + await sleep(render.afterDelay); + } + + return message; + } + + /** + * Edit a Telegram message or action with a single render object + * + * @param ctx + * @param render + */ + public async edit( + ctx: AppContext, + oldMessage: { message_id: number; chat: { id: number } }, + render: Render, + ) { + const message = ctx.message; + if (render.keyboard && render.inlineKeyboard) { + throw new Error("You can only use one keyboard at a time"); + } + + if (render.beforeDelay) { + await sleep(render.beforeDelay); + } + + // link previews are disabled by default, define render.linkPreview to enable them + if (!render.linkPreview) { + render.linkPreview = { + is_disabled: true, }; } + if (render.keyboard) { + console.warn("Edit message with custom keyboard not implemented"); + } + + if (render.removeCustomKeyboard) { + console.warn("Remove custom keyboard not implemented on edit"); + } + + if (render.forceReply) { + console.warn("Force reply not implemented on edit"); + } + + const markup: + | InlineKeyboardMarkup + | undefined = render.inlineKeyboard || undefined; + + switch (render.type) { + case RenderType.PHOTO: + throw new Error("Edit media not implemented"); + case RenderType.MARKDOWN: + await ctx.api.editMessageText( + oldMessage.chat.id, + oldMessage.message_id, + render.markdown, + { + parse_mode: "MarkdownV2", + link_preview_options: render.linkPreview, + reply_markup: markup, + }, + ); + break; + case RenderType.HTML: + await ctx.api.editMessageText( + oldMessage.chat.id, + oldMessage.message_id, + render.html, + { + parse_mode: "HTML", + link_preview_options: render.linkPreview, + reply_markup: markup, + }, + ); + break; + case RenderType.TEXT: + await ctx.api.editMessageText( + oldMessage.chat.id, + oldMessage.message_id, + render.text, + { + link_preview_options: render.linkPreview, + reply_markup: markup, + }, + ); + break; + // See https://grammy.dev/plugins/parse-mode + case RenderType.FORMAT: + throw new Error("Edit format not implemented on edit"); + case RenderType.EMPTY: + // Do nothing + break; + default: + throw new Error("Unknown render type: " + (render as Render).type); + } + + if (message && markup) { + await this.keyboard.storeLatestInSession(ctx, markup, message); + } + if (render.afterDelay) { await sleep(render.afterDelay); } + + return message; } /** @@ -146,13 +263,162 @@ export class CommunicationService extends BaseService { * @param ctx * @returns */ - public async receiveMessage(ctx: AppContext) { - const data = await this.event.onceUserMessageAsync(getIdentifier(ctx)); - return data; + public async receiveMessageOrCallbackQueryData( + ctx: AppContext, + signal: AbortSignal | null, + ) { + const userId = getIdentifier(ctx); + const eventName = + `${INLINE_BUTTON_CALLBACK_PREFIX}:${INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE}`; + + return await new Promise((resolve) => { + const onMessage = (ctx: AppContext) => { + this.event.offUser(eventName, userId, onInteractionCallbackQueryData); + signal?.removeEventListener("abort", onAbort); + resolve(ctx); + }; + + // TODO: Any elegant way to move this to the CalloutResponseEventManager? + const onInteractionCallbackQueryData = (ctx: AppContext) => { + this.event.offUserMessage(userId, onMessage); + signal?.removeEventListener("abort", onAbort); + resolve(ctx); + }; + + const onAbort = () => { + this.event.offUser(eventName, userId, onInteractionCallbackQueryData); + this.event.offUserMessage(userId, onMessage); + }; + + signal?.addEventListener("abort", onAbort, { once: true }); + this.event.onceUserMessage(userId, onMessage); + this.event.onceUser(eventName, userId, onInteractionCallbackQueryData); + }); } /** - * Wait for a specific message to be received and collect all messages of type `acceptedBefore` until the specific message is received. + * Render or update accepted answers to give the user feedback, also renders updates the inline keyboard + * @param ctx + * @param render + * @param replays + * @param lastGivenAnswerMessage + */ + protected async sendOrEditAnswersGivenAndKeyboard( + ctx: AppContext, + render: Render, + replays: ReplayAccepted[], + lastGivenAnswerMessage: Message | undefined, + ) { + const renderAnswers = this.calloutResponseRenderer.answersGiven( + replays, + render.accepted.multiple, + ); + + let inlineKeyboard = renderAnswers.inlineKeyboard || render.inlineKeyboard; + + if (render.accepted.multiple) { + if (inlineKeyboard) { + // Remove skip button + renderAnswers.inlineKeyboard = this.keyboard.removeInlineButton( + inlineKeyboard, + `${INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE}:skip`, + ); + inlineKeyboard = renderAnswers.inlineKeyboard; + + // Add done button + renderAnswers.inlineKeyboard = this.keyboard.addInlineButton( + inlineKeyboard, + this.keyboard.inlineDoneButton(), + ); + inlineKeyboard = renderAnswers.inlineKeyboard; + + // Remove already selected answers from keyboard + if (render.accepted.type === ReplayType.SELECTION) { + const numberLabels = getSelectionLabelNumberRange( + render.accepted.valueLabel, + ); + for (const numberLabel of numberLabels) { + const cbQueryData = + `${INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE}:${numberLabel}`; + const alreadyUsed = replays.find((replay) => + replay.context.callbackQuery?.data === cbQueryData + ); + if (alreadyUsed) { + renderAnswers.inlineKeyboard = this.keyboard.renameInlineButton( + inlineKeyboard, + cbQueryData, + numberLabel + " " + CHECKMARK, + ); + inlineKeyboard = renderAnswers.inlineKeyboard; + } else { + renderAnswers.inlineKeyboard = this.keyboard.renameInlineButton( + inlineKeyboard, + cbQueryData, + numberLabel, + ); + inlineKeyboard = renderAnswers.inlineKeyboard; + } + } + } + } + } + + if (lastGivenAnswerMessage) { + this.edit( + ctx, + lastGivenAnswerMessage, + renderAnswers, + ); + } else { + lastGivenAnswerMessage = await this.send( + ctx, + renderAnswers, + ); + } + + return lastGivenAnswerMessage; + } + + protected equalReplayAnswer( + replay1: ReplayAccepted, + replay2: ReplayAccepted, + ) { + // If the replay is the same reference, return true + if (replay1 === replay2) { + return true; + } + + if (replay1.type !== replay2.type) { + return false; + } + + if (replay1.isDoneMessage !== replay2.isDoneMessage) { + return false; + } + + if (replay1.isSkipMessage !== replay2.isSkipMessage) { + return false; + } + + switch (replay1.type) { + case ReplayType.NONE: + return true; + case ReplayType.TEXT: + return replay1.text === (replay2 as typeof replay1).text; + case ReplayType.SELECTION: + return replay1.value === (replay2 as typeof replay1).value; + case ReplayType.FILE: + return replay1.fileId === (replay2 as typeof replay1).fileId; + case ReplayType.CALLOUT_COMPONENT_SCHEMA: + return replay1.answer === (replay2 as typeof replay1).answer; + } + + console.warn("Unknown replay type", replay1.type); + return false; + } + + /** + * Wait for a specific message to be received and collect all messages of type `render.accepted` until the specific message is received. * @param ctx * @param render * @returns @@ -160,10 +426,13 @@ export class CommunicationService extends BaseService { protected async acceptedUntilSpecificMessage( ctx: AppContext, render: Render, + signal: AbortSignal | null, ) { let context: AppContext; let message: Message | undefined; - const replays: ReplayAccepted[] = []; + let callbackQueryData: string | undefined; + let lastGivenAnswerMessage: Message | undefined; + let replays: ReplayAccepted[] = []; if (render.accepted.type === ReplayType.NONE) { return []; @@ -172,19 +441,31 @@ export class CommunicationService extends BaseService { let replayAccepted: ReplayAccepted | undefined; do { - context = await this.receiveMessage(ctx); + if (signal?.aborted) { + return replays; + } + + context = await this.receiveMessageOrCallbackQueryData(ctx, signal); message = context.message; + callbackQueryData = context.callbackQuery?.data; - if (!message) { - console.warn("Message is undefined"); - continue; + // Answer send using a inline keyboard button + if (callbackQueryData) { + replayAccepted = this.validation.callbackQueryDataIsAccepted( + context, + render.accepted, + ); + await this.answerCallbackQuery(context); + } // Answer send using a message + else if (message) { + replayAccepted = this.validation.messageIsAccepted( + context, + render.accepted, + ); + } else { + throw new Error("Message and callback query data are undefined"); } - replayAccepted = this.validation.messageIsAccepted( - context, - render.accepted, - ); - if (!replayAccepted.accepted) { await this.send( ctx, @@ -196,17 +477,37 @@ export class CommunicationService extends BaseService { continue; } + await this.keyboard.removeLastInlineKeyboard(context); + if (replayAccepted.isDoneMessage) { // Return what we have return replays; } if (replayAccepted.isSkipMessage) { - // Only return the skip message + // Only return the skip message (handled later) return [replayAccepted]; } - replays.push(replayAccepted); + const replayAlreadyTaken = replays.find((replay) => + this.equalReplayAnswer(replay, replayAccepted as ReplayAccepted) + ); + + // If answewr is a selection and it is already in the replays, remove it + if (replayAccepted.type === ReplayType.SELECTION && replayAlreadyTaken) { + replays = replays.filter((replay) => replay !== replayAlreadyTaken); + } else { + // Answer accepted + replays.push(replayAccepted); + } + + // Send or edit given answers message + lastGivenAnswerMessage = await this.sendOrEditAnswersGivenAndKeyboard( + context, + render, + replays, + lastGivenAnswerMessage, + ); if (!render.accepted.multiple) { return replays; @@ -225,6 +526,7 @@ export class CommunicationService extends BaseService { public async receive( ctx: AppContext, render: Render, + signal: AbortSignal | null, ): Promise> { // Do not wait for any specific message if (!render.accepted || render.accepted.type === ReplayType.NONE) { @@ -237,7 +539,11 @@ export class CommunicationService extends BaseService { } // Receive all messages of specific type until a message of specific type is received - const replays = await this.acceptedUntilSpecificMessage(ctx, render); + const replays = await this.acceptedUntilSpecificMessage( + ctx, + render, + signal, + ); // Parse multiple messages if (render.accepted.multiple) { @@ -268,10 +574,14 @@ export class CommunicationService extends BaseService { * @param render * @returns */ - public async sendAndReceive(ctx: AppContext, render: Render) { + public async sendAndReceive( + ctx: AppContext, + render: Render, + signal: AbortSignal | null, + ) { await this.send(ctx, render); - const responses = await this.receive(ctx, render); + const responses = await this.receive(ctx, render, signal); const response: RenderResponse = { render, responses, @@ -295,7 +605,7 @@ export class CommunicationService extends BaseService { return signal; } try { - const response = await this.sendAndReceive(ctx, render); + const response = await this.sendAndReceive(ctx, render, signal); if (response) { responses.push(response); } diff --git a/telegram-bot/services/event.service.ts b/telegram-bot/services/event.service.ts index 40f4ec1..8f05cf7 100644 --- a/telegram-bot/services/event.service.ts +++ b/telegram-bot/services/event.service.ts @@ -25,19 +25,19 @@ export class EventService extends BaseService { * Emits a series of detailed events based on a given event name. * The method takes an event name, splits it by the ':' character, * and emits progressively more detailed events for each segment. - * @param eventName E.g. "callback_query:data:show-callout-slug:my-callout" + * @param eventName E.g. "${INLINE_BUTTON_CALLBACK_PREFIX}:show-callout-slug:my-callout" * @param ctx The Telegram context * - * For example, given the event name 'callback_query:data:show-callout-slug:my-callout', + * For example, given the event name '${INLINE_BUTTON_CALLBACK_PREFIX}:show-callout-slug:my-callout', * it emits the following events in order: * @fires callback_query * @fires callback_query:user-123456789 - * @fires callback_query:data - * @fires callback_query:data:user-123456789 - * @fires callback_query:data:show-callout-slug - * @fires callback_query:data:show-callout-slug:user-123456789 - * @fires callback_query:data:show-callout-slug:my-callout - * @fires callback_query:data:show-callout-slug:my-callout:user-123456789 + * @fires ${INLINE_BUTTON_CALLBACK_PREFIX} + * @fires ${INLINE_BUTTON_CALLBACK_PREFIX}:user-123456789 + * @fires ${INLINE_BUTTON_CALLBACK_PREFIX}:show-callout-slug + * @fires ${INLINE_BUTTON_CALLBACK_PREFIX}:show-callout-slug:user-123456789 + * @fires ${INLINE_BUTTON_CALLBACK_PREFIX}:show-callout-slug:my-callout + * @fires ${INLINE_BUTTON_CALLBACK_PREFIX}:show-callout-slug:my-callout:user-123456789 * * Or given the event name 'message', it emits the following events in order: * @fires message @@ -93,9 +93,23 @@ export class EventService extends BaseService { return this._events.on(eventName, callback); } + /** + * Listen for a Telegram bot user event + * @param eventName The event name to listen for, e.g. "message" + * @param id The Telegram user id + * @param callback The callback function to call when the event is emitted + */ + public onUser( + eventName: string, + id: number, + callback: EventTelegramBotListener, + ) { + return this._events.on(eventName + ":user-" + id, callback); + } + /** * Listen for a Telegram bot event, but only once - * @param eventName + * @param eventName The event name to listen for, e.g. "message" * @param callback The callback function to call when the event is emitted * @returns */ @@ -106,6 +120,21 @@ export class EventService extends BaseService { return this._events.once(eventName, callback); } + /** + * Listen for a Telegram bot event, but only once + * @param eventName The event name to listen for, e.g. "message" + * @param id The Telegram user id + * @param callback The callback function to call when the event is emitted + * @returns + */ + public onceUser( + eventName: string, + id: number, + callback: EventTelegramBotListener, + ) { + return this._events.once(eventName + ":user-" + id, callback); + } + /** * Returns a promise that resolves when the given event is emitted * @param eventName @@ -136,6 +165,23 @@ export class EventService extends BaseService { ); } + /** + * Stop listening for a Telegram bot user event + * @param eventName The event name to listen for, e.g. "message" + * @param id The Telegram user id + * @param callback The callback function to call when the event is emitted + */ + public offUser( + eventName: string, + id: number, + callback: EventTelegramBotListener, + ) { + return this._events.off( + eventName + ":user-" + id, + callback, + ); + } + /** * Listen for a Telegram user message * @param id The Telegram user id diff --git a/telegram-bot/services/keyboard.service.ts b/telegram-bot/services/keyboard.service.ts index 34f6df8..d03fbb2 100644 --- a/telegram-bot/services/keyboard.service.ts +++ b/telegram-bot/services/keyboard.service.ts @@ -1,6 +1,21 @@ import { BaseService } from "../core/index.ts"; -import { InlineKeyboard, Keyboard, Singleton } from "../deps/index.ts"; -import { BUTTON_CALLBACK_SHOW_CALLOUT } from "../constants/index.ts"; +import { + ForceReply, + InlineKeyboard, + InlineKeyboardButton, + InlineKeyboardMarkup, + Keyboard, + Message, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + Singleton, +} from "../deps/index.ts"; +import { + FALSY_MESSAGE_KEY, + INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, + INLINE_BUTTON_CALLBACK_SHOW_CALLOUT, + TRUTHY_MESSAGE_KEY, +} from "../constants/index.ts"; import { I18nService } from "./i18n.service.ts"; import type { AppContext, CalloutDataExt } from "../types/index.ts"; @@ -39,9 +54,9 @@ export class KeyboardService extends BaseService { /** * Create a keyboard button to select a callout. * - * To respond to the button press, listen for the `callback_query:data:show-callout-slug` event using the EventService. + * To respond to the button press, listen for the `${INLINE_BUTTON_CALLBACK_PREFIX}:show-callout-slug` event using the EventService. * - * @fires `callback_query:data:${BUTTON_CALLBACK_SHOW_CALLOUT}` + * @fires `${INLINE_BUTTON_CALLBACK_PREFIX}:${INLINE_BUTTON_CALLBACK_SHOW_CALLOUT}` * * @param callouts The Callouts to select from * @param startIndex The index of the first callout to show, starting at 1 @@ -70,7 +85,8 @@ export class KeyboardService extends BaseService { ); continue; } - const callbackData = `${BUTTON_CALLBACK_SHOW_CALLOUT}:${shortSlug}`; + const callbackData = + `${INLINE_BUTTON_CALLBACK_SHOW_CALLOUT}:${shortSlug}`; if (callbackData.length > 64) { console.error( @@ -88,7 +104,7 @@ export class KeyboardService extends BaseService { } /** - * Create a keyboard with a list of selections + * Create or extends a custom keyboard with a list of selections * @param keyboard * @param selections * @returns @@ -100,30 +116,53 @@ export class KeyboardService extends BaseService { return keyboard.row(); } + /** + * Create or extends a inline keyboard with a list of selections + * @param prefix A prefix to add to the button data, used to subscribe to the events + * @param keyboard The inline keyboard to extend + * @param selections The selections to add + * @returns + */ + public inlineSelection( + prefix = INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, + keyboard = this.inlineEmpty(), + selections: string[], + ) { + for (const selection of selections) { + keyboard.text(selection, `${prefix}:${selection}`); + } + return keyboard.row(); + } + /** * Create a inline keyboard with Yes and No buttons. * - * To respond to the button press, listen for the `callback_query:data:yes` and `callback_query:data:no` events using the EventService. - * If you have defined a prefix, the event names will be prefixed with the prefix, e.g. `callback_query:data:callout-respond:yes`. + * To respond to the button press, listen for the `${INLINE_BUTTON_CALLBACK_PREFIX}:yes` and `${INLINE_BUTTON_CALLBACK_PREFIX}:no` events using the EventService. + * If you have defined a prefix, the event names will be prefixed with the prefix, e.g. `${INLINE_BUTTON_CALLBACK_PREFIX}:callout-respond:yes`. * * @param ctx The chat context * @param prefix A prefix to add to the button data, e.g. "callout-respond" + * @param keyboard The keyboard to extend + * @param truthyLabel The label for the truthy button + * @param falsyLabel The label for the falsy button */ public inlineYesNo( prefix: string, + keyboard = this.inlineEmpty(), truthyLabel = this.i18n.t("bot.reactions.messages.truthy"), falsyLabel = this.i18n.t("bot.reactions.messages.falsy"), + truthyMessageKey = TRUTHY_MESSAGE_KEY, + falsyMessageKey = FALSY_MESSAGE_KEY, ) { - const inlineKeyboard = new InlineKeyboard(); - inlineKeyboard.text( + keyboard.text( truthyLabel, - prefix ? `${prefix}:yes` : `yes`, + prefix ? `${prefix}:${truthyMessageKey}` : `${truthyMessageKey}`, ); - inlineKeyboard.text( + keyboard.text( falsyLabel, - prefix ? `${prefix}:no` : `no`, + prefix ? `${prefix}:${falsyMessageKey}` : `${falsyMessageKey}`, ); - return inlineKeyboard; + return keyboard; } /** @@ -142,48 +181,84 @@ export class KeyboardService extends BaseService { } /** - * Creates a extends a custom keyboard with a skip button. + * Creates or extends a custom keyboard with a done button. * * @param keyboard The keyboard to extend - * @param skipLabel The label for the skip button + * @param doneLabel The label for the done button */ - public skip( + public done( keyboard = this.empty(), - skipLabel = this.i18n.t("bot.reactions.messages.skip"), + doneLabel = this.i18n.t("bot.reactions.messages.done"), ) { - return keyboard.text(skipLabel).row(); + return keyboard.text(doneLabel).row(); } /** - * Creates a extends a custom keyboard with a done button. + * Creates or extends a inline keyboard with a done button. * + * @param prefix A prefix to add to the button data, used to subscribe to the events * @param keyboard The keyboard to extend * @param doneLabel The label for the done button */ - public done( - keyboard = this.empty(), - skipLabel = this.i18n.t("bot.reactions.messages.done"), + public inlineDone( + prefix = INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE, + keyboard = this.inlineEmpty(), + doneLabel = this.i18n.t("bot.reactions.messages.done"), ) { - return keyboard.text(skipLabel).row(); + const button = this.inlineDoneButton(prefix, doneLabel); + return keyboard.text(button.text, button.callback_data).row(); } /** - * Create a keyboard for a callout response + * Creates or extends a inline keyboard with a done button. + * + * @param prefix A prefix to add to the button data, used to subscribe to the events + * @param keyboard The keyboard to extend + * @param doneLabel The label for the done button + */ + public inlineDoneButton( + prefix = `${INLINE_BUTTON_CALLBACK_CALLOUT_RESPONSE}:done`, + doneLabel = this.i18n.t("bot.reactions.messages.done"), + ): InlineKeyboardButton.CallbackButton { + return { + text: doneLabel, + callback_data: `${prefix}:done`, + }; + } + + /** + * Create a keyboard for a callout response with a skip button. * @param keyboard The keyboard to extend * @param required * @param multiple */ - public skipDone( + public skip( keyboard = this.empty(), required = false, - multiple = false, + skipLabel = this.i18n.t("bot.reactions.messages.skip"), ) { - if (multiple) { - this.done(keyboard); + if (!required) { + return keyboard.text(skipLabel).row(); } + return keyboard; + } + + /** + * Creates or extends a inline keyboard with a skip button. + * @param prefix A prefix to add to the button data, used to subscribe to the events + * @param keyboard The keyboard to extend + * @param required + * @param skipLabel The label for the skip button + */ + public inlineSkip( + prefix: string, + keyboard = this.inlineEmpty(), + required = false, + skipLabel = this.i18n.t("bot.reactions.messages.skip"), + ) { if (!required) { - this.skip(keyboard); + return keyboard.text(skipLabel, `${prefix}:skip`).row(); } return keyboard; @@ -192,8 +267,8 @@ export class KeyboardService extends BaseService { /** * Create a inline keyboard with Continue and Cancel buttons. * - * To respond to the button press, listen for the `callback_query:data:continue` and `callback_query:data:cancel` events using the EventService. - * If you have defined a prefix, the event names will be prefixed with the prefix, e.g. `callback_query:data:callout-respond:continue`. + * To respond to the button press, listen for the `${INLINE_BUTTON_CALLBACK_PREFIX}:continue` and `${INLINE_BUTTON_CALLBACK_PREFIX}:cancel` events using the EventService. + * If you have defined a prefix, the event names will be prefixed with the prefix, e.g. `${INLINE_BUTTON_CALLBACK_PREFIX}:callout-respond:continue`. * * @param prefix A prefix to add to the button data, e.g. "callout-respond" */ @@ -229,13 +304,20 @@ export class KeyboardService extends BaseService { /** * Remove an existing inline keyboard * @param ctx The chat context - * @param withMessage If true, the message will be deleted, too + * @param withMessage If true, the attached message will be deleted, too */ public async removeInlineKeyboard(ctx: AppContext, withMessage = false) { try { - // Do not delete keyboard message? + // Do not remove attached message? if (!withMessage) { const inlineKeyboard = new InlineKeyboard(); + if ( + !ctx.update.callback_query?.message?.reply_markup?.inline_keyboard + .flat().length + ) { + console.warn("No inline keyboard to remove"); + return; + } return await ctx.editMessageReplyMarkup({ reply_markup: inlineKeyboard, }); @@ -247,31 +329,180 @@ export class KeyboardService extends BaseService { } } + /** Remove a specific inline button from the keyboard */ + public removeInlineButton( + keyboard: InlineKeyboard, + buttonCallbackData: string, + ) { + const inlineKeyboard = new InlineKeyboard(); + + for (const row of keyboard.inline_keyboard) { + for (const button of row) { + if ( + (button as InlineKeyboardButton.CallbackButton).callback_data !== + buttonCallbackData + ) { + inlineKeyboard.text( + button.text, + (button as InlineKeyboardButton.CallbackButton).callback_data, + ); + } + } + inlineKeyboard.row(); + } + + return inlineKeyboard; + } + + /** Replace a specific inline button from the keyboard */ + public replaceInlineButton( + keyboard: InlineKeyboard, + buttonCallbackData: string, + newButton: InlineKeyboardButton.CallbackButton, + ) { + const inlineKeyboard = new InlineKeyboard(); + + for (const row of keyboard.inline_keyboard) { + for (const button of row) { + if ( + (button as InlineKeyboardButton.CallbackButton).callback_data === + buttonCallbackData + ) { + inlineKeyboard.text( + newButton.text, + newButton.callback_data, + ); + } else { + inlineKeyboard.text( + button.text, + (button as InlineKeyboardButton.CallbackButton).callback_data, + ); + } + } + inlineKeyboard.row(); + } + + return inlineKeyboard; + } + + /** Add a specific inline button to the keyboard */ + public addInlineButton( + keyboard: InlineKeyboard, + newButton: InlineKeyboardButton.CallbackButton, + ) { + keyboard.text( + newButton.text, + newButton.callback_data, + ); + + return keyboard; + } + + /** Rename a specific inline button from the keyboard */ + public renameInlineButton( + keyboard: InlineKeyboard, + buttonCallbackData: string, + newButtonText: string, + ) { + for (const row of keyboard.inline_keyboard) { + for (const button of row) { + if ( + (button as InlineKeyboardButton.CallbackButton).callback_data === + buttonCallbackData + ) { + button.text = newButtonText; + } + } + } + + return keyboard; + } + public async removeLastInlineKeyboard(ctx: AppContext) { const session = await ctx.session; + let removed = false; if (!session) { throw new Error("ctx with a session is required when once is true"); } - const inlineKeyboardData = session._data.latestKeyboard; - if (!inlineKeyboardData || !Object.keys(inlineKeyboardData).length) { - console.debug("No inline keyboard to remove"); + const keyboardData = session._data.latestKeyboard || null; + if (!keyboardData || !Object.keys(keyboardData).length) { + console.debug( + "No last inline keyboard to remove, keyboardData: ", + keyboardData, + ); return; } - if (inlineKeyboardData.message_id && inlineKeyboardData.chat_id) { + if ( + keyboardData.message_id && keyboardData.chat_id && + keyboardData.inlineKeyboard?.inline_keyboard.flat().length + ) { try { await ctx.api.editMessageReplyMarkup( - inlineKeyboardData.chat_id, - inlineKeyboardData.message_id, + keyboardData.chat_id, + keyboardData.message_id, { reply_markup: new InlineKeyboard(), }, ); + removed = true; } catch (error) { console.error("Error removing last inline keyboard", error); + removed = false; } } + await this.resetKeyboardInSession(ctx); + return removed; + } + + protected async resetKeyboardInSession(ctx: AppContext) { + const session = await ctx.session; session._data.latestKeyboard = null; + return session; + } + + /** + * Store the latest keyboard in the session to be able to remove it later + * @param ctx + * @param markup + * @param message + */ + public async storeLatestInSession( + ctx: AppContext, + markup: + | InlineKeyboardMarkup + | ReplyKeyboardMarkup + | ReplyKeyboardRemove + | ForceReply, + message: Message | undefined = ctx.message, + ) { + if (!message) { + throw new Error("Message is undefined"); + } + const session = await ctx.session; + if ( + markup instanceof InlineKeyboard && + markup.inline_keyboard.flat().length > 0 + ) { + session._data.latestKeyboard = { + message_id: message.message_id, + chat_id: message.chat.id, + type: "inline", + inlineKeyboard: markup, + }; + } else if ( + markup instanceof Keyboard && + markup.keyboard.flat().length > 0 + ) { + session._data.latestKeyboard = { + message_id: message.message_id, + chat_id: message.chat.id, + type: "custom", + customKeyboard: markup, + }; + } else { + console.warn("No keyboard to store"); + } } } diff --git a/telegram-bot/services/transform.service.ts b/telegram-bot/services/transform.service.ts index 680ed0a..235a97c 100644 --- a/telegram-bot/services/transform.service.ts +++ b/telegram-bot/services/transform.service.ts @@ -1,8 +1,9 @@ import { BaseService } from "../core/index.ts"; import { CalloutResponseAnswerAddress, - Context, + Message, Singleton, + Update, } from "../deps/index.ts"; import { ParsedResponseType, ReplayType } from "../enums/index.ts"; @@ -47,9 +48,9 @@ export class TransformService extends BaseService { } public parseResponseFile( - context: Context, + message?: Message, ): RenderResponseParsedFile["data"] { - const fileId = getFileIdFromMessage(context.message); + const fileId = getFileIdFromMessage(message); if (!fileId) { throw new Error("No file id found in message"); } @@ -60,26 +61,24 @@ export class TransformService extends BaseService { } public parseResponsesFile( - contexts: Context[], + messages: Message[], ): RenderResponseParsedFile["data"] { - contexts = Array.isArray(contexts) ? contexts : [contexts]; - const res = contexts.map(this.parseResponseFile); + const res = messages.map(this.parseResponseFile); return res; } public parseResponseText( - context: Context, + message?: string, ): RenderResponseParsedText["data"] { - return getTextFromMessage(context.message); + return message?.trim() || ""; } public parseResponsesText( - contexts: Context[], + messages: string[], ): RenderResponseParsedText["data"] { - contexts = Array.isArray(contexts) ? contexts : [contexts]; - const texts = contexts - .filter((context) => context.message?.text) - .map((context) => getTextFromMessage(context.message)); + const texts = messages + .filter((message) => message !== undefined) + .map((message) => message.trim()); return texts; } @@ -161,10 +160,13 @@ export class TransformService extends BaseService { } public parseResponseBoolean( - context: Context, + message?: string, ): RenderResponseParsedBoolean["data"] { - const boolStr = getTextFromMessage(context.message).toLowerCase().trim(); + const boolStr = message?.trim().toLowerCase(); let bool = false; + if (boolStr === undefined) { + return bool; + } const truthyStr = this.i18n.t("bot.reactions.messages.truthy").toLowerCase() .trim(); const falsyStr = this.i18n.t("bot.reactions.messages.falsy").toLowerCase() @@ -180,29 +182,26 @@ export class TransformService extends BaseService { } public parseResponsesBoolean( - contexts: Context[], + messages: string[], ): RenderResponseParsedBoolean["data"] { - contexts = Array.isArray(contexts) ? contexts : [contexts]; - const booleans = contexts - .filter((context) => context.message?.text) + const booleans = messages + .filter((message) => message !== undefined) .map(this.parseResponseBoolean); return booleans; } public parseResponseNumber( - context: Context, + message?: string, ): RenderResponseParsedNumber["data"] { - const text = getTextFromMessage(context.message); - return extractNumbers(text); + return extractNumbers(message?.trim()); } public parseResponsesNumber( - contexts: Context[], + messages: string[], ): RenderResponseParsedNumber["data"] { - contexts = Array.isArray(contexts) ? contexts : [contexts]; - const texts = contexts - .filter((context) => context.message?.text) + const texts = messages + .filter((message) => message !== undefined) .map(this.parseResponseNumber); return texts; @@ -210,11 +209,11 @@ export class TransformService extends BaseService { // TODO: Use CalloutComponentInputAddressSchema as return type public parseResponseCalloutComponentAddress( - context: Context, + message?: Message, ): CalloutResponseAnswerAddress { - const location = getLocationFromMessage(context.message); + const location = getLocationFromMessage(message); const address: CalloutResponseAnswerAddress = { - formatted_address: getTextFromMessage(context.message) || + formatted_address: getTextFromMessage(message) || location.address || location.title || "", @@ -230,20 +229,19 @@ export class TransformService extends BaseService { // TODO: Use CalloutComponentInputAddressSchema as return type public parseResponsesCalloutComponentAddress( - contexts: Context[], + messages: Message[], ): CalloutResponseAnswerAddress[] { - contexts = Array.isArray(contexts) ? contexts : [contexts]; - const addresses: CalloutResponseAnswerAddress[] = contexts - .filter((context) => context.message?.text) + const addresses: CalloutResponseAnswerAddress[] = messages + .filter((message) => message.text) .map(this.parseResponseCalloutComponentAddress); return addresses; } public parseResponseCalloutComponentInputUrl( - context: Context, + message?: string, ): string { - let text = this.parseResponseText(context); + let text = this.parseResponseText(message?.toLowerCase()); if (!text.startsWith("http")) { text = `https://${text}`; } @@ -251,17 +249,17 @@ export class TransformService extends BaseService { } public parseResponseAny( - context: Context, + message?: Message, ): RenderResponseParsedAny["data"] { - return this.parseResponseText(context) || this.parseResponseFile(context); + return this.parseResponseText(message?.text) || + this.parseResponseFile(message); } public parseResponsesAny( - contexts: Context[], + messages: Message[], ): RenderResponseParsedAny["data"] { - contexts = Array.isArray(contexts) ? contexts : [contexts]; - return contexts - .filter((context) => context.message?.text) + return messages + .filter((message) => message.text) .map(this.parseResponseAny); } @@ -299,9 +297,9 @@ export class TransformService extends BaseService { // Already parsed for validation return (replay as ReplayAcceptedCalloutComponentSchema).answer; case ParsedResponseType.FILE: - return this.parseResponseFile(replay.context); + return this.parseResponseFile(replay.context.message); case ParsedResponseType.TEXT: - return this.parseResponseText(replay.context); + return this.parseResponseText(replay.context.message?.text); case ParsedResponseType.SELECTION: if (render.accepted.type !== ReplayType.SELECTION) { throw new Error( @@ -310,11 +308,11 @@ export class TransformService extends BaseService { } return this.parseResponseSelection(replay, render.accepted.valueLabel); case ParsedResponseType.BOOLEAN: - return this.parseResponseBoolean(replay.context); + return this.parseResponseBoolean(replay.context.message?.text); case ParsedResponseType.NUMBER: - return this.parseResponseNumber(replay.context); + return this.parseResponseNumber(replay.context.message?.text); case ParsedResponseType.ANY: - return this.parseResponseAny(replay.context); + return this.parseResponseAny(replay.context.message); case ParsedResponseType.NONE: return this.responseNone(); default: @@ -331,6 +329,12 @@ export class TransformService extends BaseService { render: Render, ): RenderResponseParsed["data"] { const contexts = replays.map((replay) => replay.context); + const messages = contexts.filter((c) => c.message).map((c) => + c.message as (Message & Update.NonChannel) + ); + const textMessages = messages.filter((m) => m.text).map((m) => + m.text as string + ); switch (render.parseType) { case ParsedResponseType.CALLOUT_COMPONENT: // Already parsed for validation @@ -338,17 +342,17 @@ export class TransformService extends BaseService { (r) => r.answer, ); case ParsedResponseType.FILE: - return this.parseResponsesFile(contexts); + return this.parseResponsesFile(messages); case ParsedResponseType.TEXT: - return this.parseResponsesText(contexts); + return this.parseResponsesText(textMessages); case ParsedResponseType.SELECTION: return this.parseResponsesSelection(replays, render); case ParsedResponseType.BOOLEAN: - return this.parseResponsesBoolean(contexts); + return this.parseResponsesBoolean(textMessages); case ParsedResponseType.NUMBER: - return this.parseResponsesNumber(contexts); + return this.parseResponsesNumber(textMessages); case ParsedResponseType.ANY: - return this.parseResponsesAny(contexts); + return this.parseResponsesAny(messages); case ParsedResponseType.NONE: return this.responsesNone(); default: diff --git a/telegram-bot/services/validation.service.ts b/telegram-bot/services/validation.service.ts index 9c390a2..dfba25a 100644 --- a/telegram-bot/services/validation.service.ts +++ b/telegram-bot/services/validation.service.ts @@ -2,6 +2,7 @@ import { BaseService } from "../core/index.ts"; import { CalloutComponentType, calloutComponentValidator, + MaybeInaccessibleMessage, Message, Singleton, } from "../deps/index.ts"; @@ -9,8 +10,8 @@ import { RelayAcceptedFileType, ReplayType } from "../enums/index.ts"; import { extractNumbers, getFileIdFromMessage, + getKeyboardButtonFromCallbackQuery, getSimpleMimeTypes, - getTextFromMessage, isNumber, } from "../utils/index.ts"; @@ -30,6 +31,7 @@ import type { ReplayConditionText, } from "../types/index.ts"; import { ReplayAcceptedCalloutComponentSchema } from "../types/replay-accepted-callout-component-schema.ts"; +import { CHECKMARK } from "../constants/index.ts"; /** * Check conditions for a replay. @@ -42,7 +44,7 @@ export class ValidationService extends BaseService { console.debug(`${this.constructor.name} created`); } - protected messageIsAudioFile(message: Message) { + protected messageIsAudioFile(message: MaybeInaccessibleMessage) { return ( !!message.audio?.file_id || !!message.voice?.file_id || @@ -50,14 +52,14 @@ export class ValidationService extends BaseService { ); } - protected messageIsPhotoFile(message: Message) { + protected messageIsPhotoFile(message: MaybeInaccessibleMessage) { return ( !!message.photo?.length || message.document?.mime_type?.startsWith("image") ); } - protected messageIsVideoFile(message: Message) { + protected messageIsVideoFile(message: MaybeInaccessibleMessage) { return ( !!message.video?.file_id || !!message.animation?.file_id || @@ -65,23 +67,23 @@ export class ValidationService extends BaseService { ); } - protected messageIsDocumentFile(message: Message) { + protected messageIsDocumentFile(message: MaybeInaccessibleMessage) { return !!message.document?.file_id; } - protected messageIsContact(message: Message) { + protected messageIsContact(message: MaybeInaccessibleMessage) { return !!message.contact; } - protected messageIsLocation(message: Message) { + protected messageIsLocation(message: MaybeInaccessibleMessage) { return !!message.venue?.location || !!message.location; } - protected messageIsAddress(message: Message) { + protected messageIsAddress(message: MaybeInaccessibleMessage) { return !!message.venue?.address; // + message.venue?.title } - protected messageIsAnyFile(message: Message) { + protected messageIsAnyFile(message: MaybeInaccessibleMessage) { return ( this.messageIsPhotoFile(message) || this.messageIsDocumentFile(message) || @@ -101,8 +103,10 @@ export class ValidationService extends BaseService { protected messageIsFile( context: AppContext, accepted: ReplayConditionFile, + message = context.message, + textMessage = message?.text, ): ReplayAcceptedFile { - const message = context.message; + textMessage = textMessage?.trim(); let fileType: RelayAcceptedFileType = RelayAcceptedFileType.ANY; // Is not a file message if (!message) { @@ -179,11 +183,11 @@ export class ValidationService extends BaseService { protected messageIsDoneOrSkipText( context: AppContext, accepted: ReplayCondition, + message = context.message, + textMessage = message?.text, ): ReplayAcceptedText | ReplayAcceptedNone { // Capitalisation should not play a role for done messages, see https://github.com/beabee-communityrm/telegram-bot/issues/5 - const textMessage = getTextFromMessage(context.message) - ?.toLowerCase() - .trim(); + textMessage = textMessage?.trim().toLowerCase(); let isDoneMessage = false; let isSkipMessage = false; @@ -235,11 +239,13 @@ export class ValidationService extends BaseService { protected messageIsText( context: AppContext, accepted: ReplayConditionText, + message = context.message, + textMessage = message?.text, ): ReplayAcceptedText | ReplayAcceptedNone { - const message = context.message; + textMessage = textMessage?.trim(); const texts = accepted.texts?.map((t) => t.toLowerCase().trim()); - const originalText = message?.text?.trim(); - const lowerCaseText = originalText?.toLowerCase(); + const originalText = textMessage; + const lowerCaseText = textMessage?.toLowerCase(); const baseResult = { isDoneMessage: false, @@ -248,7 +254,7 @@ export class ValidationService extends BaseService { }; // Is not a text message - if (!message || !originalText || !lowerCaseText) { + if (!originalText || !lowerCaseText) { return { type: ReplayType.NONE, accepted: false, @@ -294,9 +300,11 @@ export class ValidationService extends BaseService { protected messageIsSelection( context: AppContext, accepted: ReplayConditionSelection, + message = context.message, + textMessage = message?.text, ): ReplayAcceptedSelection { - const message = context.message; - const text = getTextFromMessage(message).toLowerCase(); + // Remove the checkmark from the message to match the label + textMessage = textMessage?.replace(CHECKMARK, "").trim().toLowerCase(); const baseResult = { isDoneMessage: false, @@ -305,15 +313,17 @@ export class ValidationService extends BaseService { }; // The answer message can be the index of the value but starts with 1 - if (isNumber(text)) { - const index1 = extractNumbers(text); + if (isNumber(textMessage)) { + const index1 = extractNumbers(textMessage); const keys = Object.keys(accepted.valueLabel); const value = keys[index1 - 1]; + const label = accepted.valueLabel[value]; if (value) { return { type: ReplayType.SELECTION, accepted: true, value, + label, ...baseResult, }; } @@ -321,13 +331,14 @@ export class ValidationService extends BaseService { // The answer message can be the value directly const acceptedValue = Object.keys(accepted.valueLabel).find( - (key) => accepted.valueLabel[key].toLowerCase() === text, + (key) => accepted.valueLabel[key].toLowerCase() === textMessage, ); const isAccepted = !!acceptedValue; return { type: ReplayType.SELECTION, accepted: isAccepted, value: acceptedValue, + label: acceptedValue ? accepted.valueLabel[acceptedValue] : undefined, ...baseResult, }; } @@ -341,7 +352,10 @@ export class ValidationService extends BaseService { protected messageIsCalloutComponent( context: AppContext, accepted: ReplayConditionCalloutComponentSchema, + message?: Message, + textMessage = message?.text, ): ReplayAcceptedCalloutComponentSchema | ReplayAcceptedNone { + textMessage = textMessage?.trim(); const baseResult = { isDoneMessage: false, isSkipMessage: false, @@ -365,12 +379,12 @@ export class ValidationService extends BaseService { }; } case CalloutComponentType.INPUT_CHECKBOX: { - result.answer = this.transform.parseResponseBoolean(context); + result.answer = this.transform.parseResponseBoolean(textMessage); break; } case CalloutComponentType.INPUT_FILE: case CalloutComponentType.INPUT_SIGNATURE: { - result.answer = this.transform.parseResponseFile(context); + result.answer = this.transform.parseResponseFile(message); break; } case CalloutComponentType.INPUT_ADDRESS: @@ -381,18 +395,18 @@ export class ValidationService extends BaseService { case CalloutComponentType.INPUT_TEXT_AREA: case CalloutComponentType.INPUT_TEXT_FIELD: case CalloutComponentType.INPUT_TIME: { - result.answer = this.transform.parseResponseText(context); + result.answer = this.transform.parseResponseText(textMessage); break; } case CalloutComponentType.INPUT_URL: { result.answer = this.transform.parseResponseCalloutComponentInputUrl( - context, + textMessage, ); break; } case CalloutComponentType.INPUT_NUMBER: { - result.answer = this.transform.parseResponseNumber(context); + result.answer = this.transform.parseResponseNumber(textMessage); break; } @@ -422,7 +436,7 @@ export class ValidationService extends BaseService { } /** - * Check if a message is accepted by a condition + * Check if a message is accepted by a condition. * @param accepted * @param context * @returns @@ -430,8 +444,15 @@ export class ValidationService extends BaseService { public messageIsAccepted( context: AppContext, accepted: ReplayCondition, + message = context.message, + textMessage = message?.text, ): ReplayAccepted { - const isDoneOrSkip = this.messageIsDoneOrSkipText(context, accepted); + const isDoneOrSkip = this.messageIsDoneOrSkipText( + context, + accepted, + message, + textMessage, + ); if ( isDoneOrSkip.accepted && (isDoneOrSkip.isDoneMessage || isDoneOrSkip.isSkipMessage) @@ -463,25 +484,45 @@ export class ValidationService extends BaseService { // File response is accepted if (accepted.type === ReplayType.FILE) { - const isFile = this.messageIsFile(context, accepted); + const isFile = this.messageIsFile( + context, + accepted, + message, + textMessage, + ); return isFile; } // Text response is accepted if (accepted.type === ReplayType.TEXT) { - const isText = this.messageIsText(context, accepted); + const isText = this.messageIsText( + context, + accepted, + message, + textMessage, + ); return isText; } // Selection response is accepted if (accepted.type === ReplayType.SELECTION) { - const isSelection = this.messageIsSelection(context, accepted); + const isSelection = this.messageIsSelection( + context, + accepted, + message, + textMessage, + ); return isSelection; } // Callout component response answer is accepted if (accepted.type === ReplayType.CALLOUT_COMPONENT_SCHEMA) { - const isCalloutAnswer = this.messageIsCalloutComponent(context, accepted); + const isCalloutAnswer = this.messageIsCalloutComponent( + context, + accepted, + message, + textMessage, + ); return isCalloutAnswer; } @@ -489,4 +530,38 @@ export class ValidationService extends BaseService { `Unknown replay until type: "${(accepted as ReplayCondition)?.type}"`, ); } + + /** + * Check if a callback query data (inline button event) is accepted by a condition. + * This method uses {@link messageIsAccepted} but instead of checking the users message text, + * it uses the button label of the callback query data as the message text. + * @param accepted + * @param context + * @returns + */ + public callbackQueryDataIsAccepted( + context: AppContext, + accepted: ReplayCondition, + message = context.message, + ): ReplayAccepted { + const callbackQueryData = context.callbackQuery?.data; + + if (!callbackQueryData || !context.callbackQuery) { + throw new Error( + "[callbackQueryDataIsAccepted] Callback query data is undefined", + ); + } + + // Use the inline keyboard button label as the message text + const fakeMessage = getKeyboardButtonFromCallbackQuery( + context.callbackQuery, + )?.text; + + return this.messageIsAccepted( + context, + accepted, + message, + fakeMessage, + ); + } } diff --git a/telegram-bot/types/index.ts b/telegram-bot/types/index.ts index acbfe27..78cf728 100644 --- a/telegram-bot/types/index.ts +++ b/telegram-bot/types/index.ts @@ -33,6 +33,7 @@ export * from "./render-text.ts"; export * from "./render.ts"; export * from "./replay-accepted-any.ts"; export * from "./replay-accepted-base.ts"; +export * from "./replay-accepted-callback-query.ts"; export * from "./replay-accepted-callout-component-schema.ts"; export * from "./replay-accepted-file.ts"; export * from "./replay-accepted-none.ts"; diff --git a/telegram-bot/types/render-base.ts b/telegram-bot/types/render-base.ts index 5280df4..31ddb1a 100644 --- a/telegram-bot/types/render-base.ts +++ b/telegram-bot/types/render-base.ts @@ -35,7 +35,7 @@ export interface RenderBase { /** * Remove the custom keyboard after the user has replied. */ - removeKeyboard?: boolean; + removeCustomKeyboard?: boolean; /** * Define the types of the replay you are accepting. diff --git a/telegram-bot/types/replay-accepted-callback-query.ts b/telegram-bot/types/replay-accepted-callback-query.ts new file mode 100644 index 0000000..1b8dfba --- /dev/null +++ b/telegram-bot/types/replay-accepted-callback-query.ts @@ -0,0 +1,9 @@ +import { ReplayAcceptedBase } from "./index.ts"; +import type { ReplayType } from "../enums/index.ts"; + +export interface ReplayAcceptedCallbackQueryData extends ReplayAcceptedBase { + /** Accept or wait for text message */ + type: ReplayType.CALLBACK_QUERY_DATA; + /** Callback query data to accept */ + data: string; +} diff --git a/telegram-bot/types/replay-accepted-selection.ts b/telegram-bot/types/replay-accepted-selection.ts index 65dffe7..ba52332 100644 --- a/telegram-bot/types/replay-accepted-selection.ts +++ b/telegram-bot/types/replay-accepted-selection.ts @@ -9,4 +9,5 @@ export interface ReplayAcceptedSelection extends ReplayAcceptedBase { accepted: boolean; /** The selected option value */ value?: string; + label?: string; } diff --git a/telegram-bot/types/replay-accepted.ts b/telegram-bot/types/replay-accepted.ts index b63d2dd..ac41d94 100644 --- a/telegram-bot/types/replay-accepted.ts +++ b/telegram-bot/types/replay-accepted.ts @@ -1,5 +1,6 @@ import type { ReplayAcceptedAny, + ReplayAcceptedCallbackQueryData, ReplayAcceptedCalloutComponentSchema, ReplayAcceptedFile, ReplayAcceptedNone, @@ -13,4 +14,5 @@ export type ReplayAccepted = | ReplayAcceptedAny | ReplayAcceptedNone | ReplayAcceptedSelection - | ReplayAcceptedCalloutComponentSchema; + | ReplayAcceptedCalloutComponentSchema + | ReplayAcceptedCallbackQueryData; diff --git a/telegram-bot/types/session-state.ts b/telegram-bot/types/session-state.ts index b50739f..09572c0 100644 --- a/telegram-bot/types/session-state.ts +++ b/telegram-bot/types/session-state.ts @@ -1,6 +1,6 @@ import type { ChatState } from "../enums/index.ts"; import type { AppContext } from "./index.ts"; -import type { InlineKeyboard } from "../deps/index.ts"; +import type { InlineKeyboard, Keyboard } from "../deps/index.ts"; export interface SessionState { state: ChatState; @@ -9,18 +9,19 @@ export interface SessionState { _data: { /** Reverse reference to the context */ ctx: AppContext | null; - /** Used to cancel any current task */ + /** Used to cancel any current task completely */ abortController: AbortController | null; /** The latest inline keyboard that was sent to the user */ latestKeyboard: { message_id: number; chat_id: number; - inlineKeyboard: InlineKeyboard; + inlineKeyboard?: InlineKeyboard; + customKeyboard?: Keyboard; /** * Type of the keyboard, currently unused. * inline - InlineKeyboard */ - type: "inline"; + type: "inline" | "custom"; } | null; }; } diff --git a/telegram-bot/utils/callback-query.ts b/telegram-bot/utils/callback-query.ts new file mode 100644 index 0000000..fe4b90f --- /dev/null +++ b/telegram-bot/utils/callback-query.ts @@ -0,0 +1,14 @@ +import type { CallbackQuery, InlineKeyboardButton } from "../deps/index.ts"; + +export const getKeyboardButtonFromCallbackQuery = ( + callbackQuery: CallbackQuery, + callbackQueryEventName = callbackQuery.data, +) => { + return callbackQuery.message?.reply_markup?.inline_keyboard.flat().find(( + button, + ) => + // TODO: fix type in Grammy? + (button as InlineKeyboardButton.CallbackButton) + .callback_data === callbackQueryEventName + ); +}; diff --git a/telegram-bot/utils/callouts.ts b/telegram-bot/utils/callouts.ts index 1c5444b..5ac2782 100644 --- a/telegram-bot/utils/callouts.ts +++ b/telegram-bot/utils/callouts.ts @@ -1,4 +1,4 @@ -// TODO: Move to common or use common utils +import { range } from "./number.ts"; import { CALLOUT_RESPONSE_GROUP_KEY_SEPARATOR } from "../constants/index.ts"; import { ParsedResponseType } from "../enums/index.ts"; @@ -78,3 +78,9 @@ export const calloutComponentTypeToParsedResponseType = ( } } }; + +export const getSelectionLabelNumberRange = ( + valueLabel: Record, +) => { + return range(1, Object.keys(valueLabel).length).map(String); +}; diff --git a/telegram-bot/utils/html.ts b/telegram-bot/utils/html.ts index 35b8bc6..50a820a 100644 --- a/telegram-bot/utils/html.ts +++ b/telegram-bot/utils/html.ts @@ -3,7 +3,7 @@ import { ammoniaCleanText, ammoniaInit, } from "../deps/index.ts"; -import { ALLOWED_TAGS } from "../constants/index.ts"; +import { ALLOWED_TAGS, DOT } from "../constants/index.ts"; const initAmmonia = async () => { await ammoniaInit(); @@ -39,7 +39,23 @@ export const sanitizeHtml = (htmlContent: string): string => { const tagsToReplace: { [key: string]: string } = { "": "\n", // Replace

tags with attributes, e.g.

but not

     "

": "\n", // Replace

tags without attributes + "

": "", "": "\n", // Replace
and
tags + "

": "", + "

": "", + "

": "", + "

": "", + "

": "", + "

": "", + "

": "", + "

": "", + "
": "", + "
": "", + "
": "", + "
": "", + "
  • ": ` \n${DOT} `, + "
  • ": "", + " ": " ", // Replace   with a space // Additional specific replacements can be added here }; diff --git a/telegram-bot/utils/index.ts b/telegram-bot/utils/index.ts index dad8244..1ed4bb1 100644 --- a/telegram-bot/utils/index.ts +++ b/telegram-bot/utils/index.ts @@ -1,4 +1,5 @@ export * from "./auth.ts"; +export * from "./callback-query.ts"; export * from "./callouts.ts"; export * from "./event-dispatcher.ts"; export * from "./file.test.ts"; diff --git a/telegram-bot/utils/number.ts b/telegram-bot/utils/number.ts index d6e42e1..88e55fd 100644 --- a/telegram-bot/utils/number.ts +++ b/telegram-bot/utils/number.ts @@ -15,7 +15,10 @@ export const isNumber = ( /** * Just get the numbers of a string */ -export const extractNumbers = (str: string | number) => { +export const extractNumbers = (str: string | number | undefined) => { + if (str === undefined) { + return NaN; + } if (typeof str === "number") { return str; }