This repository has been archived on 2022-05-25. You can view files and clone it, but cannot push or open issues or pull requests.
elon-eats-my-tweets/twitterapi/client.go

157 lines
4.3 KiB
Go

package twitterapi
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
)
type Getter interface {
Get(string) (*http.Response, error)
}
// NewClient returns a new APIClient.
func NewClient(httpclient Getter) APIClient {
return &apiClient{httpclient}
}
// NewAPIClient returns a new APIClient which will authenticate with the
// provided bearer token.
func NewClientWithBearerToken(bearerToken string) APIClient {
return &apiClient{&http.Client{
Timeout: time.Second * 5,
Transport: &bearerTokenTransport{bearerToken: bearerToken},
}}
}
// apiClient implements APIClient.
type apiClient struct {
Getter
}
// bearerTokenTransport implements http.RoundTripper.
type bearerTokenTransport struct {
bearerToken string
}
// RoundTrip sets the Authorization header with the provided bearerToken, and
// forwards the request. It will panic if the request includes a body (which
// requires special handling).
func (t *bearerTokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Body != nil {
panic("bearerTokenTransport does not handle requests with non-nil body")
}
req2 := cloneRequest(req)
req2.Header.Set("Authorization", "Bearer "+t.bearerToken)
return http.DefaultTransport.RoundTrip(req2)
}
// https://go.googlesource.com/oauth2/+/f95fa95eaa936d9d87489b15d1d18b97c1ba9c28/transport.go#96
func cloneRequest(r *http.Request) *http.Request {
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}
return r2
}
// GetMe returns the currently authenticated user.
func (c *apiClient) GetMe() (*User, error) {
type oauthResponse struct {
Data *User `json:"data"`
}
resp, err := c.Get("https://api.twitter.com/2/users/me")
if err != nil {
return nil, fmt.Errorf("error fetching resource: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error fetching resource: status code %d", resp.StatusCode)
}
var oauthResp oauthResponse
if err = json.NewDecoder(resp.Body).Decode(&oauthResp); err != nil {
return nil, fmt.Errorf("error decoding resource: %v", err)
}
return oauthResp.Data, nil
}
var ErrNoTweets = errors.New("no tweets available")
// GetLastTweet returns the most recent tweet for a given user. If no tweets
// are available, ErrNoTweets will be returned.
func (c *apiClient) GetLastTweet(userID string) (*Tweet, error) {
type oauthResponse struct {
Data []*Tweet `json:"data"`
Meta TwitterMetadata `json:"meta"`
}
apiURL := "https://api.twitter.com/2/users/" + userID + "/tweets?tweet.fields=created_at"
resp, err := c.Get(apiURL)
if err != nil {
return nil, fmt.Errorf("error fetching resource: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error fetching resource: status code %d", resp.StatusCode)
}
var oauthResp oauthResponse
if err = json.NewDecoder(resp.Body).Decode(&oauthResp); err != nil {
return nil, fmt.Errorf("error decoding resource: %v", err)
}
if oauthResp.Meta.ResultCount == 0 {
return nil, ErrNoTweets
}
// the order of returned tweets seems to be chronological, but it isn't
// documented so use the metadata instead.
for _, tweet := range oauthResp.Data {
if tweet.ID == oauthResp.Meta.NewestID {
return tweet, nil
}
}
return nil, errors.New("error fetching latest tweet: could not match newest_id")
}
// GetTweets returns the latest tweets for a given user, up to the maximum
// batch size of 100 allowable by the Twitter API.
func (c *apiClient) GetTweets(userID string, sinceID string) ([]*Tweet, error) {
type oauthResponse struct {
Data []*Tweet `json:"data"`
}
apiURL := "https://api.twitter.com/2/users/" + userID + "/tweets?tweet.fields=created_at&max_results=100"
if sinceID == "" {
} else {
apiURL += "&since_id=" + sinceID
}
resp, err := c.Get(apiURL)
if err != nil {
return nil, fmt.Errorf("error fetching resource: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error fetching resource: status code %d", resp.StatusCode)
}
var oauthResp oauthResponse
if err = json.NewDecoder(resp.Body).Decode(&oauthResp); err != nil {
return nil, fmt.Errorf("error decoding resource: %v", err)
}
return oauthResp.Data, nil
}