Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -71,11 +71,28 @@ The returned event payload is the same as the input.
`SendRichMessageFailed`
```json
{
"inputMessage": { ... },
"inputMessage": {
},
"error": "..."
}
```


### GetReactionMessageInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to understand use case for this one and what it actually does 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

problem with the previous implementation: JSON standard does not allow such tokens.
To fix this, I have removed the ... token.

Trying to understand use case for this one and what it actually does 🤔

This command would retrieve the message text of reaction sent by a user.

```
{ "count": 50 , //default value is 100 mandatory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure yet what this is doing. What count means?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The count of reactions in list sorted via timestamp. Hope this clarifies.

"message": "" ,
"threadTimestamp":"...",
"reactionUser":"...", // mandatory list of reactions for a user
"channelId": "...",
"threadTimestamp":"..."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

threadTimestamp is mentioned twice 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is typo I would rectify this.

}
```
Comment on lines +84 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tabs/spaces seem to be off on this one. can you align to left with just one tab pelase?




## Events

### ReceivedMessage
Expand All @@ -95,6 +112,24 @@ The returned event payload is the same as the input.
"message": "..."
}

### ReactionAdded
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be wrong according to

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"`
}

There is no such thing as item and users are not simple string but objects/structs in itself

Copy link
Contributor Author

@songupta7 songupta7 Apr 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some reason, which go slack module version are you referring to? I am referring to slack@v0.9.1>websocket_reactions.go contains below definition. Can you please cross check again?

// reactionItem is a lighter-weight item than is returned by the reactions list. type reactionItem struct { Type stringjson:"type"Channel stringjson:"channel,omitempty"File stringjson:"file,omitempty"FileComment stringjson:"file_comment,omitempty"Timestamp stringjson:"ts,omitempty"`
}

type reactionEvent struct {
Type string json:"type"
User string json:"user"
ItemUser string json:"item_user"
Item reactionItem json:"item"
Reaction string json:"reaction"
EventTimestamp string json:"event_ts"
}

// ReactionAddedEvent represents the Reaction added event
type ReactionAddedEvent reactionEvent

// ReactionRemovedEvent represents the Reaction removed event
type ReactionRemovedEvent reactionEvent
`

"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
Expand Down
86 changes: 85 additions & 1 deletion client/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -147,6 +149,21 @@ 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)
continue
}
sl.incomingMessages <- toFlyteReactionAddedEvent(v, u, itemuser)
}
}
}
Expand Down Expand Up @@ -208,3 +225,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 ""

}
5 changes: 5 additions & 0 deletions client/slack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No TODOs please. Please add tests for new functionality.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is still hasn't been dealt with

return nil, nil, nil
}
90 changes: 90 additions & 0 deletions command/reaction_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
85 changes: 85 additions & 0 deletions command/reactions.go
Original file line number Diff line number Diff line change
@@ -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},
}
}
11 changes: 11 additions & 0 deletions command/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
Loading