Repository: sivchari/gotwtr
Pull request #44 (2021-10-12 05:43:44)

implementation likes look up by tweet
## TODO
- [x] define Options
- [x] define Response
- [x] create client method
- [x] test above

## REF
- [api reference](https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/get-tweets-id-liking_users)

sivchari: Please add test.
sivchari: please change.



Patch:
diff --git a/client.go b/client.go
index 6b780cd..59dcfc4 100644
--- a/client.go
+++ b/client.go
@@ -39,6 +39,7 @@ type Client interface {
 	LookUpUserByID(ctx context.Context, id string, opt ...*UserLookUpOption) (*UserLookUpByIDResponse, error)
 	LookUpUserByUserName(ctx context.Context, name string, opt ...*UserLookUpOption) (*UserLookUpByUserNameResponse, error)
 	LookUpUsersByUserNames(ctx context.Context, names []string, opt ...*UserLookUpOption) (*UsersLookUpByUserNamesResponse, error)
+	LookUpUsersWhoLiked(ctx context.Context, tweetID string, opt ...*LookUpUsersWhoLikedOpts) (*LookUpUsersWhoLikedResponse, error)
 	// PostFollowing(ctx context.Context, id string, tuid string) (*PostFollowingResponse, error)
 	// PostRetweet(ctx context.Context, uid string, tid string) (*PostRetweetResponse, error)
 	RetweetsLookup(ctx context.Context, id string, opt ...*RetweetsLookupOpts) (*RetweetsLookupResponse, error)
@@ -135,6 +136,10 @@ func (c *client) LookUpUsersByUserNames(ctx context.Context, names []string, opt
 	return lookUpUsersByUserNames(ctx, c, names, opt...)
 }
 
+func (c *client) LookUpUsersWhoLiked(ctx context.Context, tweetID string, opt ...*LookUpUsersWhoLikedOpts) (*LookUpUsersWhoLikedResponse, error) {
+	return lookUpUsersWhoLiked(ctx, c, tweetID, opt...)
+}
+
 func (c *client) PostFollowing(ctx context.Context, id string, tuid string) (*PostFollowingResponse, error) {
 	return postFollowing(ctx, c, id, tuid)
 }
@@ -143,6 +148,10 @@ func (c *client) PostRetweet(ctx context.Context, uid string, tid string) (*Post
 	return postRetweet(ctx, c, uid, tid)
 }
 
+func (c *client) RetrieveStreamRules(ctx context.Context, opt ...*RetrieveStreamRulesOption) (*RetrieveStreamRulesResponse, error) {
+	return retrieveStreamRules(ctx, c, opt...)
+}
+
 func (c *client) RetweetsLookup(ctx context.Context, id string, opt ...*RetweetsLookupOpts) (*RetweetsLookupResponse, error) {
 	return retweetsLookup(ctx, c, id, opt...)
 }
@@ -178,7 +187,3 @@ func (c *client) UserMentionTimeline(ctx context.Context, id string, opt ...*Use
 func (c *client) UserTweetTimeline(ctx context.Context, id string, opt ...*UserTweetTimelineOpts) (*UserTweetTimelineResponse, error) {
 	return userTweetTimeline(ctx, c, id, opt...)
 }
-
-func (c *client) RetrieveStreamRules(ctx context.Context, opt ...*RetrieveStreamRulesOption) (*RetrieveStreamRulesResponse, error) {
-	return retrieveStreamRules(ctx, c, opt...)
-}
diff --git a/endpoint.go b/endpoint.go
index 7a30b6a..08e93f0 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -5,6 +5,7 @@ var (
 	filteredStream    = "https://api.twitter.com/2/tweets/search/stream"
 	followingLookUp   = "https://api.twitter.com/2/users"
 	followersLookUp   = "https://api.twitter.com/2/users"
+	likesLookUpUsers  = "https://api.twitter.com/2/tweets"
 	recentTweetCounts = "https://api.twitter.com/2/tweets/counts/recent"
 	retweet           = "https://api.twitter.com/2/tweets"
 	sampleStream      = "https://api.twitter.com/2/tweets/sample/stream"
diff --git a/example/tweet/likes/main.go b/example/tweet/likes/main.go
new file mode 100644
index 0000000..5dc9b51
--- /dev/null
+++ b/example/tweet/likes/main.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/sivchari/gotwtr"
+)
+
+func main() {
+	client := gotwtr.New("key")
+	// get users who liked the tweet that id is "tweet_id"
+	t, err := client.LookUpUsersWhoLiked(context.Background(), "tweet_id")
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("------no option------")
+	for _, user := range t.Users {
+		fmt.Printf("id: %s, name: %s\n", user.ID, user.UserName)
+	}
+
+	// get users who liked the tweet that id is "tweet_id" with option
+	t, err = client.LookUpUsersWhoLiked(context.Background(), "tweet_id", &gotwtr.LookUpUsersWhoLikedOpts{
+		Expansions:  []gotwtr.Expansion{gotwtr.ExpansionPinnedTweetID},
+		UserFields:  []gotwtr.UserField{gotwtr.UserFieldCreatedAt},
+		TweetFields: []gotwtr.TweetField{gotwtr.TweetFieldCreatedAt},
+	})
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("------with option------")
+	for _, user := range t.Users {
+		fmt.Printf("id: %s, name: %s, created_at: %v\n", user.ID, user.UserName, user.CreatedAt)
+	}
+	for _, tweet := range t.Includes.Tweets {
+		fmt.Printf("tweet_id: %s, created_at: %v\n", tweet.ID, tweet.CreatedAt)
+	}
+}
diff --git a/likes_lookup.go b/likes_lookup.go
new file mode 100644
index 0000000..cb76aeb
--- /dev/null
+++ b/likes_lookup.go
@@ -0,0 +1,54 @@
+package gotwtr
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+func lookUpUsersWhoLiked(ctx context.Context, c *client, tweetID string, opt ...*LookUpUsersWhoLikedOpts) (*LookUpUsersWhoLikedResponse, error) {
+	// check id
+	if len(tweetID) == 0 {
+		return nil, errors.New("look up users who liked: tweetID parameter is required")
+	}
+	likesLookUpUsersURL := likesLookUpUsers + "/" + tweetID + "/liking_users"
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, likesLookUpUsersURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("look up users who liked: new request with ctx: %w", err)
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	var topt LookUpUsersWhoLikedOpts
+	switch len(opt) {
+	case 0:
+		// do nothing
+	case 1:
+		topt = *opt[0]
+	default:
+		return nil, errors.New("look up users who liked: only one option is allowed")
+	}
+	topt.addQuery(req)
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("look up users who liked response: %w", err)
+	}
+	defer resp.Body.Close()
+
+	var usersWhoLiked LookUpUsersWhoLikedResponse
+	if err := json.NewDecoder(resp.Body).Decode(&usersWhoLiked); err != nil {
+		return nil, fmt.Errorf("look up users who liked decode: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &usersWhoLiked, &HTTPError{
+			APIName: "look up users who liked",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+
+	return &usersWhoLiked, nil
+}
diff --git a/tweet.go b/tweet.go
index b2491ec..2a14cd8 100644
--- a/tweet.go
+++ b/tweet.go
@@ -340,3 +340,89 @@ type StreamResponse struct {
 	done   chan struct{}
 	wg     *sync.WaitGroup
 }
+
+type LookUpUsersWhoLikedWithheld struct {
+	Scope        string   `json:"scope"`
+	CountryCodes []string `json:"country_codes"`
+}
+
+type LookUpUsersWhoLikedEntity struct {
+	URL         *LookUpUsersWhoLikedURL         `json:"url"`
+	Description *LookUpUsersWhoLikedDescription `json:"description"`
+}
+
+type LookUpUsersWhoLikedURL struct {
+	URLs []*LookUpUsersWhoLikedURLContent `json:"urls"`
+}
+
+type LookUpUsersWhoLikedDescription struct {
+	URLs     []*LookUpUsersWhoLikedURLContent `json:"urls"`
+	HashTags []*LookUpUsersWhoLikedHashTag    `json:"hashtags"`
+	Mentions []*LookUpUsersWhoLikedMention    `json:"mentions"`
+}
+
+type LookUpUsersWhoLikedURLContent struct {
+	Start       int    `json:"start"`
+	End         int    `json:"end"`
+	URL         string `json:"url"`
+	ExpandedURL string `json:"expanded_url"`
+	DisplayURL  string `json:"display_url"`
+}
+
+type LookUpUsersWhoLikedHashTag struct {
+	Start   int    `json:"start"`
+	End     int    `json:"end"`
+	HashTag string `json:"hashtag"`
+}
+
+type LookUpUsersWhoLikedMention struct {
+	Start    int                           `json:"start"`
+	End      int                           `json:"end"`
+	UserName string                        `json:"username"`
+	CashTags []*LookUpUsersWhoLikedCashTag `json:"cashtags"`
+}
+
+type LookUpUsersWhoLikedCashTag struct {
+	Start   int    `json:"start"`
+	End     int    `json:"end"`
+	CashTag string `json:"cashtag"`
+}
+
+type LookUpUsersWhoLikedPublicMetrics struct {
+	FollowersCount int `json:"followers_count"`
+	FollowingCount int `json:"following_count"`
+	TweetCount     int `json:"tweet_count"`
+	ListedCount    int `json:"listed_count"`
+}
+
+type LookUpUsersWhoLiked struct {
+	ID              string                            `json:"id"`
+	Name            string                            `json:"name"`
+	UserName        string                            `json:"username"`
+	CreatedAt       string                            `json:"created_at,omitempty"`
+	Protected       bool                              `json:"protected,omitempty"`
+	Withheld        *LookUpUsersWhoLikedWithheld      `json:"withheld,omitempty"`
+	Location        string                            `json:"location,omitempty"`
+	URL             string                            `json:"url,omitempty"`
+	Description     string                            `json:"description,omitempty"`
+	Verified        bool                              `json:"verified,omitempty"`
+	Entities        *LookUpUsersWhoLikedEntity        `json:"entities,omitempty"`
+	ProfileImageURL string                            `json:"profile_image_url,omitempty"`
+	PublicMetrics   *LookUpUsersWhoLikedPublicMetrics `json:"public_metrics,omitempty"`
+	PinnedTweetID   string                            `json:"pinned_tweet_id,omitempty"`
+}
+
+type LookUpUsersWhoLikedResponse struct {
+	Users    []*LookUpUsersWhoLiked       `json:"data"`
+	Includes *LookUpUsersWhoLikedIncludes `json:"includes,omitempty"`
+	Meta     *LookUpUsersWhoLikedMeta     `json:"meta"`
+	Errors   []*APIResponseError          `json:"errors,omitempty"`
+}
+
+type LookUpUsersWhoLikedIncludes struct {
+	Tweets []*Tweet `json:"tweets"`
+}
+
+type LookUpUsersWhoLikedMeta struct {
+	ResultCount int `json:"result_count"`
+}
diff --git a/tweet_option.go b/tweet_option.go
index fafb6e7..114f45b 100644
--- a/tweet_option.go
+++ b/tweet_option.go
@@ -296,6 +296,40 @@ func (t *TweetCountsOption) addQuery(req *http.Request) {
 	}
 }
 
+type LookUpUsersWhoLikedOpts struct {
+	Expansions  []Expansion
+	MediaFields []MediaField
+	PlaceFields []PlaceField
+	PollFields  []PollField
+	TweetFields []TweetField
+	UserFields  []UserField
+}
+
+func (l *LookUpUsersWhoLikedOpts) addQuery(req *http.Request) {
+	q := req.URL.Query()
+	if len(l.Expansions) > 0 {
+		q.Add("expansions", strings.Join(expansionsToString(l.Expansions), ","))
+	}
+	if len(l.MediaFields) > 0 {
+		q.Add("media.fields", strings.Join(mediaFieldsToString(l.MediaFields), ","))
+	}
+	if len(l.PlaceFields) > 0 {
+		q.Add("place.fields", strings.Join(placeFieldsToString(l.PlaceFields), ","))
+	}
+	if len(l.PollFields) > 0 {
+		q.Add("poll.fields", strings.Join(pollFieldsToString(l.PollFields), ","))
+	}
+	if len(l.TweetFields) > 0 {
+		q.Add("tweet.fields", strings.Join(tweetFieldsToString(l.TweetFields), ","))
+	}
+	if len(l.UserFields) > 0 {
+		q.Add("user.fields", strings.Join(userFieldsToString(l.UserFields), ","))
+	}
+	if len(q) > 0 {
+		req.URL.RawQuery = q.Encode()
+	}
+}
+
 type RetweetsLookupOpts struct {
 	Expansions  []Expansion
 	MediaFields []MediaField


Test patch:
diff --git a/likes_lookup_test.go b/likes_lookup_test.go
new file mode 100644
index 0000000..3503f65
--- /dev/null
+++ b/likes_lookup_test.go
@@ -0,0 +1,232 @@
+package gotwtr_test
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/sivchari/gotwtr"
+)
+
+func Test_lookUpUsersWhoLiked(t *testing.T) {
+	t.Parallel()
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		opt    []*gotwtr.LookUpUsersWhoLikedOpts
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.LookUpUsersWhoLikedResponse
+		wantErr bool
+	}{
+		{
+			name: "200 success with no optional fileds",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					body := `{
+						"data": [
+							{
+								"id": "1065249714214457345",
+								"name": "Spaces",
+								"username": "TwitterSpaces"
+							},
+							{
+								"id": "783214",
+								"name": "Twitter",
+								"username": "Twitter"
+							},
+							{
+								"id": "1526228120",
+								"name": "Twitter Data",
+								"username": "TwitterData"
+							}
+						]
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+			},
+			want: &gotwtr.LookUpUsersWhoLikedResponse{
+				Users: []*gotwtr.LookUpUsersWhoLiked{
+					{
+						ID:       "1065249714214457345",
+						Name:     "Spaces",
+						UserName: "TwitterSpaces",
+					},
+					{
+						ID:       "783214",
+						Name:     "Twitter",
+						UserName: "Twitter",
+					},
+					{
+						ID:       "1526228120",
+						Name:     "Twitter Data",
+						UserName: "TwitterData",
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "200 success with optional fileds",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					body := `{
+						"data": [
+							{
+								"id": "1065249714214457345",
+								"created_at": "2018-11-21T14:24:58.000Z",
+								"name": "Spaces",
+								"pinned_tweet_id": "1389270063807598594",
+								"description": "Twitter Spaces is where live audio conversations happen.",
+								"username": "TwitterSpaces"
+							},
+							{
+								"id": "783214",
+								"created_at": "2007-02-20T14:35:54.000Z",
+								"name": "Twitter",
+								"description": "What's happening?!",
+								"username": "Twitter"
+							},
+							{
+								"id": "1526228120",
+								"created_at": "2013-06-17T23:57:45.000Z",
+								"name": "Twitter Data",
+								"description": "Data-driven insights about notable moments and conversations from Twitter, Inc., plus tips and tricks to help you get the most out of Twitter data.",
+								"username": "TwitterData"
+							},
+							{
+								"id": "2244994945",
+								"created_at": "2013-12-14T04:35:55.000Z",
+								"name": "Twitter Dev",
+								"pinned_tweet_id": "1354143047324299264",
+								"description": "The voice of the #TwitterDev team and your official source for updates, news, and events, related to the #TwitterAPI.",
+								"username": "TwitterDev"
+							},
+							{
+								"id": "6253282",
+								"created_at": "2007-05-23T06:01:13.000Z",
+								"name": "Twitter API",
+								"pinned_tweet_id": "1293595870563381249",
+								"description": "Tweets about changes and service issues. Follow @TwitterDev for more.",
+								"username": "TwitterAPI"
+							}
+						],
+						"includes": {
+							"tweets": [
+								{
+									"id": "1389270063807598594",
+									"text": "now, everyone with 600 or more followers can host a Space.nnbased on what we've learned, these accounts are likely to have a good experience hosting because of their existing audience. before bringing the ability to create a Space to everyone, we're focused on a few things. :thread:"
+								},
+								{
+									"id": "1354143047324299264",
+									"text": "Academics are one of the biggest groups using the #TwitterAPI to research what's happening. Their work helps make the world (&amp; Twitter) a better place, and now more than ever, we must enable more of it. nIntroducing :drum_with_drumsticks: the Academic Research product track!nhttps://t.co/nOFiGewAV2"
+								},
+								{
+									"id": "1293595870563381249",
+									"text": "Twitter API v2: Early Access releasednnToday we announced Early Access to the first endpoints of the new Twitter API!nn#TwitterAPI #EarlyAccess #VersionBump https://t.co/g7v3aeIbtQ"
+								}
+							]
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				opt: []*gotwtr.LookUpUsersWhoLikedOpts{
+					{
+						Expansions:  []gotwtr.Expansion{gotwtr.ExpansionPinnedTweetID},
+						UserFields:  []gotwtr.UserField{gotwtr.UserFieldCreatedAt},
+						TweetFields: []gotwtr.TweetField{gotwtr.TweetFieldCreatedAt},
+					},
+				},
+			},
+			want: &gotwtr.LookUpUsersWhoLikedResponse{
+				Users: []*gotwtr.LookUpUsersWhoLiked{
+					{
+						ID:            "1065249714214457345",
+						CreatedAt:     "2018-11-21T14:24:58.000Z",
+						Name:          "Spaces",
+						PinnedTweetID: "1389270063807598594",
+						Description:   "Twitter Spaces is where live audio conversations happen.",
+						UserName:      "TwitterSpaces",
+					},
+					{
+						ID:          "783214",
+						CreatedAt:   "2007-02-20T14:35:54.000Z",
+						Name:        "Twitter",
+						Description: "What's happening?!",
+						UserName:    "Twitter",
+					},
+					{
+						ID:          "1526228120",
+						CreatedAt:   "2013-06-17T23:57:45.000Z",
+						Name:        "Twitter Data",
+						Description: "Data-driven insights about notable moments and conversations from Twitter, Inc., plus tips and tricks to help you get the most out of Twitter data.",
+						UserName:    "TwitterData",
+					},
+					{
+						ID:            "2244994945",
+						CreatedAt:     "2013-12-14T04:35:55.000Z",
+						Name:          "Twitter Dev",
+						PinnedTweetID: "1354143047324299264",
+						Description:   "The voice of the #TwitterDev team and your official source for updates, news, and events, related to the #TwitterAPI.",
+						UserName:      "TwitterDev",
+					},
+					{
+						ID:            "6253282",
+						CreatedAt:     "2007-05-23T06:01:13.000Z",
+						Name:          "Twitter API",
+						PinnedTweetID: "1293595870563381249",
+						Description:   "Tweets about changes and service issues. Follow @TwitterDev for more.",
+						UserName:      "TwitterAPI",
+					},
+				},
+				Includes: &gotwtr.LookUpUsersWhoLikedIncludes{
+					Tweets: []*gotwtr.Tweet{
+						{
+							ID:   "1389270063807598594",
+							Text: "now, everyone with 600 or more followers can host a Space.nnbased on what we've learned, these accounts are likely to have a good experience hosting because of their existing audience. before bringing the ability to create a Space to everyone, we're focused on a few things. :thread:",
+						},
+						{
+							ID:   "1354143047324299264",
+							Text: "Academics are one of the biggest groups using the #TwitterAPI to research what's happening. Their work helps make the world (&amp; Twitter) a better place, and now more than ever, we must enable more of it. nIntroducing :drum_with_drumsticks: the Academic Research product track!nhttps://t.co/nOFiGewAV2",
+						},
+						{
+							ID:   "1293595870563381249",
+							Text: "Twitter API v2: Early Access releasednnToday we announced Early Access to the first endpoints of the new Twitter API!nn#TwitterAPI #EarlyAccess #VersionBump https://t.co/g7v3aeIbtQ",
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			c := gotwtr.New("test-key", gotwtr.WithHTTPClient(tt.args.client))
+			got, err := c.LookUpUsersWhoLiked(tt.args.ctx, "tweet_id", tt.args.opt...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("LookUpUsersWhoLiked() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(got, tt.want); diff != "" {
+				t.Errorf("LookUpUsersWhoLiked() mismatch (-want +got):\n%s", diff)
+				return
+			}
+		})
+	}
+}