Skip to content

Commit

Permalink
Add reaction handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Steven Arnott committed Oct 23, 2024
1 parent dc4cfeb commit 6ed7ff3
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 14 deletions.
12 changes: 7 additions & 5 deletions core/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ RuleSearch:
// Only check active rules.
if rule.Active {
// Init some variables for use below
processedInput, hit := getProccessedInputAndHitValue(message.Input, rule.Respond, rule.Hear)
processedInput, hit := getProccessedInputAndHitValue(message.Input, rule.Respond, rule.Hear, message.Reaction, rule.ReactionMatch)
// Determine what service we are processing the rule for
switch message.Service {
case models.MsgServiceChat, models.MsgServiceCLI:
Expand All @@ -63,12 +63,14 @@ RuleSearch:
}

// getProccessedInputAndHitValue gets the processed input from the message input and the true/false if it was a successfully hit rule.
func getProccessedInputAndHitValue(messageInput, ruleRespondValue, ruleHearValue string) (string, bool) {
func getProccessedInputAndHitValue(messageInput, ruleRespondValue, ruleHearValue, messageReaction, ruleReactionMatch string) (string, bool) {
processedInput, hit := "", false
if ruleRespondValue != "" {
processedInput, hit = utils.Match(ruleRespondValue, messageInput, true)
} else if ruleHearValue != "" { // Are we listening to everything?
_, hit = utils.Match(ruleHearValue, messageInput, false)
} else if ruleReactionMatch != "" {
processedInput, hit = utils.Match(ruleReactionMatch, messageReaction, false)
}

return processedInput, hit
Expand All @@ -80,8 +82,8 @@ func getProccessedInputAndHitValue(messageInput, ruleRespondValue, ruleHearValue
func handleChatServiceRule(outputMsgs chan<- models.Message, message models.Message, hitRule chan<- models.Rule, rule models.Rule, processedInput string, hit bool, bot *models.Bot) (bool, bool) {
match, stopSearch := false, false

if rule.Respond != "" || rule.Hear != "" {
// You can only use 'respond' OR 'hear'
if rule.Respond != "" || rule.Hear != "" || rule.ReactionMatch != "" {
// You can only use 'respond', 'hear', or 'reaction match'
if rule.Respond != "" && rule.Hear != "" {
log.Debug().Msgf("rule %#q has both 'hear' and 'match' or 'respond' defined. please choose one or the other", rule.Name)
}
Expand Down Expand Up @@ -231,7 +233,7 @@ func isValidHitChatRule(message *models.Message, rule models.Rule, processedInpu
}

// If this wasn't a 'hear' rule, handle the args
if rule.Hear == "" {
if rule.Hear == "" && rule.ReactionMatch == "" {
// Get all the args that the message sender supplied
args := utils.RuleArgTokenizer(processedInput)

Expand Down
19 changes: 11 additions & 8 deletions core/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,9 +584,11 @@ func TestUpdateReaction(t *testing.T) {

func Test_getProccessedInputAndHitValue(t *testing.T) {
type args struct {
messageInput string
ruleRespondValue string
ruleHearValue string
messageInput string
ruleRespondValue string
ruleHearValue string
messageReaction string
ruleReactionMatch string
}

tests := []struct {
Expand All @@ -595,15 +597,16 @@ func Test_getProccessedInputAndHitValue(t *testing.T) {
want string
want1 bool
}{
{"hit", args{"hello foo", "hello", "hello"}, "foo", true},
{"hit no hear value", args{"hello foo", "hello", ""}, "foo", true},
{"hit no respond value - drops args", args{"hello foo", "", "hello"}, "", true},
{"no match", args{"hello foo", "", ""}, "", false},
{"hit", args{"hello foo", "hello", "hello", "", ""}, "foo", true},
{"hit no hear value", args{"hello foo", "hello", "", "", ""}, "foo", true},
{"hit no respond value - drops args", args{"hello foo", "", "hello", "", ""}, "", true},
{"no match", args{"hello foo", "", "", "", ""}, "", false},
{"hit reaction", args{"", "", "", ":hello:", ":hello:"}, ":hello:", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := getProccessedInputAndHitValue(tt.args.messageInput, tt.args.ruleRespondValue, tt.args.ruleHearValue)
got, got1 := getProccessedInputAndHitValue(tt.args.messageInput, tt.args.ruleRespondValue, tt.args.ruleHearValue, tt.args.messageReaction, tt.args.ruleReactionMatch)
if got != tt.want {
t.Errorf("getProccessedInputAndHitValue() got = %v, want %v", got, tt.want)
}
Expand Down
2 changes: 2 additions & 0 deletions models/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type Message struct {
ChannelName string
Input string
Output string
ReactionAction string
Reaction string
Error string
Timestamp string
ThreadID string
Expand Down
1 change: 1 addition & 0 deletions models/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Rule struct {
Name string `mapstructure:"name" binding:"required"`
Respond string `mapstructure:"respond" binding:"omitempty"`
Hear string `mapstructure:"hear" binding:"omitempty"`
ReactionMatch string `mapstructure:"reaction_match" binding:"omitempty"`
Schedule string `mapstructure:"schedule"`
Args []string `mapstructure:"args" binding:"required"`
DirectMessageOnly bool `mapstructure:"direct_message_only" binding:"required"`
Expand Down
131 changes: 130 additions & 1 deletion remote/slack/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,75 @@ func populateMessage(message models.Message, msgType models.MessageType, channel
}
}

// populateMessage - populates the 'Message' object to be passed on for processing/sending.
func populateReaction(message models.Message, msgType models.MessageType, channel, action, reaction, timeStamp, link string, user *slack.User, bot *models.Bot) models.Message {
switch msgType {
case models.MsgTypeDirect, models.MsgTypeChannel, models.MsgTypePrivateChannel:
// Populate message attributes
message.Type = msgType
message.Service = models.MsgServiceChat
message.ChannelID = channel
message.ReactionAction = action
message.Reaction = reaction
message.Input = ""
message.Output = ""
message.Timestamp = timeStamp
message.SourceLink = link

// If the message read was not a dm, get the name of the channel it came from
if msgType != models.MsgTypeDirect {
name, ok := findKey(bot.Rooms, channel)
if !ok {
log.Error().Msgf("could not find name of channel %#q", channel)
}

message.ChannelName = name
}

message.Vars["_reaction.action"] = action
message.Vars["_reaction"] = reaction
// make channel variables available
message.Vars["_channel.id"] = message.ChannelID
message.Vars["_channel.name"] = message.ChannelName // will be empty if it came via DM

// make link to trigger message available
message.Vars["_source.link"] = message.SourceLink

// make timestamp information available
message.Vars["_source.timestamp"] = timeStamp

// Populate message with user information (i.e. who sent the message)
// These will be accessible on rules via ${_user.email}, ${_user.id}, etc.
if user != nil { // nil user implies a message from an api/bot (i.e. not an actual user)
message.Vars["_user.id"] = user.ID
message.Vars["_user.teamid"] = user.TeamID
message.Vars["_user.name"] = user.Name
message.Vars["_user.color"] = user.Color
message.Vars["_user.realname"] = user.RealName
message.Vars["_user.tz"] = user.TZ
message.Vars["_user.tzlabel"] = user.TZLabel
message.Vars["_user.tzoffset"] = strconv.Itoa(user.TZOffset)
message.Vars["_user.firstname"] = user.Profile.FirstName
message.Vars["_user.lastname"] = user.Profile.LastName
message.Vars["_user.realnamenormalized"] = user.Profile.RealNameNormalized
message.Vars["_user.displayname"] = user.Profile.DisplayName
message.Vars["_user.displaynamenormalized"] = user.Profile.DisplayNameNormalized
message.Vars["_user.email"] = user.Profile.Email
message.Vars["_user.skype"] = user.Profile.Skype
message.Vars["_user.phone"] = user.Profile.Phone
message.Vars["_user.title"] = user.Profile.Title
message.Vars["_user.statustext"] = user.Profile.StatusText
message.Vars["_user.statusemoji"] = user.Profile.StatusEmoji
message.Vars["_user.team"] = user.Profile.Team
}

return message
default:
log.Debug().Msgf("read message of unsupported type '%T' - unable to populate message attributes", msgType)
return message
}
}

// readFromEventsAPI utilizes the Slack API client to read event-based messages.
// This method of reading is preferred over the RTM method.
func readFromEventsAPI(api *slack.Client, vToken string, inputMsgs chan<- models.Message, bot *models.Bot) {
Expand Down Expand Up @@ -509,7 +578,7 @@ func readFromSocketMode(sm *slack.Client, inputMsgs chan<- models.Message, bot *
innerEvent := eventsAPIEvent.InnerEvent

switch ev := innerEvent.Data.(type) {
case *slackevents.AppMentionEvent, *slackevents.ReactionAddedEvent:
case *slackevents.AppMentionEvent:
continue
case *slackevents.MessageEvent:
senderID := ev.User
Expand Down Expand Up @@ -564,6 +633,66 @@ func readFromSocketMode(sm *slack.Client, inputMsgs chan<- models.Message, bot *

inputMsgs <- populateMessage(models.NewMessage(), msgType, channel, text, timestamp, threadTimestamp, link, mentioned, user, bot)
}
case *slackevents.ReactionAddedEvent:
senderID := ev.User

if senderID != "" && bot.ID != senderID {
channel := ev.Item.Channel

// determine the message type
msgType, err := getMessageType(channel)
if err != nil {
log.Error().Msg(err.Error())
}

// get information on the user
user, err := sm.GetUserInfo(senderID)
if err != nil {
log.Error().Msgf("did not get slack user info: %s", err.Error())
}

timestamp := ev.Item.Timestamp

reaction := ev.Reaction

// get the link to the message, will be empty string if there's an error
link, err := sm.GetPermalink(&slack.PermalinkParameters{Channel: channel, Ts: timestamp})
if err != nil {
log.Error().Msgf("unable to retrieve link to message: %s", err.Error())
}

inputMsgs <- populateReaction(models.NewMessage(), msgType, channel, "added", reaction, timestamp, link, user, bot)
}
case *slackevents.ReactionRemovedEvent:
senderID := ev.User

if senderID != "" && bot.ID != senderID {
channel := ev.Item.Channel

// determine the message type
msgType, err := getMessageType(channel)
if err != nil {
log.Error().Msg(err.Error())
}

// get information on the user
user, err := sm.GetUserInfo(senderID)
if err != nil {
log.Error().Msgf("did not get slack user info: %s", err.Error())
}

timestamp := ev.Item.Timestamp

reaction := ev.Reaction

// get the link to the message, will be empty string if there's an error
link, err := sm.GetPermalink(&slack.PermalinkParameters{Channel: channel, Ts: timestamp})
if err != nil {
log.Error().Msgf("unable to retrieve link to message: %s", err.Error())
}

inputMsgs <- populateReaction(models.NewMessage(), msgType, channel, "removed", reaction, timestamp, link, user, bot)
}
case *slackevents.MemberJoinedChannelEvent:
// limit to our bot
if ev.User == bot.ID {
Expand Down

0 comments on commit 6ed7ff3

Please sign in to comment.