const { OpengramContext: Context } = require('./context') const { getEntities, getText } = require('./core/helpers/utils') /** * The composer is the heart of the middleware system in Opengram. It is also the * super class of `Opengram`. * * Whenever you call `use` or `on` or some of the other * methods on your bot, you are in fact using the underlying composer instance * to register your middleware. */ class Composer { /** * Constructs a new composer based on the provided middleware. If no * middleware is given, the composer instance will simply make all context * objects pass through without touching them. * * @param {Middleware} fns The middlewares to compose as arguments */ constructor (...fns) { this.handler = Composer.compose(fns) } /** * Registers some middleware(s) that receives all updates. It is installed by * concatenating it to the end of all previously installed middleware. * * Often, this method is used to install middleware(s) that behaves like a * plugin, for example session middleware. * ```js * bot.use(session()) * ``` * * You can pass middleware separated by commas as arguments or as a chain of calls: * ```js * const { Opengram, Stage, session } = require('opengram') * const bot = require('opengram') * const stage = new Stage([...]) * bot.use(session(), stage) // As arguments * ``` * or * ```js * const { Opengram, Stage, session } = require('opengram') * const bot = require('opengram') * const stage = new Stage([...]) * bot // As chain of calls * .use(session()) * .use(stage) * ``` * * This method returns a new instance of {@link Composer}. * * @param {Middleware} fns The middleware(s) to register as arguments * @return {Composer} */ use (...fns) { this.handler = Composer.compose([this.handler, ...fns]) return this } /** * Registers some middleware(s) that will only be executed for some specific * updates, namely those matching the provided filter query. Filter queries * are a concise way to specify which updates you are interested in. * * Here are some examples of valid filter queries: * ```js * // All kinds of message updates * bot.on('message', ctx => { ... }) * * // Text messages * bot.on('text', ctx => { ... }) * * // Messages with document * bot.on('document', ctx => { ... }) * ``` * * It is possible to pass multiple filter queries in an array, i.e. * ```js * // Matches all messages that contain a video or audio * bot.on(['audio', 'video'], ctx => { ... }) * ``` * * Your middleware will be executed if _any of the provided filter queries_ * matches (logical OR). * * This method returns same as {@link Composer#use}. * * @param {UpdateType|UpdateSubtype|Array<UpdateType|UpdateSubtype>} updateTypes The update type or array of update types to use, * may also be an array or string * @param {Middleware} fns The middleware(s) to register with the given types as argument(s) * @return {Composer} */ on (updateTypes, ...fns) { return this.use(Composer.mount(updateTypes, ...fns)) } /** * Registers some middleware(s) that will only be executed when the message / channel post * contains some text (in media caption too). Is it possible to pass a regular expression to match: * ```js * // Match some text (exact match) * bot.hears('I love anime', ctx => ctx.reply('I love too')) * * // Match a regular expression * bot.hears(/\/echo (.+)/, ctx => ctx.reply(ctx.match[1])) * ``` * * > Note how `ctx.match` will contain the result of the regular expression. * > So `ctx.match[1]` refers to the part of the regex that was matched by `(.+)`, * > i.e. the text that comes after "/echo". * * You can also paste function (or array of functions) that takes the value and context as arguments and returns true * or false (or some `Truthy` result) based on them. This can be used, for example, for dynamic text matching at i18n. * **The result returned by the function will be available from** `ctx.match` * * ```js * bot.hears( * (value, ctx) => { * //... some checks ... * return ['some', 'data'] * }, * ctx => ctx.reply(`I love ${ctx.match[0]} ${ctx.match[1]}`) // Replies at all with "I love some data" * ) * ``` * * You can pass an array of triggers. Your middleware will be executed if at * least one of them matches. * * Both text and captions of the received messages will be scanned. For * example, when a photo is sent to the chat and its caption matches the * trigger, your middleware will be executed. * * If you only want to match text messages and not captions, you can do * this: * ```js * const { Composer: { hears } } = require('opengram') * // Only matches text messages for the regex * bot.on('text', hears(/\/echo (.+)/, ctx => { ... })) * ``` * * > _**Be careful, the example above may not work as expected if `channelMode` is enabled.**_ * > * > By default `text` type not match channel posts, but `channel_post` matched as `text` type and * > `ctx.message` potentially `undefined` * > when `channelMode` enabled. You can add additional chat type check for this case * * @param {Trigger|Trigger[]} triggers The text / array of * texts / regex / function to look for * @param {Middleware} fns The middleware(s) to register as argument(s) */ hears (triggers, ...fns) { return this.use(Composer.hears(triggers, ...fns)) } /** * Registers some middleware(s) that will only be executed when a certain * command is found. * ```js * // Reacts to /start commands * bot.command('start', ctx => { ... }) * // Reacts to /help commands * bot.command('help', ctx => { ... }) * ``` * * > **Note:** Commands are not matched in the middle of the text. * * ```js * bot.command('start', ctx => { ... }) * // ... does not match: * // A message saying: “some text /start some more text” * // A photo message with the caption “some text /start some more text” * ``` * * By default, commands are detected in channel posts and media captions, too. This means that * `ctx.message` for channel post or `ctx.message.text` for media is potentially `undefined`, * so you should use `ctx.channelPost` and `ctx.message.caption` accordingly * for channel posts. Alternatively, if you * want to limit your bot to finding commands only in private and group * chats, you can use * * ```js * const { Opengram, Composer: { command } } = require('opengram') * // ... * bot.on('message', command('start', ctx => ctx.reply('Only private / group messages or media with caption')))` * ``` * * or using {@link Composer.chatType}: * * ```js * const { Opengram, Composer, Composer: { command } } = require('opengram') * // ... * bot.use( * Composer.chatType( * ["private", "group", "supergroup"], * command('start', ctx => ctx.reply('Only private / group messages or media with caption')) * ) * ) * ``` * * for match all message exclude channel posts, or * * ```js * const { Opengram, Composer: { command } } = require('opengram') * // ... * bot.on('text', command('start', ctx => ctx.reply('Math commands only text, not media captions'))) * ``` * * for match only text message, not media caption * or even store a message-only version of your bot in a variable like so: * * > _**Be careful, the example above may not work as expected if `channelMode` is enabled.**_ * > * > By default `text` type not match channel posts, but `channel_post` matched as `text` type and * > `ctx.message` potentially `undefined` * > when `channelMode` enabled. You can add additional chat type check for this case * * @param {string|string[]|'start'|'settings'|'help'} commands The command or array of commands to look for * @param {Middleware} fns The middleware(s) to register as arguments */ command (commands, ...fns) { return this.use(Composer.command(commands, ...fns)) } /** * Registers some middleware(s) for callback queries, i.e. the updates that * Telegram delivers to your bot when a user clicks an inline button (that * is a button under a message). * * This method is essentially the same as calling * ```js * bot.on('callback_query', ctx => { ... }) * ``` * but it also allows you to match the query data against a given text or * regular expression. * * ```js * // Create an inline keyboard * const keyboard = Markup.inlineKeyboard([ * Markup.callbackButton('Go!', 'button-payload') * ]) * // Send a message with the keyboard * await bot.telegram.sendMessage(chat_id, 'Press a button!', keyboard.extra()) * // Listen to users pressing buttons with that specific payload * bot.action('button-payload', ctx => { ... }) * * // Listen to users pressing any button your bot ever sent * bot.on('callback_query', ctx => { ... }) * ``` * * Always remember to call * {@link Telegram#answerCbQuery} or {@link OpengramContext#answerCbQuery} * — even if you don't perform any action: {@linkplain https://core.telegram.org/bots/api#answercallbackquery} * ```js * bot.on('callback_query', async ctx => { * await ctx.answerCbQuery() * }) * ``` * * You can pass one or an array of triggers (Regexp / strings). Your middleware(s) will be executed if at * least one of them matches. * * > Note how `ctx.match` will contain the result of the regular expression. * > So `ctx.match[1]` refers to the part of the regexp that was matched by `([0-9]+)`, * > i.e. the text that comes after "button:". * > ```js * > bot.action(/button:([0-9]+)/, ctx => ctx.reply(`You choose button with number ${ctx.match[1]} in payload`)) * > const keyboard = Markup.inlineKeyboard([ * > Markup.callbackButton('Button 1', 'button:1'), * > Markup.callbackButton('Button 2', 'button:2'), * > Markup.callbackButton('Button 3', 'button:3') * > ]) * > await bot.telegram.sendMessage(chat_id, 'Press a button!', keyboard.extra()) * > ``` * * You can also paste function (or array of functions) that takes the value and context as arguments and returns true * or false (or some `Truthy` result) based on them. This can be used, for example, for dynamic text matching at i18n. * **The result returned by the function will be available from** `ctx.match` * * ```js * bot.action( * (value, ctx) => { * //... some checks ... * return ['some', 'data'] * }, * // Show cb query answer for all queries with "I love some data" * ctx => ctx.answerCbQuery(`I love ${ctx.match[0]} ${ctx.match[1]}`) * ) * ``` * * @param {Trigger|Trigger[]} triggers One or an array of * regular expressions / strings to search in the payload * @param {Middleware} fns The middleware(s) to register as arguments * @return {Composer} */ action (triggers, ...fns) { return this.use(Composer.action(triggers, ...fns)) } /** * Registers middleware for inline queries. Telegram sends an inline query * to your bot whenever a user types `@your_bot_name ...` into a text field * in Telegram. * * Your bot will then receive the entered search query and can * respond with a number of results (text, images, etc.) that the user can * pick from to send a message _via_ your bot to the respective chat. * Check [here](https://core.telegram.org/bots/inline) to read more about inline bots. * * > Note that you have to enable inline mode for you bot by contacting * > [@BotFather](https://t.me/BotFather) first. * * ```js * // Listen for users typing `@your_bot_name query` * bot.inlineQuery('query', async ctx => { * // Answer the inline query, confer https://core.telegram.org/bots/api#answerinlinequery * await ctx.answerInlineQuery( ... ) * }) * ``` * * You can pass one or an array of triggers (Regexp / strings). Your middleware(s) will be executed if at * least one of them matches. * * > Note how `ctx.match` will contain the result of the regular expression. * > So `ctx.match[1]` refers to the part of the regexp that was matched by `([0-9]+)`, * > i.e. the text that comes after "query:". * ```js * // Listen for users typing `@your_bot_name query` * bot.inlineQuery(/query:([0-9]+)/, async ctx => { * // Answer the inline query, confer https://core.telegram.org/bots/api#answerinlinequery * await ctx.answerInlineQuery([{ * type: 'article', * id: Math.random(), * title: 'Regex test', * cache_time: 1, * description: `Query Regex result: ${ctx.match[1]}`, * input_message_content: { * message_text: `Query Regex result: ${ctx.match[1]}`, * } * }]) * }) * ``` * * You can also paste function (or array of functions) that takes the value and context as arguments and returns true * or false (or some `Truthy` result) based on them. This can be used, for example, for dynamic text matching at i18n. * **The result returned by the function will be available from** `ctx.match` * * ```js * bot.inlineQuery( * (value, ctx) => { * //... some checks ... * return ['some', 'data'] * }, * // Show cb query answer for all queries with "I love some data" * ctx => ctx.answerInlineQuery([{ * type: 'article', * id: Math.random(), * title: 'Regex test', * cache_time: 1, * description: `I love ${ctx.match[0]} ${ctx.match[1]}`, * input_message_content: { * message_text: `I love ${ctx.match[0]} ${ctx.match[1]}`, * } * }]) * }) * ``` * * @param {Trigger|Trigger[]} triggers The inline query text * or array of text to match * @param {Middleware} fns The middleware(s) to register * @return {Composer} */ inlineQuery (triggers, ...fns) { return this.use(Composer.inlineQuery(triggers, ...fns)) } /** * Registers some middleware(s) for game queries, i.e. the updates that * Telegram delivers to your bot when a user clicks an inline button for the * HTML5 games platform on Telegram. * * This method is essentially the same as calling * ```js * bot.on('callback_query', ctx => { * if (ctx.callbackQuery.game_short_name) { * ... * } * }) * ``` * * @param {Middleware} fns The middleware to register as arguments * @return {Composer} */ gameQuery (...fns) { return this.use(Composer.gameQuery(...fns)) } /** * Registers middleware behind a custom filter function that operates on the * context object and decides whether to execute the middleware. * * In other words, the middleware(s) after that middleware will only be executed if the given predicate * returns `false` for the given context object. Note that the predicate * may be asynchronous, i.e. it can return a Promise of a boolean. * * This method is the same using `filter` (normal usage) with a negated * predicate. * * ```js * // Drop all message updates sent more than 6 hr in all middlewares / handlers registered after bot.drop(...) * bot.drop(ctx => { * if(!ctx.message) return false // Drop only messages * return (Date.now() / 1000) - ctx.message.date < 60 * 60 * 6 * }) * // Called only for messages with date < 6 hr after send * bot.on('message', () => ctx.reply('Good, update date less then 6 hours!')) * ``` * * @param {PredicateFn|boolean} predicate The predicate to check. Can be async, returns boolean or Promise with boolean * @return {Composer} */ drop (predicate) { return this.use(Composer.drop(predicate)) } /** * Registers middleware(s) behind a custom filter function that operates on the * context object and decides whether to execute the middleware. In * other words, the middleware will only be executed if the given predicate * returns `true` for the given context object. Otherwise, it will be * skipped and the next middleware will be executed. * * In other words, the middleware after that middleware will only be executed if the given predicate * returns `true` for the given context object. Note that the predicate * may be asynchronous, i.e. it can return a Promise of a boolean. * ```js * // Only process every second update * bot.filter(ctx => ctx.update.update_id % 2 === 0) * bot.on('message', ctx => ctx.reply('Update id of this message is divided by two without a remainder')) * ``` * * @param {PredicateFn|boolean} predicate The predicate to check. Can be async, returns boolean or Promise with boolean * @return {Composer} */ filter (predicate) { return this.use(Composer.filter(predicate)) } /** * Registers some middleware(s) that will only be executed if a certain entity is present in the update * * This method matches entity in channel post, message and media caption * * @param {EntityPredicate} predicate The predicate to check. Can be async, returns boolean or Promise with boolean * @param {Middleware} fns The middleware(s) to register * @return {Composer} */ entity (predicate, ...fns) { return this.use(Composer.entity(predicate, ...fns)) } /** * Registers some middleware(s) that will only be executed if `custom_emoji` entity is present in the update * * Shortcut to `Composer.entity('custom_emoji', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ customEmoji (...args) { return this.use(Composer.customEmoji(...args)) } /** * Registers some middleware(s) that will only be executed if `email` entity is present in the update * * Shortcut to `Composer.entity('email', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ email (...args) { return this.use(Composer.email(...args)) } /** * Registers some middleware(s) that will only be executed if `phone` entity is present in the update * * Shortcut to `Composer.entity('phone', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ phone (...args) { return this.use(Composer.phone(...args)) } /** * Registers some middleware(s) that will only be executed if `url` entity is present in the update * * Shortcut to `Composer.entity('url', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ url (...args) { return this.use(Composer.url(...args)) } /** * Registers some middleware(s) that will only be executed if `text_link` entity is present in the update * * Shortcut to `Composer.entity('text_link', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ textLink (...args) { return this.use(Composer.textLink(...args)) } /** * Registers some middleware(s) that will only be executed if `text_mention` entity is present in the update * * Shortcut to `Composer.entity('text_mention', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ textMention (...args) { return this.use(Composer.textMention(...args)) } /** * Registers some middleware(s) that will only be executed if `mention` entity is present in the update * * Shortcut to `Composer.entity('mention', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ mention (...args) { return this.use(Composer.mention(...args)) } /** * Registers some middleware(s) that will only be executed if `hashtag` entity is present in the update * * Shortcut to `Composer.entity('hashtag', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ hashtag (...args) { return this.use(Composer.hashtag(...args)) } /** * Registers some middleware(s) that will only be executed if `hashtag` entity is present in the update * * Shortcut to `Composer.entity('cashtag', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ cashtag (...args) { return this.use(Composer.cashtag(...args)) } /** * Registers some middleware(s) that will only be executed if `spoiler` entity is present in the update * * Shortcut to `Composer.entity('spoiler', ...)` * * This method matches entity in channel post, message and media caption * * @param {Middleware} args The middleware(s) to register * @return {Composer} */ spoiler (...args) { return this.use(Composer.spoiler(...args)) } /** * Registers some middleware that will only be executed when `/start` command is found. * * Shortcut to `Composer.command('start', ...)`, but with additional functionally, when you use this and * deep linking, you can get start payload from `ctx.startPayload` * * For example if user start the bot from link like this: `http://t.me/examplebot?start=1234` * * With this code, bot reply user with text of start payload: * * ```js * bot.start(ctx => ctx.reply(`Start payload: ${ctx.startPayload}`)) // Reply with "Start payload: 1234" * ``` * * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ start (...fns) { return this.command('start', Composer.tap((ctx) => { const entity = ctx.message.entities[0] ctx.startPayload = ctx.message.text.slice(entity.length + 1) }), ...fns) } /** * Registers some middleware that will only be executed when `/help` command is found. * * Shortcut to `Composer.command('help', ...)` * * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ help (...fns) { return this.command('help', ...fns) } /** * Registers some middleware that will only be executed when `/settings` command is found. * * Shortcut to `Composer.command('settings', ...)` * * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ settings (...fns) { return this.command('settings', ...fns) } /** * Returns the middleware to embed * * @return {Middleware} */ middleware () { return this.handler } /** * Generates and return middleware for reply with given arguments, has same arguments like in Opengram, * context method `reply` * * Usage example: * ```js * // Send message with text "I do not support group chats" when receive update from group chat * bot.use( * Composer.groupChat(Composer.reply('I do not support group chats')) * ) * ``` * * @see https://core.telegram.org/bots/api#sendmessage * @param {string} text Text of the message to be sent, 1-4096 characters after entities parsing * @param {ExtraSendMessage|Extra} [extra] Other parameters * @throws {TelegramError} * @return {Middleware<Promise<Message>>} */ static reply (text, extra) { return (ctx) => ctx.reply(text, extra) } /** * Generates middleware that catches all errors in the middleware(s) given to it and outputs them to the console * * @param {Middleware} fns Middlewares * @return {Middleware} */ static catchAll (...fns) { return Composer.catch((err) => { console.error() console.error((err.stack || err.toString()).replace(/^/gm, ' ')) console.error() }, ...fns) } /** * Generates middleware that catches all errors in the middleware(s) given to it and calls given error handler * * @param {Function} errorHandler Error handler which takes error and context object as arguments * @param {Middleware} fns Middleware(s) * @return {Middleware} */ static catch (errorHandler, ...fns) { const handler = Composer.compose(fns) return (ctx, next) => Promise.resolve(handler(ctx, next)) .catch((err) => errorHandler(err, ctx)) } /** * Registers some middleware that runs concurrently to the executing middleware stack. * Runs the middleware at the next event loop using * [setImmediate](https://nodejs.dev/en/learn/understanding-setimmediate) and force call `next()` * * For example, you can use that method for saving metrics and other non-priority or optional features in background * * > ❗️ If you call next in this middleware, then nothing will happen, it will be ignored * * @param {Middleware} middleware The middleware to run concurrently * @return {Middleware} */ static fork (middleware) { const handler = Composer.unwrap(middleware) return (ctx, next) => { setImmediate(handler, ctx, Composer.safePassThru()) return next(ctx) } } /** * Middleware that calls a middleware or chain of middleware and calls the `next`, whether it called `next` or not. * Allows you to execute some code and continue execution regardless of its result. * * @param {Middleware} middleware The middleware to run without access to next * @return {Middleware} */ static tap (middleware) { const handler = Composer.unwrap(middleware) return async (ctx, next) => { await Promise.resolve( handler(ctx, Composer.safePassThru()) ) return next(ctx) } } /** * Generates middleware which call next middleware * * For example, you can use it with {@link Composer.branch} or other to skip middleware (make middleware optional) * * @return {Middleware} */ static passThru () { return (ctx, next) => next(ctx) } /** * * Generates middleware which call next middleware if `next` function exists or returns empty resolved promise * * This method is similar to `Composer.passThru()`, but calls `next` if exists, otherwise returns resolved promise * * @return {Middleware} */ static safePassThru () { return (ctx, next) => typeof next === 'function' ? next(ctx) : Promise.resolve() } /** * Lazily asynchronously returns some middleware that can be generated on the fly for each context. * Pass a factory function that creates some middleware * * The factory function will be called once per context, and its result will be executed with the context object. * ```js * // The middleware returned by `createMyMiddleware` will be used only once * bot.use( * Composer.lazy(ctx => createMyMiddleware(ctx)) * ) * ``` * * You may generate this middleware in an `async` fashion. * * @param {Function} factoryFn The factory function creating the middleware * @throws {TypeError} * @return {Middleware<Promise>} */ static lazy (factoryFn) { if (typeof factoryFn !== 'function') { throw new TypeError('Argument must be a function') } return async (ctx, next) => { const middleware = await Promise.resolve(factoryFn(ctx)) return Composer.unwrap(middleware)(ctx, next) } } /** * Method that generates a middleware to output the content of the context with indented beautiful in * console using serialization * * The default for logs is `console.log`, you can pass your own log function in argument: * * ```js * const myOwnLogFn = (data) => console.log('[Logs]', data) * bot.use(Composer.log(myOwnLogFn)) * ``` * * @param {Function} logFn Custom log function * @return {Middleware} */ static log (logFn = console.log) { return Composer.fork((ctx) => logFn(JSON.stringify(ctx.update, null, 2))) } /** * * Allows you to branch between two cases for a given context object. * * This method takes a predicate function that is tested once per context * object. If it returns `true`, the first supplied middleware is executed. * If it returns `false`, the second supplied middleware is executed. Note * that the predicate may be asynchronous, i.e. it can return a Promise of a boolean. * * ```js * bot.use( * Composer.branch( * (ctx) => ctx.from.is_premium, * (ctx) => ctx.reply('This mw executed only for premium users'), * (ctx) => ctx.reply('Buy premium :(') * ) * ) * ``` * * @param {PredicateFn|boolean} predicate The predicate to check. Can be async, returns boolean or Promise with boolean * @param {Middleware} trueMiddleware The middleware for the `true` case * @param {Middleware} [falseMiddleware] The middleware for the `false` case * @return {Middleware} */ static branch (predicate, trueMiddleware, falseMiddleware) { if (typeof predicate !== 'function') { return predicate ? trueMiddleware : falseMiddleware } return Composer.lazy((ctx) => Promise.resolve(predicate(ctx)) .then((value) => value ? trueMiddleware : falseMiddleware)) } /** * Generates middleware that makes given middleware(s) optional * * Example * ```js * bot.use( * Composer.optional( * (ctx) => ctx.from.is_premium, // Check premium * // The handlers from below will be executed only if predict returns true * async (ctx, next) => { * await ctx.reply('This mw and below will be executed only for premium users') * return next() * }, * (ctx) => { * // ...other middleware.. ...code..., * }, * (ctx) => { * // ...other middleware.. ...code..., * }, * ) * ) * ``` * * @param {PredicateFn|boolean} predicate The predicate to check. Can be async, returns boolean or Promise with boolean * @param {Middleware} fns Middleware(s) * @return {Middleware} */ static optional (predicate, ...fns) { return Composer.branch(predicate, Composer.compose(fns), Composer.safePassThru()) } /** * Generates middleware behind a custom filter function that operates on the * context object and decides whether to execute the middleware. In * other words, the middleware will only be executed if the given predicate * returns `true` for the given context object. Otherwise, it will be * skipped and the next middleware will be executed. * * In other words, the middleware after that middleware will only be executed if the given predicate * returns `true` for the given context object. Note that the predicate * may be asynchronous, i.e. it can return a Promise of a boolean. * * ```js * // Only process every second update * bot.on( * 'message', * Composer.filter(ctx => ctx.update.update_id % 2 === 0) * ctx => ctx.reply('Update id of this message is divided by two without a remainder') * ) * ``` * * @param {PredicateFn|boolean} predicate The predicate to check. Can be async, returns boolean or Promise with boolean * @return {Middleware} */ static filter (predicate) { return Composer.branch(predicate, Composer.safePassThru(), () => { }) } /** * Generates middleware behind a custom filter function that operates on the * context object and decides whether to execute the middleware. * * In other words, the middleware(s) after that middleware will only be executed if the given predicate * returns `false` for the given context object. Note that the predicate * may be asynchronous, i.e. it can return a Promise of a boolean. * * This method is the same using `filter` (normal usage) with a negated * predicate. * * ```js * // Drop all message updates sent more than 6 hr in all middlewares / handlers registered after bot.drop(...) * const mw = Composer.drop(ctx => { * if(!ctx.message) return false // Drop only messages * return (Date.now() / 1000) - ctx.message.date < 60 * 60 * 6 * }) * // Called only for messages with date < 6 hr after send * bot.on('message', mw, () => ctx.reply('Good, update date less then 6 hours!')) * ``` * * @param {PredicateFn|boolean} predicate The predicate to check. Can be async, returns boolean or Promise with boolean * @return {Middleware} */ static drop (predicate) { return Composer.branch(predicate, () => { }, Composer.safePassThru()) } static dispatch (routeFn, handlers) { if (typeof routeFn === 'function') { return Composer.lazy(async (ctx) => { const route = await Promise.resolve( routeFn(ctx) ) return handlers[route] }) } else { return handlers[routeFn] } } /** * Generates middleware that execute given middleware(s) only for some specific * updates, namely those matching the provided filter query. Filter queries * are a concise way to specify which updates you are interested in. * * Here are some examples of valid filter queries: * ```js * // All kinds of message updates * bot.use( * Composer.mount('message', ctx => { ... }) * ) * * // Text messages * bot.use( * Composer.mount('text', ctx => { ... }) * ) * * // Messages with document * bot.use( * Composer.mount('document', ctx => { ... }) * ) * ``` * * It is possible to pass multiple filter queries in an array, i.e. * ```js * // Matches all messages that contain a video or audio * bot.use( * Composer.mount(['audio', 'video'], ctx => { ... }) * ) * ``` * * Your middleware will be executed if _any of the provided filter queries_ * matches (logical OR). * * @param {UpdateType|UpdateSubtype|Array<UpdateType|UpdateSubtype>} updateType The update type or array of update types to use, * may also be an array or string * @param {Middleware} fns The middleware(s) to register with the given types as argument(s) * @return {Middleware} */ static mount (updateType, ...fns) { const updateTypes = normalizeTextArguments(updateType) const predicate = (ctx) => { return updateTypes.includes(ctx.updateType) || updateTypes .some( (type) => ctx.updateSubTypes.includes(type) ) } return Composer.optional(predicate, ...fns) } /** * Generates middleware that execute given middlewares if a certain entity is present in the update * * This method matches entity in channel post, message and media caption * * @param {EntityPredicate} predicate The predicate to check. Entity name or predicate function. * If function provided, it can be sync only and returns boolean * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static entity (predicate, ...fns) { if (typeof predicate !== 'function') { const entityTypes = normalizeTextArguments(predicate) return Composer.entity((entity) => entityTypes.includes(entity.type), ...fns) } return Composer.optional((ctx) => { const message = ctx.message || ctx.channelPost const entities = getEntities(message) const text = getText(message) return entities && entities.some((entity) => predicate(entity, text.substring(entity.offset, entity.offset + entity.length), ctx) ) }, ...fns) } static entityText (entityType, predicate, ...fns) { if (fns.length === 0) { return Array.isArray(predicate) ? Composer.entity(entityType, ...predicate) : Composer.entity(entityType, predicate) } const triggers = normalizeTriggers(predicate) return Composer.entity(({ type }, value, ctx) => { if (type !== entityType) { return false } for (const trigger of triggers) { ctx.match = trigger(value, ctx) if (ctx.match) { return true } } }, ...fns) } static customEmoji (customEmoji, ...fns) { return Composer.entityText('custom_emoji', customEmoji, ...fns) } static email (email, ...fns) { return Composer.entityText('email', email, ...fns) } static phone (number, ...fns) { return Composer.entityText('phone_number', number, ...fns) } static url (url, ...fns) { return Composer.entityText('url', url, ...fns) } static textLink (link, ...fns) { return Composer.entityText('text_link', link, ...fns) } static textMention (mention, ...fns) { return Composer.entityText('text_mention', mention, ...fns) } static mention (mention, ...fns) { return Composer.entityText('mention', normalizeTextArguments(mention, '@'), ...fns) } static hashtag (hashtag, ...fns) { return Composer.entityText('hashtag', normalizeTextArguments(hashtag, '#'), ...fns) } static cashtag (cashtag, ...fns) { return Composer.entityText('cashtag', normalizeTextArguments(cashtag, '$'), ...fns) } static spoiler (text, ...fns) { return Composer.entityText('spoiler', text, ...fns) } /** * Generates middleware that execute given middlewares when some given trigger(s) returns true * * Triggers are executed for channel post / message text / callback query / inline query * * Example: * ```js * Composer.match (/[a-z]/, ...fns) * * Composer.match ([/[a-z]/, /[0-9]/], ...fns) * * Composer.match ((value, context) => { * // ...checks... * } , ...fns) * * Composer.match ( * (value, context) => { * // ...checks... * }, * (value, context) => { * // ...checks... * }, * ...fns * ) * ``` * * @param {Trigger|Trigger[]} triggers The text / array of * texts / regex / function to look for * @param {Middleware} fns The middleware(s) to register as argument(s) */ static match (triggers, ...fns) { return Composer.optional((ctx) => { const text = getText(ctx.message) || getText(ctx.channelPost) || getText(ctx.callbackQuery) || (ctx.inlineQuery && ctx.inlineQuery.query) for (const trigger of triggers) { ctx.match = trigger(text, ctx) if (ctx.match) { return true } } }, ...fns) } /** * Generates middleware that execute given middlewares when the message / channel post * contains some text (in media caption too). Is it possible to pass a regular expression to match: * ```js * // Match some text (exact match) * bot.use( * Composer.hears('I love anime', ctx => ctx.reply('I love too')) * ) * * // Match a regular expression * bot.use( * Composer.hears(/\/echo (.+)/, ctx => ctx.reply(ctx.match[1])) * ) * ``` * * > Note how `ctx.match` will contain the result of the regular expression. * > So `ctx.match[1]` refers to the part of the regex that was matched by `(.+)`, * > i.e. the text that comes after "/echo". * * You can also paste function (or array of functions) that takes the value and context as arguments and returns true * or false (or some `Truthy` result) based on them. This can be used, for example, for dynamic text matching at i18n. * **The result returned by the function will be available from** `ctx.match` * * ```js * bot.use( * Composer.hears( * (value, ctx) => { * //... some checks ... * return ['some', 'data'] * }, * ctx => ctx.reply(`I love ${ctx.match[0]} ${ctx.match[1]}`) // Replies at all with "I love some data" * ) * ) * ``` * * You can pass an array of triggers. Your middleware will be executed if at * least one of them matches. * * Both text and captions of the received messages will be scanned. For * example, when a photo is sent to the chat and its caption matches the * trigger, your middleware will be executed. * * If you only want to match text messages and not captions, you can do * this: * ```js * // Only matches text messages for the regex * bot.on('text', Composer.hears(/\/echo (.+)/, ctx => { ... })) * ``` * * > _**Be careful, the example above may not work as expected if `channelMode` is enabled.**_ * > * > By default `text` type not match channel posts, but `channel_post` matched as `text` type and * > `ctx.message` potentially `undefined` * > when `channelMode` enabled. You can add additional chat type check for this case * * @param {Trigger|Trigger[]} triggers The text / array of * texts / regex / function to look for * @param {Middleware} fns The middleware(s) to register as argument(s) */ static hears (triggers, ...fns) { return Composer.mount('text', Composer.match(normalizeTriggers(triggers), ...fns)) } /** * Generates middleware that execute given middlewares will only be executed if a certain command is found. * * ```js * // Reacts to /start commands * bot.use( * Composer.command('start', ctx => { ... }) * ) * // Reacts to /help commands * bot.use( * Composer.command('help', ctx => { ... }) * ) * ``` * * > **Note:** Commands are not matched in the middle of the text. * * ```js * bot.use( * Composer.command('start', ctx => { ... }) * ) * // ... does not match: * // A message saying: “some text /start some more text” * // A photo message with the caption “some text /start some more text” * ``` * * By default, commands are detected in channel posts and media captions, too. This means that * `ctx.message` for channel post or `ctx.message.text` for media is potentially `undefined`, * so you should use `ctx.channelPost` and `ctx.message.caption` accordingly * for channel posts. Alternatively, if you * want to limit your bot to finding commands only in private and group * chats, you can use * * ```js * const { Opengram, Composer: { command } } = require('opengram') * // ... * bot.on('message', command('start', ctx => ctx.reply('Only private / group messages or media with caption')))` * ``` * * or using {@link Composer.chatType}: * * ```js * const { Opengram, Composer, Composer: { command } } = require('opengram') * // ... * bot.use( * Composer.chatType( * ["private", "group", "supergroup"], * command('start', ctx => ctx.reply('Only private / group messages or media with caption')) * ) * ) * ``` * * for match all message exclude channel posts, or * * ```js * const { Opengram, Composer: { command } } = require('opengram') * // ... * bot.on('text', command('start', ctx => ctx.reply('Math commands only text, not media captions'))) * ``` * * for match only text message, not media caption * or even store a message-only version of your bot in a variable like so: * * > _**Be careful, the example above may not work as expected if `channelMode` is enabled.**_ * > * > By default `text` type not match channel posts, but `channel_post` matched as `text` type and * > `ctx.message` potentially `undefined` * > when `channelMode` enabled. You can add additional chat type check for this case * * @param {string|string[]|'start'|'settings'|'help'} command The command or array of commands to look for * @param {Middleware} fns The middleware(s) to register as arguments */ static command (command, ...fns) { if (fns.length === 0) { return Composer.entity('bot_command', command) } const commands = normalizeTextArguments(command, '/') return Composer.mount(['message', 'channel_post'], Composer.lazy((ctx) => { const groupCommands = ctx.me && (ctx.chat.type === 'group' || ctx.chat.type === 'supergroup') ? commands.map((command) => `${command}@${ctx.me}`) : [] return Composer.entity( (entity, value) => ( entity.offset === 0 && entity.type === 'bot_command' && (commands.includes(value) || groupCommands.includes(value)) ), ...fns) })) } /** * Generates middleware that execute given middlewares will only be executed for certain callback queries, i.e. * the updates that * * Telegram delivers to your bot when a user clicks an inline button (that * is a button under a message). * * This method is essentially the same as calling * ```js * bot.on('callback_query', ctx => { ... }) * ``` * but it also allows you to match the query data against a given text or * regular expression. * * ```js * // Create an inline keyboard * const keyboard = Markup.inlineKeyboard([ * Markup.callbackButton('Go!', 'button-payload') * ]) * // Send a message with the keyboard * await bot.telegram.sendMessage(chat_id, 'Press a button!', keyboard.extra()) * // Listen to users pressing buttons with that specific payload * bot.use( * Composer.action('button-payload', ctx => { ... }) * ) * * // Listen to users pressing any button your bot ever sent * bot.on('callback_query', ctx => { ... }) * ``` * * Always remember to call * {@link Telegram#answerCbQuery} or {@link OpengramContext#answerCbQuery} * — even if you don't perform any action: {@linkplain https://core.telegram.org/bots/api#answercallbackquery} * ```js * bot.on('callback_query', async ctx => { * await ctx.answerCbQuery() * }) * ``` * * You can pass one or an array of triggers (Regexp / strings). Your middleware(s) will be executed if at * least one of them matches. * * > Note how `ctx.match` will contain the result of the regular expression. * > So `ctx.match[1]` refers to the part of the regexp that was matched by `([0-9]+)`, * > i.e. the text that comes after "button:". * > ```js * > const mw = Composer.action(/button:([0-9]+)/, ctx => ctx.reply(`You choose button with number ${ctx.match[1]} in payload`)) * > const keyboard = Markup.inlineKeyboard([ * > Markup.callbackButton('Button 1', 'button:1'), * > Markup.callbackButton('Button 2', 'button:2'), * > Markup.callbackButton('Button 3', 'button:3') * > ]) * > * > bot.use(mw) * > await bot.telegram.sendMessage(chat_id, 'Press a button!', keyboard.extra()) * > ``` * * You can also paste function (or array of functions) that takes the value and context as arguments and returns true * or false (or some `Truthy` result) based on them. This can be used, for example, for dynamic text matching at i18n. * **The result returned by the function will be available from** `ctx.match` * * ```js * bot.use( * Composer.action( * (value, ctx) => { * //... some checks ... * return ['some', 'data'] * }, * // Show cb query answer for all queries with "I love some data" * ctx => ctx.answerCbQuery(`I love ${ctx.match[0]} ${ctx.match[1]}`) * ) * ) * ``` * * @param {Trigger|Trigger[]} triggers One or an array of * regular expressions / strings to search in the payload * @param {Middleware} fns The middleware(s) to register as arguments * @return {Middleware} */ static action (triggers, ...fns) { return Composer.mount('callback_query', Composer.match(normalizeTriggers(triggers), ...fns)) } /** * Generates middleware that execute given middleware(s) will only be executed for certain inline queries. * Telegram sends an inline query to your bot whenever a user types `@your_bot_name ...` into a text field * in Telegram. * * Your bot will then receive the entered search query and can * respond with a number of results (text, images, etc.) that the user can * pick from to send a message _via_ your bot to the respective chat. * Check [here](https://core.telegram.org/bots/inline) to read more about inline bots. * * > Note that you have to enable inline mode for you bot by contacting * > [@BotFather](https://t.me/BotFather) first. * * ```js * // Listen for users typing `@your_bot_name query` * bot.use( * Composer.inlineQuery('query', async ctx => { * // Answer the inline query, confer https://core.telegram.org/bots/api#answerinlinequery * await ctx.answerInlineQuery( ... ) * }) * ) * ``` * * You can pass one or an array of triggers (Regexp / strings). Your middleware(s) will be executed if at * least one of them matches. * * > Note how `ctx.match` will contain the result of the regular expression. * > So `ctx.match[1]` refers to the part of the regexp that was matched by `([0-9]+)`, * > i.e. the text that comes after "query:". * ```js * // Listen for users typing `@your_bot_name query` * bot.use( * Composer.inlineQuery(/query:([0-9]+)/, async ctx => { * // Answer the inline query, confer https://core.telegram.org/bots/api#answerinlinequery * await ctx.answerInlineQuery([{ * type: 'article', * id: Math.random(), * title: 'Regex test', * cache_time: 1, * description: `Query Regex result: ${ctx.match[1]}`, * input_message_content: { * message_text: `Query Regex result: ${ctx.match[1]}`, * } * }]) * }) * ) * ``` * * You can also paste function (or array of functions) that takes the value and context as arguments and returns true * or false (or some `Truthy` result) based on them. This can be used, for example, for dynamic text matching at i18n. * **The result returned by the function will be available from** `ctx.match` * * ```js * bot.inlineQuery( * (value, ctx) => { * //... some checks ... * return ['some', 'data'] * }, * // Show cb query answer for all queries with "I love some data" * ctx => ctx.answerInlineQuery([{ * type: 'article', * id: Math.random(), * title: 'Regex test', * cache_time: 1, * description: `I love ${ctx.match[0]} ${ctx.match[1]}`, * input_message_content: { * message_text: `I love ${ctx.match[0]} ${ctx.match[1]}`, * } * }]) * }) * ``` * * @param {Trigger|Trigger[]} triggers The inline query text * or array of text to match * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static inlineQuery (triggers, ...fns) { return Composer.mount('inline_query', Composer.match(normalizeTriggers(triggers), ...fns)) } /** * Generates and returns a middleware that runs the given middleware(s) only for when array of given id's contains * user id or predicate function returns true for given context * * Access-control list - allows you to create guards for middlewares * * Usage example: * ```js * bot.use( * Composer.admin( * 1234567890, * Composer.reply('Some middleware for admin of bot - 1234567890') * ) * ) * * bot.use( * Composer.admin( * [1234567890, 09876543], * Composer.reply('Some middleware for admins of bot - 1234567890 and 09876543') * ) * ) * * * function checkIsAdmin (ctx) { * // ... * return true * } * * bot.use( * Composer.admin( * ctx => checkIsAdmin(ctx), * Composer.reply('Some middleware for admins of bot') * ) * ) * ``` * * @param {PredicateFn|number|number[]} userId The predicate to check or user id / array of user id's * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static acl (userId, ...fns) { if (typeof userId === 'function') { return Composer.optional(userId, ...fns) } const allowed = Array.isArray(userId) ? userId : [userId] return Composer.optional((ctx) => !ctx.from || allowed.includes(ctx.from.id), ...fns) } /** * Generates and returns a middleware that runs the given middleware(s) only for updates user has one of given * member statuses of chat * * Usage example: * ```js * bot.use( * Composer.memberStatus( * ["creator", "administrator"], * Composer.reply('I work only for chat creator and administrator ') * ) * ) * ``` * * @param {ChatMemberStatus[]|ChatMemberStatus} status Member status of array of statuses * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static memberStatus (status, ...fns) { const statuses = Array.isArray(status) ? status : [status] return Composer.optional((ctx) => ctx.message && ctx.getChatMember(ctx.message.from.id) .then(member => member && statuses.includes(member.status)) , ...fns) } /** * Generates and returns a middleware that runs the given middleware(s) only for updates if member * status = `creator` or 'administrator' * * Usage example: * ```js * bot.use( * Composer.admin( * Composer.reply('I work only when called by chat creator and administrator ') * ) * ) * ``` * * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static admin (...fns) { return Composer.memberStatus(['administrator', 'creator'], ...fns) } /** * Generates and returns a middleware that runs the given middleware(s) only for updates if member status = `creator` * * Usage example: * ```js * bot.use( * Composer.creator( * Composer.reply('I work only when called by chat creator') * ) * ) * ``` * * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static creator (...fns) { return Composer.memberStatus('creator', ...fns) } /** * Registers some middleware for certain chat types only. * * For example, you can use this method to only receive * updates from private chats. The four chat types are `channel`, `supergroup`, `group`, and `private`. * This is especially useful when combined with other filtering logic. * * For example, this is how can you respond to /start commands only from private chats: * * Usage example: * ```js * const privateZone = new Composer() * privateZone.command("start", ctx => { ... }) * * bot.use( * Composer.chatType('private', privateZone) * ) * * bot.use( * Composer.chatType('supergroup', Composer.reply('I work only in supergroups chats')) * ) * * bot.use( * Composer.chatType(['supergroup', 'group'], Composer.reply('I work only in supergroup + group chats')) * ) * ``` * * ```js * const onlyGroup = new Composer() * * onlyGroup.hears(...) * onlyGroup.command(...) * // ... * bot.use( * Composer.chatType('group', onlyGroup) * ) * ``` * * @param {ChatType[]|ChatType} type Chat type or array of shat types * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static chatType (type, ...fns) { const types = Array.isArray(type) ? type : [type] return Composer.optional((ctx) => { const chat = ctx.chat return chat !== undefined && types.includes(chat.type) }, ...fns) } /** * Generates and returns a middleware that runs the given middleware only for updates from "private" (DM) * * Usage example: * ```js * // Send message with text "I do not support group chats" when receive update from group chat * bot.use( * Composer.privateChat(Composer.reply('I work only in group chats')) * ) * ``` * * Isolate private commands: * ```js * const private = new Composer() * * private.hears(...) * private.command(...) * * bot.use( * Composer.privateChat(private) * ) * ``` * * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static privateChat (...fns) { return Composer.chatType('private', ...fns) } /** * Creates and returns a middleware that runs the given middleware only for updates from "group" and "supergroup". * * Usage example: * ```js * // Send message with text "I do not support group chats" when receive update from group chat * bot.use( * Composer.groupChat(Composer.reply('I do not support group chats')) * ) * ``` * * Isolate group commands: * ```js * const group = new Composer() * * group.hears(...) * group.command(...) * * bot.use( * Composer.groupChat(group) * ) * ``` * * @param {Middleware} fns The middleware(s) to register * @return {Middleware} */ static groupChat (...fns) { return Composer.chatType(['group', 'supergroup'], ...fns) } static gameQuery (...fns) { return Composer.mount( 'callback_query', Composer.optional((ctx) => !!ctx.callbackQuery.game_short_name, ...fns) ) } /** * Method used for unwrapping middleware, when middleware has method with name `middleware` (middleware factory) * {@link Composer.unwrap} calls him and return result * * This method used in some other {@link Composer} methods, like {@link Composer.compose}, {@link Composer.lazy} and * other * * @param {Middleware} handler The middleware for unwrap * @throws {Error} * @return {Middleware} */ static unwrap (handler) { if (!handler) { throw new Error('Handler is undefined') } return typeof handler.middleware === 'function' ? handler.middleware() : handler } /** * Used for compose array of middlewares * * @param {Middleware[]} middlewares The middlewares for compose * @throws {Error|TypeError} * @return {Middleware} */ static compose (middlewares) { if (!Array.isArray(middlewares)) { throw new TypeError('Middlewares must be an array') } if (middlewares.length === 0) { return Composer.safePassThru() } if (middlewares.length === 1) { return Composer.unwrap(middlewares[0]) } /** * @param {OpengramContext} ctx Context Object * @param {Function} next Next middleware */ function run (ctx, next) { let index = -1 return execute(0, ctx) /** * @param {number} i Middleware index * @param {OpengramContext} context Next middleware */ async function execute (i, context) { if (!(context instanceof Context)) { throw new Error('next(ctx) called with invalid context') } if (i <= index) { throw new Error('next() called multiple times') } index = i const handler = middlewares[i] ? Composer.unwrap(middlewares[i]) : next if (!handler) { return } await handler(context, async (ctx = context) => { await execute(i + 1, ctx) }) } } return run } } /** * Converts single triggers to array of triggers and regex / strings to predicate functions * * @private * @param {Trigger|Trigger[]} triggers The text / array of * texts / regex / function to look for * @throws {TypeError} * @return {TriggerPredicateFn[]} */ function normalizeTriggers (triggers) { if (!Array.isArray(triggers)) { triggers = [triggers] } return triggers.map((trigger) => { if (!trigger) { throw new TypeError('Invalid trigger') } if (typeof trigger === 'function') { return trigger } if (trigger instanceof RegExp) { return (value) => { trigger.lastIndex = 0 return trigger.exec(value || '') } } return (value) => trigger === value ? value : null }) } /** * Converts given argument to array if not array, filter empty arguments. * If prefix given, adds prefix if not exists * * ```js * normalizeTextArguments('name') // Returns ['name'] * normalizeTextArguments(['name', 'name1']) // Returns ['name', 'name1'] * normalizeTextArguments(['@name', 'name1'], '@') // Returns ['@name', '@name1'] * ``` * * @private * @param {string|string[]} argument Arguments to normalize * @param {string} [prefix] Prefix * @return {string[]} */ function normalizeTextArguments (argument, prefix) { const args = Array.isArray(argument) ? argument : [argument] return args .filter(Boolean) .map((arg) => prefix && typeof arg === 'string' && !arg.startsWith(prefix) ? `${prefix}${arg}` : arg) } module.exports = Composer