2022-05-24 19:20:28 +00:00
|
|
|
package twitterapi
|
2022-05-22 20:11:33 +00:00
|
|
|
|
2022-05-20 22:34:20 +00:00
|
|
|
import (
|
|
|
|
"encoding/json"
|
2022-05-24 19:20:28 +00:00
|
|
|
"errors"
|
2022-05-20 22:34:20 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2022-05-22 20:11:33 +00:00
|
|
|
"time"
|
2022-05-20 22:34:20 +00:00
|
|
|
)
|
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
type Getter interface {
|
|
|
|
Get(string) (*http.Response, error)
|
|
|
|
}
|
2022-05-22 20:11:33 +00:00
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
// NewClient returns a new APIClient.
|
|
|
|
func NewClient(httpclient Getter) APIClient {
|
|
|
|
return &apiClient{httpclient}
|
2022-05-20 22:34:20 +00:00
|
|
|
}
|
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
// 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
|
2022-05-22 20:11:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
// GetMe returns the currently authenticated user.
|
|
|
|
func (c *apiClient) GetMe() (*User, error) {
|
|
|
|
type oauthResponse struct {
|
|
|
|
Data *User `json:"data"`
|
|
|
|
}
|
2022-05-22 20:11:33 +00:00
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-05-20 22:34:20 +00:00
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
var oauthResp oauthResponse
|
|
|
|
if err = json.NewDecoder(resp.Body).Decode(&oauthResp); err != nil {
|
|
|
|
return nil, fmt.Errorf("error decoding resource: %v", err)
|
|
|
|
}
|
2022-05-22 20:11:33 +00:00
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
return oauthResp.Data, nil
|
2022-05-20 22:34:20 +00:00
|
|
|
}
|
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
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) {
|
2022-05-20 22:34:20 +00:00
|
|
|
type oauthResponse struct {
|
2022-05-24 19:20:28 +00:00
|
|
|
Data []*Tweet `json:"data"`
|
|
|
|
Meta TwitterMetadata `json:"meta"`
|
2022-05-20 22:34:20 +00:00
|
|
|
}
|
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
apiURL := "https://api.twitter.com/2/users/" + userID + "/tweets?tweet.fields=created_at"
|
|
|
|
|
|
|
|
resp, err := c.Get(apiURL)
|
2022-05-20 22:34:20 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
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")
|
2022-05-22 20:11:33 +00:00
|
|
|
}
|
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
// 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) {
|
2022-05-22 20:11:33 +00:00
|
|
|
type oauthResponse struct {
|
|
|
|
Data []*Tweet `json:"data"`
|
|
|
|
}
|
|
|
|
|
2022-05-24 19:20:28 +00:00
|
|
|
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)
|
2022-05-22 20:11:33 +00:00
|
|
|
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
|
2022-05-20 22:34:20 +00:00
|
|
|
}
|