diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e1533c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "deno.enable": true, + "deno.lint": true +} diff --git a/README.md b/README.md index cddc430..1deae26 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,32 @@ # Parse Mode plugin for grammY -This plugin provides a transformer for setting default `parse_mode`, and a middleware for hydrating `Context` with familiar `reply` variant methods - i.e. `replyWithHTML`, `replyWithMarkdown`, etc. +This plugin lets you format your messages and captions with **bold**, __italic__, etc in a much more reliable and efficient way than with Markdown or HTML. +It does this by providing a transformer that injects `entities` or `caption_entities` if `text` or `caption` are FormattedString. ## Usage (Using format) ```ts -import { Bot, Composer, Context } from 'grammy'; -import { bold, fmt, hydrateReply, italic } from '@grammyjs/parse-mode'; -import type { ParseModeFlavor } from '@grammyjs/parse-mode'; +import { Api, Bot, Context } from "grammy"; +import { bold, fmt, hydrateFmt, underline } from "@grammyjs/parse-mode"; +import type { ParseModeApiFlavor, ParseModeFlavor } from "@grammyjs/parse-mode"; -const bot = new Bot>(''); +type MyApi = ParseModeApiFlavor; +type MyContext = ParseModeFlavor; +const bot = new Bot(""); -// Install format reply variant to ctx -bot.use(hydrateReply); +// Install automatic entities inject from FormattedString transformer +bot.api.config.use(hydrateFmt()); -bot.command('demo', async ctx => { - await ctx.replyFmt(fmt`${bold('bold!')} -${bold(italic('bitalic!'))} -${bold(fmt`bold ${link('blink', 'example.com')} bold`)}`); +bot.command("demo", async (ctx) => { + const boldText = fmt`This is a ${bold("bolded")} string`; + await ctx.reply(boldText); - // fmt can also be called like a regular function - await ctx.replyFmt(fmt(['', ' and ', ' and ', ''], fmt`${bold('bold')}`, fmt`${bold(italic('bitalic'))}`, fmt`${italic('italic')}`)); -}); - -bot.start(); -``` - -## Usage (Using default parse mode and utility reply methods) - -```ts -import { Bot, Composer, Context } from 'grammy'; -import { hydrateReply, parseMode } from '@grammyjs/parse-mode'; - -import type { ParseModeFlavor } from '@grammyjs/parse-mode'; - -const bot = new Bot>(''); - -// Install familiar reply variants to ctx -bot.use(hydrateReply); - -// Sets default parse_mode for ctx.reply -bot.api.config.use(parseMode('MarkdownV2')); + const underlineText = fmt`This is an ${underline("underlined")}`; + await ctx.api.sendMessage(ctx.chat.id, underlineText); -bot.command('demo', async ctx => { - await ctx.reply('*This* is _the_ default `formatting`'); - await ctx.replyWithHTML('This is withHTML formatting'); - await ctx.replyWithMarkdown('*This* is _withMarkdown_ `formatting`'); - await ctx.replyWithMarkdownV1('*This* is _withMarkdownV1_ `formatting`'); - await ctx.replyWithMarkdownV2('*This* is _withMarkdownV2_ `formatting`'); + // fmt can also be use to concat FormattedStrings + const combinedText = fmt`${boldText}\n${underlineText}`; + await bot.api.sendMessage(ctx.chat.id, combinedText); }); bot.start(); diff --git a/package.json b/package.json index 450cadd..ef615e3 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,10 @@ "build": "deno2node tsconfig.json" }, "devDependencies": { - "@grammyjs/types": "^3.0.3", "@tsconfig/node16": "^1.0.2", - "@types/node": "^16.6.1", - "deno2node": "^1.8.1", - "grammy": "^1.15.3" + "@types/node": "^20.4.7", + "deno2node": "^1.9.0", + "grammy": "^1.17.2" }, "files": [ "dist/" diff --git a/src/README.md b/src/README.md index cddc430..c85ac43 100644 --- a/src/README.md +++ b/src/README.md @@ -1,53 +1,32 @@ # Parse Mode plugin for grammY -This plugin provides a transformer for setting default `parse_mode`, and a middleware for hydrating `Context` with familiar `reply` variant methods - i.e. `replyWithHTML`, `replyWithMarkdown`, etc. +This plugin provides transformer that injects `entities` or `caption_entities` +if `text` or `caption` are FormattedString. ## Usage (Using format) ```ts -import { Bot, Composer, Context } from 'grammy'; -import { bold, fmt, hydrateReply, italic } from '@grammyjs/parse-mode'; -import type { ParseModeFlavor } from '@grammyjs/parse-mode'; +import { Api, Bot, Context } from "grammy"; +import { bold, fmt, hydrateFmt, underline } from "@grammyjs/parse-mode"; +import type { ParseModeApiFlavor, ParseModeFlavor } from "@grammyjs/parse-mode"; -const bot = new Bot>(''); +type MyApi = ParseModeApiFlavor; +type MyContext = ParseModeFlavor; +const bot = new Bot(""); -// Install format reply variant to ctx -bot.use(hydrateReply); +// Install automatic entities inject from FormattedString transformer +bot.api.config.use(hydrateFmt()); -bot.command('demo', async ctx => { - await ctx.replyFmt(fmt`${bold('bold!')} -${bold(italic('bitalic!'))} -${bold(fmt`bold ${link('blink', 'example.com')} bold`)}`); +bot.command("demo", async (ctx) => { + const boldText = fmt`This is a ${bold("bolded")} string`; + await ctx.reply(boldText); - // fmt can also be called like a regular function - await ctx.replyFmt(fmt(['', ' and ', ' and ', ''], fmt`${bold('bold')}`, fmt`${bold(italic('bitalic'))}`, fmt`${italic('italic')}`)); -}); - -bot.start(); -``` - -## Usage (Using default parse mode and utility reply methods) - -```ts -import { Bot, Composer, Context } from 'grammy'; -import { hydrateReply, parseMode } from '@grammyjs/parse-mode'; - -import type { ParseModeFlavor } from '@grammyjs/parse-mode'; - -const bot = new Bot>(''); - -// Install familiar reply variants to ctx -bot.use(hydrateReply); - -// Sets default parse_mode for ctx.reply -bot.api.config.use(parseMode('MarkdownV2')); + const underlineText = fmt`This is an ${underline("underlined")}`; + await ctx.api.sendMessage(ctx.chat.id, underlineText); -bot.command('demo', async ctx => { - await ctx.reply('*This* is _the_ default `formatting`'); - await ctx.replyWithHTML('This is withHTML formatting'); - await ctx.replyWithMarkdown('*This* is _withMarkdown_ `formatting`'); - await ctx.replyWithMarkdownV1('*This* is _withMarkdownV1_ `formatting`'); - await ctx.replyWithMarkdownV2('*This* is _withMarkdownV2_ `formatting`'); + // fmt can also be use to concat FormattedStrings + const combinedText = fmt`${boldText}\n${underlineText}`; + await bot.api.sendMessage(ctx.chat.id, combinedText); }); bot.start(); diff --git a/src/deps.deno.ts b/src/deps.deno.ts index cbb9ef8..5faaf70 100644 --- a/src/deps.deno.ts +++ b/src/deps.deno.ts @@ -1,9 +1,10 @@ export type { + Api, Context, NextFunction, Transformer, } from "https://lib.deno.dev/x/grammy@^1.0/mod.ts"; export type { + InputTextMessageContent, MessageEntity, - ParseMode, -} from "https://esm.sh/@grammyjs/types@2"; +} from "https://lib.deno.dev/x/grammy@^1.0/types.ts"; diff --git a/src/deps.node.ts b/src/deps.node.ts index 2489246..6cd47ff 100644 --- a/src/deps.node.ts +++ b/src/deps.node.ts @@ -1,2 +1,2 @@ -export type { Context, NextFunction, Transformer } from "grammy"; -export type { MessageEntity, ParseMode } from "@grammyjs/types"; +export type { Api, Context, NextFunction, Transformer } from "grammy"; +export type { InputTextMessageContent, MessageEntity } from "grammy/types"; diff --git a/src/format.ts b/src/format.ts index 6110fa8..8488281 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,7 +1,7 @@ import type { MessageEntity } from "./deps.deno.ts"; /** - * Objects that implement this interface implement a `.toString()` + * Objects that implement this interface implement a `.toString()` * method that returns a `string` value representing the object. */ export interface Stringable { @@ -26,11 +26,11 @@ class FormattedString implements Stringable { entities: MessageEntity[]; /** - * Creates a new `FormattedString`. Useful for constructing a + * Creates a new `FormattedString`. Useful for constructing a * `FormattedString` from user's formatted message * @param text Plain text value * @param entities Format entities - * + * * ```ts * // Constructing a new `FormattedString` from user's message * const userMsg = new FormattedString(ctx.message.text, ctx.entities()); @@ -147,34 +147,45 @@ const customEmoji = (placeholder: Stringable, emoji: number) => { * @param chatId The chat ID to link to. * @param messageId The message ID to link to. */ -const linkMessage = (stringLike: Stringable, chatId: number, messageId: number) => { +const linkMessage = ( + stringLike: Stringable, + chatId: number, + messageId: number, +) => { if (chatId > 0) { - console.warn("linkMessage can only be used for supergroups and channel messages. Refusing to transform into link."); + console.warn( + "linkMessage can only be used for supergroups and channel messages. Refusing to transform into link.", + ); return stringLike; } else if (chatId < -1002147483647 || chatId > -1000000000000) { - console.warn("linkMessage is not able to link messages whose chatIds are greater than -1000000000000 or less than -1002147483647 at this moment. Refusing to transform into link."); + console.warn( + "linkMessage is not able to link messages whose chatIds are greater than -1000000000000 or less than -1002147483647 at this moment. Refusing to transform into link.", + ); return stringLike; } else { - return link(stringLike, `https://t.me/c/${(chatId + 1000000000000) * -1}/${messageId}`); + return link( + stringLike, + `https://t.me/c/${(chatId + 1000000000000) * -1}/${messageId}`, + ); } }; // === Format tagged template function /** - * This is the format tagged template function. It accepts a template literal - * containing any mix of `Stringable` and `string` values, and constructs a + * This is the format tagged template function. It accepts a template literal + * containing any mix of `Stringable` and `string` values, and constructs a * `FormattedString` that represents the combination of all the given values. - * The constructed `FormattedString` also implements Stringable, and can be used + * The constructed `FormattedString` also implements Stringable, and can be used * in further `fmt` tagged templates. * @param rawStringParts An array of `string` parts found in the tagged template * @param stringLikes An array of `Stringable`s found in the tagged template - * + * * ```ts * // Using return values of fmt in fmt * const left = fmt`${bold('>>>')} >>>`; * const right = fmt`<<< ${bold('<<<')}`; - * + * * const final = fmt`${left} ${ctx.msg.text} ${right}`; * await ctx.replyFmt(final); * ``` diff --git a/src/hydrate.ts b/src/hydrate.ts index 7c34884..0342e1c 100644 --- a/src/hydrate.ts +++ b/src/hydrate.ts @@ -1,75 +1,256 @@ -import type { - Context, - MessageEntity, - NextFunction, - ParseMode, -} from "./deps.deno.ts"; -import { FormattedString, type Stringable } from "./format.ts"; +import type { Api, Context, InputTextMessageContent } from "./deps.deno.ts"; -type Tail> = T extends [head: infer E1, ...tail: infer E2] - ? E2 +import { FormattedString } from "./format.ts"; +import { buildTransformer } from "./transformer.ts"; + +type Head> = T extends + [head: infer E1, ...tail: infer E2] ? E1 + : never; + +type Tail> = T extends + [head: infer E1, ...tail: infer E2] ? E2 : []; +type InputTextMessageContentX = + & Omit + & { + message_text: string | FormattedString; + }; + /** - * Context flavor for `Context` that will be hydrated with - * an additional set of reply methods from `hydrateReply` + * Context flavor that will hydrate methods to accept FormattedString */ -type ParseModeFlavor = C & { - replyFmt: ( - stringLike: Stringable, +type ParseModeFlavor = Omit & { + answerInlineQuery: ( + results: Parameters[0] extends readonly (infer Q)[] + ? readonly ( + Q extends { type: "article"; input_message_content: infer R1 } + ? Omit & { + input_message_content: R1 | InputTextMessageContentX; + } + : Q extends { caption?: string; input_message_content?: infer R2 } + ? Omit & { + caption?: string | FormattedString; + input_message_content?: R2 | InputTextMessageContentX; + } + : Q extends { input_message_content?: infer R3 } + ? Omit & { + input_message_content?: R3 | InputTextMessageContentX; + } + : Q + )[] + : never, + ...args: Tail> + ) => ReturnType; + copyMessage: ( + chat_id: Head>, + other?: Head>> & { + caption?: FormattedString; + }, + ...args: Tail>> + ) => ReturnType; + editMessageCaption: ( + other?: Head> & { + caption?: FormattedString; + }, + ...args: Tail> + ) => ReturnType; + editMessageMedia: ( + media: Head> & { + caption?: FormattedString; + }, + ...args: Tail> + ) => ReturnType; + editMessageText: ( + text: FormattedString, + ...args: Tail> + ) => ReturnType; + reply: ( + text: FormattedString, ...args: Tail> ) => ReturnType; - replyWithHTML: C["reply"]; - replyWithMarkdown: C["reply"]; - replyWithMarkdownV1: C["reply"]; - replyWithMarkdownV2: C["reply"]; + replyWithAnimation: ( + animation: Head>, + other?: Head>> & { + caption?: FormattedString; + }, + ...args: Tail>> + ) => ReturnType; + replyWithAudio: ( + audio: Head>, + other?: Head>> & { + caption?: FormattedString; + }, + ...args: Tail>> + ) => ReturnType; + replyWithDocument: ( + document: Head>, + other?: Head>> & { + caption?: FormattedString; + }, + ...args: Tail>> + ) => ReturnType; + replyWithPhoto: ( + photo: Head>, + other?: Head>> & { + caption?: FormattedString; + }, + ...args: Tail>> + ) => ReturnType; + replyWithPoll: ( + question: Head>, + options: Head>>, + other?: Head>>> & { + explanation?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; + replyWithVideo: ( + photo: Head>, + other?: Head>> & { + caption?: FormattedString; + }, + ...args: Tail>> + ) => ReturnType; + replyWithVoice: ( + photo: Head>, + other?: Head>> & { + caption?: FormattedString; + }, + ...args: Tail>> + ) => ReturnType; }; /** - * @deprecated Use ParseModeFlavor instead of ParseModeContext + * Api flavor that will hydrate methods to accept FormattedString */ -type ParseModeContext = ParseModeFlavor; - -const buildReplyWithParseMode = ( - parseMode: ParseMode, - ctx: ParseModeFlavor, -) => { - return (...args: Parameters) => { - const [text, payload, ...rest] = args; - return ctx.reply( - text, - { ...payload, parse_mode: parseMode }, - ...rest as any, - ); - }; +type ParseModeApiFlavor = Omit & { + answerInlineQuery: ( + inline_query_id: Head>, + results: Tail>[0] extends + readonly (infer Q)[] ? readonly ( + Q extends { type: "article"; input_message_content: infer R1 } + ? Omit & { + input_message_content: R1 | InputTextMessageContentX; + } + : Q extends { caption?: string; input_message_content?: infer R2 } + ? Omit & { + caption?: string | FormattedString; + input_message_content?: R2 | InputTextMessageContentX; + } + : Q extends { input_message_content?: infer R3 } + ? Omit & { + input_message_content?: R3 | InputTextMessageContentX; + } + : Q + )[] + : never, + ...args: Tail>> + ) => ReturnType; + copyMessage: ( + chat_id: Head>, + from_chat_id: Head>>, + message_id: Head>>>, + other?: Tail>>> & { + caption?: FormattedString; + }, + ...args: Tail>>>> + ) => ReturnType; + editMessageCaption: ( + chat_id: Head>, + message_id: Head>>, + other?: Head>>> & { + caption?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; + editMessageCaptionInline: ( + inline_message_id: Head>, + other?: Head>> & { + caption?: FormattedString; + }, + ...args: Tail>> + ) => ReturnType; + editMessageMedia: ( + chat_id: Head>, + message_id: Head>>, + media: Head>>> & { + caption?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; + editMessageText: ( + chat_id: Head>, + message_id: Head>>, + text: FormattedString, + ...args: Tail>>> + ) => ReturnType; + sendMessage: ( + chat_id: Head>, + text: FormattedString, + ...args: Tail>> + ) => ReturnType; + sendAnimation: ( + chat_id: Head>, + animation: Head>>, + other?: Head>>> & { + caption?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; + sendAudio: ( + chat_id: Head>, + audio: Head>>, + other?: Head>>> & { + caption?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; + sendDocument: ( + chat_id: Head>, + document: Head>>, + other?: Head>>> & { + caption?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; + sendPhoto: ( + chat_id: Head>, + photo: Head>>, + other?: Head>>> & { + caption?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; + sendPoll: ( + chat_id: Head>, + question: Head>>, + options: Head>>>, + other?: Head>>>> & { + caption?: FormattedString; + }, + ...args: Tail>>>> + ) => ReturnType; + sendVideo: ( + chat_id: Head>, + video: Head>>, + other?: Head>>> & { + caption?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; + sendVoice: ( + chat_id: Head>, + voice: Head>>, + other?: Head>>> & { + caption?: FormattedString; + }, + ...args: Tail>>> + ) => ReturnType; }; -/** - * Hydrates a context with an additional set of reply methods - * @param ctx The context to hydrate - * @param next The next middleware function - */ -const middleware = async ( - ctx: ParseModeFlavor, - next: NextFunction, -) => { - ctx.replyFmt = (stringLike, ...args) => { - const [payload, ...rest] = args; - const entities = stringLike instanceof FormattedString - ? { entities: stringLike.entities } - : undefined; - return ctx.reply( - stringLike.toString(), - { ...payload, ...entities }, - ...rest as any, - ) as ReturnType; - }; - - ctx.replyWithHTML = buildReplyWithParseMode("HTML", ctx); - ctx.replyWithMarkdown = buildReplyWithParseMode("MarkdownV2", ctx); - ctx.replyWithMarkdownV1 = buildReplyWithParseMode("Markdown", ctx); - ctx.replyWithMarkdownV2 = buildReplyWithParseMode("MarkdownV2", ctx); - return next(); +export { + buildTransformer as hydrateFmt, + type ParseModeApiFlavor, + type ParseModeFlavor, }; - -export { middleware as hydrateReply, type ParseModeFlavor, type ParseModeContext }; diff --git a/src/transformer.ts b/src/transformer.ts index c25b203..dfa308b 100644 --- a/src/transformer.ts +++ b/src/transformer.ts @@ -1,72 +1,106 @@ import type { Transformer } from "./deps.deno.ts"; -const wellKnownParseModesMap = new Map([ - ["html", "HTML"], - ["markdown", "Markdown"], - ["markdownv2", "MarkdownV2"], -]); +import { FormattedString } from "./format.ts"; +import { + isCaptionEntitiesPayload, + isCaptionEntitiesResult, + isExplanationEntitiesPayload, + isMediaEntitiesPayload, + isMessageTextEntitiesContent, + isTextEntitiesPayload, +} from "./utils.ts"; /** - * Creates a new transformer for the given parse mode. - * @param parseMode {string} The parse mode to use. If the parse mode is not in the well known parse modes map, it will be used as is. - * @see https://core.telegram.org/bots/api#formatting-options for well known parse modes. + * Creates a new transformer that extracts entities from FormattedString text element. + * @see Usage https://grammy.dev/plugins/parse-mode#usage-improving-formatting-experience * @returns {Transformer} The transformer. */ -const buildTransformer = (parseMode: string) => { - const normalisedParseMode = - wellKnownParseModesMap.get(parseMode.toLowerCase()) ?? parseMode; - if (!wellKnownParseModesMap.has(parseMode.toLowerCase())) { - console.warn( - `Could not find parse_mode: ${parseMode}. If this is a valid parse_mode, you should ignore this message.`, - ); - } - +const buildTransformer = () => { const transformer: Transformer = (prev, method, payload, signal) => { if (!payload || "parse_mode" in payload) { return prev(method, payload, signal); } - switch (method) { - case "editMessageMedia": + if ( + isCaptionEntitiesPayload(method, payload) && + payload.caption instanceof FormattedString + ) { + const caption = payload.caption; + const entities = payload.caption_entities; + payload.caption = caption.toString(); + payload.caption_entities = [ + ...(entities ? entities : []), + ...caption.entities, + ]; + } else if ( + isExplanationEntitiesPayload(method, payload) && + payload.explanation instanceof FormattedString + ) { + const explanation = payload.explanation; + const entities = payload.explanation_entities; + payload.explanation = explanation.toString(); + payload.explanation_entities = [ + ...(entities ? entities : []), + ...explanation.entities, + ]; + } else if (isMediaEntitiesPayload(method, payload)) { + const iterableMedia = payload.media instanceof Array + ? payload.media + : [payload.media]; + for (const media of iterableMedia) { + if (media.caption instanceof FormattedString) { + const caption = media.caption; + const entities = media.caption_entities; + media.caption = caption.toString(); + media.caption_entities = [ + ...(entities ? entities : []), + ...caption.entities, + ]; + } + } + } else if ( + isTextEntitiesPayload(method, payload) && + payload.text instanceof FormattedString + ) { + const text = payload.text; + const entities = payload.entities; + payload.text = text.toString(); + payload.entities = [...(entities ? entities : []), ...text.entities]; + } else if ("results" in payload && payload.results instanceof Array) { + for (const result of payload.results) { if ( - "media" in payload && - !("parse_mode" in payload.media) + isCaptionEntitiesResult(result) && + result.caption instanceof FormattedString ) { - // @ts-ignore - payload.media.parse_mode = normalisedParseMode; + const caption = result.caption; + const entities = result.caption_entities; + result.caption = caption.toString(); + result.caption_entities = [ + ...(entities ? entities : []), + ...caption.entities, + ]; } - break; - case "answerInlineQuery": - if ("results" in payload) { - for (const result of payload.results) { - if ( - "input_message_content" in result && - // @ts-ignore - !("parse_mode" in result.input_message_content) - ) { - // @ts-ignore - result.input_message_content.parse_mode = normalisedParseMode; - } - else if (!("parse_mode" in result)) { - // @ts-ignore - result.parse_mode = normalisedParseMode; - } + if ("input_message_content" in result && result.input_message_content) { + const content = result.input_message_content; + if ( + isMessageTextEntitiesContent(content) && + content.message_text instanceof FormattedString + ) { + const text = content.message_text; + const entities = content.entities; + content.message_text = text.toString(); + content.entities = [ + ...(entities ? entities : []), + ...text.entities, + ]; } } - break; - - default: - payload = { ...payload, ...{ parse_mode: normalisedParseMode } }; + } } - - return prev( - method, - payload, - signal, - ); + return prev(method, payload, signal); }; return transformer; }; -export { buildTransformer as parseMode }; +export { buildTransformer }; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..a9067a0 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,101 @@ +import type { MessageEntity } from "./deps.deno.ts"; + +import { FormattedString } from "./format.ts"; + +interface CaptionEntities { + caption?: FormattedString | string; + caption_entities?: MessageEntity[]; +} + +interface ExplanationEntities { + explanation?: FormattedString | string; + explanation_entities?: MessageEntity[]; +} + +interface MediaEntities { + media: CaptionEntities | CaptionEntities[]; +} + +interface MessageTextEntities { + message_text: FormattedString | string; + entities?: MessageEntity[]; +} + +interface TextEntities { + text: FormattedString | string; + entities?: MessageEntity[]; +} + +const captionEntitiesMethod = new Set([ + "copyMessage", + "editMessageCaption", + "sendAnimation", + "sendAudio", + "sendDocument", + "sendPhoto", + "sendVideo", + "sendVoice", +]); + +const explanationEntitiesMethod = new Set([ + "sendPoll", +]); + +const mediaEntitiesMethod = new Set([ + "editMessageMedia", + "sendMediaGroup", +]); + +const textEntitiesMethod = new Set([ + "editMessageText", + "sendMessage", +]); + +function isCaptionEntitiesPayload( + method: string, + _payload: unknown, +): _payload is CaptionEntities { + return captionEntitiesMethod.has(method); +} + +function isExplanationEntitiesPayload( + method: string, + _payload: unknown, +): _payload is ExplanationEntities { + return explanationEntitiesMethod.has(method); +} + +function isMediaEntitiesPayload( + method: string, + _payload: unknown, +): _payload is MediaEntities { + return mediaEntitiesMethod.has(method); +} + +function isTextEntitiesPayload( + method: string, + _payload: unknown, +): _payload is TextEntities { + return textEntitiesMethod.has(method); +} + +function isCaptionEntitiesResult( + result: {}, +): result is CaptionEntities { + return result instanceof Object && "caption" in result; +} + +function isMessageTextEntitiesContent( + content: {}, +): content is MessageTextEntities { + return content instanceof Object && "message_text" in content; +} + +export { + isCaptionEntitiesPayload, + isCaptionEntitiesResult, + isExplanationEntitiesPayload, + isMediaEntitiesPayload, + isMessageTextEntitiesContent, + isTextEntitiesPayload, +};