Refactor API calls
authorMikael Berthe <mikael@lilotux.net>
Sat, 15 Apr 2017 10:26:36 +0200
changeset 120 579912e9d0ef
parent 119 22c8c58ad61b
child 121 d192d9d0adfd
Refactor API calls
account.go
app.go
cmd/gondole-cli/main.go
favourites.go
gondole.go
gondole_test.go
instance.go
notifications.go
report.go
search.go
status.go
streams.go
timelines.go
--- a/account.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/account.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,7 +1,6 @@
 package gondole
 
 import (
-	"encoding/json"
 	"fmt"
 	"strconv"
 
@@ -19,110 +18,75 @@
 }
 
 // getSingleAccount returns an account entity
-// The target can be "account", "verify_credentials", "follow", "unfollow",
-// "block", "unblock", "mute", "unmute", "follow_requests/authorize" or
-// "follow_requests/reject".
-// The id is optional and depends on the target.
-func (g *Client) getSingleAccount(target string, id int) (*Account, error) {
+// The operation 'op' can be "account", "verify_credentials", "follow",
+// "unfollow", "block", "unblock", "mute", "unmute",
+// "follow_requests/authorize" or // "follow_requests/reject".
+// The id is optional and depends on the operation.
+func (g *Client) getSingleAccount(op string, id int) (*Account, error) {
 	var endPoint string
 	method := rest.Get
 	strID := strconv.Itoa(id)
 
-	switch target {
+	switch op {
 	case "account":
 		endPoint = "accounts/" + strID
 	case "verify_credentials":
 		endPoint = "accounts/verify_credentials"
 	case "follow", "unfollow", "block", "unblock", "mute", "unmute":
-		endPoint = "accounts/" + strID + "/" + target
+		endPoint = "accounts/" + strID + "/" + op
 		method = rest.Post
 	case "follow_requests/authorize", "follow_requests/reject":
 		// The documentation is incorrect, the endpoint actually
 		// is "follow_requests/:id/{authorize|reject}"
-		endPoint = target[:16] + strID + "/" + target[16:]
+		endPoint = op[:16] + strID + "/" + op[16:]
 		method = rest.Post
 	default:
 		return nil, ErrInvalidParameter
 	}
 
-	req := g.prepareRequest(endPoint)
-	req.Method = method
-
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("getAccount (%s): %s", target, err.Error())
-	}
-
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var account Account
-	err = json.Unmarshal([]byte(r.Body), &account)
-	if err != nil {
-		return nil, fmt.Errorf("getAccount (%s) API: %s", target, err.Error())
+	if err := g.apiCall(endPoint, method, nil, &account); err != nil {
+		return nil, err
 	}
 	return &account, nil
 }
 
 // getMultipleAccounts returns a list of account entities
-// The target can be "followers", "following", "search", "blocks", "mutes",
-// "follow_requests".
-// The id is optional and depends on the target.
-func (g *Client) getMultipleAccounts(target string, opts *getAccountsOptions) ([]Account, error) {
+// The operation 'op' can be "followers", "following", "search", "blocks",
+// "mutes", "follow_requests".
+// The id is optional and depends on the operation.
+func (g *Client) getMultipleAccounts(op string, opts *getAccountsOptions) ([]Account, error) {
 	var endPoint string
-	switch target {
+
+	switch op {
 	case "followers", "following":
 		if opts == nil || opts.ID < 1 {
 			return []Account{}, ErrInvalidID
 		}
-		endPoint = "accounts/" + strconv.Itoa(opts.ID) + "/" + target
+		endPoint = "accounts/" + strconv.Itoa(opts.ID) + "/" + op
 	case "follow_requests", "blocks", "mutes":
-		endPoint = target
+		endPoint = op
 	case "search":
 		if opts == nil || opts.Q == "" {
 			return []Account{}, ErrInvalidParameter
 		}
-		endPoint = "accounts/" + target
+		endPoint = "accounts/" + op
 	default:
 		return nil, ErrInvalidParameter
 	}
 
-	req := g.prepareRequest(endPoint)
-
 	// Handle target-specific query parameters
-	if target == "search" {
-		req.QueryParams["q"] = opts.Q
+	params := make(apiCallParams)
+	if op == "search" {
+		params["q"] = opts.Q
 		if opts.Limit > 0 {
-			req.QueryParams["limit"] = strconv.Itoa(opts.Limit)
+			params["limit"] = strconv.Itoa(opts.Limit)
 		}
 	}
 
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("getAccount (%s): %s", target, err.Error())
-	}
-
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var accounts []Account
-	err = json.Unmarshal([]byte(r.Body), &accounts)
-	if err != nil {
-		return nil, fmt.Errorf("getAccount (%s) API: %s", target, err.Error())
+	if err := g.apiCall(endPoint, rest.Get, params, &accounts); err != nil {
+		return nil, err
 	}
 	return accounts, nil
 }
@@ -190,34 +154,18 @@
 }
 
 // FollowRemoteAccount follows a remote account
-// The parameter 'id' is a URI (username@domain).
-func (g *Client) FollowRemoteAccount(id string) (*Account, error) {
-	if id == "" {
+// The parameter 'uri' is a URI (e.g. "username@domain").
+func (g *Client) FollowRemoteAccount(uri string) (*Account, error) {
+	if uri == "" {
 		return nil, ErrInvalidID
 	}
 
-	req := g.prepareRequest("follows")
-	req.Method = rest.Post
-	req.QueryParams["uri"] = id
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("FollowRemoteAccount: %s", err.Error())
-	}
+	params := make(apiCallParams)
+	params["uri"] = uri
 
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var account Account
-	err = json.Unmarshal([]byte(r.Body), &account)
-	if err != nil {
-		return nil, fmt.Errorf("FollowRemoteAccount API: %s", err.Error())
+	if err := g.apiCall("follows", rest.Post, params, &account); err != nil {
+		return nil, err
 	}
 	if account.ID == 0 {
 		return nil, ErrEntityNotFound
@@ -302,38 +250,22 @@
 		return nil, ErrInvalidID
 	}
 
-	req := g.prepareRequest("accounts/relationships")
-
 	if len(accountIDs) > 1 { // XXX
 		return nil, fmt.Errorf("accounts/relationships currently does not work with more than 1 ID")
 	}
-	req.QueryParams["id"] = strconv.Itoa(accountIDs[0])
+
+	params := make(apiCallParams)
+	params["id"] = strconv.Itoa(accountIDs[0])
 	/*
 		for i, id := range accountIDList {
 			qID := fmt.Sprintf("id[%d]", i+1)
-			req.QueryParams[qID] = strconv.Itoa(id)
+			params[qID] = strconv.Itoa(id)
 		}
 	*/
 
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("GetAccountRelationships: %s", err.Error())
-	}
-
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var rl []Relationship
-	err = json.Unmarshal([]byte(r.Body), &rl)
-	if err != nil {
-		return nil, fmt.Errorf("accounts/relationships API: %s", err.Error())
+	if err := g.apiCall("accounts/relationships", rest.Get, params, &rl); err != nil {
+		return nil, err
 	}
 	return rl, nil
 }
@@ -347,34 +279,17 @@
 	}
 
 	endPoint := "accounts/" + strconv.Itoa(accountID) + "/" + "statuses"
-	req := g.prepareRequest(endPoint)
-
+	params := make(apiCallParams)
 	if onlyMedia {
-		req.QueryParams["only_media"] = "true"
+		params["only_media"] = "true"
 	}
 	if excludeReplies {
-		req.QueryParams["exclude_replies"] = "true"
-	}
-
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("GetAccountStatuses: %s", err.Error())
+		params["exclude_replies"] = "true"
 	}
 
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var sl []Status
-	err = json.Unmarshal([]byte(r.Body), &sl)
-	if err != nil {
-		return nil, fmt.Errorf("accounts/statuses API: %s", err.Error())
+	if err := g.apiCall(endPoint, rest.Get, params, &sl); err != nil {
+		return nil, err
 	}
 	return sl, nil
 }
--- a/app.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/app.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,8 +1,6 @@
 package gondole
 
 import (
-	"encoding/json"
-	"log"
 	"net/url"
 	"strings"
 
@@ -37,30 +35,22 @@
 		InstanceURL: instanceURL,
 	}
 
-	req := g.prepareRequest("apps")
+	params := make(apiCallParams)
+	params["client_name"] = name
+	params["scopes"] = strings.Join(scopes, " ")
 	if redirectURI != "" {
-		req.QueryParams["redirect_uris"] = redirectURI
+		params["redirect_uris"] = redirectURI
 	} else {
-		req.QueryParams["redirect_uris"] = NoRedirect
-	}
-	req.QueryParams["client_name"] = name
-	req.QueryParams["scopes"] = strings.Join(scopes, " ")
-	req.Method = rest.Post
-
-	r, err := rest.API(req)
-	if err != nil {
-		log.Fatalf("error can not register app: %v", err)
+		params["redirect_uris"] = NoRedirect
 	}
 
-	var resp registerApp
-
-	err = json.Unmarshal([]byte(r.Body), &resp)
-	if err != nil {
-		log.Fatalf("error can not register app: %v", err)
+	var app registerApp
+	if err := g.apiCall("apps", rest.Post, params, &app); err != nil {
+		return nil, err
 	}
 
-	g.ID = resp.ClientID
-	g.Secret = resp.ClientSecret
+	g.ID = app.ClientID
+	g.Secret = app.ClientSecret
 
 	return
 }
--- a/cmd/gondole-cli/main.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/cmd/gondole-cli/main.go	Sat Apr 15 10:26:36 2017 +0200
@@ -108,6 +108,9 @@
 		}
 
 		instance, err = gondole.NewApp("gondole-cli", scopes, gondole.NoRedirect, instanceURL)
+		if err != nil {
+			log.Fatalf("error: can not register application:", err.Error())
+		}
 
 		server := &Server{
 			ID:          instance.ID,
--- a/favourites.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/favourites.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,32 +1,15 @@
 package gondole
 
 import (
-	"encoding/json"
-	"fmt"
-
 	"github.com/sendgrid/rest"
 )
 
 // GetFavourites returns the list of the user's favourites
 func (g *Client) GetFavourites() ([]Status, error) {
 	var faves []Status
-
-	req := g.prepareRequest("favourites")
-	r, err := rest.API(req)
+	err := g.apiCall("favourites", rest.Get, nil, &faves)
 	if err != nil {
-		return faves, fmt.Errorf("favourites API query: %s", err.Error())
+		return nil, err
 	}
-
-	println(r.Body)
-	err = json.Unmarshal([]byte(r.Body), &faves)
-	if err != nil {
-		var errorRes Error
-		err2 := json.Unmarshal([]byte(r.Body), &errorRes)
-		if err2 == nil {
-			return faves, fmt.Errorf("%s", errorRes.Text)
-		}
-		return faves, fmt.Errorf("favourites API: %s", err.Error())
-	}
-
 	return faves, nil
 }
--- a/gondole.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/gondole.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,12 +1,16 @@
 package gondole
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 
 	"github.com/sendgrid/rest"
 )
 
+// apiCallParams is a map with the parameters for an API call
+type apiCallParams map[string]string
+
 const (
 	// GondoleVersion contains the version of the Gondole implementation
 	GondoleVersion = "0.0"
@@ -27,16 +31,12 @@
 	ErrInvalidID         = errors.New("incorrect entity ID")
 )
 
-// prepareRequest insert all pre-defined stuff
-func (g *Client) prepareRequest(what string) (req rest.Request) {
-	var endPoint string
+// prepareRequest inserts all pre-defined stuff
+func (g *Client) prepareRequest(target string, method rest.Method, params apiCallParams) (req rest.Request) {
+	endPoint := g.APIBase + "/" + target
 
-	endPoint = g.APIBase + "/" + what
-	// Add at least one option, the APIkey if present
+	// Request headers
 	hdrs := make(map[string]string)
-	opts := make(map[string]string)
-
-	// Insert our sig
 	hdrs["User-Agent"] = fmt.Sprintf("Gondole/%s", GondoleVersion)
 	if g.userToken != nil {
 		hdrs["Authorization"] = fmt.Sprintf("Bearer %s", g.userToken.AccessToken)
@@ -45,7 +45,36 @@
 	req = rest.Request{
 		BaseURL:     endPoint,
 		Headers:     hdrs,
-		QueryParams: opts,
+		Method:      method,
+		QueryParams: params,
 	}
 	return
 }
+
+// apiCall makes a call to the Mastodon API server
+func (g *Client) apiCall(endPoint string, method rest.Method, params apiCallParams, data interface{}) error {
+	// Prepare query
+	req := g.prepareRequest(endPoint, method, params)
+
+	// Make API call
+	r, err := rest.API(req)
+	if err != nil {
+		return fmt.Errorf("API query (%s) failed: %s", endPoint, err.Error())
+	}
+
+	// Check for error reply
+	var errorResult Error
+	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
+		// The empty object is not an error
+		if errorResult.Text != "" {
+			return fmt.Errorf("%s", errorResult.Text)
+		}
+	}
+
+	// Not an error reply; let's unmarshal the data
+	err = json.Unmarshal([]byte(r.Body), &data)
+	if err != nil {
+		return fmt.Errorf("cannot decode API response (%s): %s", method, err.Error())
+	}
+	return nil
+}
--- a/gondole_test.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/gondole_test.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,20 +1,20 @@
 package gondole
 
 import (
-    "testing"
-    "github.com/stretchr/testify/assert"
+	"testing"
+
+	"github.com/sendgrid/rest"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestPrepareRequest(t *testing.T) {
-    g := &Client{
-        Name: "foo",
-        ID: "666",
-        Secret: "biiiip",
-        APIBase: "http://example.com",
-    }
+	g := &Client{
+		Name:    "foo",
+		ID:      "666",
+		Secret:  "biiiip",
+		APIBase: "http://example.com",
+	}
 
-    req := g.prepareRequest("bar")
-    assert.NotNil(t, req.Headers, "not nil")
-    assert.NotNil(t, req.QueryParams, "not nil")
+	req := g.prepareRequest("bar", rest.Get, nil)
+	assert.NotNil(t, req.Headers, "not nil")
 }
-
--- a/instance.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/instance.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,34 +1,14 @@
 package gondole
 
 import (
-	"encoding/json"
-	"fmt"
-
 	"github.com/sendgrid/rest"
 )
 
 // GetCurrentInstance returns current instance information
 func (g *Client) GetCurrentInstance() (*Instance, error) {
-	req := g.prepareRequest("instance")
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("instance: %s", err.Error())
-	}
-
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var i Instance
-	err = json.Unmarshal([]byte(r.Body), &i)
-	if err != nil {
-		return nil, fmt.Errorf("instance API: %s", err.Error())
+	if err := g.apiCall("instance", rest.Get, nil, &i); err != nil {
+		return nil, err
 	}
 	return &i, nil
 }
--- a/notifications.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/notifications.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,8 +1,6 @@
 package gondole
 
 import (
-	"encoding/json"
-	"fmt"
 	"strconv"
 
 	"github.com/sendgrid/rest"
@@ -11,23 +9,9 @@
 // GetNotifications returns the list of the user's notifications
 func (g *Client) GetNotifications() ([]Notification, error) {
 	var notifications []Notification
-
-	req := g.prepareRequest("notifications")
-	r, err := rest.API(req)
-	if err != nil {
-		return notifications, fmt.Errorf("notifications API query: %s", err.Error())
+	if err := g.apiCall("notifications", rest.Get, nil, &notifications); err != nil {
+		return nil, err
 	}
-
-	err = json.Unmarshal([]byte(r.Body), &notifications)
-	if err != nil {
-		var errorRes Error
-		err2 := json.Unmarshal([]byte(r.Body), &errorRes)
-		if err2 == nil {
-			return notifications, fmt.Errorf("%s", errorRes.Text)
-		}
-		return notifications, fmt.Errorf("notifications API: %s", err.Error())
-	}
-
 	return notifications, nil
 }
 
@@ -35,40 +19,23 @@
 // The returned notification can be nil if there is an error or if the
 // requested notification does not exist.
 func (g *Client) GetNotification(id int) (*Notification, error) {
-	var notification Notification
-
-	req := g.prepareRequest("notifications/" + strconv.Itoa(id))
-	r, err := rest.API(req)
-	if err != nil {
-		return &notification, fmt.Errorf("notification API query: %s", err.Error())
+	if id < 1 {
+		return nil, ErrInvalidID
 	}
 
-	err = json.Unmarshal([]byte(r.Body), &notification)
-	if err != nil {
-		var errorRes Error
-		err2 := json.Unmarshal([]byte(r.Body), &errorRes)
-		if err2 == nil {
-			return &notification, fmt.Errorf("%s", errorRes.Text)
-		}
-		return &notification, fmt.Errorf("notification API: %s", err.Error())
+	var endPoint = "notifications/" + strconv.Itoa(id)
+	var notification Notification
+	if err := g.apiCall(endPoint, rest.Get, nil, &notification); err != nil {
+		return nil, err
 	}
-
 	if notification.ID == 0 {
 		return nil, ErrEntityNotFound
 	}
-
 	return &notification, nil
 }
 
 // ClearNotifications deletes all notifications from the Mastodon server for
 // the authenticated user
 func (g *Client) ClearNotifications() error {
-	req := g.prepareRequest("notifications/clear")
-	req.Method = rest.Post
-	_, err := rest.API(req)
-	if err != nil {
-		return fmt.Errorf("notifications/clear API query: %s", err.Error())
-	}
-
-	return nil // TODO: check returned object (should be empty)
+	return g.apiCall("notifications/clear", rest.Post, nil, &Notification{})
 }
--- a/report.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/report.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,8 +1,6 @@
 package gondole
 
 import (
-	"encoding/json"
-	"fmt"
 	"strconv"
 
 	"github.com/sendgrid/rest"
@@ -10,26 +8,9 @@
 
 // GetReports returns the current user's reports
 func (g *Client) GetReports() ([]Report, error) {
-	req := g.prepareRequest("reports")
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("reports: %s", err.Error())
-	}
-
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var reports []Report
-	err = json.Unmarshal([]byte(r.Body), &reports)
-	if err != nil {
-		return nil, fmt.Errorf("reports API: %s", err.Error())
+	if err := g.apiCall("reports", rest.Get, nil, &reports); err != nil {
+		return nil, err
 	}
 	return reports, nil
 }
@@ -41,32 +22,15 @@
 		return nil, ErrInvalidParameter
 	}
 
-	req := g.prepareRequest("reports")
-	req.Method = rest.Post
-	req.QueryParams["account_id"] = strconv.Itoa(accountID)
+	params := make(apiCallParams)
+	params["account_id"] = strconv.Itoa(accountID)
 	// XXX Sending only the first one since I'm not sure arrays work
-	req.QueryParams["status_ids"] = strconv.Itoa(statusIDs[0])
-	req.QueryParams["comment"] = comment
-
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("reports: %s", err.Error())
-	}
+	params["status_ids"] = strconv.Itoa(statusIDs[0])
+	params["comment"] = comment
 
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var report Report
-	err = json.Unmarshal([]byte(r.Body), &report)
-	if err != nil {
-		return nil, fmt.Errorf("reports API: %s", err.Error())
+	if err := g.apiCall("reports", rest.Post, params, &report); err != nil {
+		return nil, err
 	}
 	return &report, nil
 }
--- a/search.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/search.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,9 +1,6 @@
 package gondole
 
 import (
-	"encoding/json"
-	"fmt"
-
 	"github.com/sendgrid/rest"
 )
 
@@ -12,30 +9,16 @@
 	if query == "" {
 		return nil, ErrInvalidParameter
 	}
-	req := g.prepareRequest("search")
-	req.QueryParams["q"] = query
+
+	params := make(apiCallParams)
+	params["q"] = query
 	if resolve {
-		req.QueryParams["resolve"] = "true"
-	}
-	r, err := rest.API(req)
-	if err != nil {
-		return nil, fmt.Errorf("search: %s", err.Error())
+		params["resolve"] = "true"
 	}
 
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return nil, fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
 	var results Results
-	err = json.Unmarshal([]byte(r.Body), &results)
-	if err != nil {
-		return nil, fmt.Errorf("search API: %s", err.Error())
+	if err := g.apiCall("search", rest.Get, params, &results); err != nil {
+		return nil, err
 	}
 	return &results, nil
 }
--- a/status.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/status.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,7 +1,6 @@
 package gondole
 
 import (
-	"encoding/json"
 	"fmt"
 	"strconv"
 
@@ -23,59 +22,41 @@
 }
 
 // queryStatusData queries the statuses API
-// The subquery can be empty or "status" (the status itself), "context",
+// The operation 'op' can be empty or "status" (the status itself), "context",
 // "card", "reblogged_by", "favourited_by".
 // The data argument will receive the object(s) returned by the API server.
-func (g *Client) queryStatusData(statusID int, subquery string, data interface{}) error {
-	endPoint := "statuses/" + strconv.Itoa(statusID)
-
+func (g *Client) queryStatusData(statusID int, op string, data interface{}) error {
 	if statusID < 1 {
 		return ErrInvalidID
 	}
 
-	if subquery != "" && subquery != "status" {
-		switch subquery {
+	endPoint := "statuses/" + strconv.Itoa(statusID)
+
+	if op != "" && op != "status" {
+		switch op {
 		case "context", "card", "reblogged_by", "favourited_by":
 		default:
 			return ErrInvalidParameter
 		}
 
-		endPoint += "/" + subquery
-	}
-	req := g.prepareRequest(endPoint)
-	r, err := rest.API(req)
-	if err != nil {
-		return fmt.Errorf("status/%s API query: %s", subquery, err.Error())
+		endPoint += "/" + op
 	}
 
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
-	err = json.Unmarshal([]byte(r.Body), &data)
-	if err != nil {
-		return fmt.Errorf("status/%s API: %s", subquery, err.Error())
-	}
-	return nil
+	return g.apiCall(endPoint, rest.Get, nil, data)
 }
 
 // updateStatusData updates the statuses
-// The subquery can be empty or "status" (to post a status), "delete" (for
-// deleting a status), "reblog", "unreblog", "favourite", "unfavourite".
+// The operation 'op' can be empty or "status" (to post a status), "delete"
+// (for deleting a status), "reblog", "unreblog", "favourite", "unfavourite".
 // The data argument will receive the object(s) returned by the API server.
-func (g *Client) updateStatusData(subquery string, opts updateStatusOptions, data interface{}) error {
+func (g *Client) updateStatusData(op string, opts updateStatusOptions, data interface{}) error {
 	method := rest.Post
 	endPoint := "statuses"
+	params := make(apiCallParams)
 
-	switch subquery {
+	switch op {
 	case "", "status":
-		subquery = "status"
+		op = "status"
 		if opts.Status == "" {
 			return ErrInvalidParameter
 		}
@@ -98,55 +79,33 @@
 		if opts.ID < 1 {
 			return ErrInvalidID
 		}
-		endPoint += "/" + strconv.Itoa(opts.ID) + "/" + subquery
+		endPoint += "/" + strconv.Itoa(opts.ID) + "/" + op
 	default:
 		return ErrInvalidParameter
 	}
 
-	req := g.prepareRequest(endPoint)
-	req.Method = method
-
 	// Form items for a new toot
-	if subquery == "status" {
-		req.QueryParams["status"] = opts.Status
+	if op == "status" {
+		params["status"] = opts.Status
 		if opts.InReplyToID > 0 {
-			req.QueryParams["in_reply_to_id"] = strconv.Itoa(opts.InReplyToID)
+			params["in_reply_to_id"] = strconv.Itoa(opts.InReplyToID)
 		}
 		for i, id := range opts.MediaIDs {
 			qID := fmt.Sprintf("media_ids[%d]", i+1)
-			req.QueryParams[qID] = strconv.Itoa(id)
+			params[qID] = strconv.Itoa(id)
 		}
 		if opts.Sensitive {
-			req.QueryParams["sensitive"] = "true"
+			params["sensitive"] = "true"
 		}
 		if opts.SpoilerText != "" {
-			req.QueryParams["spoiler_text"] = opts.SpoilerText
+			params["spoiler_text"] = opts.SpoilerText
 		}
 		if opts.Visibility != "" {
-			req.QueryParams["visibility"] = opts.Visibility
+			params["visibility"] = opts.Visibility
 		}
 	}
 
-	r, err := rest.API(req)
-	if err != nil {
-		return fmt.Errorf("status/%s API query: %s", subquery, err.Error())
-	}
-
-	// Check for error reply
-	var errorResult Error
-	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
-		// The empty object is not an error
-		if errorResult.Text != "" {
-			return fmt.Errorf("%s", errorResult.Text)
-		}
-	}
-
-	// Not an error reply; let's unmarshal the data
-	err = json.Unmarshal([]byte(r.Body), &data)
-	if err != nil {
-		return fmt.Errorf("status/%s API: %s", subquery, err.Error())
-	}
-	return nil
+	return g.apiCall(endPoint, method, params, data)
 }
 
 // GetStatus returns a status
--- a/streams.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/streams.go	Sat Apr 15 10:26:36 2017 +0200
@@ -28,7 +28,7 @@
 // The stream name can be "user", "public" or "hashtag".
 // For "hashtag", the hashTag argument cannot be empty.
 func (g *Client) openStream(streamName, hashTag string) (*http.Response, error) {
-	req := g.prepareRequest("streaming/" + streamName)
+	params := make(apiCallParams)
 
 	switch streamName {
 	case "user", "public":
@@ -36,15 +36,17 @@
 		if hashTag == "" {
 			return nil, ErrInvalidParameter
 		}
-		req.QueryParams["tag"] = hashTag
+		params["tag"] = hashTag
 	default:
 		return nil, ErrInvalidParameter
 	}
 
+	req := g.prepareRequest("streaming/"+streamName, rest.Get, params)
 	reqObj, err := rest.BuildRequestObject(req)
 	if err != nil {
 		return nil, fmt.Errorf("cannot build stream request: %s", err.Error())
 	}
+
 	resp, err := rest.MakeRequest(reqObj)
 	if err != nil {
 		return nil, fmt.Errorf("cannot open stream: %s", err.Error())
--- a/timelines.go	Sat Apr 15 00:39:43 2017 +0200
+++ b/timelines.go	Sat Apr 15 10:26:36 2017 +0200
@@ -1,7 +1,6 @@
 package gondole
 
 import (
-	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -14,7 +13,6 @@
 // local instance.
 func (g *Client) GetTimelines(timeline string, local bool) ([]Status, error) {
 	var endPoint string
-	var tl []Status
 
 	switch {
 	case timeline == "home", timeline == "public":
@@ -22,33 +20,21 @@
 	case strings.HasPrefix(timeline, ":"):
 		hashtag := timeline[1:]
 		if hashtag == "" {
-			return tl, fmt.Errorf("timelines API: empty hashtag")
+			return nil, fmt.Errorf("timelines API: empty hashtag")
 		}
 		endPoint = "timelines/tag/" + hashtag
 	default:
-		return tl, fmt.Errorf("GetTimelines: bad timelines argument")
-	}
-
-	req := g.prepareRequest(endPoint)
-
-	if timeline == "public" && local {
-		req.QueryParams["local"] = "true"
+		return nil, fmt.Errorf("GetTimelines: bad timelines argument")
 	}
 
-	r, err := rest.API(req)
-	if err != nil {
-		return tl, fmt.Errorf("timelines API query: %s", err.Error())
+	params := make(apiCallParams)
+	if timeline == "public" && local {
+		params["local"] = "true"
 	}
 
-	err = json.Unmarshal([]byte(r.Body), &tl)
-	if err != nil {
-		var errorRes Error
-		err2 := json.Unmarshal([]byte(r.Body), &errorRes)
-		if err2 == nil {
-			return tl, fmt.Errorf("%s", errorRes.Text)
-		}
-		return tl, fmt.Errorf("timelines API: %s", err.Error())
+	var tl []Status
+	if err := g.apiCall(endPoint, rest.Get, params, &tl); err != nil {
+		return nil, err
 	}
-
 	return tl, nil
 }