Skip to content

Commit a2ecff0

Browse files
committed
Merge branch 'main' into eventsub
2 parents 0dd1d45 + 255ad06 commit a2ecff0

22 files changed

+534
-277
lines changed

bot/alertHandler.go

Lines changed: 0 additions & 64 deletions
This file was deleted.

bot/bitsHandler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func (bot *Bot) RegisterBitsHandler(messageTemplate HandlerTemplate) {
1818
Name: evt.Sender,
1919
Amount: evt.Amount,
2020
}
21-
bot.metricsCache.Put(viewer.LastBits, metric.String())
21+
bot.dataStore.Put(viewer.LastBits, metric.String())
2222
}
2323
}),
2424
)

bot/bot.go

Lines changed: 158 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package bot
22

33
import (
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
3246
func 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
4467
func (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
107236
func (bot *Bot) listen() {

bot/commandHandler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func (bot *Bot) HandleCommands(knownCommands []Command) {
3838
// If the Command is an alias for another command, change message contents and send back to the Bot
3939
if command.IsAlias {
4040
evt.Message = command.AliasFor
41-
bot.receiveEvent(evt)
41+
bot.ReceiveEvent(evt)
4242
break
4343
}
4444

bot/event.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const (
99
RAID
1010
)
1111

12-
// All-encompassing model for Events that the Bot understands
12+
// Event is an all-encompassing model for Events that the Bot understands
1313
// NOTE: This struct is referenced by config.yaml. Make changes carefully
1414
type Event struct {
1515
Type int // Identify what kind of Event we are receiving

bot/pollHandler.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package bot
2+
3+
import (
4+
"medgebot/logger"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
// RegisterPollHandler collects Poll answers from Chat messages
10+
func (bot *Bot) RegisterPollHandler() {
11+
bot.RegisterHandler(
12+
NewHandler(func(evt Event) {
13+
if strings.HasPrefix(evt.Message, "!poll") {
14+
bot.SendPollMessage()
15+
}
16+
17+
if !bot.IsPollRunning() {
18+
return
19+
}
20+
21+
// Message should just be a number (and within range of answers). Otherwise reject as a vote
22+
vote, err := strconv.Atoi(evt.Message)
23+
if err != nil {
24+
return // Assumed to not be a valid vote
25+
}
26+
27+
alreadyVoted, err := bot.dataStore.GetOrDefault("voters", "")
28+
if err != nil {
29+
logger.Error(err, "fetch voters from bot.dataStore")
30+
return
31+
}
32+
33+
if strings.Contains(alreadyVoted, evt.Sender) {
34+
return // Can't vote multiple times
35+
}
36+
37+
// Valid vote - append their vote and note that they voted
38+
bot.dataStore.Append("voters", ",", evt.Sender)
39+
bot.AddPollVote(vote)
40+
}),
41+
)
42+
}

bot/raidHandler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func (bot *Bot) RegisterRaidHandler(messageTemplate HandlerTemplate, delaySecond
2424
Name: evt.Sender,
2525
Amount: evt.Amount,
2626
}
27-
bot.metricsCache.Put(viewer.LastRaider, metric.String())
27+
bot.dataStore.Put(viewer.LastRaider, metric.String())
2828
}
2929
}),
3030
)

0 commit comments

Comments
 (0)