Skip to content
This repository was archived by the owner on Mar 11, 2024. It is now read-only.

Commit 4dad28f

Browse files
toolateforteddyfabioxgn
authored andcommitted
Allow Protocol Argument Passing (#112)
Due to the generic nature of this framework, platform specific behaviors are hard to take advantage of. With this PR, protocol plugins can offer additional information for message handlers to read, and accept additional information as well. In particular, this change was tested to allow a Slack response to be made as a threaded reply, instead of only ever in the main channel. EG: func threadedEcho(cmd *bot.Cmd) (bot.CmdResult, error) { var params *slack.PostMessageParameters if ev, ok := cmd.MessageData.ProtoMsg.(*slack.MessageEvent); ok { params = &slack.PostMessageParameters{ AsUser: true, ThreadTimestamp: ev.Timestamp, } } return bot.CmdResult{ Message: fmt.Sprintf("%s: %s", cmd.User.Nick, cmd.Raw), Channel: cmd.Channel, ProtoParams: params, }, nil }
1 parent 9b89c72 commit 4dad28f

File tree

7 files changed

+216
-61
lines changed

7 files changed

+216
-61
lines changed

bot.go

+46-9
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,31 @@ type Config struct {
5555
type responseMessage struct {
5656
target, message string
5757
sender *User
58+
protoParams interface{}
59+
}
60+
61+
// OutgoingMessage collects all the information for a message to go out.
62+
type OutgoingMessage struct {
63+
Target string
64+
Message string
65+
Sender *User
66+
ProtoParams interface{}
5867
}
5968

6069
// ResponseHandler must be implemented by the protocol to handle the bot responses
6170
type ResponseHandler func(target, message string, sender *User)
6271

72+
// ResponseHandlerV2 may be implemented by the protocol to handle the bot responses
73+
type ResponseHandlerV2 func(OutgoingMessage)
74+
6375
// ErrorHandler will be called when an error happens
6476
type ErrorHandler func(msg string, err error)
6577

6678
// Handlers that must be registered to receive callbacks from the bot
6779
type Handlers struct {
68-
Response ResponseHandler
69-
Errored ErrorHandler
80+
Response ResponseHandler
81+
ResponseV2 ResponseHandlerV2
82+
Errored ErrorHandler
7083
}
7184

7285
func logErrorHandler(msg string, err error) {
@@ -158,7 +171,7 @@ func (b *Bot) startPeriodicCommands() {
158171

159172
// MessageReceived must be called by the protocol upon receiving a message
160173
func (b *Bot) MessageReceived(channel *ChannelData, message *Message, sender *User) {
161-
command, err := parse(message.Text, channel, sender)
174+
command, err := parse(message, channel, sender)
162175
if err != nil {
163176
b.SendMessage(channel.Channel, err.Error(), sender)
164177
return
@@ -189,23 +202,47 @@ func (b *Bot) MessageReceived(channel *ChannelData, message *Message, sender *Us
189202

190203
// SendMessage queues a message for a target recipient, optionally from a particular sender.
191204
func (b *Bot) SendMessage(target string, message string, sender *User) {
192-
message = b.executeFilterCommands(&FilterCmd{
205+
b.SendMessageV2(OutgoingMessage{
193206
Target: target,
194207
Message: message,
195-
User: sender})
208+
Sender: sender,
209+
})
210+
}
211+
212+
// SendMessageV2 queues a message.
213+
func (b *Bot) SendMessageV2(om OutgoingMessage) {
214+
message := b.executeFilterCommands(&FilterCmd{
215+
Target: om.Target,
216+
Message: om.Message,
217+
User: om.Sender,
218+
})
196219
if message == "" {
197220
return
198221
}
199222

200223
select {
201-
case b.msgsToSend <- responseMessage{target, message, sender}:
224+
case b.msgsToSend <- responseMessage{
225+
target: om.Target,
226+
message: message,
227+
sender: om.Sender,
228+
protoParams: om.ProtoParams,
229+
}:
202230
default:
203231
b.errored("Failed to queue message to send.", errors.New("Too busy"))
204232
}
205233
}
206234

207-
func (b *Bot) sendResponse(target, message string, sender *User) {
208-
b.handlers.Response(target, message, sender)
235+
func (b *Bot) sendResponse(resp responseMessage) {
236+
if b.handlers.ResponseV2 != nil {
237+
b.handlers.ResponseV2(OutgoingMessage{
238+
Message: resp.message,
239+
ProtoParams: resp.protoParams,
240+
Sender: resp.sender,
241+
Target: resp.target,
242+
})
243+
return
244+
}
245+
b.handlers.Response(resp.target, resp.message, resp.sender)
209246
}
210247

211248
func (b *Bot) errored(msg string, err error) {
@@ -218,7 +255,7 @@ func (b *Bot) processMessages() {
218255
for {
219256
select {
220257
case msg := <-b.msgsToSend:
221-
b.sendResponse(msg.target, msg.message, msg.sender)
258+
b.sendResponse(msg)
222259
case <-b.done:
223260
return
224261
}

cmd.go

+22-9
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ func (c *ChannelData) URI() string {
4141

4242
// Message holds the message info - for IRC and Slack networks, this can include whether the message was an action.
4343
type Message struct {
44-
Text string // The actual content of this Message
45-
IsAction bool // True if this was a '/me does something' message
44+
Text string // The actual content of this Message
45+
IsAction bool // True if this was a '/me does something' message
46+
ProtoMsg interface{} // The underlying object that we got from the protocol pkg
4647
}
4748

4849
// FilterCmd holds information about what is output being filtered - message and
@@ -108,15 +109,17 @@ type customCommand struct {
108109

109110
// CmdResult is the result message of V2 commands
110111
type CmdResult struct {
111-
Channel string // The channel where the bot should send the message
112-
Message string // The message to be sent
112+
Channel string // The channel where the bot should send the message
113+
Message string // The message to be sent
114+
ProtoParams interface{}
113115
}
114116

115117
// CmdResultV3 is the result message of V3 commands
116118
type CmdResultV3 struct {
117-
Channel string
118-
Message chan string
119-
Done chan bool
119+
Channel string
120+
Message chan string
121+
Done chan bool
122+
ProtoParams interface{}
120123
}
121124

122125
const (
@@ -389,7 +392,12 @@ func (b *Bot) handleCmd(c *Cmd) {
389392
}
390393

391394
if result.Message != "" {
392-
b.SendMessage(result.Channel, result.Message, c.User)
395+
b.SendMessageV2(OutgoingMessage{
396+
Target: result.Channel,
397+
Message: result.Message,
398+
Sender: c.User,
399+
ProtoParams: result.ProtoParams,
400+
})
393401
}
394402
case v3:
395403
result, err := cmd.CmdFuncV3(c)
@@ -401,7 +409,12 @@ func (b *Bot) handleCmd(c *Cmd) {
401409
select {
402410
case message := <-result.Message:
403411
if message != "" {
404-
b.SendMessage(result.Channel, message, c.User)
412+
b.SendMessageV2(OutgoingMessage{
413+
Target: result.Channel,
414+
Message: message,
415+
Sender: c.User,
416+
ProtoParams: result.ProtoParams,
417+
})
405418
}
406419
case <-result.Done:
407420
return

cmd_test.go

+83-6
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import (
1212
)
1313

1414
var (
15-
channel string
16-
replies chan string
17-
cmdError chan string
18-
user *User
19-
msgs []string
20-
errs []string
15+
channel string
16+
replies chan string
17+
cmdError chan string
18+
user *User
19+
msgs []string
20+
errs []string
21+
protoParams interface{}
2122
)
2223

2324
const (
@@ -52,6 +53,13 @@ func responseHandler(target string, message string, sender *User) {
5253
replies <- message
5354
}
5455

56+
func responseHandlerV2(om OutgoingMessage) {
57+
channel = om.Target
58+
user = om.Sender
59+
protoParams = om.ProtoParams
60+
replies <- om.Message
61+
}
62+
5563
func errorHandler(msg string, err error) {
5664
cmdError <- fmt.Sprintf("%s: %s", msg, err)
5765
}
@@ -63,6 +71,7 @@ func reset() {
6371
cmdError = make(chan string, 10)
6472
msgs = []string{}
6573
errs = []string{}
74+
protoParams = nil
6675
commands = make(map[string]*customCommand)
6776
periodicCommands = make(map[string]PeriodicConfig)
6877
passiveCommands = make(map[string]*customCommand)
@@ -81,6 +90,19 @@ func newBot() *Bot {
8190
)
8291
}
8392

93+
func newBotV2() *Bot {
94+
return New(&Handlers{
95+
Response: responseHandler,
96+
ResponseV2: responseHandlerV2,
97+
Errored: errorHandler,
98+
},
99+
&Config{
100+
Protocol: "test",
101+
Server: "test",
102+
},
103+
)
104+
}
105+
84106
func registerValidCommand() {
85107
RegisterCommand(cmd, cmdDescription, cmdExampleArgs,
86108
func(c *Cmd) (string, error) {
@@ -496,6 +518,37 @@ func TestCmdV2(t *testing.T) {
496518
}
497519
}
498520

521+
func TestCmdV2WithProtoParams(t *testing.T) {
522+
reset()
523+
RegisterCommandV2("cmd", "", "",
524+
func(c *Cmd) (CmdResult, error) {
525+
return CmdResult{
526+
Channel: "#channel",
527+
Message: "message",
528+
ProtoParams: &CmdResult{Message: "Nested!"},
529+
}, nil
530+
})
531+
532+
b := newBotV2()
533+
b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"})
534+
535+
waitMessages(t, 1, 0)
536+
537+
if channel != "#channel" {
538+
t.Error("Wrong channel")
539+
}
540+
if !reflect.DeepEqual([]string{"message"}, msgs) {
541+
t.Error("Invalid reply")
542+
}
543+
if pa, ok := protoParams.(*CmdResult); ok {
544+
if pa.Message != "Nested!" {
545+
t.Error("Information lost in copying.")
546+
}
547+
} else {
548+
t.Error("Failed to pass proto args through.")
549+
}
550+
}
551+
499552
func TestCmdV2WithoutSpecifyingChannel(t *testing.T) {
500553
reset()
501554
RegisterCommandV2("cmd", "", "",
@@ -687,6 +740,30 @@ func TestFilterCommandSilence(t *testing.T) {
687740
}
688741
}
689742

743+
func TestFilterCommandSilenceSendV2(t *testing.T) {
744+
reset()
745+
passiveCommands = make(map[string]*customCommand)
746+
ping := func(cmd *PassiveCmd) (string, error) { return "pong", nil }
747+
silenced := func(cmd *FilterCmd) (string, error) { return "", nil }
748+
errored := func(cmd *FilterCmd) (string, error) { return "Ignored", errors.New("error") }
749+
750+
RegisterPassiveCommand("ping", ping)
751+
RegisterFilterCommand("silenced", silenced)
752+
RegisterFilterCommand("errored", errored)
753+
754+
b := newBotV2()
755+
b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "test"}, &User{Nick: "user"})
756+
757+
waitMessages(t, 0, 1)
758+
759+
if len(msgs) != 0 {
760+
t.Fatal("Expected no messages!")
761+
}
762+
if len(errs) != 1 {
763+
t.Error("Expected 1 error")
764+
}
765+
}
766+
690767
// how to test channels..
691768
// https://www.hugopicado.com/2016/10/01/testing-over-golang-channels.html
692769

help.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ const (
1414
)
1515

1616
func (b *Bot) help(c *Cmd) {
17-
cmd, _ := parse(CmdPrefix+c.RawArgs, c.ChannelData, c.User)
17+
msg := &Message{
18+
Text: CmdPrefix + c.RawArgs,
19+
}
20+
cmd, _ := parse(msg, c.ChannelData, c.User)
1821
if cmd == nil {
1922
b.showAvailabeCommands(c.Channel, c.User)
2023
return

parser.go

+11-13
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,20 @@ var (
1313
re = regexp.MustCompile("\\s+") // Matches one or more spaces
1414
)
1515

16-
func parse(s string, channel *ChannelData, user *User) (*Cmd, error) {
17-
c := &Cmd{Raw: s}
18-
s = strings.TrimSpace(s)
16+
func parse(m *Message, channel *ChannelData, user *User) (*Cmd, error) {
17+
s := strings.TrimSpace(m.Text)
1918

2019
if !strings.HasPrefix(s, CmdPrefix) {
2120
return nil, nil
2221
}
2322

24-
c.Channel = strings.TrimSpace(channel.Channel)
25-
c.ChannelData = channel
26-
c.User = user
27-
28-
// Trim the prefix and extra spaces
29-
c.Message = strings.TrimPrefix(s, CmdPrefix)
30-
c.Message = strings.TrimSpace(c.Message)
23+
c := &Cmd{
24+
Channel: strings.TrimSpace(channel.Channel),
25+
ChannelData: channel,
26+
Message: strings.TrimSpace(strings.TrimPrefix(s, CmdPrefix)),
27+
Raw: m.Text,
28+
User: user,
29+
}
3130

3231
// check if we have the command and not only the prefix
3332
if c.Message == "" {
@@ -48,9 +47,8 @@ func parse(s string, channel *ChannelData, user *User) (*Cmd, error) {
4847
c.Args = parsedArgs
4948
}
5049

51-
c.MessageData = &Message{
52-
Text: c.Message,
53-
}
50+
m.Text = c.Message
51+
c.MessageData = m
5452

5553
return c, nil
5654
}

parser_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func TestParser(t *testing.T) {
9898

9999
for _, test := range tests {
100100
t.Run(test.msg, func(t *testing.T) {
101-
cmd, _ := parse(test.msg, channel, user)
101+
cmd, _ := parse(&Message{Text: test.msg}, channel, user)
102102
if test.expected != nil && cmd != nil {
103103
if test.expected.Raw != cmd.Raw {
104104
t.Errorf("Expected Raw:\n%#v\ngot:\n%#v", test.expected.Raw, cmd.Raw)
@@ -156,7 +156,11 @@ func TestParser(t *testing.T) {
156156
}
157157

158158
func TestInvalidArguments(t *testing.T) {
159-
cmd, err := parse("!cmd Invalid \"arg", &ChannelData{Channel: "#go-bot"}, &User{Nick: "user123"})
159+
cmd, err := parse(
160+
&Message{Text: "!cmd Invalid \"arg"},
161+
&ChannelData{Channel: "#go-bot"},
162+
&User{Nick: "user123"},
163+
)
160164
if err == nil {
161165
t.Error("Expected error, got nil")
162166
}

0 commit comments

Comments
 (0)