-
Notifications
You must be signed in to change notification settings - Fork 511
Description
Introduction
There are multiple missing pieces from the current Telebot implementation.
One of these pieces is a feature, known as the context. I don't believe that it should be included in the framework itself, as the state machine and the logic associated with it—is largely dependant on the implemention, therefore it must be implemented in the user code.
That said, we repeatedly end up implementing the context one way, or another. From the perspective of the code, it's trivial to do so. What is much more complicated is implementing complex multi-step interactions. This requires a large number of states, which adds to unnecessary verbosity and complicates the logic even further. To make things easier, I propose to introduce the concept of the flow.
What is the flow?
The flow is simply a high-level algorithm, a series of actions and branches, which naturally occur in the logic of any sophisticated bot.
Please, consider the following algorithm:
/startA greeting message is shown, accompanied with a list of commands and an inline keyboard with key actions.- At this point, the state is empty and there is no current interaction.
- One of the many stateful interactions may be triggered.
- Either a command, or an inline button—invokes an interaction.
- Generally, it doesn't matter which one it was, what matters is the flow.
- The user state is now set to this particular interaction.
- The bot requires a piece of media.
- Depending on whether if it's a photo, a video or an audio recording, this step should leave to three further interactions.
- Important: to remember the path of the flow, i.e. all the previous steps, including the outer interaction.
- The user might want to go back and choose a different file without cancelling the whole interaction. This can be done by deleting the sent media, or simply invoking a back action.
- One of the three interactions get invoked, when a series of data is requested (a form.)
- Each step of the way, data is harvested, validated separetely, and once form is validated as the whole.
- At any given point, user is ought to be able to either go back in the form, or go back to the previous interaction.
- Important: to note that the form itself is a distinct iteration with its own state.
- Once the interaction is complete, the user can be brought back to a certain waiting state, which may or may (step 1) not be completely stateless.
- Depending on the particular waiting state, a different set of interactions may be available.
In order to implement the aforementioned algorithm, currently you would have to create a state machine of your own, and laboriously spell out each and every state, alongside with the numerous transition rules. Principally speaking, this is trivial, but as the interactions require multiple kinds of media and have many intermittent requirements, the implementation would have to be spread out across different handlers. The code will quickly grow uncontrollably.
Proposal
The approach spelled out below is only but a first impression, much of it is open to discussion. I should fix a few principles to consider, when discussing it: (a) the state machine must not be naively exposed from the point of the flow, (b) the interaction must be functionally described by its steps, not the other way around, (c) interactions are always happening one-on-one between the bot and the user, (d) the flow is controlled via errors, handled by the interactions from the inner to the outer. Keep this in mind.
I will now walk though the key building blocks.
State
State of the interaction is implementation-dependant.
type State struct {
Data interface{}
Parent int // # of path position
}Path
Path is an interaction queue.
type Path struct {
History []Interaction
Global State
Local []State
}
func (*Path) Interaction() Interaction
{} // current interaction
func (*Path) Global() State
{} // global state
func (*Path) Local() State
{} // current interaction's state
func (*Path) Forward(Interaction, State)
{} // pushes tail forward
func (*Path) Back() (Interaction, State)
{} // pushes tail back and returns itThis struct maintains the snapshots of all states at any given interaction, so the rollback can be done from any point. The state is each and different for any given
Interaction
This is the logical building block, a state machine interface.
type Interaction interface {
Type() string // unique interaction name
Step() string // interaction step, if any
Enter(*Path) error // if validated, the interaction becomes the driver
Back(*Path) error
Push(*Path, *Update) error
Cancel(*Path) error
}Interaction implements the state of its own, while the path maintains the global state. Before the first message is pushed, Enter is called to validate the enter condition; if nil is returned, the interaction is added to the tail of the path along with the path state at the time of entrance.
When Back is called, it can either return nil, in which case the tail is simply rolled back to the last occurance of the interaction in the path, or a Rollback, which may specify the exact position and/or override the the state of the interaction at that point.
Interactions must be implemented to be valid Telebot handlers.
Flow
Flow is an interaction that outlines the high-level series of actions (interactions) in the builder fashion.
type stepFn func (*Path, *Message) error
type Flow interface {
Interaction
Text(step string, stepFn) Flow
Photo(step string, stepFn) Flow
Video(step string, stepFn) Flow
Poll(step string, stepFn) Flow
Dice(step string, stepFn) Flow
// ...
Then(step string, Interaction) Flow
Or(step string, Interaction...) Flow
Check(func(*Path) error) Flow
}(I'm not sure whether if it should be a struct or interface, but it probably should be an interface.)
Flow can be used to set up a series of data fetches and validations for all the supported types of updates. Essentially, it's the high-level representation of the algorithm, where everything is supposed to come together using custom fetches, Or and Then to compose, Check to validate in-between steps.
Flows implement regular interactions and can be created with Begin(step string).
Rollback
Rollback is an error that can override the current position and the state of the path.
type Rollback struct {
Err error
Position int
State State
}
func (*Rollback) Error() error
{} // a well-formatted rollback error
func (*Rollback) Override(int, State)
{} // position and the state of the overrideConclusion
This is it, for now. Hopefully, the API mentioned above is sufficient to build most, if not any of the bot interactions. Take a look at the actual code that is roughly implementing the algorithm outlined in the introduction.
alogorithm := tele.Begin("start").
Text("get username", func(path *tele.Path, msg *tele.Message) error {
}).
Or("get photo or video", getPhoto, getVideo).
Check("validate input", validation).
Then("complex interaction", &complexInteraction{})
bot.Handle("/start", algorithm)Please feel free to ask questions, as well as point out features and imperfections.
Cheers,
Ian