Repository: sivchari/gotwtr
Pull request #34 (2021-10-06 12:23:17)

Impl sampled stream
Add sampled stream APIs

relate to #30 


Patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 4272945..7117661 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -17,3 +17,5 @@ jobs:
           reporter: github-pr-review
           go_version: "1.17"
           fail_on_error: true
+          filter_mode: diff_context
+
diff --git a/client.go b/client.go
index 98a11ce..c4de5a5 100644
--- a/client.go
+++ b/client.go
@@ -20,6 +20,7 @@ type Client interface {
 	LookUpTweetByID(ctx context.Context, id string, opt ...*TweetOption) (*TweetLookUpByIDResponse, error)
 	UserTweetTimeline(ctx context.Context, id string, opt ...*UserTweetTimelineOpts) (*UserTweetTimelineResponse, error)
 	UserMentionTimeline(ctx context.Context, id string, opt ...*UserMentionTimelineOpts) (*UserMentionTimelineResponse, error)
+	SampledStream(ctx context.Context, opt ...*SampledStreamOpts) (*SampledStreamResponse, error)
 	// User
 	// Media
 	// Poll
@@ -71,6 +72,10 @@ func (c *client) SearchRecentTweets(ctx context.Context, query string, opt ...*T
 	return searchRecentTweets(ctx, c, query, opt...)
 }
 
+func (c *client) SampledStream(ctx context.Context, opt ...*SampledStreamOpts) (*SampledStreamResponse, error) {
+	return sampledStream(ctx, c, 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 e048c7a..11bed73 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -5,5 +5,6 @@ var (
 	tweetLookUp       = "https://api.twitter.com/2/tweets"
 	timeline          = "https://api.twitter.com/2/users"
 	tweetRecentSearch = "https://api.twitter.com/2/tweets/search/recent"
+	sampleStream      = "https://api.twitter.com/2/tweets/sample/stream"
 	recentTweetCounts = "https://api.twitter.com/2/tweets/counts/recent"
 )
diff --git a/example/tweet/stream/sampled.go b/example/tweet/stream/sampled.go
new file mode 100644
index 0000000..a545983
--- /dev/null
+++ b/example/tweet/stream/sampled.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/sivchari/gotwtr"
+)
+
+func main() {
+	client := gotwtr.New("key")
+	// sampled stream
+	tsr, err := client.SampledStream(context.Background())
+	if err != nil {
+		panic(err)
+	}
+	for _, t := range tsr.Tweets {
+		fmt.Println(t)
+	}
+}
diff --git a/sampled_stream.go b/sampled_stream.go
new file mode 100644
index 0000000..1431ce4
--- /dev/null
+++ b/sampled_stream.go
@@ -0,0 +1,48 @@
+package gotwtr
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+func sampledStream(ctx context.Context, c *client, opt ...*SampledStreamOpts) (*SampledStreamResponse, error) {
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, sampleStream, nil)
+	if err != nil {
+		return nil, fmt.Errorf("sampled stream new request with ctx: %w", err)
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	var topt SampledStreamOpts
+	switch len(opt) {
+	case 0:
+		// do nothing
+	case 1:
+		topt = *opt[0]
+	default:
+		return nil, errors.New("sampled stream: only one option is allowed")
+	}
+	topt.addQuery(req)
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("sampled stream response: %w", err)
+	}
+	defer resp.Body.Close()
+
+	var sampledStream SampledStreamResponse
+	if err := json.NewDecoder(resp.Body).Decode(&sampledStream); err != nil {
+		return nil, fmt.Errorf("sampled stream decode: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &sampledStream, &HTTPError{
+			APIName: "sampled stream",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+
+	return &sampledStream, nil
+}
diff --git a/tweet.go b/tweet.go
index ec61cea..dfd4d30 100644
--- a/tweet.go
+++ b/tweet.go
@@ -208,6 +208,12 @@ type TweetSearchMeta struct {
 	NextToken   string `json:"next_token,omitempty"`
 }
 
+type SampledStreamResponse struct {
+	Tweets   []*Tweet            `json:"data"`
+	Includes *TweetIncludes      `json:"includes,omitempty"`
+	Errors   []*APIResponseError `json:"errors,omitempty"`
+}
+
 type TimeseriesCount struct {
 	Start      string `json:"start"`
 	End        string `json:"end"`
diff --git a/tweet_option.go b/tweet_option.go
index 89a17cd..cf2d9f5 100644
--- a/tweet_option.go
+++ b/tweet_option.go
@@ -229,6 +229,41 @@ func (t UserMentionTimelineOpts) addQuery(req *http.Request) {
 	}
 }
 
+type SampledStreamOpts struct {
+	// BackfillMinutes int `json:"backfill_minutes"`
+	Expansions  []Expansion
+	MediaFields []MediaField
+	PlaceFields []PlaceField
+	PollFields  []PollField
+	TweetFields []TweetField
+	UserFields  []UserField
+}
+
+func (t SampledStreamOpts) addQuery(req *http.Request) {
+	q := req.URL.Query()
+	if len(t.Expansions) > 0 {
+		q.Add("expansions", strings.Join(expansionsToString(t.Expansions), ","))
+	}
+	if len(t.MediaFields) > 0 {
+		q.Add("media.fields", strings.Join(mediaFieldsToString(t.MediaFields), ","))
+	}
+	if len(t.PlaceFields) > 0 {
+		q.Add("place.fields", strings.Join(placeFieldsToString(t.PlaceFields), ","))
+	}
+	if len(t.PollFields) > 0 {
+		q.Add("poll.fields", strings.Join(pollFieldsToString(t.PollFields), ","))
+	}
+	if len(t.TweetFields) > 0 {
+		q.Add("tweet.fields", strings.Join(tweetFieldsToString(t.TweetFields), ","))
+	}
+	if len(t.UserFields) > 0 {
+		q.Add("user.fields", strings.Join(userFieldsToString(t.UserFields), ","))
+	}
+	if len(q) > 0 {
+		req.URL.RawQuery = q.Encode()
+	}
+}
+
 type TweetCountsOption struct {
 	StartTime   time.Time
 	EndTime     time.Time


Test patch:
diff --git a/sampled_stream_test.go b/sampled_stream_test.go
new file mode 100644
index 0000000..d1e468a
--- /dev/null
+++ b/sampled_stream_test.go
@@ -0,0 +1,121 @@
+package gotwtr_test
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/sivchari/gotwtr"
+)
+
+func Test_sampledStream(t *testing.T) {
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		opt    []*gotwtr.SampledStreamOpts
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.SampledStreamResponse
+		wantErr bool
+	}{
+		{
+			name: "200 ok default payload",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					body := `{
+						"data": [
+							{
+								"author_id": "2244994945",
+								"created_at": "2020-02-14T19:00:55.000Z",
+								"id": "1228393702244134912",
+								"text": "What did the developer write in their Valentine’s card?\n  \nwhile(true) {\n    I = Love(You);  \n}"
+							},
+							{
+								"author_id": "2244994945",
+								"created_at": "2020-02-12T17:09:56.000Z",
+								"id": "1227640996038684673",
+								"text": "Doctors: Googling stuff online does not make you a doctor\n\nDevelopers: https://t.co/mrju5ypPkb"
+							},
+							{
+								"author_id": "2244994945",
+								"created_at": "2019-11-27T20:26:41.000Z",
+								"id": "1199786642791452673",
+								"text": "C#"
+							}
+						],
+						"includes": {
+							"users": [
+								{
+									"created_at": "2013-12-14T04:35:55.000Z",
+									"id": "2244994945",
+									"name": "Twitter Dev",
+									"username": "TwitterDev"
+								}
+							]
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				opt: []*gotwtr.SampledStreamOpts{},
+			},
+			want: &gotwtr.SampledStreamResponse{
+				Tweets: []*gotwtr.Tweet{
+					{
+						AuthorID:  "2244994945",
+						CreatedAt: "2020-02-14T19:00:55.000Z",
+						ID:        "1228393702244134912",
+						Text:      "What did the developer write in their Valentine’s card?\n  \nwhile(true) {\n    I = Love(You);  \n}",
+					},
+					{
+						AuthorID:  "2244994945",
+						CreatedAt: "2020-02-12T17:09:56.000Z",
+						ID:        "1227640996038684673",
+						Text:      "Doctors: Googling stuff online does not make you a doctor\n\nDevelopers: https://t.co/mrju5ypPkb",
+					},
+					{
+						AuthorID:  "2244994945",
+						CreatedAt: "2019-11-27T20:26:41.000Z",
+						ID:        "1199786642791452673",
+						Text:      "C#",
+					},
+				},
+				Includes: &gotwtr.TweetIncludes{
+					Users: []*gotwtr.User{
+						{
+							CreatedAt: "2013-12-14T04:35:55.000Z",
+							ID:        "2244994945",
+							Name:      "Twitter Dev",
+							UserName:  "TwitterDev",
+						},
+					},
+				},
+			},
+			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.SampledStream(tt.args.ctx, tt.args.opt...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("SampledStream() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(got, tt.want); diff != "" {
+				t.Errorf("SampledStream() mismatch (-want +got):\n%s", diff)
+				return
+			}
+		})
+	}
+}