package twitter //go:generate mockery --recursive --name Getter --output ../generated/mocks import ( "encoding/json" "fmt" "net/http" "time" ) // ElonID is the Twitter ID of @elonmusk. const ElonID = "44196397" // User represents a Twitter user. type User struct { ID string Name string Username string } // Tweet represents a tweet. type Tweet struct { ID string Text string } // 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 } type Getter interface { Get(string) (*http.Response, error) } // NewAPIClient returns a new APIClient. func NewAPIClient(httpclient Getter) *APIClient { return &APIClient{httpclient} } // NewAPIClient returns a new APIClient which will authenticate with the // provided bearer token. func NewAPIClientWithBearerToken(bearerToken string) *APIClient { return &APIClient{&http.Client{ Timeout: time.Second * 5, Transport: &bearerTokenTransport{bearerToken: bearerToken}, }} } // APIClient interacts with the Twitter API V2. type APIClient struct { Getter } // 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 } // GetElonTweets returns the latest tweets for a given user. func (c *APIClient) GetTweets(userID string, sinceID string) ([]*Tweet, error) { type oauthResponse struct { Data []*Tweet `json:"data"` } resp, err := c.Get(fmt.Sprintf("https://api.twitter.com/2/users/%s/tweets?since_id=%s", userID, sinceID)) 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 }