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 }