-
Notifications
You must be signed in to change notification settings - Fork 0
/
conversation.go
137 lines (119 loc) · 4.02 KB
/
conversation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package handlers
import (
"errors"
"fmt"
tele "github.com/jxo-me/gfbot"
)
const (
Entry = "Entry"
)
// The Conversation handler is an advanced handler which allows for running a sequence of commands in a stateful manner.
// An example of this flow can be found at t.me/Botfather; upon receiving the "/newbot" command, the user is asked for
// the name of their bot, which is sent as a separate message.
//
// The bot's internal state allows it to check at which point of the conversation the user is, and decide how to handle
// the next update.
type Conversation struct {
ServiceName string
// EntryHandler is the handler to start the conversation.
EntryHandler tele.IHandler
// SubHandlers is the map of possible states, with a list of possible handlers for each one.
SubHandlers map[string][]tele.IHandler
ExitName string
// The following are all optional fields:
// ExitHandler is the handler to exit the current conversation partway (eg /cancel commands)
ExitHandler tele.IHandler
// If True, a user can restart the conversation by hitting one of the entry points.
AllowReEntry bool
}
type ConversationOpts struct {
ExitName string
// ExitHandler is the list of handlers to exit the current conversation partway (eg /cancel commands). This returns
// EndConversation() by default, unless otherwise specified.
ExitHandler tele.IHandler
// If True, a user can restart the conversation at any time by hitting one of the entry points again.
AllowReEntry bool
}
func NewConversation(entryName string, entryPoint tele.IHandler, subHandlers map[string][]tele.IHandler, opts *ConversationOpts) Conversation {
c := Conversation{
ServiceName: entryName,
EntryHandler: entryPoint,
SubHandlers: subHandlers,
}
if opts != nil {
c.ExitName = opts.ExitName
c.ExitHandler = opts.ExitHandler
c.AllowReEntry = opts.AllowReEntry
}
return c
}
func (c Conversation) CheckUpdate(ctx tele.Context) bool {
// Note: Kinda sad that this error gets lost.
h, _ := c.getNextHandler(ctx)
return h != nil
}
func (c Conversation) HandleUpdate(ctx tele.Context) error {
next, err := c.getNextHandler(ctx)
if err != nil {
return fmt.Errorf("failed to get next handler in conversation: %w", err)
}
if next == nil {
// Note: this should be impossible
return nil
}
var stateChange *StateChange
err = next.HandleUpdate(ctx)
if !errors.As(err, &stateChange) {
// We don't wrap this error, as users might want to handle it explicitly
return err
}
if stateChange.End {
// Mark the conversation as ended by deleting the conversation reference.
err = ctx.Bot().Store().Delete(ctx)
if err != nil {
return fmt.Errorf("failed to end conversation: %w", err)
}
}
if stateChange.NextState != nil {
// If the next state is defined, then move to it.
if _, ok := c.SubHandlers[*stateChange.NextState]; !ok {
// Check if the "next" state is a supported state.
return fmt.Errorf("unknown state: %w", stateChange)
}
err = ctx.Bot().Store().Next(ctx, *stateChange.NextState)
if err != nil {
return fmt.Errorf("failed to update conversation state: %w", err)
}
}
return nil
}
func (c Conversation) Name() string {
return c.ServiceName
}
// getNextHandler goes through all the handlers in the conversation, until it finds a handler that matches.
// If no matching handler is found, returns nil.
func (c Conversation) getNextHandler(ctx tele.Context) (tele.IHandler, error) {
// Check if a conversation has already started for this user.
currState, _ := ctx.Bot().Store().Get(ctx)
cmd := ctx.Message().Text
if ctx.Callback() != nil && ctx.Callback().Unique != "" {
cmd = "\f" + ctx.Callback().Unique
}
switch cmd {
case c.ServiceName:
// add state
_ = ctx.Bot().Store().Set(ctx, tele.State{ServiceName: c.ServiceName, Data: map[string]any{}})
return c.EntryHandler, nil
case c.ExitName:
if currState != nil {
return wrappedExitHandler{h: c.ExitHandler}, nil
}
default:
if currState != nil {
if next := tele.CheckHandlerList(c.SubHandlers[currState.Key], ctx); next != nil {
return next, nil
}
}
}
return nil, nil
}