diff --git a/examples/reactions/reactions.go b/examples/reactions/reactions.go index 122dc2d74..89abe06e3 100644 --- a/examples/reactions/reactions.go +++ b/examples/reactions/reactions.go @@ -10,23 +10,21 @@ import ( func main() { debug := flag.Bool("debug", false, "Show JSON output") + channelID := flag.String("channel", "", "Channel ID (required)") flag.Parse() // Get token from environment variable - apiToken := os.Getenv("SLACK_BOT_TOKEN") + apiToken := os.Getenv("SLACK_USER_TOKEN") if apiToken == "" { - fmt.Println("SLACK_BOT_TOKEN environment variable is required") + fmt.Println("SLACK_USER_TOKEN environment variable is required") os.Exit(1) } api := slack.New(apiToken, slack.OptionDebug(*debug)) var ( - postAsUserName string - postAsUserID string - postToUserName string - postToUserID string - postToChannelID string + postAsUserName string + postAsUserID string ) // Find the user to post as. @@ -40,29 +38,19 @@ func main() { postAsUserName = authTest.User postAsUserID = authTest.UserID - // Posting to DM with self causes a conversation with slackbot. - postToUserName = authTest.User - postToUserID = authTest.UserID - - // Find the channel. - channel, _, _, err := api.OpenConversation(&slack.OpenConversationParameters{ChannelID: postToUserID}) - if err != nil { - fmt.Printf("Error opening IM: %s\n", err) - return - } - postToChannelID = channel.ID - - fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID) + fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, *channelID) // Post a message. - channelID, timestamp, err := api.PostMessage(postToChannelID, slack.MsgOptionText("Is this any good?", false)) + _, timestamp, err := api.PostMessage(*channelID, slack.MsgOptionText("Is this any good?", false)) if err != nil { fmt.Printf("Error posting message: %s\n", err) return } - // Grab a reference to the message. - msgRef := slack.NewRefToMessage(channelID, timestamp) + // // Grab a reference to the message. + msgRef := slack.NewRefToMessage(*channelID, timestamp) + + fmt.Printf("Adding reaction to message with reference %v\n", msgRef) // React with :+1: if err = api.AddReaction("+1", msgRef); err != nil { @@ -77,21 +65,27 @@ func main() { } // Get all reactions on the message. - msgReactions, err := api.GetReactions(msgRef, slack.NewGetReactionsParameters()) + msgReactionsResp, err := api.GetReactions(msgRef, slack.NewGetReactionsParameters()) if err != nil { fmt.Printf("Error getting reactions: %s\n", err) return } fmt.Printf("\n") - fmt.Printf("%d reactions to message...\n", len(msgReactions)) - for _, r := range msgReactions { - fmt.Printf(" %d users say %s\n", r.Count, r.Name) + fmt.Printf("%d reactions to message...\n", len(msgReactionsResp.Reactions)) + for _, r := range msgReactionsResp.Reactions { + fmt.Printf(" %d users say %s in channel %s\n", r.Count, r.Name, msgReactionsResp.Item.Channel) } // List all of the users reactions. - listReactions, _, err := api.ListReactions(slack.NewListReactionsParameters()) + listParams := slack.NewListReactionsParameters() + fmt.Printf("Listing reactions with params: User=%q, TeamID=%q, Count=%d, Page=%d, Full=%v\n", + listParams.User, listParams.TeamID, listParams.Count, listParams.Page, listParams.Full) + listReactions, _, err := api.ListReactions(listParams) if err != nil { - fmt.Printf("Error listing reactions: %s\n", err) + fmt.Printf("Error listing reactions: %v\n", err) + if slackErr, ok := err.(slack.SlackErrorResponse); ok { + fmt.Printf(" ResponseMetadata.Messages: %v\n", slackErr.ResponseMetadata.Messages) + } return } fmt.Printf("\n") @@ -111,14 +105,14 @@ func main() { } // Get all reactions on the message. - msgReactions, err = api.GetReactions(msgRef, slack.NewGetReactionsParameters()) + msgReactionsResp, err = api.GetReactions(msgRef, slack.NewGetReactionsParameters()) if err != nil { fmt.Printf("Error getting reactions: %s\n", err) return } fmt.Printf("\n") - fmt.Printf("%d reactions to message after removing cry...\n", len(msgReactions)) - for _, r := range msgReactions { + fmt.Printf("%d reactions to message after removing cry...\n", len(msgReactionsResp.Reactions)) + for _, r := range msgReactionsResp.Reactions { fmt.Printf(" %d users say %s\n", r.Count, r.Name) } } diff --git a/reactions.go b/reactions.go index 240f5ba92..743b22a1f 100644 --- a/reactions.go +++ b/reactions.go @@ -33,29 +33,40 @@ func NewGetReactionsParameters() GetReactionsParameters { } type getReactionsResponseFull struct { - Type string - M struct { - Reactions []ItemReaction + Type string + Channel string `json:"channel,omitempty"` // channel is at the root level for message types + M struct { + *Message // message structure already contains reactions } `json:"message"` F struct { + *File Reactions []ItemReaction } `json:"file"` FC struct { + *Comment Reactions []ItemReaction } `json:"comment"` SlackResponse } -func (res getReactionsResponseFull) extractReactions() []ItemReaction { - switch res.Type { +func (res getReactionsResponseFull) extractReactedItem() ReactedItem { + item := ReactedItem{} + item.Type = res.Type + + switch item.Type { case "message": - return res.M.Reactions + item.Channel = res.Channel + item.Message = res.M.Message + item.Reactions = res.M.Reactions case "file": - return res.F.Reactions + item.File = res.F.File + item.Reactions = res.F.Reactions case "file_comment": - return res.FC.Reactions + item.File = res.F.File + item.Comment = res.FC.Comment + item.Reactions = res.FC.Reactions } - return []ItemReaction{} + return item } const ( @@ -200,15 +211,15 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item return response.Err() } -// GetReactions returns details about the reactions on an item. +// GetReactions returns item and details about the reactions on an item. // For more details, see GetReactionsContext documentation. -func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { +func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) (ReactedItem, error) { return api.GetReactionsContext(context.Background(), item, params) } -// GetReactionsContext returns details about the reactions on an item with a custom context. +// GetReactionsContext returns item and details about the reactions on an item with a custom context. // Slack API docs: https://api.slack.com/methods/reactions.get -func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { +func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) (ReactedItem, error) { values := url.Values{ "token": {api.token}, } @@ -224,20 +235,20 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params if item.Comment != "" { values.Set("file_comment", item.Comment) } - if params.Full != DEFAULT_REACTIONS_FULL { + if params.Full { values.Set("full", strconv.FormatBool(params.Full)) } response := &getReactionsResponseFull{} if err := api.postMethod(ctx, "reactions.get", values, response); err != nil { - return nil, err + return ReactedItem{}, err } if err := response.Err(); err != nil { - return nil, err + return ReactedItem{}, err } - return response.extractReactions(), nil + return response.extractReactedItem(), nil } // ListReactions returns information about the items a user reacted to. @@ -264,7 +275,7 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction if params.Page != DEFAULT_REACTIONS_PAGE { values.Add("page", strconv.Itoa(params.Page)) } - if params.Full != DEFAULT_REACTIONS_FULL { + if params.Full { values.Add("full", strconv.FormatBool(params.Full)) } diff --git a/reactions_test.go b/reactions_test.go index cf1ed42ed..9cec7e565 100644 --- a/reactions_test.go +++ b/reactions_test.go @@ -139,11 +139,11 @@ func TestSlack_GetReactions(t *testing.T) { once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) tests := []struct { - ref ItemRef - params GetReactionsParameters - wantParams map[string]string - json string - wantReactions []ItemReaction + ref ItemRef + params GetReactionsParameters + wantParams map[string]string + json string + wantReactedItem ReactedItem }{ { NewRefToMessage("ChannelID", "123"), @@ -153,24 +153,41 @@ func TestSlack_GetReactions(t *testing.T) { "timestamp": "123", }, `{"ok": true, - "type": "message", - "message": { - "reactions": [ - { - "name": "astonished", - "count": 3, - "users": [ "U1", "U2", "U3" ] - }, - { - "name": "clock1", - "count": 3, - "users": [ "U1", "U2" ] - } - ] - }}`, - []ItemReaction{ - {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}}, - {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}}, + "type": "message", + "channel": "ChannelID", + "message": { + "text": "lorem ipsum dolor sit amet", + "ts": "123", + "user": "U2147483828", + "reactions": [ + { + "name": "astonished", + "count": 3, + "users": [ "U1", "U2", "U3" ] + }, + { + "name": "clock1", + "count": 3, + "users": [ "U1", "U2" ] + } + ] + }}`, + ReactedItem{ + Item: Item{ + Type: "message", + Channel: "ChannelID", + Message: &Message{ + Msg: Msg{ + Text: "lorem ipsum dolor sit amet", + User: "U2147483828", + Timestamp: "123", + }, + }, + }, + Reactions: []ItemReaction{ + {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}}, + {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}}, + }, }, }, { @@ -183,6 +200,19 @@ func TestSlack_GetReactions(t *testing.T) { `{"ok": true, "type": "file", "file": { + "id": "F0A12BCDE", + "created": 1531763342, + "timestamp": 1531763342, + "name": "tedair.gif", + "title": "tedair.gif", + "mimetype": "image/gif", + "filetype": "gif", + "pretty_type": "GIF", + "user": "U012A3BCD", + "editable": false, + "size": 137531, + "mode": "hosted", + "is_external": false, "reactions": [ { "name": "astonished", @@ -196,13 +226,25 @@ func TestSlack_GetReactions(t *testing.T) { } ] }}`, - []ItemReaction{ - {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}}, - {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}}, + ReactedItem{ + Item: Item{ + Type: "file", File: &File{ + Name: "tedair.gif", + ID: "F0A12BCDE", + Created: 1531763342, + Timestamp: 1531763342, + User: "U012A3BCD", + Editable: false, + Size: 137531, + }, + }, + Reactions: []ItemReaction{ + {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}}, + {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}}, + }, }, }, { - NewRefToComment("FileCommentID"), GetReactionsParameters{}, map[string]string{ @@ -210,8 +252,22 @@ func TestSlack_GetReactions(t *testing.T) { }, `{"ok": true, "type": "file_comment", - "file": {}, + "file": { + "id": "F0A12BCDE", + "created": 1531763342, + "timestamp": 1531763342, + "name": "tedair.gif", + "title": "tedair.gif", + "mimetype": "image/gif", + "filetype": "gif", + "pretty_type": "GIF", + "user": "U012A3BCD", + "editable": false, + "size": 137531, + "is_external": false + }, "comment": { + "comment": "lorem ipsum dolor sit amet comment", "reactions": [ { "name": "astonished", @@ -225,9 +281,19 @@ func TestSlack_GetReactions(t *testing.T) { } ] }}`, - []ItemReaction{ - {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}}, - {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}}, + ReactedItem{ + Item: Item{ + Type: "file_comment", File: &File{ + Name: "tedair.gif", + }, + Comment: &Comment{ + Comment: "lorem ipsum dolor sit amet comment", + }, + }, + Reactions: []ItemReaction{ + {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}}, + {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}}, + }, }, }, } @@ -240,12 +306,70 @@ func TestSlack_GetReactions(t *testing.T) { if err != nil { t.Fatalf("%d: Unexpected error: %s", i, err) } - if !reflect.DeepEqual(got, test.wantReactions) { - t.Errorf("%d: Got reaction %#v, want %#v", i, got, test.wantReactions) + if !reflect.DeepEqual(got.Reactions, test.wantReactedItem.Reactions) { + t.Errorf("%d: Got reaction %#v, want %#v", i, got.Reactions, test.wantReactedItem.Reactions) } if !reflect.DeepEqual(rh.gotParams, test.wantParams) { t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams) } + + switch got.Type { + case "message": + if got.Message == nil { + t.Fatalf("%d: Got message %#v, want %#v", i, got.Message, test.wantReactedItem.Message) + } + + if got.Message.Text != test.wantReactedItem.Message.Text { + t.Errorf("%d: Got message text %#v, want %#v", i, got.Message.Text, test.wantReactedItem.Message.Text) + } + if got.Channel != test.wantReactedItem.Channel { + t.Errorf("%d: Got channel %#v, want %#v", i, got.Channel, test.wantReactedItem.Channel) + } + if got.Message.User != test.wantReactedItem.Message.User { + t.Errorf("%d: Got message user %#v, want %#v", i, got.Message.User, test.wantReactedItem.Message.User) + } + if got.Message.Timestamp != test.wantReactedItem.Message.Timestamp { + t.Errorf("%d: Got message timestamp %#v, want %#v", i, got.Message.Timestamp, test.wantReactedItem.Message.Timestamp) + } + case "file": + if got.File == nil { + t.Fatalf("%d: Got file %#v, want %#v", i, got.File, test.wantReactedItem.File) + } + if got.File.Name != test.wantReactedItem.File.Name { + t.Errorf("%d: Got file name %#v, want %#v", i, got.File.Name, test.wantReactedItem.File.Name) + } + if got.File.ID != test.wantReactedItem.File.ID { + t.Errorf("%d: Got file ID %#v, want %#v", i, got.File.ID, test.wantReactedItem.File.ID) + } + if got.File.Created != test.wantReactedItem.File.Created { + t.Errorf("%d: Got file created %#v, want %#v", i, got.File.Created, test.wantReactedItem.File.Created) + } + if got.File.Timestamp != test.wantReactedItem.File.Timestamp { + t.Errorf("%d: Got file timestamp %#v, want %#v", i, got.File.Timestamp, test.wantReactedItem.File.Timestamp) + } + if got.File.User != test.wantReactedItem.File.User { + t.Errorf("%d: Got file user %#v, want %#v", i, got.File.User, test.wantReactedItem.File.User) + } + if got.File.Editable != test.wantReactedItem.File.Editable { + t.Errorf("%d: Got file editable %#v, want %#v", i, got.File.Editable, test.wantReactedItem.File.Editable) + } + if got.File.Size != test.wantReactedItem.File.Size { + t.Errorf("%d: Got file size %#v, want %#v", i, got.File.Size, test.wantReactedItem.File.Size) + } + case "file_comment": + if got.Comment == nil { + t.Fatalf("%d: Got comment %#v, want %#v", i, got.Comment, test.wantReactedItem.Comment) + } + if got.File == nil { + t.Fatalf("%d: Got file %#v, want %#v", i, got.File, test.wantReactedItem.File) + } + if got.File.Name != test.wantReactedItem.File.Name { + t.Errorf("%d: Got file name %#v, want %#v", i, got.File.Name, test.wantReactedItem.File.Name) + } + if got.Comment.Comment != test.wantReactedItem.Comment.Comment { + t.Errorf("%d: Got comment comment %#v, want %#v", i, got.Comment.Comment, test.wantReactedItem.Comment.Comment) + } + } } } diff --git a/slack.go b/slack.go index 756106fe4..10873edfd 100644 --- a/slack.go +++ b/slack.go @@ -53,9 +53,9 @@ type authTestResponseFull struct { AuthTestResponse } -// Client for the slack api. type ParamOption func(*url.Values) +// Client for the slack api. type Client struct { token string appLevelToken string @@ -145,14 +145,14 @@ func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestRespo } // Debugf print a formatted debug line. -func (api *Client) Debugf(format string, v ...interface{}) { +func (api *Client) Debugf(format string, v ...any) { if api.debug { api.log.Output(2, fmt.Sprintf(format, v...)) } } // Debugln print a debug line. -func (api *Client) Debugln(v ...interface{}) { +func (api *Client) Debugln(v ...any) { if api.debug { api.log.Output(2, fmt.Sprintln(v...)) } @@ -164,11 +164,11 @@ func (api *Client) Debug() bool { } // post to a slack web method. -func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf interface{}) error { +func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf any) error { return postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api) } // get a slack web method. -func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf interface{}) error { +func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf any) error { return getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api) }