tucnak / telebot

Telebot is a Telegram bot framework in Go.
MIT License
4.04k stars 469 forks source link

Flow #657

Open shindakioku opened 8 months ago

shindakioku commented 8 months ago

Let's discuss the flow process based on live examples. The code may not be production-ready, of course, and without tests, but it should be sufficient for manual testing. Unfortunately, I haven't worked with the Telegram bot API for the last two years, so I may not be able to cover all cases. I'm asking for help from anyone who wants to contribute. Just pull my branch, try to describe your own flows, and provide feedback.

Anyone who wants to help can take a look at flow_simple_manual_testing/main.go, please. I've written two examples there, so you can test and modify as you want.

Updated: Please do not attempt to use my bot API token; I have already revoked it.

shindakioku commented 8 months ago

I will soon add some details for the "short" features.

tucnak commented 8 months ago

Hey, first of all, thanks for putting in the effort; state-machine behaviour is one aspect of the API that we had discussed time and time again with very little to show in terms of implementation. Well, I have built quite a few bots over the years, and indeed employed various approaches to state-management. Flow at the time was my best attempt at "generalising" that experience, yet unfortunately it coincided with big changes in my life, that is to say I haven't touched Telegram bots in a long time now.

My money would be on @demget to provide most accurate feedback as he's much more knowledgable than me, however I'll do my best to point out some things about your implemnetation along the way.

shindakioku commented 8 months ago

I've completed some refactoring. Firstly, I updated the API, and now it looks like:

b := &tele.Bot{}
sendUserMessage := func(message string) func(flow.State) error {
    return func(state flow.State) error {
        return state.Read(flow.StateContextKey).(tele.Context).Reply(message)
    }
}
stepCompletedLogging := func(state flow.State, step *flow.Step) error {
    log.Println("Step completed")

    return nil
}
// Configure flow bus
flowBus := flow.NewBus(5 * time.Minute)
// Handle any text by flow bus
b.Handle(tele.OnText, flowBus.Handle)
// First flow
var email, password string
b.Handle("/start", flowBus.Flow(
    flow.New().
        Next(
            flow.NewStep(sendUserMessage("Enter email:")).
                Validate(nonEmptyValidator).
                Assign(TextAssigner(&email)).
                Then(stepCompletedLogging),
        ).
        Next(
            flow.NewStep(sendUserMessage("Enter password:")).
                Assign(TextAssigner(&password)),
        ).
        Then(func(state flow.State) error {
            log.Println("Steps are completed!")

            return state.Read(flow.StateContextKey).(tele.Context).Reply("Done")
        }),
))

Secondly, I had to make [State] an interface as you suggested. It's a good point.

Additionally, I've simplified the contract for [Machine].

type Machine interface {
    Back(state State) error
    Next(state State) error
    ToStep(step int, state State) error
    ActiveStep() int
}

Now, I'm considering using 'then' for the step. I believe we need to provide some DTO/interface for the completed step's information. With that opportunity, we can create a generalized solution (logging/metrics, etc.).

shindakioku commented 8 months ago

I suppose we can begin discussing the code now. The main area is ready, particularly the API (I don't plan to change it at the moment). Additionally, I've added metadata information for the flow/steps (the last one is a bit lacking, but...). There are a few features that I need to implement, but we can review them later since they don't require changes to the code base; these features are just supplementary.

Idle flows: Naturally, we need to terminate idle flows after a user timeout and provide control over that to the user. I intend to achieve this by introducing a new [MetaDataFailureStage]. Since we already know the last step that the user completed, this shouldn't pose a problem. We need to discuss the behavior of [assigner] and [validation]. I'm confident that there's a good opportunity here, and we need to develop some useful implementations. At the very least, we should validate prompts by type (text/media/etc.), implement a letters range validator, and for the assigner, a basic one that fills the user variable.

The final API appears as follows:

b := &tele.Bot{}
sendUserMessage := func(message string) func(flow.State) error {
    return func(state flow.State) error {
        return state.Read(flow.StateContextKey).(tele.Context).Reply(message)
    }
}
loggingEachStep := func(state flow.State, metadata flow.StepMetaData) {
    log.Println(fmt.Sprintf("Step completed [%d]", metadata.Step))
}

flowBus := flow.NewBus(b, 5*time.Minute)
b.Handle(tele.OnText, flowBus.Handle)

var email, password string
err := flowBus.Flow(
    "/start",
    flow.New().
                OnEachStep(loggingEachStep).
        Next(flow.NewStep(sendUserMessage("Enter email:")).Assign(TextAssigner(&email))).
        Next(flow.NewStep(sendUserMessage("Enter password:")).Assign(TextAssigner(&password))).
        Then(registerUser).
        Catch(flowFailed),
)
AnatoliiKobzarGL commented 1 month ago

@shindakioku

Then(registerUser).

How registerUser will have access to the variables email and password? will this approach force me to use handlers as anonymous functions?