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