UNPKG

3.64 kBPlain TextView Raw
1import BaseScene from './base'
2import Composer from '../composer'
3import Context from '../context'
4import d from 'debug'
5import { SessionContext } from '../session'
6const debug = d('telegraf:scenes:context')
7
8const noop = () => Promise.resolve()
9const now = () => Math.floor(Date.now() / 1000)
10
11export interface SceneContext<D extends SceneSessionData = SceneSessionData>
12 extends Context {
13 session: SceneSession<D>
14 scene: SceneContextScene<SceneContext<D>, D>
15}
16
17export interface SceneSessionData {
18 current?: string
19 expires?: number
20 state?: object
21}
22
23export interface SceneSession<S extends SceneSessionData = SceneSessionData> {
24 __scenes: S
25}
26
27export interface SceneContextSceneOptions<D extends SceneSessionData> {
28 ttl?: number
29 default?: string
30 defaultSession: D
31}
32
33export default class SceneContextScene<
34 C extends SessionContext<SceneSession<D>>,
35 D extends SceneSessionData = SceneSessionData
36> {
37 private readonly options: SceneContextSceneOptions<D>
38
39 constructor(
40 private readonly ctx: C,
41 private readonly scenes: Map<string, BaseScene<C>>,
42 options: Partial<SceneContextSceneOptions<D>>
43 ) {
44 // @ts-expect-error {} might not be assignable to D
45 const fallbackSessionDefault: D = {}
46
47 this.options = { defaultSession: fallbackSessionDefault, ...options }
48 }
49
50 get session(): D {
51 const defaultSession = this.options.defaultSession
52
53 let session = this.ctx.session?.__scenes ?? defaultSession
54 if (session.expires !== undefined && session.expires < now()) {
55 session = defaultSession
56 }
57 if (this.ctx.session === undefined) {
58 this.ctx.session = { __scenes: session }
59 } else {
60 this.ctx.session.__scenes = session
61 }
62 return session
63 }
64
65 get state() {
66 return (this.session.state ??= {})
67 }
68
69 set state(value) {
70 this.session.state = { ...value }
71 }
72
73 get current() {
74 const sceneId = this.session.current ?? this.options.default
75 return sceneId === undefined || !this.scenes.has(sceneId)
76 ? undefined
77 : this.scenes.get(sceneId)
78 }
79
80 reset() {
81 if (this.ctx.session !== undefined)
82 this.ctx.session.__scenes = this.options.defaultSession
83 }
84
85 async enter(
86 sceneId: string,
87 initialState: object = {},
88 silent: boolean = false
89 ) {
90 if (!this.scenes.has(sceneId)) {
91 throw new Error(`Can't find scene: ${sceneId}`)
92 }
93 if (!silent) {
94 await this.leave()
95 }
96 debug('Entering scene', sceneId, initialState, silent)
97 this.session.current = sceneId
98 this.state = initialState
99 const ttl = this.current?.ttl ?? this.options.ttl
100 if (ttl !== undefined) {
101 this.session.expires = now() + ttl
102 }
103 if (this.current === undefined || silent) {
104 return
105 }
106 const handler =
107 'enterMiddleware' in this.current &&
108 typeof this.current.enterMiddleware === 'function'
109 ? this.current.enterMiddleware()
110 : this.current.middleware()
111 return await handler(this.ctx, noop)
112 }
113
114 reenter() {
115 return this.session.current === undefined
116 ? undefined
117 : this.enter(this.session.current, this.state)
118 }
119
120 private leaving = false
121 async leave() {
122 if (this.leaving) return
123 debug('Leaving scene')
124 try {
125 this.leaving = true
126 if (this.current === undefined) {
127 return
128 }
129 const handler =
130 'leaveMiddleware' in this.current &&
131 typeof this.current.leaveMiddleware === 'function'
132 ? this.current.leaveMiddleware()
133 : Composer.passThru()
134 await handler(this.ctx, noop)
135 return this.reset()
136 } finally {
137 this.leaving = false
138 }
139 }
140}