-
Notifications
You must be signed in to change notification settings - Fork 1
/
state.go
262 lines (227 loc) · 6.14 KB
/
state.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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// This file is part of Botgoram
// Botgoram is free software: see LICENSE.txt for more details.
package botgoram
import (
"errors"
"log"
"regexp"
"github.com/Patrolavia/telegram"
)
// valid message types
const (
TextMsg = "TEXT"
FileMsg = "FILE"
AudioMsg = "AUDIO"
PhotoMsg = "PHOTO"
StickerMsg = "STICKER"
VideoMsg = "VIDEO"
VoiceMsg = "VOICE"
ContactMsg = "CONTACT"
LocationMsg = "LOCATION"
VenueMsg = "VENUE"
)
func msgType(msg *telegram.Message) string {
switch {
case msg.Venue != nil:
return VenueMsg
case msg.Location != nil:
return LocationMsg
case msg.Contact != nil:
return ContactMsg
case msg.Voice != nil:
return VoiceMsg
case msg.Video != nil:
return VideoMsg
case msg.Sticker != nil:
return StickerMsg
case msg.Photo != nil:
return PhotoMsg
case msg.Audio != nil:
return AudioMsg
case msg.Document != nil:
return FileMsg
default:
return TextMsg
}
}
var commandSpliter *regexp.Regexp
func init() {
cs, err := regexp.Compile(`^(\S+)(\s*.*)?$`)
if err != nil {
log.Fatalf("Cannot compile regular expression for spliting command, contact botgoram developer!")
}
commandSpliter = cs
}
// ErrNoMatch denotes the message does not match the transitor
var ErrNoMatch = errors.New("No matching transitor!")
// InitialState is predefined state id for initial state
const InitialState = ""
// Transitor transits to next state according to message
//
// Which transitor to call
//
// Basically, we test message type to determine which transitor to call.
// If there is no transitor for this type, or transitor returns error code,
// which means match failed, the fallback transitors (if any) will be called.
//
// There is one exception: text messages. Text messages will be matched against
// special "Command" type before trying to call text transitors.
//
// Forwarded and replied message will go transitors handling forward/reply first,
// no matter which type it is. These messages will fallback to message type
// transitors when match failed.
//
// You should take care of not registering same transitor to a state twice, or
// the transitor will be called twice when match failed.
//
// Order of transitors
//
// Sometimes you need to register more than one transitor to a type. For example,
// an image bot might want to transit to different state according to image file
// format. The order we run transitor is just the order you register it.
type Transitor func(msg *telegram.Message, state State) (next string, err error)
// State describes how you can act with FSM and state data.
type State interface {
Data() interface{}
SetData(interface{})
User() *telegram.Victim // who this state associate with
ID() string // retrive current state id
Transit(id string) // directly transit to another state without transitor
// Transit again base on this state.
// Retransit() have lower priority than Transit(id), if you call
// Transit(id) anywhere before or after Retransit(), the state will
// transit to id, without testing any transitor.
Retransit()
// register transitors by message types
Register(mt string, t Transitor)
// Command is a special text message type, will be matched before text type.
// A text message matches /^(\S+)(\s*.*)?$/ will go here before text type, and
// we use first matching group to find out which transitor to call, case-sensitive.
// (We use \S in regexp so you can define command in any language)
RegisterCommand(cmd string, t Transitor)
RegisterFallback(Transitor)
test(msg *telegram.Message) (next string, err error)
clone(user *telegram.Victim) State
next() *string
re() bool
}
type transitors []Transitor
func (ts transitors) test(msg *telegram.Message, cur State) (next string, err error) {
err = ErrNoMatch
for _, t := range ts {
if next, err = t(msg, cur); err == nil {
return
}
}
return
}
type state struct {
data interface{}
user *telegram.Victim
id string
forward transitors
reply transitors
types map[string]transitors
command map[string]transitors
text transitors
fallback transitors
chain *string
retransit bool
}
func newState(id string) State {
return &state{
id: id,
types: make(map[string]transitors),
command: make(map[string]transitors),
}
}
func (s *state) clone(user *telegram.Victim) State {
c := *s
c.user = user
return &c
}
func (s *state) Retransit() {
s.retransit = true
}
func (s *state) re() bool {
return s.retransit
}
func (s *state) Transit(id string) {
s.chain = &id
}
func (s *state) next() *string {
return s.chain
}
func (s *state) Data() interface{} {
return s.data
}
func (s *state) SetData(data interface{}) {
s.data = data
}
func (s *state) User() *telegram.Victim {
return s.user
}
func (s *state) ID() string {
return s.id
}
func (s *state) RegisterForward(t Transitor) {
s.forward = append(s.forward, t)
}
func (s *state) RegisterReply(t Transitor) {
s.reply = append(s.reply, t)
}
func (s *state) Register(mt string, t Transitor) {
s.types[mt] = append(s.types[mt], t)
}
func (s *state) RegisterCommand(cmd string, t Transitor) {
s.command[cmd] = append(s.command[cmd], t)
}
func (s *state) RegisterFallback(t Transitor) {
s.fallback = append(s.fallback, t)
}
func (s *state) test(msg *telegram.Message) (next string, err error) {
doTest := func(ts transitors) (next string, err error) {
if len(ts) == 0 {
return next, ErrNoMatch
}
return ts.test(msg, s)
}
// TODO: find better way to test commands.
testCmd := func() (next string, err error) {
err = ErrNoMatch
matches := commandSpliter.FindStringSubmatch(msg.Text)
if len(matches) != 3 {
return
}
cmd, ok := s.command[matches[1]]
if !ok {
return
}
return doTest(cmd)
}
// process forwarded message and replied message
if msg.ForwardFrom != nil {
if next, err = doTest(s.forward); err == nil {
return
}
}
if msg.ReplyTo != nil {
if next, err = doTest(s.reply); err == nil {
return
}
}
mt := msgType(msg)
// process command message
if mt == TextMsg {
if next, err = testCmd(); err == nil {
return
}
}
if _, ok := s.types[mt]; ok {
if next, err = doTest(s.types[mt]); err == nil {
return
}
}
next, err = doTest(s.fallback)
return
}