From bff4a1e8d1fbfbd9841154308cd8dea3fc6ca2ce Mon Sep 17 00:00:00 2001 From: songupta Date: Wed, 23 Feb 2022 20:28:11 +0530 Subject: [PATCH 1/5] Added functionality for ReactionAdded Event and GetReactionMessageInfo command - added failed go unit tests --- README.md | 46 +++++++++++++++++++++--- client/slack.go | 85 +++++++++++++++++++++++++++++++++++++++++++- client/slack_test.go | 5 +++ command/util_test.go | 11 ++++++ main.go | 2 ++ main_test.go | 16 +++++++-- 6 files changed, 156 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e505843..3106b78 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ Pack requires go version min. 1.11 The plugin is configured using environment variables: -ENV VAR | Default | Description | Example - ------------------------------- | ------- | ----------------------------------------- | --------------------- -FLYTE_API | - | The API endpoint to use | http://localhost:8080 -FLYTE_SLACK_TOKEN | - | The Slack Bot API token to use | token_abc +| ENV VAR | Default | Description | Example | +|-------------------|---------|--------------------------------|-----------------------| +| FLYTE_API | - | The API endpoint to use | http://localhost:8080 | +| FLYTE_SLACK_TOKEN | - | The Slack Bot API token to use | token_abc | Example `FLYTE_API=http://localhost:8080 FLYTE_SLACK_TOKEN=token_abc ./flyte-slack` @@ -71,11 +71,28 @@ The returned event payload is the same as the input. `SendRichMessageFailed` ```json { - "inputMessage": { ... }, + "inputMessage": { + }, "error": "..." } ``` + +### GetReactionMessageInfo +This command would retrieve the message text of reaction sent by a user. + +``` +{ "count": 50 , //default value is 100 mandatory + "message": "" , + "threadTimestamp":"...", + "reactionUser":"...", // mandatory list of reactions for a user + "channelId": "...", + "threadTimestamp":"..." + } +``` + + + ## Events ### ReceivedMessage @@ -95,6 +112,25 @@ The returned event payload is the same as the input. "message": "..." } +### ReactionAdded + { + + "type":"reaction_added", + "user":"...", //user that adds the reaction + "itemUser":"...", //user that writes the message, file etc. + "item": { + "type" :"message", + "channel" :"...", + "file" :"", + "fileComment" :"", + "timestamp" :"..." + }, + "reaction" :"...", //value of the reaction + "EventTimestamp" :"..." + } + + + ## Example Flows The following flow will allow you to type any message into a Slack channel where this pack is diff --git a/client/slack.go b/client/slack.go index f507a1b..081807d 100644 --- a/client/slack.go +++ b/client/slack.go @@ -31,6 +31,7 @@ type client interface { SendMessage(message *slack.OutgoingMessage) PostMessage(channel string, opts ...slack.MsgOption) (string, string, error) GetConversations(params *slack.GetConversationsParameters) (channels []slack.Channel, nextCursor string, err error) + ListReactions(params slack.ListReactionsParameters) ([]slack.ReactedItem, *slack.Paging, error) } // our slack implementation makes consistent use of channel id @@ -41,6 +42,7 @@ type Slack interface { // GetConversations is a heavy call used to fetch data about all channels in a workspace // intended to be cached, not called each time this is needed GetConversations() ([]types.Conversation, error) + GetReactionMessageText(count int, user string, channelId, threadTimestamp string) (text string) } type slackClient struct { @@ -69,7 +71,7 @@ func NewSlack(token string) Slack { const ( getConversationsLimit = 1000 // max 1000 - excludeArchived = true + excludeArchived = true ) func (sl *slackClient) GetConversations() ([]types.Conversation, error) { @@ -147,6 +149,20 @@ func (sl *slackClient) handleMessageEvents() { continue } sl.incomingMessages <- toFlyteMessageEvent(v, u) + + case *slack.ReactionAddedEvent: + logger.Debugf("received reaction event for type = %v", v) + u, err := sl.client.GetUserInfo(v.User) + + if err != nil { + logger.Errorf("cannot get info about user=%s: %v", v.User, err) + continue + } + itemuser, err := sl.client.GetUserInfo(v.ItemUser) + if err != nil { + logger.Errorf("cannot get info about item user=%v: %v", v.ItemUser, err) + } + sl.incomingMessages <- toFlyteReactionAddedEvent(v, u, itemuser) } } } @@ -208,3 +224,70 @@ func newUser(u *slack.User) user { LastName: u.Profile.LastName, } } + +func newReactionEvent(e *slack.ReactionAddedEvent, u *slack.User, itemuser *slack.User) reactionAddedEvent { + return reactionAddedEvent{ + ReactionUser: newUser(u), + ReactionName: e.Reaction, + EventTimestamp: e.EventTimestamp, + ItemTimestamp: e.Item.Timestamp, + ItemType: e.Item.Type, + ChannelId: e.Item.Channel, + ItemUser: newUser(itemuser), + } + +} + +type reactionAddedEvent struct { + ReactionUser user `json:"user"` + ReactionName string `json:"reaction"` + EventTimestamp string `json:"eventTimestamp"` + ItemType string `json:"type"` + ItemTimestamp string `json:"itemTimestamp"` + ItemUser user `json:"itemUser"` + ChannelId string `json:"channelId"` +} + +func toFlyteReactionAddedEvent(event *slack.ReactionAddedEvent, user *slack.User, itemuser *slack.User) flyte.Event { + + return flyte.Event{ + EventDef: flyte.EventDef{Name: "ReactionAdded"}, + Payload: newReactionEvent(event, user, itemuser), + } +} + +func (sl *slackClient) GetReactionMessageText(count int, user string, channelId, threadTimestamp string) (text string) { + params := slack.ListReactionsParameters{ + Count: count, + User: user, + } + logger.Debugf("Count = %v user = %s channel id= %v threadTimestamp= %v", count, user, channelId, threadTimestamp) + + reaction, paging, err := sl.client.ListReactions(params) + if err != nil { + logger.Debugf("Error = %v", err) + } + + for i := range reaction { + if reaction[i].Type == "message" { + if reaction[i].Channel == channelId { + if reaction[i].Message.Timestamp == threadTimestamp { + logger.Debugf("reaction match found added: Value of Type = %v , channel = %v Msg Timestamp = %v , Text = %v", + reaction[i].Type, reaction[i].Channel, reaction[i].Message.ThreadTimestamp, + reaction[i].Message.Text) + return reaction[i].Message.Text + } else { + logger.Debugf("reaction match not found with provided Timestamp") + } + + } else { + logger.Debugf("reaction match not found with provided Channel") + } + + } + } + + logger.Debugf("Value of paging = %v", paging) + return "" + +} diff --git a/client/slack_test.go b/client/slack_test.go index 47381f4..de8afbd 100644 --- a/client/slack_test.go +++ b/client/slack_test.go @@ -321,3 +321,8 @@ func (m *MockClient) PostMessage(channel string, opts ...slack.MsgOption) (strin func (m *MockClient) GetConversations(params *slack.GetConversationsParameters) (channels []slack.Channel, nextCursor string, err error) { return nil, "", err } +func (m *MockClient) ListReactions(params slack.ListReactionsParameters) ([]slack.ReactedItem, *slack.Paging, error) { + params = params + //TODO mock the list reactions + return nil, nil, nil +} diff --git a/command/util_test.go b/command/util_test.go index f104416..1e7bc0c 100644 --- a/command/util_test.go +++ b/command/util_test.go @@ -49,3 +49,14 @@ func (m *MockSlack) IncomingMessages() <-chan flyte.Event { func (m *MockSlack) GetConversations() ([]types.Conversation, error) { return []types.Conversation(nil), nil } + +func (m *MockSlack) GetReactionMessageText(count int, user string, channelId, threadTimestamp string) (text string) { + //TODO Mock this test func + count = 50 + channelId = "CXXXXX" + threadTimestamp = "765523.455667" + if user == "UXXXXX" { + return "This is sample test reaction message for user" + } + return "" +} diff --git a/main.go b/main.go index 8a4e522..c5cd913 100644 --- a/main.go +++ b/main.go @@ -79,9 +79,11 @@ func GetPackDef(slack client.Slack, cache cache.Cache) flyte.PackDef { command.SendMessage(slack), command.SendRichMessage(slack), command.GetChannelInfo(slack, cache), + command.GetReactionMessageInfo(slack), }, EventDefs: []flyte.EventDef{ {Name: "ReceivedMessage"}, + {Name: "ReactionAdded"}, }, } } diff --git a/main_test.go b/main_test.go index 3aa84f3..3f80b6b 100644 --- a/main_test.go +++ b/main_test.go @@ -32,15 +32,17 @@ func TestPackDefinitionIsPopulated(t *testing.T) { assert.Equal(t, "Slack", packDef.Name) assert.Equal(t, "https://github.com/ExpediaGroup/flyte-slack/blob/master/README.md", packDef.HelpURL.String()) require.Equal(t, 0, len(packDef.Labels)) - require.Equal(t, 3, len(packDef.Commands)) - require.Equal(t, 1, len(packDef.EventDefs)) + require.Equal(t, 4, len(packDef.Commands)) + require.Equal(t, 2, len(packDef.EventDefs)) } // --- dummy Slack implementation --- type DummySlack struct{} -func (DummySlack) SendMessage(message, channelId, threadTimestamp string) {} +func (DummySlack) SendMessage(message, channelId, threadTimestamp string) { + +} func (DummySlack) SendRichMessage(client.RichMessage) (string, string, error) { return "", "", nil } @@ -51,3 +53,11 @@ func (DummySlack) IncomingMessages() <-chan flyte.Event { func (DummySlack) GetConversations() ([]types.Conversation, error) { return []types.Conversation{}, nil } + +func (s DummySlack) GetReactionMessageText(count int, user string, channelId, threadTimestamp string) (text string) { + count = count + user = user + channelId = channelId + threadTimestamp = threadTimestamp + return "" +} From de007266ce13ac57c9f25c87c5f8fccc2fa5f90b Mon Sep 17 00:00:00 2001 From: songupta Date: Wed, 23 Feb 2022 20:31:20 +0530 Subject: [PATCH 2/5] Added functionality for ReactionAdded Event and GetReactionMessageInfo command - added reaction files --- command/reaction_test.go | 90 ++++++++++++++++++++++++++++++++++++++++ command/reactions.go | 85 +++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 command/reaction_test.go create mode 100644 command/reactions.go diff --git a/command/reaction_test.go b/command/reaction_test.go new file mode 100644 index 0000000..f29dd4c --- /dev/null +++ b/command/reaction_test.go @@ -0,0 +1,90 @@ +/* +Copyright (C) 2018 Expedia Group. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package command + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +var ReactionMockSlack *MockSlack + +func TestGetReactionListCommandIsPopulated(t *testing.T) { + + command := GetReactionMessageInfo(ReactionMockSlack) + + assert.Equal(t, "GetReactionMessageInfo", command.Name) + require.Equal(t, 2, len(command.OutputEvents)) + assert.Equal(t, "GetReactionMessageInfoSuccess", command.OutputEvents[0].Name) + assert.Equal(t, "GetReactionMessageInfoFailed", command.OutputEvents[1].Name) +} + +func TestGetReactionListReturnsGetReactionListSuccess(t *testing.T) { + + BeforeMessage() + defer AfterMessage() + + handler := GetReactionMessageInfo(ReactionMockSlack).Handler + event := handler([]byte(`{"count": 50 , + "message": "" , + "threadTimestamp":"1645441176.871569", + "reactionUser":"UXXXXXX", + "channelId": "CXXXXXXX", + "threadTimestamp":"1645441176.871569" + }`)) + output := event.Payload.(GetReactionMessageInfoOutput) + assert.Equal(t, "GetReactionMessageInfoSuccess", event.EventDef.Name) + assert.Equal(t, "", output.Message) + assert.Equal(t, "CXXXXXXX", output.ChannelId) +} + +func TestGetReactionListReturnsGetReactionMessageInfoFailedMissingTimestamp(t *testing.T) { + + BeforeMessage() + defer AfterMessage() + + handler := GetReactionMessageInfo(ReactionMockSlack).Handler + event := handler([]byte(`{"count": 50 , + "message": "" , + "reactionUser":"UXXXXXX", + "channelId": "CXXXXXXX", + "threadTimestamp":"" + }`)) + output := event.Payload.(GetReactionMessageInfoFailed) + output = output + assert.Equal(t, "GetReactionMessageInfoFailed", event.EventDef.Name) + assert.Equal(t, "missing Message Timestamp field", output.Error) +} + +func TestGetReactionListReturnsGetReactionMessageInfoFailedMissingReactionUser(t *testing.T) { + + BeforeMessage() + defer AfterMessage() + + handler := GetReactionMessageInfo(ReactionMockSlack).Handler + event := handler([]byte(`{"count": 50 , + "message": "" , + "reactionUser":"", + "channelId": "CXXXXXXX", + "threadTimestamp":"213.4445" + }`)) + output := event.Payload.(GetReactionMessageInfoFailed) + output = output + assert.Equal(t, "GetReactionMessageInfoFailed", event.EventDef.Name) + assert.Equal(t, "missing user id field", output.Error) +} diff --git a/command/reactions.go b/command/reactions.go new file mode 100644 index 0000000..a24c7ce --- /dev/null +++ b/command/reactions.go @@ -0,0 +1,85 @@ +package command + +import ( + "encoding/json" + "fmt" + "github.com/ExpediaGroup/flyte-slack/client" + "github.com/HotelsDotCom/flyte-client/flyte" + "strings" +) + +var ( + getReactionMessageInfoEventDef = flyte.EventDef{Name: "GetReactionMessageInfoSuccess"} + getReactionMessageInfoFailedEventDef = flyte.EventDef{Name: "GetReactionMessageInfoFailed"} +) + +type GetReactionMessageInfoInput struct { + Count int `json:"count"` + Message string `json:"message"` + ThreadTimestamp string `json:"threadTimestamp"` + User string `json:"reactionUser"` + ChannelId string `json:"channelId"` + ItemUser string `json:"itemUser"` +} + +type GetReactionMessageInfoOutput struct { + GetReactionMessageInfoInput +} + +type GetReactionMessageInfoFailed struct { + GetReactionMessageInfoOutput + Error string `json:"error"` +} + +func GetReactionMessageInfo(slack client.Slack) flyte.Command { + + return flyte.Command{ + Name: "GetReactionMessageInfo", + OutputEvents: []flyte.EventDef{getReactionMessageInfoEventDef, getReactionMessageInfoFailedEventDef}, + Handler: getReactionMessageInfoHandler(slack), + } +} + +func getReactionMessageInfoHandler(slack client.Slack) func(json.RawMessage) flyte.Event { + + return func(rawInput json.RawMessage) flyte.Event { + input := GetReactionMessageInfoInput{} + if err := json.Unmarshal(rawInput, &input); err != nil { + return flyte.NewFatalEvent(fmt.Sprintf("input is not valid: %v", err)) + } + + errorMessages := []string{} + if input.ThreadTimestamp == "" { + errorMessages = append(errorMessages, "missing Message Timestamp field") + } + if input.ChannelId == "" { + errorMessages = append(errorMessages, "missing channel id field") + } + if input.User == "" { + errorMessages = append(errorMessages, "missing user id field") + } + if len(errorMessages) != 0 { + return getReactionMessageFailedEvent(input.Message, input.ChannelId, strings.Join(errorMessages, ", ")) + } + + issueSummary := slack.GetReactionMessageText(input.Count, input.User, input.ChannelId, input.ThreadTimestamp) + return getReactionMessageSuccessInfoEvent(issueSummary, input.ChannelId) + } +} + +func getReactionMessageSuccessInfoEvent(message, channelId string) flyte.Event { + + return flyte.Event{ + EventDef: getReactionMessageInfoEventDef, + Payload: GetReactionMessageInfoOutput{GetReactionMessageInfoInput: GetReactionMessageInfoInput{Message: message, ChannelId: channelId}}, + } +} + +func getReactionMessageFailedEvent(message, channelId string, err string) flyte.Event { + + output := GetReactionMessageInfoOutput{GetReactionMessageInfoInput{Message: message, ChannelId: channelId}} + return flyte.Event{ + EventDef: getReactionMessageInfoFailedEventDef, + Payload: GetReactionMessageInfoFailed{GetReactionMessageInfoOutput: output, Error: err}, + } +} From 742d1ea6c7eb07dad6502476c9a66e883a5fd8f1 Mon Sep 17 00:00:00 2001 From: songupta Date: Wed, 23 Feb 2022 20:40:04 +0530 Subject: [PATCH 3/5] Added functionality for ReactionAdded Event and GetReactionMessageInfo command --- client/slack.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/slack.go b/client/slack.go index 081807d..89c44b1 100644 --- a/client/slack.go +++ b/client/slack.go @@ -161,6 +161,7 @@ func (sl *slackClient) handleMessageEvents() { itemuser, err := sl.client.GetUserInfo(v.ItemUser) if err != nil { logger.Errorf("cannot get info about item user=%v: %v", v.ItemUser, err) + continue } sl.incomingMessages <- toFlyteReactionAddedEvent(v, u, itemuser) } From 66ddeabaf56d48706ca39b15386743a3b65cbda4 Mon Sep 17 00:00:00 2001 From: songupta7 <97512350+songupta7@users.noreply.github.com> Date: Wed, 20 Apr 2022 12:38:22 +0530 Subject: [PATCH 4/5] Update README.md Co-authored-by: pamelin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3106b78..b4e8263 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ This command would retrieve the message text of reaction sent by a user. "timestamp" :"..." }, "reaction" :"...", //value of the reaction - "EventTimestamp" :"..." + "eventTimestamp" :"..." } From 3aed6510129b30728be8a5650fc2597d38054e46 Mon Sep 17 00:00:00 2001 From: songupta7 <97512350+songupta7@users.noreply.github.com> Date: Wed, 20 Apr 2022 12:38:45 +0530 Subject: [PATCH 5/5] Update README.md Co-authored-by: pamelin --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b4e8263..abaf720 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,6 @@ This command would retrieve the message text of reaction sent by a user. ### ReactionAdded { - "type":"reaction_added", "user":"...", //user that adds the reaction "itemUser":"...", //user that writes the message, file etc.