Repository: sivchari/gotwtr
Pull request #28 (2021-10-05 12:30:40)

[IMPL] Recent Tweet Counts (fixed)
I've implemented https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent.
Note that
- The test data is Self-created data.
- I didn't implement https://developer.twitter.com/en/docs/twitter-api/tweets/counts/api-reference/get-tweets-counts-all (because this feature is only for the Academic Research product track. )
If it is inappropriate, please say so in the review comments.


Patch:
diff --git a/client.go b/client.go
index f05c6be..98a11ce 100644
--- a/client.go
+++ b/client.go
@@ -25,6 +25,7 @@ type Client interface {
 	// Poll
 	// Place
 	SearchRecentTweets(ctx context.Context, query string, opt ...*TweetSearchOption) (*TweetSearchResponse, error)
+	CountsRecentTweet(ctx context.Context, query string, opt ...*TweetCountsOption) (*TweetCountsResponse, error)
 }
 
 var _ Client = (*client)(nil)
@@ -69,3 +70,7 @@ func (c *client) UserMentionTimeline(ctx context.Context, id string, opt ...*Use
 func (c *client) SearchRecentTweets(ctx context.Context, query string, opt ...*TweetSearchOption) (*TweetSearchResponse, error) {
 	return searchRecentTweets(ctx, c, query, opt...)
 }
+
+func (c *client) CountsRecentTweet(ctx context.Context, query string, opt ...*TweetCountsOption) (*TweetCountsResponse, error) {
+	return countsRecentTweet(ctx, c, query, opt...)
+}
diff --git a/endpoint.go b/endpoint.go
index 01feba4..e048c7a 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -5,4 +5,5 @@ var (
 	tweetLookUp       = "https://api.twitter.com/2/tweets"
 	timeline          = "https://api.twitter.com/2/users"
 	tweetRecentSearch = "https://api.twitter.com/2/tweets/search/recent"
+	recentTweetCounts = "https://api.twitter.com/2/tweets/counts/recent"
 )
diff --git a/example/tweet/counts/counts.go b/example/tweet/counts/counts.go
new file mode 100644
index 0000000..799a79c
--- /dev/null
+++ b/example/tweet/counts/counts.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+
+	"github.com/sivchari/gotwtr"
+)
+
+func main() {
+	client := gotwtr.New("key")
+	ts, err := client.CountsRecentTweet(context.Background(), "from:TwitterDev")
+	if err != nil {
+		log.Fatal(err)
+	}
+	println(ts.Meta.TotalTweetCount)
+	for _, t := range ts.Counts {
+		fmt.Println(t)
+	}
+}
diff --git a/example/tweet/search/main.go b/example/tweet/search/main.go
index da8141a..c5ec0e8 100644
--- a/example/tweet/search/main.go
+++ b/example/tweet/search/main.go
@@ -8,7 +8,7 @@ import (
 )
 
 func main() {
-	client := gotwtr.New("AAAAAAAAAAAAAAAAAAAAAIWbBAEAAAAA8ahKqaHCJ%2FVsAi%2F4GQeTIw31Ioc%3DjzFYjJuReRDZW0gHRCQHCsVoPA3vMrgDOUptyzZepLEFBImSQr")
+	client := gotwtr.New("key")
 	tsr, err := client.SearchRecentTweets(context.Background(), "go", &gotwtr.TweetSearchOption{
 		TweetFields: []gotwtr.TweetField{
 			gotwtr.TweetFieldAuthorID,
diff --git a/tweet.go b/tweet.go
index f53db0c..ec61cea 100644
--- a/tweet.go
+++ b/tweet.go
@@ -207,3 +207,18 @@ type TweetSearchMeta struct {
 	OldestID    string `json:"oldest_id"`
 	NextToken   string `json:"next_token,omitempty"`
 }
+
+type TimeseriesCount struct {
+	Start      string `json:"start"`
+	End        string `json:"end"`
+	TweetCount int    `json:"tweet_count"`
+}
+
+type TweetCountMeta struct {
+	TotalTweetCount int `json:"total_tweet_count"`
+}
+
+type TweetCountsResponse struct {
+	Counts []*TimeseriesCount `json:"data"`
+	Meta   *TweetCountMeta    `json:"meta"`
+}
diff --git a/tweet_counts.go b/tweet_counts.go
new file mode 100644
index 0000000..903f4a4
--- /dev/null
+++ b/tweet_counts.go
@@ -0,0 +1,58 @@
+package gotwtr
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+func countsRecentTweet(ctx context.Context, c *client, query string, opt ...*TweetCountsOption) (*TweetCountsResponse, error) {
+	switch {
+	case len(query) == 0:
+		return nil, errors.New("tweet counts: query parameter is required")
+	case tweetSearchMaxQueryLength < len(query):
+		return nil, errors.New("tweet counts: query parameter length is less than 512")
+	default:
+	}
+
+	recentTweetCounts := recentTweetCounts + "?query=" + query
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, recentTweetCounts, nil)
+	if err != nil {
+		return nil, fmt.Errorf("tweet counts new request with ctx: %w", err)
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	var topt TweetCountsOption
+	switch len(opt) {
+	case 0:
+		// do nothing
+	case 1:
+		topt = *opt[0]
+	default:
+		return nil, errors.New("tweet counts: only one option is allowed")
+	}
+	topt.addQuery(req)
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("tweet counts response: %w", err)
+	}
+	defer resp.Body.Close()
+
+	var tweet TweetCountsResponse
+	if err := json.NewDecoder(resp.Body).Decode(&tweet); err != nil {
+		return nil, fmt.Errorf("tweet counts decode: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &tweet, &HTTPError{
+			APIName: "tweet counts",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+
+	return &tweet, nil
+}
diff --git a/tweet_option.go b/tweet_option.go
index efc339f..89a17cd 100644
--- a/tweet_option.go
+++ b/tweet_option.go
@@ -228,3 +228,35 @@ func (t UserMentionTimelineOpts) addQuery(req *http.Request) {
 		req.URL.RawQuery = q.Encode()
 	}
 }
+
+type TweetCountsOption struct {
+	StartTime   time.Time
+	EndTime     time.Time
+	SinceID     string
+	UntilID     string
+	Granularity string
+}
+
+func (t *TweetCountsOption) addQuery(req *http.Request) {
+	q := req.URL.Query()
+	if !t.StartTime.IsZero() {
+		// YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339).
+		q.Add("start_time", t.StartTime.Format(time.RFC3339))
+	}
+	if !t.EndTime.IsZero() {
+		// YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339).
+		q.Add("end_time", t.EndTime.Format(time.RFC3339))
+	}
+	if len(t.SinceID) > 0 {
+		q.Add("since_id", t.SinceID)
+	}
+	if len(t.UntilID) > 0 {
+		q.Add("until_id", t.UntilID)
+	}
+	if len(t.Granularity) > 0 {
+		q.Add("granularity", t.Granularity)
+	}
+	if len(q) > 0 {
+		req.URL.RawQuery = q.Encode()
+	}
+}


Test patch:
diff --git a/tweet_counts_test.go b/tweet_counts_test.go
new file mode 100644
index 0000000..5996c93
--- /dev/null
+++ b/tweet_counts_test.go
@@ -0,0 +1,149 @@
+package gotwtr_test
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/sivchari/gotwtr"
+)
+
+func Test_client_TweetCounts(t *testing.T) {
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		query  string
+		opt    []*gotwtr.TweetCountsOption
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.TweetCountsResponse
+		wantErr bool
+	}{
+		{
+			name: "200 ok default",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"data": [
+							{
+								"end": "2021-09-27T15:00:00.000Z",
+								"start": "2021-09-27T14:00:00.000Z",
+								"tweet_count": 2
+							},
+							{
+								"end": "2021-09-27T16:00:00.000Z",
+								"start": "2021-09-27T15:00:00.000Z",
+								"tweet_count": 2
+							}
+						],
+						"meta": {
+        					"total_tweet_count": 4
+    					}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				query: "from:TwitterDev",
+				opt:   []*gotwtr.TweetCountsOption{},
+			},
+			want: &gotwtr.TweetCountsResponse{
+				Counts: []*gotwtr.TimeseriesCount{
+					{
+						Start:      "2021-09-27T14:00:00.000Z",
+						End:        "2021-09-27T15:00:00.000Z",
+						TweetCount: 2,
+					},
+					{
+						Start:      "2021-09-27T15:00:00.000Z",
+						End:        "2021-09-27T16:00:00.000Z",
+						TweetCount: 2,
+					},
+				},
+				Meta: &gotwtr.TweetCountMeta{
+					TotalTweetCount: 4,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "200 ok specify optional fields",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"data": [
+							{
+								"end": "2021-09-28T00:00:00.000Z",
+								"start": "2021-09-27T00:00:00.000Z",
+								"tweet_count": 4
+							},
+							{
+								"end": "2021-09-29T00:00:00.000Z",
+								"start": "2021-09-28T00:00:00.000Z",
+								"tweet_count": 3
+							}
+						],
+						"meta": {
+        					"total_tweet_count": 7
+    					}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				query: "from:TwitterDev",
+				opt: []*gotwtr.TweetCountsOption{
+					{
+						StartTime:   time.Now(),
+						Granularity: "day",
+						SinceID:     "0",
+					},
+				},
+			},
+			want: &gotwtr.TweetCountsResponse{
+				Counts: []*gotwtr.TimeseriesCount{
+					{
+						Start:      "2021-09-27T00:00:00.000Z",
+						End:        "2021-09-28T00:00:00.000Z",
+						TweetCount: 4,
+					},
+					{
+						Start:      "2021-09-28T00:00:00.000Z",
+						End:        "2021-09-29T00:00:00.000Z",
+						TweetCount: 3,
+					},
+				},
+				Meta: &gotwtr.TweetCountMeta{
+					TotalTweetCount: 7,
+				},
+			},
+			wantErr: false,
+		},
+	}
+	for i, 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.CountsRecentTweet(tt.args.ctx, tt.args.query, tt.args.opt...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("client.TweetCounts() index = %v error = %v, wantErr %v", i, err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(got, tt.want); diff != "" {
+				t.Errorf("client.TweetCounts() index = %v mismatch (-want +got):\n%s", i, diff)
+				return
+			}
+		})
+	}
+}