Repository: sivchari/gotwtr
Pull request #157 (2022-11-22 14:53:50)

Feat me api
Impl me API.

codecov[bot]: # [Codecov](https://codecov.io/gh/sivchari/gotwtr/pull/157?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sivchari) Report
> Merging [#157](https://codecov.io/gh/sivchari/gotwtr/pull/157?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sivchari) (7492bff) into [main](https://codecov.io/gh/sivchari/gotwtr/commit/b187ec64db21f2bcf0bab05d640cc69641c9975b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sivchari) (b187ec6) will **decrease** coverage by `0.30%`.
> The diff coverage is `34.28%`.

```diff
@@            Coverage Diff             @@
##             main     #157      +/-   ##
==========================================
- Coverage   56.42%   56.11%   -0.31%     
==========================================
  Files          40       41       +1     
  Lines        2492     2527      +35     
==========================================
+ Hits         1406     1418      +12     
- Misses        653      668      +15     
- Partials      433      441       +8     
```


| [Impacted Files](https://codecov.io/gh/sivchari/gotwtr/pull/157?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sivchari) | Coverage Δ | |
|---|---|---|
| [user\_option.go](https://codecov.io/gh/sivchari/gotwtr/pull/157/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sivchari#diff-dXNlcl9vcHRpb24uZ28=) | `60.00% <11.11%> (-8.63%)` | :arrow_down: |
| [me.go](https://codecov.io/gh/sivchari/gotwtr/pull/157/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sivchari#diff-bWUuZ28=) | `40.00% <40.00%> (ø)` | |
| [client.go](https://codecov.io/gh/sivchari/gotwtr/pull/157/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sivchari#diff-Y2xpZW50Lmdv) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sivchari)


Patch:
diff --git a/client.go b/client.go
index 60de62f..35e95ab 100644
--- a/client.go
+++ b/client.go
@@ -16,6 +16,7 @@ const (
 
 type OAuth interface {
 	GenerateAppOnlyBearerToken(ctx context.Context) (bool, error)
+	// TODO: oauth2/invalidate_token
 }
 
 type Tweets interface {
@@ -37,7 +38,7 @@ type Tweets interface {
 	// Manage Tweets
 	DeleteTweet(ctx context.Context, tweetID string) (*DeleteTweetResponse, error)
 	PostTweet(ctx context.Context, body *PostTweetOption) (*PostTweetResponse, error)
-	// TODO: Quote Tweets
+	// TODO: /2/tweets/:id/quote_tweets
 	// Retweets
 	UndoRetweet(ctx context.Context, userID string, sourceTweetID string) (*UndoRetweetResponse, error)
 	RetweetsLookup(ctx context.Context, tweetID string, opt ...*RetweetsLookupOption) (*RetweetsResponse, error)
@@ -79,7 +80,7 @@ type Users interface {
 	RetrieveSingleUserWithID(ctx context.Context, userID string, opt ...*RetrieveUserOption) (*UserResponse, error)
 	RetrieveMultipleUsersWithUserNames(ctx context.Context, userNames []string, opt ...*RetrieveUserOption) (*UsersResponse, error)
 	RetrieveSingleUserWithUserName(ctx context.Context, userName string, opt ...*RetrieveUserOption) (*UserResponse, error)
-	// TODO: /2/users/me
+	Me(ctx context.Context, opt ...*MeOption) (*MeResponse, error)
 }
 
 type Spaces interface {
@@ -342,6 +343,11 @@ func (c *Client) RetrieveSingleUserWithUserName(ctx context.Context, userName st
 	return retrieveSingleUserWithUserName(ctx, c.client, userName, opt...)
 }
 
+// Me returns information about an authorized user.
+func (c *Client) Me(ctx context.Context, opt ...*MeOption) (*MeResponse, error) {
+	return me(ctx, c.client, opt...)
+}
+
 // Followers returns a list of users who are followers of the specified userID.
 func (c *Client) Followers(ctx context.Context, userID string, opt ...*FollowOption) (*FollowersResponse, error) {
 	return followers(ctx, c.client, userID, opt...)
diff --git a/endpoint.go b/endpoint.go
index 520b331..b71bba5 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -3,6 +3,7 @@ package gotwtr
 // EndpointURL is the base URL for the Twitter V2 API.
 const (
 	generateAppOnlyBearerTokenURL = "https://api.twitter.com/oauth2/token?grant_type=client_credentials"
+	// TODO: oauth2/invalidate_token
 )
 
 const (
@@ -24,7 +25,7 @@ const (
 	// Manage Tweets
 	deleteTweetURL = "https://api.twitter.com/2/tweets/%v"
 	postTweetURL   = "https://api.twitter.com/2/tweets"
-	// TODO: Quote Tweets
+	// TODO: /2/tweets/:id/quote_tweets
 	// Retweets
 	undoRetweetURL    = "https://api.twitter.com/2/users/%v/retweets/%v"
 	retweetsLookupURL = "https://api.twitter.com/2/tweets/%v/retweeted_by"
@@ -66,7 +67,7 @@ const (
 	retrieveSingleUserWithIDURL           = "https://api.twitter.com/2/users/%v"
 	retrieveMultipleUsersWithUserNamesURL = "https://api.twitter.com/2/users/by?usernames="
 	retrieveSingleUserWithUserNameURL     = "https://api.twitter.com/2/users/by/username/%v"
-	// TODO: /2/users/me
+	meURL                                 = "https://api.twitter.com/2/users/me"
 )
 
 const (
diff --git a/me.go b/me.go
new file mode 100644
index 0000000..cdbe4e8
--- /dev/null
+++ b/me.go
@@ -0,0 +1,47 @@
+package gotwtr
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+func me(ctx context.Context, c *client, opt ...*MeOption) (*MeResponse, error) {
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("me new request with ctx: %w", err)
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	var mopt MeOption
+	switch len(opt) {
+	case 0:
+		// do nothing
+	case 1:
+		mopt = *opt[0]
+	default:
+		return nil, errors.New("me: only one option is allowed")
+	}
+	mopt.addQuery(req)
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("me response: %w", err)
+	}
+	defer resp.Body.Close()
+
+	var me MeResponse
+	if err := json.NewDecoder(resp.Body).Decode(&me); err != nil {
+		return nil, fmt.Errorf("me decode: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &me, &HTTPError{
+			APIName: "me",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+	return &me, nil
+}
diff --git a/user.go b/user.go
index 06311af..4b2f3a9 100644
--- a/user.go
+++ b/user.go
@@ -24,7 +24,7 @@ const (
 	UserFieldURL             UserField = "url"
 	UserFieldUserName        UserField = "username"
 	UserFieldVerified        UserField = "verified"
-	UserFieldWithHeld        UserField = "withheld"
+	UserFieldWithHeld        UserField = "withhel"
 )
 
 type User struct {
@@ -233,3 +233,35 @@ type MutesMeta struct {
 type MutingBody struct {
 	TargetUserID string `json:"target_user_id"`
 }
+
+type MeResponse struct {
+	Me     *Me                 `json:"data"`
+	Errors []*APIResponseError `json:"errors,omitempty"`
+}
+
+type Me 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        *MeWithheld        `json:"withheld,omitempty"`
+	Location        string             `json:"location,omitempty"`
+	URL             string             `json:"url,omitempty"`
+	Description     string             `json:"description,omitempty"`
+	Verified        bool               `json:"verified,omitempty"`
+	Entities        *UserEntity        `json:"entities,omitempty"`
+	ProfileImageURL string             `json:"profile_image_url,omitempty"`
+	PublicMetrics   *UserPublicMetrics `json:"public_metrics,omitempty"`
+	PinnedTweetID   string             `json:"pinned_tweet_id,omitempty"`
+	Includes        *MeIncludes        `json:"includes,omitempty"`
+}
+
+type MeWithheld struct {
+	CountryCodes []string `json:"country_codes"`
+	Scope        string   `json:"scope"`
+}
+
+type MeIncludes struct {
+	Tweets []*Tweet
+}
diff --git a/user_option.go b/user_option.go
index bb90386..b07aa76 100644
--- a/user_option.go
+++ b/user_option.go
@@ -125,3 +125,25 @@ func userFieldsToString(ufs []UserField) []string {
 	}
 	return slice
 }
+
+type MeOption struct {
+	Expansions  []Expansion
+	TweetFields []TweetField
+	UserFields  []UserField
+}
+
+func (m *MeOption) addQuery(req *http.Request) {
+	q := req.URL.Query()
+	if len(m.Expansions) > 0 {
+		q.Add("expansions", strings.Join(expansionsToString(m.Expansions), ","))
+	}
+	if len(m.TweetFields) > 0 {
+		q.Add("tweet.fields", strings.Join(tweetFieldsToString(m.TweetFields), ","))
+	}
+	if len(m.UserFields) > 0 {
+		q.Add("user.fields", strings.Join(userFieldsToString(m.UserFields), ","))
+	}
+	if len(q) > 0 {
+		req.URL.RawQuery = q.Encode()
+	}
+}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1409543..09c0394 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -38,7 +38,3 @@ jobs:
       - name: test
         run: go test -race -coverprofile="coverage.txt" -covermode=atomic ./...
 
-      - name: upload coverage
-        uses: codecov/codecov-action@v1
-        with:
-          token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/example_test.go b/example_test.go
index 6586cba..a178e3a 100644
--- a/example_test.go
+++ b/example_test.go
@@ -311,6 +311,15 @@ func ExampleClient_RetrieveMultipleUsersWithUserNames() {
 	}
 }
 
+func ExampleClient_Me() {
+	client := gotwtr.New("key")
+	me, err := client.Me(context.Background())
+	if err != nil {
+		log.Fatal(err)
+	}
+	log.Printf("me is %+v", me)
+}
+
 func ExampleClient_RetrieveSingleUserWithUserName() {
 	client := gotwtr.New("key")
 	un, err := client.RetrieveSingleUserWithUserName(context.Background(), "username")
diff --git a/hide_replies_test.go b/hide_replies_test.go
index 7ab8126..a472c65 100644
--- a/hide_replies_test.go
+++ b/hide_replies_test.go
@@ -13,9 +13,9 @@ import (
 )
 
 var (
-	//go:embed embed/hide_replies_hidden.json
+	//go:embed testdata/hide_replies_hidden.json
 	hideRepliesHidden []byte
-	//go:embed embed/hide_replies_unhidden.json
+	//go:embed testdata/hide_replies_unhidden.json
 	hideRepliesUnhidden []byte
 )
 
diff --git a/me_test.go b/me_test.go
new file mode 100644
index 0000000..045f525
--- /dev/null
+++ b/me_test.go
@@ -0,0 +1,70 @@
+package gotwtr_test
+
+import (
+	"context"
+	_ "embed"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"github.com/sivchari/gotwtr"
+)
+
+//go:embed testdata/me.json
+var me []byte
+
+func Test_me(t *testing.T) {
+	t.Parallel()
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		opt    []*gotwtr.MeOption
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.MeResponse
+		wantErr bool
+	}{
+		{
+			name: "200 ok default payload",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(string(me))),
+					}
+				}),
+				opt: []*gotwtr.MeOption{},
+			},
+			want: &gotwtr.MeResponse{
+				Me: &gotwtr.Me{
+					ID:       "2244994945",
+					Name:     "TwitterDev",
+					UserName: "Twitter Dev",
+				},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			c := gotwtr.New("key", gotwtr.WithHTTPClient(tt.args.client))
+			got, err := c.Me(tt.args.ctx, tt.args.opt...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("client.Me() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(tt.want, got); diff != "" {
+				t.Errorf("client.Me() mismatch (-want +got):\n%s", diff)
+				return
+			}
+		})
+	}
+}
diff --git a/embed/count_all_tweets.json b/testdata/count_all_tweets.json
similarity index 100%
rename from embed/count_all_tweets.json
rename to testdata/count_all_tweets.json
diff --git a/embed/hide_replies_hidden.json b/testdata/hide_replies_hidden.json
similarity index 100%
rename from embed/hide_replies_hidden.json
rename to testdata/hide_replies_hidden.json
diff --git a/embed/hide_replies_unhidden.json b/testdata/hide_replies_unhidden.json
similarity index 100%
rename from embed/hide_replies_unhidden.json
rename to testdata/hide_replies_unhidden.json
diff --git a/testdata/me.json b/testdata/me.json
new file mode 100644
index 0000000..31ec774
--- /dev/null
+++ b/testdata/me.json
@@ -0,0 +1,7 @@
+{
+  "data": {
+    "id": "2244994945",
+    "name": "TwitterDev",
+    "username": "Twitter Dev"
+  }
+}
diff --git a/tweet_counts_test.go b/tweet_counts_test.go
index ae00571..d782a3a 100644
--- a/tweet_counts_test.go
+++ b/tweet_counts_test.go
@@ -15,7 +15,7 @@ import (
 )
 
 var (
-	//go:embed embed/count_all_tweets.json
+	//go:embed testdata/count_all_tweets.json
 	countAllTweets []byte
 )