Skip to content

Commit

Permalink
feat!: reintroduce scene.step()
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

This reintroduces scene.step() as a building block for scenes.

Unlike scene.use(), the new naming explicitly denotes that scenes/steps
are not "normal" composers and handle next() differently.

The scene runner now ignores next() and always proceeds to the next
step.
  • Loading branch information
IlyaSemenov committed Apr 30, 2023
1 parent 2e46889 commit e7ad523
Show file tree
Hide file tree
Showing 11 changed files with 48 additions and 69 deletions.
23 changes: 10 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,12 @@ import { BotContext } from "../bot"

export const mainScene = new Scene<BotContext>("main")

// Define scene flow with middlewares.
// Make sure you call next() or the scene will stop.
mainScene.use(async (ctx, next) => {
// Define scene flow with steps.
mainScene.step(async (ctx) => {
await ctx.reply("Entering main scene...")
return next()
})

// do() is a shortcut for use() which automatically calls next()
mainScene.do(async (ctx) => {
mainScene.step(async (ctx) => {
await ctx.reply("Enter your name:")
})

Expand All @@ -109,7 +106,7 @@ mainScene.wait().on("message:text", async (ctx) => {
})

// Add more steps...
mainScene.do(async (ctx) => {
mainScene.step(async (ctx) => {
await ctx.reply("Proceeding...")
})

Expand All @@ -120,7 +117,7 @@ mainScene.label("start")
// See sample captcha implementation below.
mainScene.call("captcha")

mainScene.do(async (ctx) => {
mainScene.step(async (ctx) => {
await ctx.reply(`Please choose:`, {
reply_markup: {
inline_keyboard: [
Expand Down Expand Up @@ -150,7 +147,7 @@ mainScene.wait().on("callback_query:data", async (ctx) => {
}
})

mainScene.do((ctx) => ctx.reply(`Main scene finished`))
mainScene.step((ctx) => ctx.reply(`Main scene finished`))
```

### Scene/step argument
Expand All @@ -165,7 +162,7 @@ bot.command("start", (ctx) =>
)
)

mainScene.do(async (ctx) => {
mainScene.step(async (ctx) => {
await ctx.reply(`Enter your name, ${ctx.scene.arg?.title || "mortal"}:`)
})
```
Expand Down Expand Up @@ -203,7 +200,7 @@ import { generateCaptcha } from "some-captcha-module"
import { BotContext } from "../bot"

const captchaScene = new Scene<BotContext, { secret: string }>("captcha")
captchaScene.do(async (ctx) => {
captchaScene.step(async (ctx) => {
const { secret, image } = await generateCaptcha()
ctx.scene.session = { secret }
await ctx.reply(`Enter the letters you see below:`)
Expand Down Expand Up @@ -236,7 +233,7 @@ import { Scene } from "grammy-scenes"
import { BotContext } from "../bot"

const jobScene = new Scene<BotContext>("job")
jobScene.do(async (ctx) => {
jobScene.step(async (ctx) => {
await ctx.reply(`Starting job...`)
const token = ctx.scene.createNotifyToken()
startJob({ chat_id: ctx.chat!.id, token })
Expand Down Expand Up @@ -341,7 +338,7 @@ scene.always().do(async (ctx) => {
}
})

scene.do((ctx) => {
scene.step((ctx) => {
ctx.scene.session = { foo_id: 123 } // Save ID to session
})

Expand Down
21 changes: 7 additions & 14 deletions src/scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,12 @@ export class Scene<
/**
* Add a scene step.
*/
use(...middleware: Array<MiddlewareFn<SceneFlavoredContext<C, S>>>) {
step(...middleware: Array<MiddlewareFn<SceneFlavoredContext<C, S>>>) {
const step = new StepComposer<SceneFlavoredContext<C, S>, S>(...middleware)
this._steps.push(step)
return step
}

/**
* Add a scene step which will always call the next step (unless explicitly aborted).
*/
do(middleware: MiddlewareFn<SceneFlavoredContext<C, S>>) {
return this.use().do(middleware)
}

/**
* Mark a named position in scene to be used by scene.goto()
*/
Expand All @@ -63,31 +56,31 @@ export class Scene<
* ```
*/
wait() {
this.use((ctx) => {
this.step((ctx) => {
ctx.scene._wait()
})
return this.do((ctx) => {
return this.step().do((ctx) => {
ctx.scene._must_resume()
})
}

/** Set payload for ctx.scene.arg in next step */
arg(arg: any) {
return this.use().arg(arg)
return this.step().arg(arg)
}

/** Call nested scene, then go to the next step. */
call(sceneId: string, arg?: any) {
this.use().call(sceneId, arg)
this.step().call(sceneId, arg)
}

/** Exit scene. */
exit(arg?: any) {
this.use().exit(arg)
this.step().exit(arg)
}

/** Go to named step. */
goto(name: string, arg?: any) {
this.use().goto(name, arg)
this.step().goto(name, arg)
}
}
16 changes: 4 additions & 12 deletions src/scenes-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,22 @@ export class ScenesManager<
const step = scene._steps[frame.pos]
// TODO: distinguish case where missing step is caused by invalid session data vs. normal scene finish.
if (step) {
let finished: boolean
const composer = new Composer<SceneFlavoredContext<C, any>>()
if (scene._always) {
// TODO: don't run _always middleware for the next step of the same scene
composer.use(scene._always)
}
composer.use(step)
const handler = composer.middleware()
const step_mw = composer.middleware()
const inner_ctx = this.ctx as any
const scene_manager = new SceneManager(frame, opts)
opts = undefined
inner_ctx.scene = scene_manager
try {
finished = false
await handler(inner_ctx, async () => {
finished = true
})
await step_mw(inner_ctx, async () => undefined)
} finally {
delete inner_ctx.scene
}

if (scene_manager._want_enter) {
// Replace stack with new scene.
const { scene_id, arg } = scene_manager._want_enter
Expand Down Expand Up @@ -120,14 +115,11 @@ export class ScenesManager<
frame.pos++
this.ctx.session.scenes ??= { stack }
return
} else if (finished) {
// Middleware called next(), thus proceed to next step.
} else {
// Nothing interesting happened. Proceed to next step.
frame.pos++
opts = { arg: scene_manager.next_arg }
continue
} else {
// Middleware didn't call next() and didn't ask to wait; stop execution.
return
}
}
stack.shift()
Expand Down
2 changes: 1 addition & 1 deletion tests/always.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ scene.always().do((ctx) => {
ctx.foo = important_value
})

scene.do(async (ctx) => {
scene.step(async (ctx) => {
await ctx.reply(
`Context correct: ${ctx.foo === important_value}. Now send me something.`
)
Expand Down
8 changes: 4 additions & 4 deletions tests/arg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ const scene = new Scene<BotContext>("main")

scene.arg("foo")

scene.do(async (ctx) => {
scene.step(async (ctx) => {
assert(ctx.scene.arg === "foo")
})

scene.do(async (ctx) => {
scene.step(async (ctx) => {
assert(ctx.scene.arg === undefined)
ctx.scene.next_arg = "bar"
})

scene.do(async (ctx) => {
scene.step(async (ctx) => {
assert(ctx.scene.arg === "bar")
})

scene.do((ctx) => ctx.reply("Test passed"))
scene.step((ctx) => ctx.reply("Test passed"))

create_bot([scene])
14 changes: 7 additions & 7 deletions tests/captcha.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { Scene } from "grammy-scenes"
import { BotContext, create_bot } from "./lib/bot"

const captcha_scene = new Scene<BotContext, { secret: string }>("captcha")
captcha_scene.do(async (ctx) => {
captcha_scene.step(async (ctx) => {
await ctx.reply(
`You must solve captcha ${ctx.scene.arg?.again ? ` again` : ``}!`
)
Expand All @@ -71,27 +71,27 @@ captcha_scene.wait().setup((scene) => {
})
scene.on("message:sticker", (ctx) => ctx.reply("No stickers please."))
})
captcha_scene.do((ctx) => ctx.reply("Captcha solved!"))
captcha_scene.step((ctx) => ctx.reply("Captcha solved!"))

const welcome_scene = new Scene("welcome")
welcome_scene.do((ctx) =>
welcome_scene.step((ctx) =>
ctx.reply(`Welcome to ${ctx.scene.arg?.name || "scene"}`)
)

const main_scene = new Scene("main")
main_scene.call("welcome", { name: "Main Scene" })
main_scene.do((ctx) => ctx.reply("First captcha is obligatory"))
main_scene.step((ctx) => ctx.reply("First captcha is obligatory"))
main_scene.call("captcha")
main_scene.label("captcha")
main_scene.do(async (ctx) => {
main_scene.step(async (ctx) => {
if (Math.random() < 0.3) {
await ctx.reply("You are lucky, no second captcha")
} else {
await ctx.reply("You are not lucky, you must solve second captcha")
ctx.scene.call("captcha")
}
})
main_scene.do(async (ctx) => {
main_scene.step(async (ctx) => {
await ctx.reply(`Do you want to try your luck once again?`, {
reply_markup: {
inline_keyboard: [
Expand All @@ -111,6 +111,6 @@ main_scene.wait().on("callback_query:data", async (ctx) => {
ctx.scene.resume()
}
})
main_scene.do((ctx) => ctx.reply(`Main scene finished`))
main_scene.step((ctx) => ctx.reply(`Main scene finished`))

create_bot([main_scene, captcha_scene, welcome_scene])
6 changes: 3 additions & 3 deletions tests/enter-in-wait.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { BotContext, create_bot } from "./lib/bot"

const scene1 = new Scene<BotContext>("main")

scene1.do(async (ctx) => {
scene1.step(async (ctx) => {
await ctx.reply(`Please choose:`, {
reply_markup: {
inline_keyboard: [
Expand All @@ -65,11 +65,11 @@ scene1.wait().on("callback_query:data", async (ctx) => {
}
})

scene1.do((ctx) => ctx.reply(`Scene 1 complete`))
scene1.step((ctx) => ctx.reply(`Scene 1 complete`))

const scene2 = new Scene<BotContext>("scene2")

scene2.do((ctx) => ctx.reply(`Scene 2, enter your name:`))
scene2.step((ctx) => ctx.reply(`Scene 2, enter your name:`))
scene2.wait().on("message:text", async (ctx) => {
await ctx.reply(
`Hello, ${ctx.message.text}. This is the end, you should not see Scene 1.`
Expand Down
4 changes: 2 additions & 2 deletions tests/exit-with-arg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const scene = new Scene<BotContext>("main")

scene.call("inner", 10)

scene.do(async (ctx) => {
scene.step(async (ctx) => {
assert(ctx.scene.arg === 20)
await ctx.reply("All OK!")
})

const inner = new Scene<BotContext>("inner")

inner.do(async (ctx) => {
inner.step(async (ctx) => {
assert(ctx.scene.arg === 10)
ctx.scene.exit(20)
})
Expand Down
8 changes: 4 additions & 4 deletions tests/exit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { BotContext, create_bot } from "./lib/bot"

const scene = new Scene<BotContext>("main")

scene.do(async (ctx) => {
scene.step(async (ctx) => {
await ctx.reply(`Proceed or Exit?`, {
reply_markup: {
inline_keyboard: [
Expand All @@ -44,14 +44,14 @@ scene.wait().on("callback_query:data", async (ctx) => {
}
})

scene.do(async (ctx) => {
scene.step(async (ctx) => {
await ctx.reply(`Step 1`)
})
scene.do(async (ctx) => {
scene.step(async (ctx) => {
await ctx.reply(`Step 2`)
ctx.scene.exit()
})
scene.do(async (ctx) => {
scene.step(async (ctx) => {
await ctx.reply(`Step 3 (should not see this)`)
})

Expand Down
6 changes: 3 additions & 3 deletions tests/notify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const jobs: Job[] = []

const scene = new Scene<BotContext>("main")

scene.do(async (ctx) => {
scene.step(async (ctx) => {
const token = ctx.scene.createNotifyToken()
await ctx.reply(`Starting job...`)
setTimeout(() => {
Expand All @@ -51,7 +51,7 @@ scene.wait().setup((scene) => {
})
})

scene.do((ctx) => ctx.reply("Enter your name"))
scene.step((ctx) => ctx.reply("Enter your name"))

scene.wait().setup((scene) => {
scene.onNotify(async (ctx) => {
Expand All @@ -64,7 +64,7 @@ scene.wait().setup((scene) => {
})
})

scene.do((ctx) => ctx.reply("Finished"))
scene.step((ctx) => ctx.reply("Finished"))

create_bot([scene], (bot) => {
setInterval(() => {
Expand Down
9 changes: 3 additions & 6 deletions tests/readme-example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,12 @@ import { BotContext, create_bot } from "./lib/bot"

const mainScene = new Scene<BotContext>("main")

// Define scene flow with middlewares.
// Make sure you call next() or the scene will stop.
mainScene.use(async (ctx, next) => {
// Define scene flow with steps.
mainScene.step(async (ctx) => {
await ctx.reply("Entering main scene...")
return next()
})

// do() is a shortcut for use() which automatically calls next()
mainScene.do(async (ctx) => {
mainScene.step(async (ctx) => {
await ctx.reply("Enter your name:")
})

Expand Down

0 comments on commit e7ad523

Please sign in to comment.