IlyaSemenov / grammy-scenes

Nested named scenes for grammY
MIT License
27 stars 0 forks source link

Composer calling 2 times #16

Closed roviks closed 1 year ago

roviks commented 1 year ago

Hi! If the person did not click on the button, but wrote the text, I have to resend the text with the button so that he clicks in order to continue

When my bot restarts, the entire session is cleared, so I save the step in the scene in the database. Therefore, at the beginning, I check at which step I stopped and continue from that moment. With this method, I have 2 times called composer. Is there any other way to do this?


scene.step(async (ctx) => {
  const nextStepKey = ctx.session.nextStep // step-1

  if (nextStepKey) {
    ctx.scene.goto(nextStepKey)
  }
})

// there are more steps before

scene.wait("step-1").setup((scene) => {
  scene.on('callback_query:data', async (ctx) => {
    console.log("success"); // go next
  })

  scene.on('message:text', async (ctx) => {
    // this code calling 2 times
    console.log('CURRENT STEP step-1')
  })
})

Console:

IlyaSemenov commented 1 year ago

I didn't fully grasp what you're doing with the session/context, but I think you're misusing it. grammy-scenes saves the current step on its own, it's supposed to gracefully handle bot restart.

On each wait, it saves the execution stack (current step position):

https://github.com/IlyaSemenov/grammy-scenes/blob/7337c7fdc19e4769fe5677be26f667c34c7ede5f/src/scenes-manager.ts#L170-L174

which means simply putting it to the session:

https://github.com/IlyaSemenov/grammy-scenes/blob/7337c7fdc19e4769fe5677be26f667c34c7ede5f/src/scenes-manager.ts#L133

The session is supposed to be persisted with something like https://github.com/grammyjs/storages.

Then in the middleware, on the next user event (possibly after bot restart), it resumes from where it wait'ed:

https://github.com/IlyaSemenov/grammy-scenes/blob/7337c7fdc19e4769fe5677be26f667c34c7ede5f/src/scenes-composer.ts#L37-L39


If you still want to abuse the scene runner workflow, I suppose you could add a pre-step doing nothing:

scene.label("pre-step-1").step(async (ctx) => {
  // Do nothing. Use it for goto.
})

scene.wait("step-1").setup(() { /* do as before */ })

Then if you goto to it, it won't run whatever handlers are defined in wait().setup() until the next event.

roviks commented 1 year ago

For example, When the user has an active scene and waits for "step-1" at that moment the bot restarts (for whatever reason). In this case, after the user writes he will receive the next step?

IlyaSemenov commented 1 year ago

For example, When the user has an active scene and waits for "step-1" at that moment the bot restarts (for whatever reason). In this case, after the user writes he will receive the next step?

If the scene stopped on wait, the bot restarted, and then the user sends a message, the scene manager will execute whatever handlers (middlewares) are defined inside setup() that immediately follows wait(). You should not handle bot restart yourself, that works fully transparently.

That, of course, assumes that the grammy sessions are using some kind of persistent storage backend.

roviks commented 1 year ago

I think it's not working. After bot restart no active scene, that's why i use middleware to enter scene. In this case, the step is reset

bot.use(async (ctx, next) => {
  const tgId = ctx.session.tgId ?? ''
  const startedScene = ctx.session.startedScene
  if (
    startedScene  && !getActiveScene(tgId)
  ) {
    await ctx.scenes.enter(getSceneDayKey(startedScene))
    setActiveScene(tgId, getSceneDayKey(startedScene))
    await next()
    return
  }
  await next()
})
IlyaSemenov commented 1 year ago

I think I've told everything above and I'm not following what else do you want. The scenes module does not store any local variables and it is thus agnostic to bot restarts. It keeps its state in ctx.session.scenes (you don't overwrite it by chance?) and relies on its persistence.

The active scene is invoked here:

// Actually run scenes
bot.use(scenes)

You may check if the control flow comes there and if the stack is valid (if it points to the correct step):

bot.use((ctx, next) => {
  console.log('resuming scene from', ctx.session.scenes)
  return next()
})
// Actually run scenes
bot.use(scenes)

Unless you come up with a proper reproduction (the code that I could run and repeat the problem), I won't be able to assist further.

One thing I would have checked is if you're not forgetting to use await. See, if you call an async handler without awaiting it, the middleware will return immediately and the session will be persisted too soon (the scene stack will be put into a session object that've been already persisted).


By the way, in the code that you've included, you're probably mistaken in that you're calling await ctx.scenes.enter(...) followed by await next(). In grammy, you're supposed to call next() only if you didn't handle the event. However, as you've called scene enter, you're presumably achieved what you wanted and should not have called next.

That itself unlikely has something to do with your problem, but this kind of mistake (if that is a mistake — sorry if it's there for a reason) shows probable lack of understanding of how middleware stack together. Which means something similar could be causing the original problem.

roviks commented 1 year ago

I checked my code and sure that I'm not overriding ctx.session.scenes.

Entering scene:

image

After bot restart, i have nothing in ctx.session.scenes:

image

Middlewares:

bot.use(
  session({
    initial: () => ({})
  })
)

bot.use(wizardScenes.manager())

bot.use((ctx, next) => {
  console.log('resuming scene from', ctx.session.scenes)
  return next()
})

bot.use(pseudoUpdate)
bot.use(wizardScenes)
IlyaSemenov commented 1 year ago

As I already mentioned three times, the scenes module relies on persisted sessions.

This code does not persist sessions, they are gone on bot restart:

bot.use(
  session({
    initial: () => ({})
  })
)

This (for example) does:

import { PsqlAdapter } from '@grammyjs/storage-psql';

bot.use(
  session({
    initial: () => ({}),
    storage: await PsqlAdapter.create({ tableName: 'sessions', client }),
  })
);