Skip to main content

πŸ’« Stage & Scenes

In Opengram, scenes are a tool that allows you to control what happens in your bot, and organize commands and user messages in a structured way. This is especially useful when your bot works with multiple commands or complex sequences of actions.

A typical use case for scenes is the sequential acceptance of input from the user.

Many beginners, when faced with the task of receiving data sequentially, try to do something like:

bot.hears(..., ctx => {
bot.on('message', ...)
})

Which is fundamentally wrong, and most likely will not even work, but even if it does, it will lead to the appearance of new handlers in the chain, without the possibility of deleting them, and the subsequent memory leak

The scene allows you to create separate handlers areas and move in and out of those areas.

When the user is in the scene, messages and other updates sent by telegram from the user get into the scene, where you can process them and stop or continue processing.

Scenes types / differences​

Scenes are divided into two types:

  • Wizard is a special type of scene that allows you to control the sequence of messages and actions in your bot. They are very similar to regular scenes, but have some additional features that allow you to control the sequence of messages and actions.

    For example, using the Scene Wizard, you can arrange to receive a sequence of messages that the user sends to the bot.

    In the Wizard scene, the logic is divided into separate stages, between which you can switch by calling the appropriate methods, such as ctx.wizard.next, ctx.wizard.back.

    Each stage can contain a group of handlers, for example, for the text and for the button

  • Base scenes are a lighter abstraction that allows you to register a group of handlers that will be called when the user is in the scene. They can also be used to accept user input in sequence, but mostly for single questions.

Base and Wizard scenes require specifying text identifier when created, to enter to them via ctx.scene.enter('scene_name')

Each of them has a separate session area to store data that is required as long as the scene is active:

  • ctx.scene.state - for BaseScene
  • ctx.wizard.state - for WizardScene

The data in them will be available until user in the scene, after exiting they are destroyed.

Scenes also have handlers that can be called before leave or when entering a scene.

How to use scenes​

Setup​

The first thing to do before using it is to register sessions, since Stage uses them to store state and its data.

note

For more details you can visit the session documentation, this part assumes you know what sessions are and how they work

Once you've register the sessions, you can export the Stage, instantiate it, and register scenes in Stage.

note

Stage middleware must be registered after the session.

❌ Wrong:

const stage = new Stage(...)

bot.use(stage)
bot.use(session())

βœ… Good:

const stage = new Stage(...)

bot.use(session())
bot.use(stage)

BaseScene​

Example:

const { Opengram, session, Stage, Scenes } = require('opengram')

const bot = new Opengram(process.env.BOT_TOKEN)

bot.use(session())

// Create BaseScene with id "first"
const firstScene = new Scenes.BaseScene('first')
// Send "Hi, how are you?" when entering the scene
firstScene.enter(ctx => ctx.reply('Hi, how are you?'))
firstScene.on('text', async ctx => {
const { text } = ctx.message

ctx.scene.state = text // You can save the data for future responses, but this example does not use it.
await ctx.reply(`You say: ${text}`)

// Leave from scene
// Don't forget to await / return on ctx.scene.leave for bot.catch to work properly
await ctx.scene.leave()
})
// If received not text message, send "Hmm it doesn't look like text. Try again :)"
firstScene.use(ctx => ctx.reply("Hmm it doesn't look like text. Try again :)"))
// Send "Bye, bye!" when leave scene
firstScene.leave(ctx => ctx.reply('Bye, bye!'))

// You can pass array of scenes to Stage constructor
// or register using register method, for example:
// const stage = new Stage()
// stage.register(firstScene)
const stage = new Stage([firstScene])
bot.use(stage)

// Enter to "first" BaseScene when call /start command
// Also you can pass initial state for ctx.wizard.state in second argument
// Example: ctx.scene.enter('first', { propNameL: 'some value' })
bot.start(ctx => ctx.scene.enter('first'))

bot.launch()

WizardScene​

Unlike the base scene, in the Wizard you can only filter update types using separate Composer instances or manually, as the default handlers intercept all update types.

Example:

const { Opengram, session, Stage, Scenes, Extra, Composer, Markup } = require('opengram')

const bot = new Opengram(process.env.BOT_TOKEN)

bot.use(session())

// Keyboard
const keyboard = Extra.markup(m => {
return m.inlineKeyboard(
[14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24].map(x => m.callbackButton(x, `years:${x}`)),
{ columns: 3 }
)
})

// Create first step
const firstStep = new Composer()
firstStep.on('text', async ctx => {
const { text } = ctx.message
ctx.wizard.state.answers.push(text)
await ctx.reply(`You send ${text}. How old are you? (send number) Or select by button`, keyboard)
ctx.wizard.next()
})

