feat(dispatcher): wizard scene
This commit is contained in:
parent
257f5392ea
commit
6c8eeb01d2
5 changed files with 137 additions and 14 deletions
|
@ -481,11 +481,20 @@ export namespace filters {
|
|||
* Filter service messages by action type
|
||||
*/
|
||||
export const action = <T extends Exclude<MessageAction, null>['type']>(
|
||||
type: T
|
||||
type: MaybeArray<T>
|
||||
): UpdateFilter<
|
||||
Message,
|
||||
{ action: Extract<MessageAction, { type: T }> }
|
||||
> => (msg) => msg.action?.type === type
|
||||
> => {
|
||||
if (Array.isArray(type)) {
|
||||
const index: Partial<Record<T, true>> = {}
|
||||
type.forEach((it) => (index[it] = true))
|
||||
|
||||
return (msg) => (msg.action?.type as any) in index
|
||||
}
|
||||
|
||||
return (msg) => msg.action?.type === type
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter messages containing a photo
|
||||
|
@ -676,13 +685,13 @@ export namespace filters {
|
|||
{ match: RegExpMatchArray }
|
||||
> => (obj) => {
|
||||
let m: RegExpMatchArray | null = null
|
||||
if (obj?.constructor === Message) {
|
||||
if (obj.constructor === Message) {
|
||||
m = obj.text.match(regex)
|
||||
} else if (obj?.constructor === InlineQuery) {
|
||||
} else if (obj.constructor === InlineQuery) {
|
||||
m = obj.query.match(regex)
|
||||
} else if (obj?.constructor === ChosenInlineResult) {
|
||||
} else if (obj.constructor === ChosenInlineResult) {
|
||||
m = obj.id.match(regex)
|
||||
} else if (obj?.constructor === CallbackQuery) {
|
||||
} else if (obj.constructor === CallbackQuery) {
|
||||
if (obj.raw.data) m = obj.dataStr!.match(regex)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,3 +4,6 @@ export * from './filters'
|
|||
export * from './handler'
|
||||
export * from './propagation'
|
||||
export * from './updates'
|
||||
export * from './wizard'
|
||||
|
||||
export { UpdateState, IStateStorage } from './state'
|
||||
|
|
|
@ -6,11 +6,6 @@ import { MaybeAsync } from '@mtcute/core'
|
|||
*
|
||||
* The key is additionally prefixed with current scene, if any.
|
||||
*
|
||||
* Defaults to:
|
||||
* - If private chat, `msg.chat.id`
|
||||
* - If group chat, `msg.chat.id + '_' + msg.sender.id`
|
||||
* - If channel, `msg.chat.id`
|
||||
*
|
||||
* @param msg Message or callback from which to derive the key
|
||||
* @param scene Current scene UID, or `null` if none
|
||||
*/
|
||||
|
|
|
@ -57,7 +57,16 @@ export class UpdateState<State, SceneName extends string = string> {
|
|||
* @param fallback Default state value
|
||||
* @param force Whether to ignore cached state (def. `false`)
|
||||
*/
|
||||
async get(fallback?: State, force?: boolean): Promise<State>
|
||||
async get(fallback: State, force?: boolean): Promise<State>
|
||||
|
||||
/**
|
||||
* Retrieve the state from the storage, falling back to default
|
||||
* if not found
|
||||
*
|
||||
* @param fallback Default state value
|
||||
* @param force Whether to ignore cached state (def. `false`)
|
||||
*/
|
||||
async get(fallback?: State, force?: boolean): Promise<State | null>
|
||||
/**
|
||||
* Retrieve the state from the storage
|
||||
*
|
||||
|
@ -88,13 +97,36 @@ export class UpdateState<State, SceneName extends string = string> {
|
|||
* Set new state to the storage
|
||||
*
|
||||
* @param state New state
|
||||
* @param ttl TTL for the new state
|
||||
* @param ttl TTL for the new state (in seconds)
|
||||
*/
|
||||
async set(state: State, ttl?: number): Promise<void> {
|
||||
this._cached = state
|
||||
await this._localStorage.setState(this._localKey, state, ttl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given object to the current state.
|
||||
*
|
||||
* > **Note**: If the storage currently has no state,
|
||||
* > then `state` will be used as-is, which might
|
||||
* > result in incorrect typings. Beware!
|
||||
*
|
||||
* Basically a shorthand to calling `.get()`,
|
||||
* modifying and then calling `.set()`
|
||||
*
|
||||
* @param state State to be merged
|
||||
* @param ttl TTL for the new state (in seconds)
|
||||
* @param forceLoad Whether to force load the old state from storage
|
||||
*/
|
||||
async merge(state: Partial<State>, ttl?: number, forceLoad = false): Promise<void> {
|
||||
const old = await this.get(forceLoad)
|
||||
if (!old) {
|
||||
await this.set(state as State, ttl)
|
||||
} else {
|
||||
await this.set({ ...old, ...state }, ttl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the state from the storage
|
||||
*/
|
||||
|
@ -107,7 +139,7 @@ export class UpdateState<State, SceneName extends string = string> {
|
|||
* Enter some scene
|
||||
*
|
||||
* @param scene Scene name
|
||||
* @param ttl TTL for the scene
|
||||
* @param ttl TTL for the scene (in seconds)
|
||||
*/
|
||||
async enter(scene: SceneName, ttl?: number): Promise<void> {
|
||||
this._scene = scene
|
||||
|
|
84
packages/dispatcher/src/wizard.ts
Normal file
84
packages/dispatcher/src/wizard.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { MaybeAsync, Message } from '@mtcute/client'
|
||||
import { Dispatcher } from './dispatcher'
|
||||
import { NewMessageHandler } from './handler'
|
||||
import { UpdateState } from './state'
|
||||
import { filters } from './filters'
|
||||
|
||||
/**
|
||||
* Action for the wizard scene.
|
||||
*
|
||||
* `Next`: Continue to the next registered step
|
||||
* (or exit, if this is the last step)
|
||||
*
|
||||
* `Stay`: Stay on the same step
|
||||
*
|
||||
* `Exit`: Exit from the wizard scene
|
||||
*
|
||||
* You can also return a `number` to jump to that step
|
||||
*/
|
||||
export enum WizardSceneAction {
|
||||
Next = 'next',
|
||||
Stay = 'stay',
|
||||
Exit = 'exit'
|
||||
}
|
||||
|
||||
interface WizardInternalState {
|
||||
$step: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Wizard is a special type of Dispatcher
|
||||
* that can be used to simplify implementing
|
||||
* step-by-step scenes.
|
||||
*/
|
||||
export class WizardScene<State, SceneName extends string = string> extends Dispatcher<
|
||||
State & WizardInternalState,
|
||||
SceneName
|
||||
> {
|
||||
private _steps = 0
|
||||
|
||||
/**
|
||||
* Get the total number of registered steps
|
||||
*/
|
||||
get totalSteps(): number {
|
||||
return this._steps
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a step to the wizard
|
||||
*/
|
||||
addStep(handler: (msg: Message, state: UpdateState<State, SceneName>) => MaybeAsync<WizardSceneAction | number>): void {
|
||||
const step = this._steps++
|
||||
|
||||
const filter = filters.state<WizardInternalState>((it) => it.$step === step)
|
||||
|
||||
this.onNewMessage(
|
||||
step === 0
|
||||
? filters.or(filters.stateEmpty, filter)
|
||||
: filter,
|
||||
async (msg: Message, state) => {
|
||||
const result = await handler(msg, state)
|
||||
|
||||
if (typeof result === 'number') {
|
||||
await state.merge({ $step: result })
|
||||
return
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case 'next': {
|
||||
const next = step + 1
|
||||
if (next === this._steps) {
|
||||
await state.exit()
|
||||
} else {
|
||||
await state.merge({ $step: next })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'exit':
|
||||
await state.exit()
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue