Skip to content

Commit

Permalink
added the basic client (#132)
Browse files Browse the repository at this point in the history
* added the basic client

* added basic unit test

* added example

* added to the readme

* added the comments
  • Loading branch information
g8rswimmer authored Mar 26, 2022
1 parent 199451c commit 0dcb24d
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 0 deletions.
1 change: 1 addition & 0 deletions v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ The following APIs are supported, with the examples [here](./_examples/tweets)
* [Timelines](https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/introduction)
* [Hide Replies](https://developer.twitter.com/en/docs/twitter-api/tweets/hide-replies/introduction)
* [Search](https://developer.twitter.com/en/docs/twitter-api/tweets/search/introduction)
* [Quote Tweets](https://developer.twitter.com/en/docs/twitter-api/tweets/quote-tweets/introduction)

### Users
The following APIs are supported, with the examples [here](./_examples/users)
Expand Down
4 changes: 4 additions & 0 deletions v2/_examples/tweets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ The examples can be run my providing some options, including the authorization t
### [Hide Replies](https://developer.twitter.com/en/docs/twitter-api/tweets/hide-replies/introduction)

* [Hides or unhides a reply to a Tweet](./hide-replies/tweet-hide-replies/main.go)

### [Quote Tweets](https://developer.twitter.com/en/docs/twitter-api/tweets/quote-tweets/introduction)

* [Returns Quote Tweets for a Tweet specified by the requested Tweet ID](./quote/quote-tweets/main.go)
63 changes: 63 additions & 0 deletions v2/_examples/tweets/quote/quote-tweets/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"

twitter "github.com/g8rswimmer/go-twitter/v2"
)

type authorize struct {
Token string
}

func (a authorize) Add(req *http.Request) {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.Token))
}

/**
In order to run, the user will need to provide the bearer token and the list of tweet ids.
**/
func main() {
token := flag.String("token", "", "twitter API token")
id := flag.String("id", "", "twitter id")
flag.Parse()

client := &twitter.Client{
Authorizer: authorize{
Token: *token,
},
Client: http.DefaultClient,
Host: "https://api.twitter.com",
}
opts := twitter.QuoteTweetsLookupOpts{
Expansions: []twitter.Expansion{twitter.ExpansionAuthorID},
TweetFields: []twitter.TweetField{twitter.TweetFieldCreatedAt, twitter.TweetFieldConversationID, twitter.TweetFieldAttachments, twitter.TweetFieldAuthorID, twitter.TweetFieldPublicMetrics},
UserFields: []twitter.UserField{twitter.UserFieldUserName},
}

fmt.Println("Callout to quote tweet lookup callout")

tweetResponse, err := client.QuoteTweetsLookup(context.Background(), *id, opts)
if err != nil {
log.Panicf("tweet quote lookup error: %v", err)
}

dictionaries := tweetResponse.Raw.TweetDictionaries()

enc, err := json.MarshalIndent(dictionaries, "", " ")
if err != nil {
log.Panic(err)
}
fmt.Println(string(enc))

enc, err = json.MarshalIndent(tweetResponse.Meta, "", " ")
if err != nil {
log.Panic(err)
}
fmt.Println(string(enc))
}
70 changes: 70 additions & 0 deletions v2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const (
listUserMemberMaxResults = 100
userListFollowedMaxResults = 100
listuserFollowersMaxResults = 100
quoteTweetMaxResults = 100
quoteTweetMinResults = 10
)

// Client is used to make twitter v2 API callouts.
Expand Down Expand Up @@ -3918,3 +3920,71 @@ func (c *Client) ComplianceBatchJobLookup(ctx context.Context, jobType Complianc
RateLimit: rl,
}, nil
}

// QuoteTweetsLookup returns quote tweets for a tweet specificed by the requested tweet id
func (c *Client) QuoteTweetsLookup(ctx context.Context, tweetID string, opts QuoteTweetsLookupOpts) (*QuoteTweetsLookupResponse, error) {
switch {
case len(tweetID) == 0:
return nil, fmt.Errorf("quote tweets lookup: an id is required: %w", ErrParameter)
case opts.MaxResults == 0:
case opts.MaxResults < quoteTweetMinResults:
return nil, fmt.Errorf("quote tweets lookup: a min results [%d] is required [current: %d]: %w", quoteTweetMinResults, opts.MaxResults, ErrParameter)
case opts.MaxResults > quoteTweetMaxResults:
return nil, fmt.Errorf("quote tweets lookup: a max results [%d] is required [current: %d]: %w", quoteTweetMaxResults, opts.MaxResults, ErrParameter)
default:
}

ep := quoteTweetLookupEndpoint.urlID(c.Host, tweetID)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, ep, nil)
if err != nil {
return nil, fmt.Errorf("quote tweets lookup request: %w", err)
}
req.Header.Add("Accept", "application/json")
c.Authorizer.Add(req)
opts.addQuery(req)

resp, err := c.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("quote tweets lookup response: %w", err)
}
defer resp.Body.Close()

decoder := json.NewDecoder(resp.Body)

rl := rateFromHeader(resp.Header)

if resp.StatusCode != http.StatusOK {
e := &ErrorResponse{}
if err := decoder.Decode(e); err != nil {
return nil, &HTTPError{
Status: resp.Status,
StatusCode: resp.StatusCode,
URL: resp.Request.URL.String(),
RateLimit: rl,
}
}
e.StatusCode = resp.StatusCode
e.RateLimit = rl
return nil, e
}

respBody := struct {
*TweetRaw
Meta *QuoteTweetsLookupMeta `json:"meta"`
}{}

