Skip to content

Commit d862e31

Browse files
authored
Merge pull request #84 from osbytes/poll-command
Poll command
2 parents fbbb7f6 + 5863768 commit d862e31

6 files changed

Lines changed: 3848 additions & 0 deletions

File tree

internal/devy/bot.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ package devy
22

33
import (
44
"bot/internal/github"
5+
"bot/pkg/unicode"
56
"context"
7+
"fmt"
68

79
"github.com/bwmarrin/discordgo"
810
"github.com/pkg/errors"
911
)
1012

13+
var (
14+
pollPrefix = fmt.Sprintf("%s **POLL**", unicode.Emojis[":memo:"])
15+
)
16+
1117
type Bot struct {
1218
discord *discordgo.Session
1319
githubService github.GithubServicer
@@ -30,6 +36,8 @@ func (b Bot) Start(ctx context.Context) error {
3036

3137
b.discord.AddHandler(b.messageCreate)
3238

39+
b.discord.AddHandler(b.messageReactionAdd)
40+
3341
select {
3442
case <-ctx.Done():
3543
return ctx.Err()

internal/devy/commands.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package devy
33
import (
44
"bot/pkg/env"
55
"bot/pkg/infra"
6+
"bot/pkg/strs"
7+
"bot/pkg/unicode"
68
"context"
79
"fmt"
810
"strings"
@@ -16,6 +18,9 @@ var (
1618
channelMessageSendF = channelMessageSend
1719
guildMemberRoleRemoveF = guildMemberRoleRemove
1820
guildMemberRoleAddF = guildMemberRoleAdd
21+
messageReactionAddF = messageReactionAdd
22+
messageReactionRemoveF = messageReactionRemove
23+
channelMessageF = channelMessage
1924
)
2025

2126
func channelFromState(s *discordgo.State, channelID string) (*discordgo.Channel, error) {
@@ -34,6 +39,18 @@ func guildMemberRoleRemove(s *discordgo.Session, guildID, userID, roleID string)
3439
return s.GuildMemberRoleRemove(guildID, userID, roleID)
3540
}
3641

42+
func messageReactionAdd(s *discordgo.Session, channelID, messageID, emojiID string) error {
43+
return s.MessageReactionAdd(channelID, messageID, emojiID)
44+
}
45+
46+
func messageReactionRemove(s *discordgo.Session, channelID, messageID, emojiID, userID string) error {
47+
return s.MessageReactionRemove(channelID, messageID, emojiID, userID)
48+
}
49+
50+
func channelMessage(s *discordgo.Session, channelID, messageID string) (*discordgo.Message, error) {
51+
return s.ChannelMessage(channelID, messageID)
52+
}
53+
3754
type CommandHandler func(session *discordgo.Session, message *discordgo.MessageCreate, channel *discordgo.Channel, bot *Bot)
3855

3956
type Command struct {
@@ -95,6 +112,12 @@ var commandMap = map[string]Command{
95112
Args: []string{},
96113
Handler: devyDeveloperCommandHandler,
97114
},
115+
"!poll": {
116+
Name: "!poll",
117+
Description: "creates a poll in the poll channel if specified on devy or the current channel. question and options must be wrapped with double quotes (\"question...\" \"option 1\" \"option 2\")",
118+
Args: []string{"question", "option", "option..."},
119+
Handler: pollCommandHandler,
120+
},
98121
}
99122

100123
func streakCurrentCommandHandler(session *discordgo.Session, message *discordgo.MessageCreate, channel *discordgo.Channel, bot *Bot) {
@@ -266,3 +289,48 @@ func devyDeveloperCommandHandler(session *discordgo.Session, message *discordgo.
266289

267290
_, _ = channelMessageSendF(session, channel.ID, fmt.Sprintf("%s devy developer role for user %s", action, message.Author.Username))
268291
}
292+
293+
func pollCommandHandler(session *discordgo.Session, message *discordgo.MessageCreate, channel *discordgo.Channel, bot *Bot) {
294+
pollChannelID := env.GetString("DISCORD_POLL_CHANNEL_ID", "")
295+
if len(pollChannelID) == 0 {
296+
pollChannelID = message.ChannelID
297+
}
298+
299+
arguments := strs.AllBetweenPattern(message.Content, "\"")
300+
if len(arguments) <= 2 {
301+
_, _ = channelMessageSendF(session, channel.ID, "polls must have more than one option")
302+
303+
return
304+
}
305+
306+
question := arguments[0]
307+
options := arguments[1:]
308+
309+
emojis := []string{}
310+
for _, e := range unicode.Emojis {
311+
emojis = append(emojis, e)
312+
if len(emojis) == len(options) {
313+
break
314+
}
315+
}
316+
317+
pollMessageStr := fmt.Sprintf("%s\n\n", question)
318+
319+
for i, e := range emojis {
320+
pollMessageStr += fmt.Sprintf("\t%s\t%s\n", e, options[i])
321+
}
322+
323+
msg, err := channelMessageSendF(session, pollChannelID, fmt.Sprintf("%s\n\n%s", pollPrefix, pollMessageStr))
324+
if err != nil {
325+
infra.Logger.Error().Err(err).Msg("channel message send")
326+
327+
_, _ = channelMessageSendF(session, pollChannelID, "something went wrong creating poll")
328+
329+
return
330+
}
331+
332+
for _, emj := range emojis {
333+
_ = messageReactionAddF(session, pollChannelID, msg.ID, emj)
334+
}
335+
336+
}

internal/devy/handlers.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package devy
22

33
import (
4+
"bot/pkg/infra"
45
"strings"
56

67
"github.com/bwmarrin/discordgo"
@@ -64,3 +65,39 @@ func (b *Bot) messageCreate(session *discordgo.Session, message *discordgo.Messa
6465
command.Handler(session, message, channel, b)
6566

6667
}
68+
69+
func (b *Bot) messageReactionAdd(session *discordgo.Session, message *discordgo.MessageReactionAdd) {
70+
// Ignore all messages created by the bot itself
71+
// This isn't required in this specific example but it's a good practice.
72+
if message.UserID == session.State.User.ID {
73+
return
74+
}
75+
76+
msg, err := channelMessageF(session, message.ChannelID, message.MessageID)
77+
if err != nil {
78+
infra.Logger.Error().Err(err).Msg("get channel message")
79+
80+
return
81+
}
82+
83+
if !strings.HasPrefix(msg.Content, pollPrefix) {
84+
return
85+
}
86+
87+
// remove emoji if not one of poll emojis
88+
// TODO: need a better way to determine if the reaction emoji is one of the poll emojis. This code does not consider that the question itself may contain an emoji and that emoji would be allowed to be used in the reactions
89+
if !strings.Contains(msg.Content, message.Emoji.Name) {
90+
_ = messageReactionRemoveF(session, message.ChannelID, message.MessageID, message.Emoji.Name, message.UserID)
91+
92+
return
93+
}
94+
95+
for _, reaction := range msg.Reactions {
96+
if reaction.Emoji.Name == message.MessageReaction.Emoji.Name {
97+
continue
98+
}
99+
100+
_ = messageReactionRemoveF(session, message.ChannelID, message.MessageID, reaction.Emoji.Name, message.UserID)
101+
}
102+
103+
}

pkg/strs/strings.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package strs
2+
3+
// TODO: there is probably a simplification in the logic in this method AllBetweenPattern. We unfortunately can't use a regular expression since go does not support before text matching (?=re) https://github.com/google/re2/wiki/Syntax https://github.com/google/re2/wiki/WhyRE2
4+
func AllBetweenPattern(str, pattern string) []string {
5+
stringsMatched := []string{}
6+
7+
matchingPatternIdx := 0
8+
patternMatched := false
9+
10+
currentMatch := ""
11+
12+
for _, char := range str {
13+
if byte(char) == pattern[matchingPatternIdx] {
14+
matchingPatternIdx++
15+
16+
if !patternMatched && matchingPatternIdx == len(pattern) {
17+
patternMatched = true
18+
matchingPatternIdx = 0
19+
continue
20+
}
21+
}
22+
23+
if patternMatched {
24+
currentMatch += string(char)
25+
26+
if matchingPatternIdx == len(pattern) {
27+
patternMatched = false
28+
matchingPatternIdx = 0
29+
30+
matchedStr := currentMatch[:len(currentMatch)-len(pattern)]
31+
stringsMatched = append(stringsMatched, matchedStr)
32+
currentMatch = ""
33+
}
34+
}
35+
36+
}
37+
38+
return stringsMatched
39+
}

pkg/strs/strings_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package strs
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestAllBetweenPattern(t *testing.T) {
9+
type args struct {
10+
s string
11+
pattern string
12+
}
13+
tests := []struct {
14+
name string
15+
args args
16+
want []string
17+
}{
18+
{
19+
name: "no match",
20+
args: args{
21+
s: "abcsome text to extractabc",
22+
pattern: "abcd",
23+
},
24+
want: []string{},
25+
},
26+
{
27+
name: "empty string between pattern",
28+
args: args{
29+
s: "abcabcabc",
30+
pattern: "abc",
31+
},
32+
want: []string{""},
33+
},
34+
{
35+
name: "multi string pattern single match",
36+
args: args{
37+
s: "abcsome text to extractabc",
38+
pattern: "abc",
39+
},
40+
want: []string{"some text to extract"},
41+
},
42+
{
43+
name: "multi string pattern multi match",
44+
args: args{
45+
s: "abcsome text to extractabcabcsome other textabc",
46+
pattern: "abc",
47+
},
48+
want: []string{"some text to extract", "some other text"},
49+
},
50+
}
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
got := AllBetweenPattern(tt.args.s, tt.args.pattern)
54+
if !reflect.DeepEqual(got, tt.want) {
55+
t.Errorf("AllBetweenPattern() = %v, want %v", got, tt.want)
56+
}
57+
})
58+
}
59+
}

0 commit comments

Comments
 (0)