@@ -2,9 +2,12 @@ package bot
22
33import (
44 "errors"
5+ "fmt"
56 "medgebot/cache"
7+ "medgebot/logger"
68 "strings"
79 "sync"
10+ "time"
811)
912
1013// Bot handles various feature processing based on Stream events
@@ -25,20 +28,40 @@ type Bot struct {
2528 listening bool
2629
2730 // Cache for various handler metrics
28- metricsCache cache.Cache
31+ dataStore cache.Cache
32+
33+ // polls
34+ pollRunning bool
35+ pollQuestion string
36+ pollAnswers []PollAnswer
37+ }
38+
39+ // PollAnswer keeps track of an Answer label and number of votes for that answer
40+ type PollAnswer struct {
41+ Answer string
42+ Count int
2943}
3044
3145// New produces a newly instantiated Bot
3246func New (metricsCache cache.Cache ) Bot {
3347 return Bot {
34- consumers : make ([]Handler , 0 ),
35- clients : make ([]Client , 0 ),
36- events : make (chan Event , 0 ),
37- listening : false ,
38- metricsCache : metricsCache ,
48+ consumers : make ([]Handler , 0 ),
49+ clients : make ([]Client , 0 ),
50+ events : make (chan Event , 0 ),
51+ listening : false ,
52+ dataStore : metricsCache ,
53+ pollRunning : false ,
3954 }
4055}
4156
57+ // Start the bot and listen for incoming events
58+ func (bot * Bot ) Start () error {
59+ // Ensure single concurrent reader, per doc requirements
60+ go bot .listen ()
61+
62+ return nil
63+ }
64+
4265// RegisterClient links a Client that will send data TO the Bot.
4366// This method also set's the Client's Destination channel
4467func (bot * Bot ) RegisterClient (client Client ) {
@@ -58,50 +81,156 @@ func (bot *Bot) SetChatClient(client ChatClient) {
5881 bot .chatClient = client
5982}
6083
61- // sendEvent sends a Bot event to Write-enabled clients
62- func (bot * Bot ) sendEvent (evt Event ) {
63- bot .chatClient .Channel () <- evt
64- }
65-
66- // receiveEvent is a way for Handlers to re-queue Events to be reprocessed. Ex: alias commands
67- func (bot * Bot ) receiveEvent (evt Event ) {
68- bot .events <- evt
69- }
84+ // RegisterHandler registers a function that will be called concurrently when a message is received
85+ func (bot * Bot ) RegisterHandler (consumer Handler ) error {
86+ if bot .listening {
87+ return errors .New ("RegisterHandler called after bot already listening" )
88+ }
7089
71- // Start the bot and listen for incoming events
72- func (bot * Bot ) Start () error {
73- // Ensure single concurrent reader, per doc requirements
74- go bot .listen ()
90+ bot .Lock ()
91+ defer bot .Unlock ()
7592
93+ consumers := append (bot .consumers , consumer )
94+ bot .consumers = consumers
7695 return nil
7796}
7897
98+ // ReceiveEvent is a way for code to directly queue Events to be processed. Ex: alias commands
99+ func (bot * Bot ) ReceiveEvent (evt Event ) {
100+ bot .events <- evt
101+ }
102+
79103// SendMessage sends a message to the given channel, without prefix
80- func (bot * Bot ) SendMessage (message string ) {
104+ func (bot * Bot ) SendMessage (message string , args ... interface {} ) {
81105 if strings .TrimSpace (message ) == "" {
82106 return
83107 }
84108
85109 evt := NewChatEvent ()
86- evt .Message = message
110+ evt .Message = fmt . Sprintf ( message , args ... )
87111
88112 go bot .sendEvent (evt )
89113}
90114
91- // RegisterHandler registers a function that will be called concurrently when a message is received
92- func (bot * Bot ) RegisterHandler (consumer Handler ) error {
93- if bot .listening {
94- return errors .New ("RegisterHandler called after bot already listening" )
115+ // IsPollRunning checks if a Poll is currently active
116+ func (bot * Bot ) IsPollRunning () bool {
117+ return bot .pollRunning
118+ }
119+
120+ // StartPoll starts a poll within the Bot. Returns error if poll already running.
121+ // Store reference to Poll contents for Handler use later
122+ func (bot * Bot ) StartPoll (duration time.Duration , question string , answers []string ) error {
123+ if bot .IsPollRunning () {
124+ return errors .New ("Poll already running" )
95125 }
96126
97- bot .Lock ()
98- defer bot .Unlock ()
127+ bot .ClearPoll ()
128+ logger .Info ("Starting new Poll: %s" , question )
129+ bot .pollRunning = true
130+ bot .pollQuestion = question
131+
132+ pollAnswers := make ([]PollAnswer , len (answers ))
133+ for idx , answer := range answers {
134+ pollAnswers [idx ] = PollAnswer {
135+ Answer : answer ,
136+ Count : 0 ,
137+ }
138+ }
139+ bot .pollAnswers = pollAnswers
140+
141+ bot .SendPollMessage ()
142+
143+ // Spawn off goroutine to close the poll after the given Duration
144+ go func (bot * Bot , dur time.Duration ) {
145+ select {
146+ case <- time .After (dur ):
147+ bot .closePoll ()
148+ }
149+ }(bot , duration )
99150
100- consumers := append (bot .consumers , consumer )
101- bot .consumers = consumers
102151 return nil
103152}
104153
154+ // SendPollMessage sends the current Poll message, if a poll is running
155+ func (bot * Bot ) SendPollMessage () {
156+ if ! bot .pollRunning {
157+ bot .SendMessage ("No poll running" )
158+ return
159+ }
160+
161+ formattedAnswers := ""
162+ for idx , pollAnswer := range bot .pollAnswers {
163+ formattedAnswers += fmt .Sprintf ("%d: %s | " , idx + 1 , pollAnswer .Answer )
164+ }
165+
166+ bot .SendMessage ("Poll started! Type a number only in chat to vote! Question: %s - | %s" , bot .pollQuestion , formattedAnswers )
167+ }
168+
169+ // AddPollVote increments the Count for the given Answer key
170+ func (bot * Bot ) AddPollVote (key int ) {
171+ if key < 1 || key > len (bot .pollAnswers ) {
172+ // Invalid vote. Skip
173+ return
174+ }
175+
176+ bot .pollAnswers [key - 1 ].Count ++
177+ }
178+
179+ // GetPollState returns the current question, answers, and vote counts for each answer
180+ func (bot * Bot ) GetPollState () (question string , answers []PollAnswer ) {
181+ return bot .pollQuestion , bot .pollAnswers
182+ }
183+
184+ // closePoll ends an active poll
185+ func (bot * Bot ) closePoll () {
186+ highestIdx := - 1
187+ winningAnswer := PollAnswer {}
188+ for idx , answer := range bot .pollAnswers {
189+ if answer .Count >= winningAnswer .Count {
190+ highestIdx = idx
191+ winningAnswer = answer
192+ }
193+ }
194+
195+ // If no votes - exit immediately
196+ if highestIdx == - 1 {
197+ bot .SendMessage ("No poll winner" )
198+ bot .ClearPoll ()
199+ return
200+ }
201+
202+ // Format winning response and account for ties
203+ winnerStr := fmt .Sprintf ("[%d] %s with %d votes" , highestIdx + 1 , winningAnswer .Answer , winningAnswer .Count )
204+ for idx , answer := range bot .pollAnswers {
205+ if idx == highestIdx {
206+ continue
207+ }
208+
209+ if answer .Count == winningAnswer .Count {
210+ winnerStr += " | "
211+ winnerStr += fmt .Sprintf ("[%d] %s with %d votes" , idx + 1 , answer .Answer , answer .Count )
212+ }
213+ }
214+ bot .SendMessage ("Poll Winner(s): %s" , winnerStr )
215+
216+ // Ensure poll clears
217+ logger .Info ("Closing poll" )
218+ bot .ClearPoll ()
219+ }
220+
221+ // ClearPoll clears out any existing Poll state in the Bot and the dataStore
222+ func (bot * Bot ) ClearPoll () {
223+ bot .pollRunning = false
224+ bot .pollQuestion = ""
225+ bot .pollAnswers = []PollAnswer {}
226+ bot .dataStore .Clear ("voters" )
227+ }
228+
229+ // sendEvent sends a Bot event to Write-enabled clients
230+ func (bot * Bot ) sendEvent (evt Event ) {
231+ bot .chatClient .Channel () <- evt
232+ }
233+
105234// Start listening for Events on the inbound channel and broadcast out
106235// to the Handlers
107236func (bot * Bot ) listen () {
0 commit comments