Use links from headers
authorMikael Berthe <mikael@lilotux.net>
Sat, 29 Apr 2017 12:16:16 +0200
changeset 155 0c581e0108da
parent 154 eb83fd052cc5
child 156 70aadba26338
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.
account.go
api.go
app.go
favourites.go
instance.go
notifications.go
report.go
search.go
status.go
timelines.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
 }
 
--- 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 {
--- 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
 	}
 
--- 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
 }
--- 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
--- 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, &notifications); err != nil {
+	var links apiLinks
+	if err := mc.apiCall("notifications", rest.Get, nil, lopt, &links, &notifications); 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, &notifSlice); 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, &notification); err != nil {
+	if err := mc.apiCall(endPoint, rest.Get, nil, nil, nil, &notification); 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
 }
--- 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
--- 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
--- 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"
--- 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
 }