diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b6e5e47..1f10ae2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > if user.Has2FA != nil && *user.Has2FA { ... } > ``` +- **`ListReactions` now uses cursor-based pagination** — `ListReactionsParameters` replaces + `Count`/`Page` with `Cursor`/`Limit`, and `ListReactions`/`ListReactionsContext` now return + `([]ReactedItem, string, error)` where the string is the next cursor, instead of + `([]ReactedItem, *Paging, error)`. ([#825]) + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewListReactionsParameters() + > params.Count = 100 + > params.Page = 2 + > items, paging, err := api.ListReactions(params) + > + > // After + > params := slack.NewListReactionsParameters() + > params.Limit = 100 + > items, nextCursor, err := api.ListReactions(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + +- **`ListStars`/`GetStarred` now use cursor-based pagination** — `StarsParameters` replaces + `Count`/`Page` with `Cursor`/`Limit` (and adds `TeamID`), and `ListStars`/`ListStarsContext`/ + `GetStarred`/`GetStarredContext` now return `string` (next cursor) instead of `*Paging`. + Slack's `stars.list` API no longer returns `paging` data — only `response_metadata.next_cursor`. + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewStarsParameters() + > params.Count = 100 + > params.Page = 2 + > items, paging, err := api.ListStars(params) + > + > // After + > params := slack.NewStarsParameters() + > params.Limit = 100 + > items, nextCursor, err := api.ListStars(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + +- **`GetAccessLogs` now uses cursor-based pagination** — `AccessLogParameters` replaces + `Count`/`Page` with `Cursor`/`Limit` (and adds `Before`), and `GetAccessLogs`/ + `GetAccessLogsContext` now return `string` (next cursor) instead of `*Paging`. + Slack's `team.accessLogs` API warns `use_cursor_pagination_instead` when using the old + parameters. + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewAccessLogParameters() + > params.Count = 100 + > params.Page = 2 + > logins, paging, err := api.GetAccessLogs(params) + > + > // After + > params := slack.NewAccessLogParameters() + > params.Limit = 100 + > logins, nextCursor, err := api.GetAccessLogs(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + ### Fixed - **`WorkflowButtonBlockElement` missing from `UnmarshalJSON`** — `workflow_button` blocks diff --git a/examples/reactions/reactions.go b/examples/reactions/reactions.go index 89abe06e3..eede3d413 100644 --- a/examples/reactions/reactions.go +++ b/examples/reactions/reactions.go @@ -78,8 +78,8 @@ func main() { // List all of the users reactions. 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) + fmt.Printf("Listing reactions with params: User=%q, TeamID=%q, Cursor=%q, Limit=%d, Full=%v\n", + listParams.User, listParams.TeamID, listParams.Cursor, listParams.Limit, listParams.Full) listReactions, _, err := api.ListReactions(listParams) if err != nil { fmt.Printf("Error listing reactions: %v\n", err) diff --git a/examples/stars/stars.go b/examples/stars/stars.go index cc3f9bacb..72f615b5c 100644 --- a/examples/stars/stars.go +++ b/examples/stars/stars.go @@ -3,27 +3,37 @@ package main import ( "flag" "fmt" + "os" "github.com/slack-go/slack" ) func main() { var ( - apiToken string - debug bool + debug bool + team string ) - flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token") + // Get token from environment variable + apiToken := os.Getenv("SLACK_USER_TOKEN") + if apiToken == "" { + fmt.Println("SLACK_USER_TOKEN environment variable is required") + os.Exit(1) + } + flag.BoolVar(&debug, "debug", false, "Show JSON output") + flag.StringVar(&team, "team", "", "Team ID (required for Enterprise Grid)") flag.Parse() api := slack.New(apiToken, slack.OptionDebug(debug)) - // Get all stars for the usr. + // Get all stars for the user. params := slack.NewStarsParameters() + params.TeamID = team + starredItems, _, err := api.GetStarred(params) if err != nil { - fmt.Printf("Error getting stars: %s\n", err) + fmt.Printf("Error getting stars: %v\n", err) return } for _, s := range starredItems { diff --git a/reactions.go b/reactions.go index 743b22a1f..18befa699 100644 --- a/reactions.go +++ b/reactions.go @@ -70,18 +70,16 @@ func (res getReactionsResponseFull) extractReactedItem() ReactedItem { } const ( - DEFAULT_REACTIONS_USER = "" - DEFAULT_REACTIONS_COUNT = 100 - DEFAULT_REACTIONS_PAGE = 1 - DEFAULT_REACTIONS_FULL = false + DEFAULT_REACTIONS_USER = "" + DEFAULT_REACTIONS_FULL = false ) // ListReactionsParameters is the inputs to find all reactions by a user. type ListReactionsParameters struct { User string TeamID string - Count int - Page int + Cursor string + Limit int Full bool } @@ -89,10 +87,8 @@ type ListReactionsParameters struct { // performed by a user. func NewListReactionsParameters() ListReactionsParameters { return ListReactionsParameters{ - User: DEFAULT_REACTIONS_USER, - Count: DEFAULT_REACTIONS_COUNT, - Page: DEFAULT_REACTIONS_PAGE, - Full: DEFAULT_REACTIONS_FULL, + User: DEFAULT_REACTIONS_USER, + Full: DEFAULT_REACTIONS_FULL, } } @@ -112,8 +108,8 @@ type listReactionsResponseFull struct { Reactions []ItemReaction } `json:"comment"` } - Paging `json:"paging"` SlackResponse + ResponseMetadata `json:"response_metadata"` } func (res listReactionsResponseFull) extractReactedItems() []ReactedItem { @@ -253,13 +249,13 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params // ListReactions returns information about the items a user reacted to. // For more details, see ListReactionsContext documentation. -func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) { +func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, string, error) { return api.ListReactionsContext(context.Background(), params) } // ListReactionsContext returns information about the items a user reacted to with a custom context. // Slack API docs: https://api.slack.com/methods/reactions.list -func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) { +func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, string, error) { values := url.Values{ "token": {api.token}, } @@ -269,11 +265,11 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction if params.TeamID != "" { values.Add("team_id", params.TeamID) } - if params.Count != DEFAULT_REACTIONS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) } - if params.Page != DEFAULT_REACTIONS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } if params.Full { values.Add("full", strconv.FormatBool(params.Full)) @@ -282,12 +278,12 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction response := &listReactionsResponseFull{} err := api.postMethod(ctx, "reactions.list", values, response) if err != nil { - return nil, nil, err + return nil, "", err } if err := response.Err(); err != nil { - return nil, nil, err + return nil, "", err } - return response.extractReactedItems(), &response.Paging, nil + return response.extractReactedItems(), response.ResponseMetadata.Cursor, nil } diff --git a/reactions_test.go b/reactions_test.go index 9cec7e565..57eece5d3 100644 --- a/reactions_test.go +++ b/reactions_test.go @@ -27,12 +27,12 @@ func (rh *reactionsHandler) accumulateFormValue(k string, r *http.Request) { func (rh *reactionsHandler) handler(w http.ResponseWriter, r *http.Request) { rh.accumulateFormValue("channel", r) - rh.accumulateFormValue("count", r) + rh.accumulateFormValue("cursor", r) rh.accumulateFormValue("file", r) rh.accumulateFormValue("file_comment", r) rh.accumulateFormValue("full", r) + rh.accumulateFormValue("limit", r) rh.accumulateFormValue("name", r) - rh.accumulateFormValue("page", r) rh.accumulateFormValue("timestamp", r) rh.accumulateFormValue("user", r) w.Header().Set("Content-Type", "application/json") @@ -429,11 +429,8 @@ func TestSlack_ListReactions(t *testing.T) { } } ], - "paging": { - "count": 100, - "total": 4, - "page": 1, - "pages": 1 + "response_metadata": { + "next_cursor": "dXNlcjpVMDYxTkZUVDI=" }}` want := []ReactedItem{ { @@ -463,17 +460,18 @@ func TestSlack_ListReactions(t *testing.T) { }, } wantParams := map[string]string{ - "user": "User", - "count": "200", - "page": "2", - "full": "true", + "user": "User", + "cursor": "somecursor", + "limit": "200", + "full": "true", } + wantCursor := "dXNlcjpVMDYxTkZUVDI=" params := NewListReactionsParameters() params.User = "User" - params.Count = 200 - params.Page = 2 + params.Cursor = "somecursor" + params.Limit = 200 params.Full = true - got, paging, err := api.ListReactions(params) + got, nextCursor, err := api.ListReactions(params) if err != nil { t.Fatalf("Unexpected error: %s", err) } @@ -490,7 +488,7 @@ func TestSlack_ListReactions(t *testing.T) { if !reflect.DeepEqual(rh.gotParams, wantParams) { t.Errorf("Got params %#v, want %#v", rh.gotParams, wantParams) } - if reflect.DeepEqual(paging, Paging{}) { - t.Errorf("Want paging data, got empty struct") + if nextCursor != wantCursor { + t.Errorf("Got cursor %q, want %q", nextCursor, wantCursor) } } diff --git a/stars.go b/stars.go index 51926854e..0adb28c53 100644 --- a/stars.go +++ b/stars.go @@ -8,31 +8,28 @@ import ( ) const ( - DEFAULT_STARS_USER = "" - DEFAULT_STARS_COUNT = 100 - DEFAULT_STARS_PAGE = 1 + DEFAULT_STARS_USER = "" ) type StarsParameters struct { - User string - Count int - Page int + User string + Cursor string + Limit int + TeamID string } type StarredItem Item type listResponseFull struct { - Items []Item `json:"items"` - Paging `json:"paging"` + Items []Item `json:"items"` SlackResponse + ResponseMetadata `json:"response_metadata"` } // NewStarsParameters initialises StarsParameters with default values func NewStarsParameters() StarsParameters { return StarsParameters{ - User: DEFAULT_STARS_USER, - Count: DEFAULT_STARS_COUNT, - Page: DEFAULT_STARS_PAGE, + User: DEFAULT_STARS_USER, } } @@ -100,37 +97,40 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I // ListStars returns information about the stars a user added. // For more information see the ListStarsContext documentation. -func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) { +func (api *Client) ListStars(params StarsParameters) ([]Item, string, error) { return api.ListStarsContext(context.Background(), params) } // ListStarsContext returns information about the stars a user added with a custom context. // Slack API docs: https://api.slack.com/methods/stars.list -func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) { +func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, string, error) { values := url.Values{ "token": {api.token}, } if params.User != DEFAULT_STARS_USER { values.Add("user", params.User) } - if params.Count != DEFAULT_STARS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Page != DEFAULT_STARS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.TeamID != "" { + values.Add("team_id", params.TeamID) } response := &listResponseFull{} err := api.postMethod(ctx, "stars.list", values, response) if err != nil { - return nil, nil, err + return nil, "", err } if err := response.Err(); err != nil { - return nil, nil, err + return nil, "", err } - return response.Items, &response.Paging, nil + return response.Items, response.ResponseMetadata.Cursor, nil } // GetStarred returns a list of StarredItem items. @@ -139,31 +139,31 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) // be looking at according to what is in the Type: // // for _, item := range items { -// switch c.Type { -// case "file_comment": -// log.Println(c.Comment) -// case "file": -// ... +// switch c.Type { +// case "file_comment": +// log.Println(c.Comment) +// case "file": +// ... // } // // This function still exists to maintain backwards compatibility. // I exposed it as returning []StarredItem, so it shall stay as StarredItem. -func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) { +func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, string, error) { return api.GetStarredContext(context.Background(), params) } // GetStarredContext returns a list of StarredItem items with a custom context // For more details see GetStarred -func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, *Paging, error) { - items, paging, err := api.ListStarsContext(ctx, params) +func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, string, error) { + items, nextCursor, err := api.ListStarsContext(ctx, params) if err != nil { - return nil, nil, err + return nil, "", err } starredItems := make([]StarredItem, len(items)) for i, item := range items { starredItems[i] = StarredItem(item) } - return starredItems, paging, nil + return starredItems, nextCursor, nil } type listResponsePaginated struct { diff --git a/stars_test.go b/stars_test.go index 53f0e28bc..1b53a7a26 100644 --- a/stars_test.go +++ b/stars_test.go @@ -28,10 +28,13 @@ func (sh *starsHandler) accumulateFormValue(k string, r *http.Request) { func (sh *starsHandler) handler(w http.ResponseWriter, r *http.Request) { sh.accumulateFormValue("user", r) sh.accumulateFormValue("count", r) + sh.accumulateFormValue("cursor", r) sh.accumulateFormValue("channel", r) sh.accumulateFormValue("file", r) sh.accumulateFormValue("file_comment", r) + sh.accumulateFormValue("limit", r) sh.accumulateFormValue("page", r) + sh.accumulateFormValue("team_id", r) sh.accumulateFormValue("timestamp", r) w.Header().Set("Content-Type", "application/json") w.Write([]byte(sh.response)) @@ -187,11 +190,8 @@ func TestSlack_ListStars(t *testing.T) { } } ], - "paging": { - "count": 100, - "total": 4, - "page": 1, - "pages": 1 + "response_metadata": { + "next_cursor": "dXNlcjpVMDYxTkZUVDI=" }}` want := []Item{ NewMessageItem("C1", &Message{Msg: Msg{ @@ -209,13 +209,14 @@ func TestSlack_ListStars(t *testing.T) { wantStarred[i] = StarredItem(item) } wantParams := map[string]string{ - "count": "200", - "page": "2", + "cursor": "somecursor", + "limit": "200", } + wantCursor := "dXNlcjpVMDYxTkZUVDI=" params := NewStarsParameters() - params.Count = 200 - params.Page = 2 - got, paging, err := api.ListStars(params) + params.Cursor = "somecursor" + params.Limit = 200 + got, nextCursor, err := api.ListStars(params) if err != nil { t.Fatalf("Unexpected error: %s", err) } @@ -231,11 +232,12 @@ func TestSlack_ListStars(t *testing.T) { if !reflect.DeepEqual(rh.gotParams, wantParams) { t.Errorf("Got params %#v, want %#v", rh.gotParams, wantParams) } - if reflect.DeepEqual(paging, Paging{}) { - t.Errorf("Want paging data, got empty struct") + if nextCursor != wantCursor { + t.Errorf("Got cursor %q, want %q", nextCursor, wantCursor) } // Test GetStarred - gotStarred, paging, err := api.GetStarred(params) + rh.gotParams = make(map[string]string) // reset + gotStarred, nextCursor, err := api.GetStarred(params) if err != nil { t.Fatalf("Unexpected error: %s", err) } @@ -248,10 +250,7 @@ func TestSlack_ListStars(t *testing.T) { fmt.Printf("Comment %#v\n", item.Comment) } } - if !reflect.DeepEqual(rh.gotParams, wantParams) { - t.Errorf("Got params %#v, want %#v", rh.gotParams, wantParams) - } - if reflect.DeepEqual(paging, Paging{}) { - t.Errorf("Want paging data, got empty struct") + if nextCursor != wantCursor { + t.Errorf("Got cursor %q, want %q", nextCursor, wantCursor) } } diff --git a/team.go b/team.go index 35b699271..55364a9b4 100644 --- a/team.go +++ b/team.go @@ -6,11 +6,6 @@ import ( "strconv" ) -const ( - DEFAULT_LOGINS_COUNT = 100 - DEFAULT_LOGINS_PAGE = 1 -) - type TeamResponse struct { Team TeamInfo `json:"team"` SlackResponse @@ -46,8 +41,8 @@ type TeamProfileField struct { type LoginResponse struct { Logins []Login `json:"logins"` - Paging `json:"paging"` SlackResponse + ResponseMetadata `json:"response_metadata"` } type Login struct { @@ -75,16 +70,14 @@ type BillingActive struct { // AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request type AccessLogParameters struct { TeamID string - Count int - Page int + Cursor string + Limit int + Before int } // NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set func NewAccessLogParameters() AccessLogParameters { - return AccessLogParameters{ - Count: DEFAULT_LOGINS_COUNT, - Page: DEFAULT_LOGINS_PAGE, - } + return AccessLogParameters{} } func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) { @@ -193,31 +186,34 @@ func (api *Client) GetTeamProfileContext(ctx context.Context, teamID ...string) // GetAccessLogs retrieves a page of logins according to the parameters given. // For more information see the GetAccessLogsContext documentation. -func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) { +func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, string, error) { return api.GetAccessLogsContext(context.Background(), params) } // GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context. // Slack API docs: https://api.slack.com/methods/team.accessLogs -func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) { +func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, string, error) { values := url.Values{ "token": {api.token}, } if params.TeamID != "" { values.Add("team_id", params.TeamID) } - if params.Count != DEFAULT_LOGINS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Page != DEFAULT_LOGINS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.Before != 0 { + values.Add("before", strconv.Itoa(params.Before)) } response, err := api.accessLogsRequest(ctx, "team.accessLogs", values) if err != nil { - return nil, nil, err + return nil, "", err } - return response.Logins, &response.Paging, nil + return response.Logins, response.ResponseMetadata.Cursor, nil } type GetBillableInfoParams struct { diff --git a/team_test.go b/team_test.go index d0055d177..22186d361 100644 --- a/team_test.go +++ b/team_test.go @@ -150,11 +150,8 @@ func getTeamAccessLogs(rw http.ResponseWriter, r *http.Request) { "country": null, "region": null }], - "paging": { - "count": 2, - "total": 2, - "page": 1, - "pages": 1 + "response_metadata": { + "next_cursor": "dGVhbV9pZDo5MDAwMTcw" } }`) rw.Write(response) @@ -166,7 +163,10 @@ func TestGetAccessLogs(t *testing.T) { once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) - logins, paging, err := api.GetAccessLogs(NewAccessLogParameters()) + params := NewAccessLogParameters() + params.Limit = 2 + params.TeamID = "T12345" + logins, nextCursor, err := api.GetAccessLogs(params) if err != nil { t.Errorf("Unexpected error: %s", err) return @@ -222,17 +222,8 @@ func TestGetAccessLogs(t *testing.T) { t.Fatal(ErrIncorrectResponse) } - // test the paging - if paging.Count != 2 { - t.Fatal(ErrIncorrectResponse) - } - if paging.Total != 2 { - t.Fatal(ErrIncorrectResponse) - } - if paging.Page != 1 { - t.Fatal(ErrIncorrectResponse) - } - if paging.Pages != 1 { - t.Fatal(ErrIncorrectResponse) + // test the cursor + if nextCursor != "dGVhbV9pZDo5MDAwMTcw" { + t.Fatalf("Expected cursor %q, got %q", "dGVhbV9pZDo5MDAwMTcw", nextCursor) } }