/
controller.go
235 lines (190 loc) · 6.37 KB
/
controller.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
package web
import (
"log"
"math/rand"
"net/http"
"github.com/gorilla/websocket"
"github.com/microcosm-cc/bluemonday"
"github.com/pkg/errors"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// BotIDCreator is a callback that gets a http request before it is hijacked and upgraded
// to WebSocket. Its sole job is to identify the bot ID from the request and return it.
type BotIDCreator func(*http.Request) (BotID, error)
// defaultBotIDCreator is used to generate ids by default, just generates a random integer
// for each connection.
func defaultBotIDCreator(*http.Request) (BotID, error) {
return BotID(rand.Int63()), nil // ¯\_(ツ)_/¯
}
// ErrorHandler is a function that can be specified to intercept different errors occuring
// during operation that are not a result of a user action
type ErrorHandler func(err error)
// defaultErrorHandler is the default function used if nothing is specified,
// it simply logs and exits
func defaultErrorHandler(err error) {
log.Fatal(err)
}
// Controller is the main interface to manage and control bots at a particular endpoint.
// Essentially, you create a Controller object, and set the ConnectionHandler as the request
// handler for a particular endpoint, and clients connecting to that endpoint will be managed
// by this object.
type Controller struct {
botAdded chan *Bot
sanitizer *bluemonday.Policy
cs ControllerStore
conversations ConversationRegistry
convs ConversationStore
messages chan *MessagePair
idCreator BotIDCreator
errHandler ErrorHandler
}
// NewController creates a new Controller object. It can take options for specifying
// the ControllerStore, ConversationStore, BotIDCreator and ErrorHandler.
func NewController(options ...func(*Controller) error) (*Controller, error) {
c := &Controller{
sanitizer: bluemonday.UGCPolicy(),
botAdded: make(chan *Bot),
messages: make(chan *MessagePair),
conversations: NewConversationRegistry(),
}
for _, opt := range options {
if err := opt(c); err != nil {
return nil, errors.Wrap(err, "NewController Failed")
}
}
if c.cs == nil {
c.cs = NewMemoryControllerStore()
}
if c.convs == nil {
c.convs = NewMemoryConversationStore()
}
if c.idCreator == nil {
c.idCreator = defaultBotIDCreator
}
if c.errHandler == nil {
c.errHandler = defaultErrorHandler
}
return c, nil
}
// WithControllerStore can be passed as an option to NewController with the
// desired implementation of ControllerStore.
func WithControllerStore(store ControllerStore) func(*Controller) error {
return func(c *Controller) error {
if store == nil {
return ErrNilControllerStore
}
c.cs = store
return nil
}
}
// WithConversationStore can be passed as an option to NewController with the
// desired implementation of ConversationStore.
func WithConversationStore(store ConversationStore) func(*Controller) error {
return func(c *Controller) error {
if store == nil {
return ErrNilConversationStore
}
c.convs = store
return nil
}
}
// WithBotIDCreator can be passed as an option to NewController with the
// desired implementation of BotIDCreator.
func WithBotIDCreator(f BotIDCreator) func(*Controller) error {
return func(c *Controller) error {
if f == nil {
return ErrNilIDCreator
}
c.idCreator = f
return nil
}
}
// WithErrorHandler can be passed as an option to NewController with the
// desired implementation of ErrorHandler.
func WithErrorHandler(f ErrorHandler) func(*Controller) error {
return func(c *Controller) error {
if f == nil {
return ErrNilErrorHandler
}
c.errHandler = f
return nil
}
}
// ConnectionHandler returns a http.HandlerFunc that can be used to intercept
// and handle connections from clients. It essentially creates a new Bot for each connection,
// and upgrades the connection to WebSocket.
func (c *Controller) ConnectionHandler() http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(res, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
botID, err := c.idCreator(req)
if err != nil {
http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
conn, err := upgrader.Upgrade(res, req, nil)
if err != nil {
// NOTE: this is not needed, the upgrader sends http.StatusBadRequest on its own
// TODO: maybe use errhandler also here, the error may be informative
// http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
bot, err := c.addBot(botID, conn, req)
if err != nil {
// TODO: maybe use errhandler also here, the error may be informative
http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
go bot.write()
go bot.read()
res.WriteHeader(http.StatusOK)
}
}
// addBot adds a new Bot from a *websocket.Conn
func (c *Controller) addBot(botID BotID, conn *websocket.Conn, req *http.Request) (*Bot, error) {
if err := c.cs.Add(botID); err != nil {
return nil, errors.Wrap(err, "Could not Add bot")
}
store, err := c.cs.Get(botID)
if err != nil {
return nil, errors.Wrap(err, "Could not Get bot")
}
bot := newBot(botID, conn, c.sanitizer, c.messages, store, c.conversations, c.convs, c.errHandler)
bot.remove = func() {
if err := c.removeBot(bot); err != nil {
c.errHandler(err)
}
}
go func() { c.botAdded <- bot }()
return bot, nil
}
// removeBot removes a bot from a closed webSocket.Conn
func (c *Controller) removeBot(bot *Bot) error {
close(bot.outgoingMessages)
err1, err2 := c.cs.Remove(bot.id), bot.conn.Close()
bot.conn = nil
if err2 != nil {
return err2
}
return err1
}
// BotAdded gets a receive only channel that'll get a bot payload whenever a new connection
// is obtained. You can use this to send messages like "Hi, I'm Online" to new users.
func (c *Controller) BotAdded() <-chan *Bot {
return c.botAdded
}
// Messages returns a receive only channel that will get a bot-message pair every time a new message
// comes over.
func (c *Controller) Messages() <-chan *MessagePair {
return c.messages
}
// RegisterConversation registers a new Conversation with the Controller, so it can
// be used with bot.StartConversation(name)
func (c *Controller) RegisterConversation(name string, conv *Conversation) error {
return c.conversations.Add(name, conv)
}