# HG changeset patch # User Mikael Berthe # Date 1493460976 -7200 # Node ID 0c581e0108daa77553463f4919f4f0c2afc5e27a # Parent eb83fd052cc5f90f12be813fb672f9801b132dec Use links from headers Keep querying the API until the requested limit is reached, using the headers. If no limit is set, a single query is made. diff -r eb83fd052cc5 -r 0c581e0108da account.go --- a/account.go Sat Apr 29 10:51:45 2017 +0200 +++ b/account.go Sat Apr 29 12:16:16 2017 +0200 @@ -59,7 +59,7 @@ } var account Account - if err := mc.apiCall(endPoint, method, nil, nil, &account); err != nil { + if err := mc.apiCall(endPoint, method, nil, nil, nil, &account); err != nil { return nil, err } return &account, nil @@ -90,6 +90,11 @@ return []Account{}, ErrInvalidParameter } endPoint = "accounts/" + op + case "reblogged_by", "favourited_by": + if opts == nil || opts.ID < 1 { + return []Account{}, ErrInvalidID + } + endPoint = "statuses/" + strconv.Itoa(opts.ID) + "/" + op default: return nil, ErrInvalidParameter } @@ -101,9 +106,22 @@ } var accounts []Account - if err := mc.apiCall(endPoint, rest.Get, params, lopt, &accounts); err != nil { + var links apiLinks + if err := mc.apiCall(endPoint, rest.Get, params, lopt, &links, &accounts); err != nil { return nil, err } + if lopt != nil { // Fetch more pages to reach our limit + var accountSlice []Account + for lopt.Limit > len(accounts) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall(endPoint, rest.Get, params, newlopt, &links, &accountSlice); err != nil { + return nil, err + } + accounts = append(accounts, accountSlice...) + accountSlice = accountSlice[:0] // Clear struct + } + } return accounts, nil } @@ -180,7 +198,7 @@ params["uri"] = uri var account Account - if err := mc.apiCall("follows", rest.Post, params, nil, &account); err != nil { + if err := mc.apiCall("follows", rest.Post, params, nil, nil, &account); err != nil { return nil, err } if account.ID == 0 { @@ -281,7 +299,7 @@ } var rl []Relationship - if err := mc.apiCall("accounts/relationships", rest.Get, params, nil, &rl); err != nil { + if err := mc.apiCall("accounts/relationships", rest.Get, params, nil, nil, &rl); err != nil { return nil, err } return rl, nil @@ -305,9 +323,22 @@ } var sl []Status - if err := mc.apiCall(endPoint, rest.Get, params, lopt, &sl); err != nil { + var links apiLinks + if err := mc.apiCall(endPoint, rest.Get, params, lopt, &links, &sl); err != nil { return nil, err } + if lopt != nil { // Fetch more pages to reach our limit + var statusSlice []Status + for lopt.Limit > len(sl) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall(endPoint, rest.Get, params, newlopt, &links, &statusSlice); err != nil { + return nil, err + } + sl = append(sl, statusSlice...) + statusSlice = statusSlice[:0] // Clear struct + } + } return sl, nil } diff -r eb83fd052cc5 -r 0c581e0108da api.go --- a/api.go Sat Apr 29 10:51:45 2017 +0200 +++ b/api.go Sat Apr 29 12:16:16 2017 +0200 @@ -13,12 +13,72 @@ "fmt" "net/http" "net/url" + "regexp" "strconv" "strings" "github.com/sendgrid/rest" ) +type apiLinks struct { + next, prev *LimitParams +} + +func parseLink(links []string) (*apiLinks, error) { + if len(links) == 0 { + return nil, nil + } + + al := new(apiLinks) + linkRegex := regexp.MustCompile(`<([^>]+)>; rel="([^"]+)`) + for _, l := range links { + m := linkRegex.FindAllStringSubmatch(l, -1) + for _, submatch := range m { + if len(submatch) != 3 { + continue + } + // Parse URL + u, err := url.Parse(submatch[1]) + if err != nil { + return al, err + } + var lp *LimitParams + since := u.Query().Get("since_id") + max := u.Query().Get("max_id") + lim := u.Query().Get("limit") + if since == "" && max == "" { + continue + } + lp = new(LimitParams) + if since != "" { + lp.SinceID, err = strconv.Atoi(since) + if err != nil { + return al, err + } + } + if max != "" { + lp.MaxID, err = strconv.Atoi(max) + if err != nil { + return al, err + } + } + if lim != "" { + lp.Limit, err = strconv.Atoi(lim) + if err != nil { + return al, err + } + } + switch submatch[2] { + case "prev": + al.prev = lp + case "next": + al.next = lp + } + } + } + return al, nil +} + // restAPI actually does the HTTP query // It is a copy of rest.API with better handling of parameters with multiple values func restAPI(request rest.Request) (*rest.Response, error) { @@ -103,7 +163,9 @@ } // apiCall makes a call to the Mastodon API server -func (mc *Client) apiCall(endPoint string, method rest.Method, params apiCallParams, limitOptions *LimitParams, data interface{}) error { +// If links is not nil, the prev/next links from the API response headers +// will be set (if they exist) in the structure. +func (mc *Client) apiCall(endPoint string, method rest.Method, params apiCallParams, limitOptions *LimitParams, links *apiLinks, data interface{}) error { if mc == nil { return fmt.Errorf("use of uninitialized madon client") } @@ -135,6 +197,16 @@ return fmt.Errorf("API query (%s) failed: %s", endPoint, err.Error()) } + if links != nil { + pLinks, err := parseLink(r.Headers["Link"]) + if err != nil { + return fmt.Errorf("cannot decode header links (%s): %s", method, err.Error()) + } + if pLinks != nil { + *links = *pLinks + } + } + // Check for error reply var errorResult Error if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil { diff -r eb83fd052cc5 -r 0c581e0108da app.go --- a/app.go Sat Apr 29 10:51:45 2017 +0200 +++ b/app.go Sat Apr 29 12:16:16 2017 +0200 @@ -68,7 +68,7 @@ } var app registerApp - if err := mc.apiCall("apps", rest.Post, params, nil, &app); err != nil { + if err := mc.apiCall("apps", rest.Post, params, nil, nil, &app); err != nil { return nil, err } diff -r eb83fd052cc5 -r 0c581e0108da favourites.go --- a/favourites.go Sat Apr 29 10:51:45 2017 +0200 +++ b/favourites.go Sat Apr 29 12:16:16 2017 +0200 @@ -13,9 +13,22 @@ // GetFavourites returns the list of the user's favourites func (mc *Client) GetFavourites(lopt *LimitParams) ([]Status, error) { var faves []Status - err := mc.apiCall("favourites", rest.Get, nil, lopt, &faves) + var links apiLinks + err := mc.apiCall("favourites", rest.Get, nil, lopt, &links, &faves) if err != nil { return nil, err } + if lopt != nil { // Fetch more pages to reach our limit + var faveSlice []Status + for lopt.Limit > len(faves) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall("favourites", rest.Get, nil, newlopt, &links, &faveSlice); err != nil { + return nil, err + } + faves = append(faves, faveSlice...) + faveSlice = faveSlice[:0] // Clear struct + } + } return faves, nil } diff -r eb83fd052cc5 -r 0c581e0108da instance.go --- a/instance.go Sat Apr 29 10:51:45 2017 +0200 +++ b/instance.go Sat Apr 29 12:16:16 2017 +0200 @@ -13,7 +13,7 @@ // GetCurrentInstance returns current instance information func (mc *Client) GetCurrentInstance() (*Instance, error) { var i Instance - if err := mc.apiCall("instance", rest.Get, nil, nil, &i); err != nil { + if err := mc.apiCall("instance", rest.Get, nil, nil, nil, &i); err != nil { return nil, err } return &i, nil diff -r eb83fd052cc5 -r 0c581e0108da notifications.go --- a/notifications.go Sat Apr 29 10:51:45 2017 +0200 +++ b/notifications.go Sat Apr 29 12:16:16 2017 +0200 @@ -15,9 +15,22 @@ // GetNotifications returns the list of the user's notifications func (mc *Client) GetNotifications(lopt *LimitParams) ([]Notification, error) { var notifications []Notification - if err := mc.apiCall("notifications", rest.Get, nil, lopt, ¬ifications); err != nil { + var links apiLinks + if err := mc.apiCall("notifications", rest.Get, nil, lopt, &links, ¬ifications); err != nil { return nil, err } + if lopt != nil { // Fetch more pages to reach our limit + var notifSlice []Notification + for lopt.Limit > len(notifications) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall("notifications", rest.Get, nil, newlopt, &links, ¬ifSlice); err != nil { + return nil, err + } + notifications = append(notifications, notifSlice...) + notifSlice = notifSlice[:0] // Clear struct + } + } return notifications, nil } @@ -31,7 +44,7 @@ var endPoint = "notifications/" + strconv.Itoa(notificationID) var notification Notification - if err := mc.apiCall(endPoint, rest.Get, nil, nil, ¬ification); err != nil { + if err := mc.apiCall(endPoint, rest.Get, nil, nil, nil, ¬ification); err != nil { return nil, err } if notification.ID == 0 { @@ -48,11 +61,13 @@ endPoint := "notifications/dismiss" params := apiCallParams{"id": strconv.Itoa(notificationID)} - return mc.apiCall(endPoint, rest.Post, params, nil, &Notification{}) + err := mc.apiCall(endPoint, rest.Post, params, nil, nil, &Notification{}) + return err } // ClearNotifications deletes all notifications from the Mastodon server for // the authenticated user func (mc *Client) ClearNotifications() error { - return mc.apiCall("notifications/clear", rest.Post, nil, nil, &Notification{}) + err := mc.apiCall("notifications/clear", rest.Post, nil, nil, nil, &Notification{}) + return err } diff -r eb83fd052cc5 -r 0c581e0108da report.go --- a/report.go Sat Apr 29 10:51:45 2017 +0200 +++ b/report.go Sat Apr 29 12:16:16 2017 +0200 @@ -16,7 +16,7 @@ // GetReports returns the current user's reports func (mc *Client) GetReports(lopt *LimitParams) ([]Report, error) { var reports []Report - if err := mc.apiCall("reports", rest.Get, nil, lopt, &reports); err != nil { + if err := mc.apiCall("reports", rest.Get, nil, lopt, nil, &reports); err != nil { return nil, err } return reports, nil @@ -40,7 +40,7 @@ } var report Report - if err := mc.apiCall("reports", rest.Post, params, nil, &report); err != nil { + if err := mc.apiCall("reports", rest.Post, params, nil, nil, &report); err != nil { return nil, err } return &report, nil diff -r eb83fd052cc5 -r 0c581e0108da search.go --- a/search.go Sat Apr 29 10:51:45 2017 +0200 +++ b/search.go Sat Apr 29 12:16:16 2017 +0200 @@ -23,7 +23,7 @@ } var results Results - if err := mc.apiCall("search", rest.Get, params, nil, &results); err != nil { + if err := mc.apiCall("search", rest.Get, params, nil, nil, &results); err != nil { return nil, err } return &results, nil diff -r eb83fd052cc5 -r 0c581e0108da status.go --- a/status.go Sat Apr 29 10:51:45 2017 +0200 +++ b/status.go Sat Apr 29 12:16:16 2017 +0200 @@ -48,7 +48,7 @@ endPoint += "/" + op } - return mc.apiCall(endPoint, rest.Get, nil, nil, data) + return mc.apiCall(endPoint, rest.Get, nil, nil, nil, data) } // updateStatusData updates the statuses @@ -114,7 +114,7 @@ } } - return mc.apiCall(endPoint, method, params, nil, data) + return mc.apiCall(endPoint, method, params, nil, nil, data) } // GetStatus returns a status @@ -152,16 +152,14 @@ // GetStatusRebloggedBy returns a list of the accounts who reblogged a status func (mc *Client) GetStatusRebloggedBy(statusID int, lopt *LimitParams) ([]Account, error) { - var accounts []Account - err := mc.queryStatusData(statusID, "reblogged_by", &accounts) - return accounts, err + o := &getAccountsOptions{ID: statusID, Limit: lopt} + return mc.getMultipleAccounts("reblogged_by", o) } // GetStatusFavouritedBy returns a list of the accounts who favourited a status func (mc *Client) GetStatusFavouritedBy(statusID int, lopt *LimitParams) ([]Account, error) { - var accounts []Account - err := mc.queryStatusData(statusID, "favourited_by", &accounts) - return accounts, err + o := &getAccountsOptions{ID: statusID, Limit: lopt} + return mc.getMultipleAccounts("favourited_by", o) } // PostStatus posts a new "toot" diff -r eb83fd052cc5 -r 0c581e0108da timelines.go --- a/timelines.go Sat Apr 29 10:51:45 2017 +0200 +++ b/timelines.go Sat Apr 29 12:16:16 2017 +0200 @@ -39,8 +39,21 @@ } var tl []Status - if err := mc.apiCall(endPoint, rest.Get, params, lopt, &tl); err != nil { + var links apiLinks + if err := mc.apiCall(endPoint, rest.Get, params, lopt, &links, &tl); err != nil { return nil, err } + if lopt != nil { // Fetch more pages to reach our limit + var statusSlice []Status + for lopt.Limit > len(tl) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall(endPoint, rest.Get, params, newlopt, &links, &statusSlice); err != nil { + return nil, err + } + tl = append(tl, statusSlice...) + statusSlice = statusSlice[:0] // Clear struct + } + } return tl, nil }