if err := decoder.Decode(&respBody); err != nil {
return nil, &ResponseDecodeError{
Name: "quote tweets lookup",
Err: err,
RateLimit: rl,
}
}

return &QuoteTweetsLookupResponse{
Raw: respBody.TweetRaw,
Meta: respBody.Meta,
RateLimit: rl,
}, nil
}
108 changes: 108 additions & 0 deletions v2/client_tweet_quote_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package twitter

import (
"context"
"io"
"log"
"net/http"
"reflect"
"strings"
"testing"
)

func TestClient_QuoteTweetsLookup(t *testing.T) {
type fields struct {
Authorizer Authorizer
Client *http.Client
Host string
}
type args struct {
tweetID string
opts QuoteTweetsLookupOpts
}
tests := []struct {
name string
fields fields
args args
want *QuoteTweetsLookupResponse
wantErr bool
}{
{
name: "success",
fields: fields{
Authorizer: &mockAuth{},
Host: "https://www.test.com",
Client: mockHTTPClient(func(req *http.Request) *http.Response {
if req.Method != http.MethodGet {
log.Panicf("the method is not correct %s %s", req.Method, http.MethodGet)
}
if strings.Contains(req.URL.String(), quoteTweetLookupEndpoint.urlID("", "tweet-1234")) == false {
log.Panicf("the url is not correct %s %s", req.URL.String(), listLookupEndpoint)
}
body := `{
"data": [
{
"id": "1503982413004914689",
"text": "RT @suhemparack: Super excited to share our course on Getting started with the #TwitterAPI v2 for academic research\n\nIf you know students w…"
}
],
"meta": {
"result_count": 1,
"next_token": "axdnchiqasch"
}
}`
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(body)),
Header: func() http.Header {
h := http.Header{}
h.Add(rateLimit, "15")
h.Add(rateRemaining, "12")
h.Add(rateReset, "1644461060")
return h
}(),
}
}),
},
args: args{
tweetID: "tweet-1234",
},
want: &QuoteTweetsLookupResponse{
Raw: &TweetRaw{
Tweets: []*TweetObj{
{
ID: "1503982413004914689",
Text: "RT @suhemparack: Super excited to share our course on Getting started with the #TwitterAPI v2 for academic research\n\nIf you know students w…",
},
},
},
Meta: &QuoteTweetsLookupMeta{
ResultCount: 1,
NextToken: "axdnchiqasch",
},
RateLimit: &RateLimit{
Limit: 15,
Remaining: 12,
Reset: Epoch(1644461060),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{
Authorizer: tt.fields.Authorizer,
Client: tt.fields.Client,
Host: tt.fields.Host,
}
got, err := c.QuoteTweetsLookup(context.Background(), tt.args.tweetID, tt.args.opts)
if (err != nil) != tt.wantErr {
t.Errorf("Client.QuoteTweetsLookup() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Client.QuoteTweetsLookup() = %v, want %v", got, tt.want)
}
})
}
}
1 change: 1 addition & 0 deletions v2/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const (
spaceTweetsLookupEndpoint endpoint = "2/spaces/{id}/tweets"
spaceSearchEndpoint endpoint = "2/spaces/search"
complianceJobsEndpiont endpoint = "2/compliance/jobs"
quoteTweetLookupEndpoint endpoint = "2/tweets/{id}/quote_tweets"

idTag = "{id}"
)
Expand Down
63 changes: 63 additions & 0 deletions v2/tweet_quote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package twitter

import (
"net/http"
"strconv"
"strings"
)

// QuoteTweetsLookupOpts are the options for the quote tweets
type QuoteTweetsLookupOpts struct {
MaxResults int
PaginationToken string
Expansions []Expansion
MediaFields []MediaField
PlaceFields []PlaceField
PollFields []PollField
TweetFields []TweetField
UserFields []UserField
}

func (qt QuoteTweetsLookupOpts) addQuery(req *http.Request) {
q := req.URL.Query()
if len(qt.Expansions) > 0 {
q.Add("expansions", strings.Join(expansionStringArray(qt.Expansions), ","))
}
if len(qt.MediaFields) > 0 {
q.Add("media.fields", strings.Join(mediaFieldStringArray(qt.MediaFields), ","))
}
if len(qt.PlaceFields) > 0 {
q.Add("place.fields", strings.Join(placeFieldStringArray(qt.PlaceFields), ","))
}
if len(qt.PollFields) > 0 {
q.Add("poll.fields", strings.Join(pollFieldStringArray(qt.PollFields), ","))
}
if len(qt.TweetFields) > 0 {
q.Add("tweet.fields", strings.Join(tweetFieldStringArray(qt.TweetFields), ","))
}
if len(qt.UserFields) > 0 {
q.Add("user.fields", strings.Join(userFieldStringArray(qt.UserFields), ","))
}
if qt.MaxResults > 0 {
q.Add("max_results", strconv.Itoa(qt.MaxResults))
}
if len(qt.PaginationToken) > 0 {
q.Add("pagination_token", qt.PaginationToken)
}
if len(q) > 0 {
req.URL.RawQuery = q.Encode()
}
}

// QuoteTweetsLookupResponse is the response from the quote tweet
type QuoteTweetsLookupResponse struct {
Raw *TweetRaw
Meta *QuoteTweetsLookupMeta
RateLimit *RateLimit
}

// QuoteTweetsLookupMeta is the meta data from the response
type QuoteTweetsLookupMeta struct {
ResultCount int `json:"result_count"`
NextToken string `json:"next_token"`
}

0 comments on commit 0dcb24d

Please sign in to comment.