Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added the basic client #132

Merged
merged 5 commits into from
Mar 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
}