plugins/fingerquoter.go
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
}