From ad920238f06e0b1bfd1c4781f38b28f34bd86f13 Mon Sep 17 00:00:00 2001 From: Eluda <111eluda111@gmail.com> Date: Sat, 5 Nov 2022 17:29:06 +0100 Subject: [PATCH] Add text effects keyboard and command to bot. --- package.json | 4 +- src/bot.ts | 108 ++++++++++++++++++++++++++++- src/textEffects.ts | 168 +++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 10 +++ 4 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 src/textEffects.ts diff --git a/package.json b/package.json index 94cf000a..fbf8f158 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "devDependencies": { "@types/express": "^4.17.14", + "@types/lodash": "^4.14.188", "@types/node": "^18.11.7", "@types/node-fetch": "^2.6.2", "env-cmd": "^10.1.0", @@ -15,6 +16,7 @@ }, "dependencies": { "express": "^4.18.2", - "grammy": "^1.11.2" + "grammy": "^1.11.2", + "lodash": "^4.17.21" } } diff --git a/src/bot.ts b/src/bot.ts index c3a96352..92cdcfbf 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,5 +1,9 @@ -import { Bot, webhookCallback } from "grammy"; +import { Bot, InlineKeyboard, webhookCallback } from "grammy"; +import { chunk } from "lodash"; import express from "express"; +import { applyTextEffect } from "./textEffects"; + +import type { Variant as TextEffectVariant } from "./textEffects"; // Create a bot using the Telegram token const bot = new Bot(process.env.TELEGRAM_TOKEN || ""); @@ -8,6 +12,108 @@ const bot = new Bot(process.env.TELEGRAM_TOKEN || ""); bot.command("start", (ctx) => ctx.reply("Welcome! Up and running.")); bot.command("yo", (ctx) => ctx.reply(`Yo ${ctx.from?.username}`)); +// Handle the /effect command +type Effect = { code: TextEffectVariant; label: string }; +const allEffects: Effect[] = [ + { + code: "w", + label: "Monospace", + }, + { + code: "b", + label: "Bold", + }, + { + code: "i", + label: "Italic", + }, + { + code: "d", + label: "Doublestruck", + }, + { + code: "o", + label: "Circled", + }, + { + code: "q", + label: "Squared", + }, +]; + +const effectCallbackCodeAccessor = (effectCode: TextEffectVariant) => + `effect-${effectCode}`; + +const effectsKeyboardAccessor = (effectCodes: string[]) => { + const effectsAccessor = (effectCodes: string[]) => + effectCodes.map((code) => + allEffects.find((effect) => effect.code === code) + ); + const effects = effectsAccessor(effectCodes); + + const keyboard = new InlineKeyboard(); + const chunkedEffects = chunk(effects, 3); + for (const effectsChunk of chunkedEffects) { + for (const effect of effectsChunk) { + effect && + keyboard.text(effect.label, effectCallbackCodeAccessor(effect.code)); + } + keyboard.row(); + } + + return keyboard; +}; + +const textEffectResponseAccessor = ( + originalText: string, + modifiedText?: string +) => + `Original: ${originalText}` + + (modifiedText ? `\nModified: ${modifiedText}` : ""); + +const parseTextEffectResponse = ( + response: string +): { + originalText: string; + modifiedText?: string; +} => { + const originalText = (response.match(/Original: (.*)/) as any)[1]; + const modifiedTextMatch = response.match(/Modified: (.*)/); + + let modifiedText; + if (modifiedTextMatch) modifiedText = modifiedTextMatch[1]; + + if (!modifiedTextMatch) return { originalText }; + else return { originalText, modifiedText }; +}; + +bot.command("effect", (ctx) => + ctx.reply(textEffectResponseAccessor(ctx.match), { + reply_markup: effectsKeyboardAccessor( + allEffects.map((effect) => effect.code) + ), + }) +); + +// Handle text effects from the effect keyboard +for (const effect of allEffects) { + const allEffectCodes = allEffects.map((effect) => effect.code); + + bot.callbackQuery(effectCallbackCodeAccessor(effect.code), async (ctx) => { + const { originalText } = parseTextEffectResponse(ctx.msg?.text || ""); + const modifiedText = applyTextEffect(originalText, effect.code); + + await ctx.editMessageText( + textEffectResponseAccessor(originalText, modifiedText), + { + reply_markup: effectsKeyboardAccessor( + allEffectCodes.filter((code) => code !== effect.code) + ), + } + ); + }); +} + // Handle all other messages bot.on("message", (ctx) => ctx.reply("Got another message!")); diff --git a/src/textEffects.ts b/src/textEffects.ts new file mode 100644 index 00000000..2d9d2056 --- /dev/null +++ b/src/textEffects.ts @@ -0,0 +1,168 @@ +/** + * (c) David Konrad 2018- + * MIT License + * + * Javascript function to convert plain text to unicode variants + * + * Loosely based on the nodejs monotext CLI utility https://github.com/cpsdqs/monotext + * (c) cpsdqs 2016 + * + * For more inspiration see http://unicode.org/charts/ + * + */ + +/* + * supported unicode variants + * + * m: monospace + * b: bold + * i: italic + * c: script (Mathematical Alphanumeric Symbols) + * g: gothic / fraktur + * d: double-struck + * s: sans-serif + * o: circled text + * p: parenthesized latin letters + * q: squared text + * w: fullwidth + */ + +type Variant = "m" | "b" | "i" | "c" | "g" | "d" | "s" | "o" | "p" | "q" | "w"; +type Flags = "underline" | "strike" | "u,s"; + +function applyTextEffect(str: string, variant?: Variant, flags?: Flags) { + const offsets: any = { + m: [0x1d670, 0x1d7f6], + b: [0x1d400, 0x1d7ce], + i: [0x1d434, 0x00030], + bi: [0x1d468, 0x00030], + c: [0x0001d49c, 0x00030], + bc: [0x1d4d0, 0x00030], + g: [0x1d504, 0x00030], + d: [0x1d538, 0x1d7d8], + bg: [0x1d56c, 0x00030], + s: [0x1d5a0, 0x1d7e2], + bs: [0x1d5d4, 0x1d7ec], + is: [0x1d608, 0x00030], + bis: [0x1d63c, 0x00030], + o: [0x24b6, 0x2460], + on: [0x0001f150, 0x2460], + p: [0x249c, 0x2474], + q: [0x1f130, 0x00030], + qn: [0x0001f170, 0x00030], + w: [0xff21, 0xff10], + u: [0x2090, 0xff10], + }; + + const variantOffsets: any = { + monospace: "m", + bold: "b", + italic: "i", + "bold italic": "bi", + script: "c", + "bold script": "bc", + gothic: "g", + "gothic bold": "bg", + doublestruck: "d", + sans: "s", + "bold sans": "bs", + "italic sans": "is", + "bold italic sans": "bis", + parenthesis: "p", + circled: "o", + "circled negative": "on", + squared: "q", + "squared negative": "qn", + fullwidth: "w", + }; + + // special characters (absolute values) + const special: any = { + m: { + " ": 0x2000, + "-": 0x2013, + }, + i: { + h: 0x210e, + }, + g: { + C: 0x212d, + H: 0x210c, + I: 0x2111, + R: 0x211c, + Z: 0x2128, + }, + d: { + C: 0x2102, + H: 0x210d, + N: 0x2115, + P: 0x2119, + Q: 0x211a, + R: 0x211d, + Z: 0x2124, + }, + o: { + "0": 0x24ea, + "1": 0x2460, + "2": 0x2461, + "3": 0x2462, + "4": 0x2463, + "5": 0x2464, + "6": 0x2465, + "7": 0x2466, + "8": 0x2467, + "9": 0x2468, + }, + on: {}, + p: {}, + q: {}, + qn: {}, + w: {}, + }; + //support for parenthesized latin letters small cases + //support for full width latin letters small cases + //support for circled negative letters small cases + //support for squared letters small cases + //support for squared letters negative small cases + ["p", "w", "on", "q", "qn"].forEach((t) => { + for (var i = 97; i <= 122; i++) { + special[t][String.fromCharCode(i)] = offsets[t][0] + (i - 97); + } + }); + + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + const numbers = "0123456789"; + + const getType = function (variant: any) { + if (variantOffsets[variant]) return variantOffsets[variant]; + if (offsets[variant]) return variant; + return "m"; //monospace as default + }; + const getFlag = function (flag: any, flags: any) { + if (!flags) return false; + return flag.split("|").some((f: any) => flags.split(",").indexOf(f) > -1); + }; + + const type = getType(variant); + const underline = getFlag("underline|u", flags); + const strike = getFlag("strike|s", flags); + let result = ""; + + for (let c of str) { + let index; + if (special[type] && special[type][c]) + c = String.fromCodePoint(special[type][c]); + if (type && (index = chars.indexOf(c)) > -1) { + result += String.fromCodePoint(index + offsets[type][0]); + } else if (type && (index = numbers.indexOf(c)) > -1) { + result += String.fromCodePoint(index + offsets[type][1]); + } else { + result += c; + } + if (underline) result += "\u0332"; // add combining underline + if (strike) result += "\u0336"; // add combining strike + } + return result; +} + +export { Variant, applyTextEffect }; diff --git a/yarn.lock b/yarn.lock index 1f0f7334..f0a0fcc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -86,6 +86,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/lodash@^4.14.188": + version "4.14.188" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.188.tgz#e4990c4c81f7c9b00c5ff8eae389c10f27980da5" + integrity sha512-zmEmF5OIM3rb7SbLCFYoQhO4dGt2FRM9AMkxvA3LaADOF1n8in/zGJlWji9fmafLoNyz+FoL6FE0SLtGIArD7w== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -596,6 +601,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"