-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathfingerquoter.go
172 lines (138 loc) · 5 KB
/
fingerquoter.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
package plugins
import (
"fmt"
"github.com/alexandre-normand/slackscot"
"github.com/alexandre-normand/slackscot/actions"
"github.com/alexandre-normand/slackscot/config"
"github.com/alexandre-normand/slackscot/plugin"
"math/rand"
"regexp"
"strconv"
)
const (
channelIDsKey = "channelIDs"
ignoredChannelIDsKey = "ignoredChannelIDs"
frequencyKey = "frequency"
)
const (
// FingerQuoterPluginName holds identifying name for the finger quoter plugin
FingerQuoterPluginName = "fingerQuoter"
)
// FingerQuoter holds the plugin data for the finger quoter plugin
type FingerQuoter struct {
*slackscot.Plugin
channels []string
ignoredChannels []string
frequency int
}
// Regular expressions to find candidate words. They must be at least 5 characters long
// and can include any word character (include hyphen and underscore)
var candidateWordsStarting = regexp.MustCompile("(?:^|\\s)([\\w-]{5,})")
var candidateWordsEnding = regexp.MustCompile("([\\w-]{5,})(?:$|\\s)")
// NewFingerQuoter creates a new instance of the plugin
func NewFingerQuoter(config *config.PluginConfig) (p *slackscot.Plugin, err error) {
if ok := config.IsSet(frequencyKey); !ok {
return nil, fmt.Errorf("Missing %s config key: %s", FingerQuoterPluginName, frequencyKey)
}
f := new(FingerQuoter)
f.channels = config.GetStringSlice(channelIDsKey)
f.ignoredChannels = config.GetStringSlice(ignoredChannelIDsKey)
f.frequency = config.GetInt(frequencyKey)
f.Plugin = plugin.New(FingerQuoterPluginName).
WithHearAction(actions.NewHearAction().
Hidden().
WithMatcher(f.trigger).
WithUsage("just converse").
WithDescription("finger quoter listens to what people say and (sometimes) finger quotes a word").
WithAnswerer(f.fingerQuoteMsg).
Build()).
Build()
return f.Plugin, err
}
func (f *FingerQuoter) trigger(m *slackscot.IncomingMessage) bool {
if !isChannelEnabled(m.Channel, f.channels, f.ignoredChannels) {
return false
}
ts, err := strconv.ParseFloat(m.Timestamp, 64)
if err != nil {
f.Logger.Debugf("[%s] Skipping message [%v] because of error converting timestamp to float: %v\n", FingerQuoterPluginName, m, err)
return false
}
fullTs := ts * 1000000.
// Make the random generator use a seed based on the message id so that we preserve the same matches when messages get updated
randomGen := rand.New(rand.NewSource(int64(fullTs)))
// Determine if we're going to react this time or not
return randomGen.Int31n(int32(f.frequency)) == 0
}
func (f *FingerQuoter) fingerQuoteMsg(m *slackscot.IncomingMessage) *slackscot.Answer {
candidates := findCandidateWords(m.NormalizedText)
if len(candidates) > 0 {
ts, err := strconv.ParseFloat(m.Timestamp, 64)
if err != nil {
f.Logger.Debugf("[%s] Skipping message [%v] because of error converting timestamp to float: %v\n", FingerQuoterPluginName, m, err)
} else {
fullTs := ts * 1000000.
// Make the random generator use a seed based on the message id so that we preserve the same matches when messages get updated
randomGen := rand.New(rand.NewSource(int64(fullTs)))
i := randomGen.Int31n(int32(len(candidates)))
return &slackscot.Answer{Text: fmt.Sprintf("\"%s\"", candidates[i])}
}
}
// Not this time friends, skip it
return nil
}
// findCandidateWords looks at an input string and finds acceptable candidates for finger quoting
func findCandidateWords(t string) (candidates []string) {
matchesStarting := candidateWordsStarting.FindAllStringSubmatch(t, -1)
matchesEnding := candidateWordsEnding.FindAllStringSubmatch(t, -1)
candidatesStarting := getWordMatches(matchesStarting)
candidatesEnding := getWordMatches(matchesEnding)
return intersection(candidatesStarting, candidatesEnding)
}
// getWordMatches returns an array of matching words given a raw array of matches
func getWordMatches(m [][]string) (words []string) {
for _, match := range m {
candidate := match[1]
words = append(words, candidate)
}
return words
}
// intersection returns the common elements present in both a and b
func intersection(a []string, b []string) (intersection []string) {
m := make(map[string]bool)
for _, item := range a {
m[item] = true
}
for _, item := range b {
if _, ok := m[item]; ok {
intersection = append(intersection, item)
}
}
return intersection
}
func isChannelEnabled(channelID string, whitelist []string, ignoredChannels []string) bool {
if isChannelWhiteListed(channelID, whitelist) && !isChannelIgnored(channelID, ignoredChannels) {
return true
}
return false
}
func isChannelIgnored(channelID string, ignoredChannels []string) bool {
for _, c := range ignoredChannels {
if c == channelID {
return true
}
}
return false
}
func isChannelWhiteListed(channelID string, whitelist []string) bool {
// Default to all channels whitelisted if none specified which is either that the element is missing or if the only element is empty string
if len(whitelist) == 0 || (len(whitelist) == 1 && whitelist[0] == "") {
return true
}
for _, c := range whitelist {
if c == channelID {
return true
}
}
return false
}