// If received not text message, send "Hmm it doesn't look like text. Try again :)"
firstStep.use(ctx => ctx.reply("Hmm it doesn't look like number. Try again :)", keyboard))


// Create second step with text & buttons handlers
const secondStep = new Composer()

secondStep.on('text', async ctx => {
const { text } = ctx.message
const years = +text // Cast to number

// Validate
if (Number.isNaN(years)) {
return await ctx.reply('Wrong answer, please send number or select via buttons', keyboard)
}

ctx.wizard.state.answers.push(text)
await ctx.reply(`You are ${text} years old`)
// Leave from scene
// Don't forget to await / return on ctx.scene.leave for bot.catch to work properly
await ctx.scene.leave()
})

secondStep.action(/^years:([0-9]{2})$/, async ctx => {
await ctx.answerCbQuery()
await ctx.editMessageReplyMarkup(Markup.inlineKeyboard([]))
const [,years] = ctx.match
ctx.wizard.state.answers.push(years)
await ctx.reply(`You are ${years} years old`)
// Leave from scene
// Don't forget to await / return on ctx.scene.leave for bot.catch to work properly
await ctx.scene.leave()
})

// Create WizardScene with id "firstWizard"
const firstScene = new Scenes.WizardScene('firstWizard', firstStep, secondStep)

// Send "Hi, how are you?" when entering the scene
firstScene.enter(ctx => ctx.reply('Hi, how are you?'))

// Send result answers list
firstScene.leave(async ctx => {
const answersList = ctx.wizard.state.answers
.map((answer, index) => `${index + 1}. ${answer}`)
.join('\n')
await ctx.reply(`Answers:\n${answersList}`)
})

// You can pass array of scenes to Stage constructor
// or register using register method, for example:
// const stage = new Stage()
// stage.register(firstScene)
const stage = new Stage([firstScene])
bot.use(stage)

// Enter to "firstWizard" WizardScene when call /start command
// Don't forget to await / return on ctx.scene.enter for bot.catch to work properly
// Also you can pass initial state for ctx.wizard.state in second argument
bot.start(ctx => ctx.scene.enter('firstWizard', { answers: [] }))

bot.launch()

Using scene with other plugins​

If you want to use some plugins with scenes, just register them before Stage:

bot.use(plugin1)
bot.use(plugin2)
bot.use(plugin3)
bot.use(stage)

Or you can register plugin only for specific scene like other handlers:

const scene = new Scenes.BaseScene(...)

scene.use(plugin1)
scene.use(plugin2)
scene.use(plugin3)

// ... other middlewares ...

or for wizard:

const wizardScene = new WizardScene(..., step1, step2, step3)

// This executed before wizard steps handlers
wizardScene.use(plugin1)
wizardScene.use(plugin2)
wizardScene.use(plugin3)

Scenes options​

You can specify handlers, enterHandlers, leaveHandlers, and override ttl (Time-to-live) for specific scene in second argument of BaseScene and WizardScene:

const base = new Scenes.BaseScene('first', {
handlers: [...], // Steps(s), optional
enterHandlers: [...], // Enter handler(s), optional
leaveHandlers: [...], // Enter handler(s), optional
ttl: 60 // TTL in seconds, optional
})

const wizard = new Scenes.WizardScene('second', {
handlers: [...], // Steps(s), optional
enterHandlers: [...], // Enter handler(s), optional
leaveHandlers: [...], // Enter handler(s), optional
ttl: 60 // TTL in seconds, optional
}, ...)

Important to remember when using scenes​

  • Always return or await calls of ctx.scene.enter(...) and ctx.scene.leave(...).

    When you call enter / leave, it calls leave or enter scene handler and if some error trowed in them, your bot can crash.

  • Use ctx.scene.state / ctx.wizard.state instead of ctx.session for data that is only needed in the scene.

  • Scenes metadata and state stored in the session, it means session state stored by chat id + from id by default. For every chat, user can be in a separated scene.

    If you need to change this behavior, you can change the session key generator, or create second Stage + session instances and define it for some middlewares chains.

  • Every handler just middleware and have ctx & next arguments.

    If you call next on a scene handler, the update will jump to the next matching scene handler, or failing that, it will go out of the scene scope and go to downstream middleware after Stage. This also means that if no handler catches the update, it will go out of the scene scope. To prevent this behavior in the examples above, you can find lines like this:

    firstScene.use(ctx => ctx.reply("Hmm it doesn't look like text. Try again :)"))

    This line intercepts all updates of the non-text type and displays a message.

  • When you call ctx.scene.enter and the user is currently in a scene, leave is automatically called and after enters another scene