Repository: sivchari/gotwtr
Pull request #217 (2025-07-16 03:14:26)

Feature/volume streams 10 percent


Patch:
diff --git a/client.go b/client.go
index 943908d..fe2f76c 100644
--- a/client.go
+++ b/client.go
@@ -59,7 +59,7 @@ type Tweets interface {
 	RetrieveSingleTweet(ctx context.Context, tweetID string, opt ...*RetriveTweetOption) (*TweetResponse, error)
 	// Volume stream
 	VolumeStreams(ctx context.Context, ch chan<- VolumeStreamsResponse, errCh chan<- error, opt ...*VolumeStreamsOption) *VolumeStreams
-	// TODO: /2/tweets/sample10/stream
+	VolumeStreams10(ctx context.Context, ch chan<- VolumeStreamsResponse, errCh chan<- error, opt ...*VolumeStreamsOption) *VolumeStreams
 }
 
 type Users interface {
@@ -316,6 +316,11 @@ func (c *Client) VolumeStreams(ctx context.Context, ch chan<- VolumeStreamsRespo
 	return volumeStreams(ctx, c.client, ch, errCh, opt...)
 }
 
+// VolumeStreams10 streams about 10% of all Tweets in real-time.
+func (c *Client) VolumeStreams10(ctx context.Context, ch chan<- VolumeStreamsResponse, errCh chan<- error, opt ...*VolumeStreamsOption) *VolumeStreams {
+	return volumeStreams10(ctx, c.client, ch, errCh, opt...)
+}
+
 // RetweetsLookup allows you to get information about who has Retweeted a Tweet.
 func (c *Client) RetweetsLookup(ctx context.Context, tweetID string, opt ...*RetweetsLookupOption) (*RetweetsResponse, error) {
 	return retweetsLookup(ctx, c.client, tweetID, opt...)
diff --git a/endpoint.go b/endpoint.go
index a49dce1..6b34462 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -14,8 +14,8 @@ const (
 	deleteTweetURL = "https://api.twitter.com/2/tweets/%v"
 	postTweetURL   = "https://api.twitter.com/2/tweets"
 	// Timelines
-	userMentionTimelineURL             = "https://api.twitter.com/2/users/%v/mentions"
-	userTweetTimelineURL               = "https://api.twitter.com/2/users/%v/tweets"
+	userMentionTimelineURL              = "https://api.twitter.com/2/users/%v/mentions"
+	userTweetTimelineURL                = "https://api.twitter.com/2/users/%v/tweets"
 	userReverseChronologicalTimelineURL = "https://api.twitter.com/2/users/%v/timelines/reverse_chronological"
 	// Search Tweets
 	searchAllTweetsURL    = "https://api.twitter.com/2/tweets/search/all"
@@ -28,7 +28,8 @@ const (
 	retrieveStreamRulesURL = "https://api.twitter.com/2/tweets/search/stream/rules"
 	addOrDeleteRulesURL    = "https://api.twitter.com/2/tweets/search/stream/rules"
 	// Volume streams
-	volumeStreamsURL = "https://api.twitter.com/2/tweets/sample/stream"
+	volumeStreamsURL   = "https://api.twitter.com/2/tweets/sample/stream"
+	volumeStreams10URL = "https://api.twitter.com/2/tweets/sample10/stream"
 	// Retweets
 	undoRetweetURL    = "https://api.twitter.com/2/users/%v/retweets/%v"
 	retweetsLookupURL = "https://api.twitter.com/2/tweets/%v/retweeted_by"
diff --git a/user.go b/user.go
index ddc9dc9..ccf387c 100644
--- a/user.go
+++ b/user.go
@@ -12,41 +12,41 @@ type UserField string
 */
 
 const (
-	UserFieldID              UserField = "id"
-	UserFieldName            UserField = "name"
-	UserFieldUserName        UserField = "username"
-	UserFieldCreatedAt       UserField = "created_at"
-	UserFieldDescription     UserField = "description"
-	UserFieldEntities        UserField = "entities"
-	UserFieldLocation        UserField = "location"
-	UserFieldPinnedTweetID   UserField = "pinned_tweet_id"
-	UserFieldProfileImageURL UserField = "profile_image_url"
-	UserFieldProtected       UserField = "protected"
-	UserFieldPublicMetrics   UserField = "public_metrics"
-	UserFieldURL             UserField = "url"
-	UserFieldVerified        UserField = "verified"
-	UserFieldVerifiedType    UserField = "verified_type"
+	UserFieldID               UserField = "id"
+	UserFieldName             UserField = "name"
+	UserFieldUserName         UserField = "username"
+	UserFieldCreatedAt        UserField = "created_at"
+	UserFieldDescription      UserField = "description"
+	UserFieldEntities         UserField = "entities"
+	UserFieldLocation         UserField = "location"
+	UserFieldPinnedTweetID    UserField = "pinned_tweet_id"
+	UserFieldProfileImageURL  UserField = "profile_image_url"
+	UserFieldProtected        UserField = "protected"
+	UserFieldPublicMetrics    UserField = "public_metrics"
+	UserFieldURL              UserField = "url"
+	UserFieldVerified         UserField = "verified"
+	UserFieldVerifiedType     UserField = "verified_type"
 	UserFieldSubscriptionType UserField = "subscription_type"
-	UserFieldWithHeld        UserField = "withheld"
+	UserFieldWithHeld         UserField = "withheld"
 )
 
 type User struct {
-	ID              string             `json:"id"`
-	Name            string             `json:"name"`
-	UserName        string             `json:"username"`
-	CreatedAt       string             `json:"created_at,omitempty"`
-	Description     string             `json:"description,omitempty"`
-	Entities        *UserEntity        `json:"entities,omitempty"`
-	Location        string             `json:"location,omitempty"`
-	PinnedTweetID   string             `json:"pinned_tweet_id,omitempty"`
-	ProfileImageURL string             `json:"profile_image_url,omitempty"`
-	Protected       bool               `json:"protected,omitempty"`
-	PublicMetrics   *UserPublicMetrics `json:"public_metrics,omitempty"`
-	URL             string             `json:"url,omitempty"`
-	Verified        bool               `json:"verified,omitempty"`
-	VerifiedType    string             `json:"verified_type,omitempty"`
-	SubscriptionType string            `json:"subscription_type,omitempty"`
-	Withheld        *UserWithheld      `json:"withheld,omitempty"`
+	ID               string             `json:"id"`
+	Name             string             `json:"name"`
+	UserName         string             `json:"username"`
+	CreatedAt        string             `json:"created_at,omitempty"`
+	Description      string             `json:"description,omitempty"`
+	Entities         *UserEntity        `json:"entities,omitempty"`
+	Location         string             `json:"location,omitempty"`
+	PinnedTweetID    string             `json:"pinned_tweet_id,omitempty"`
+	ProfileImageURL  string             `json:"profile_image_url,omitempty"`
+	Protected        bool               `json:"protected,omitempty"`
+	PublicMetrics    *UserPublicMetrics `json:"public_metrics,omitempty"`
+	URL              string             `json:"url,omitempty"`
+	Verified         bool               `json:"verified,omitempty"`
+	VerifiedType     string             `json:"verified_type,omitempty"`
+	SubscriptionType string             `json:"subscription_type,omitempty"`
+	Withheld         *UserWithheld      `json:"withheld,omitempty"`
 }
 
 type UserEntity struct {
diff --git a/volume_stream.go b/volume_stream.go
index 2f6b5ca..f0793fe 100644
--- a/volume_stream.go
+++ b/volume_stream.go
@@ -58,6 +58,7 @@ func volumeStreams(ctx context.Context, c *client, ch chan<- VolumeStreamsRespon
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, volumeStreamsURL, nil)
 	if err != nil {
 		errCh <- fmt.Errorf("sampled stream new request with ctx: %w", err)
+		return nil
 	}
 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
 
@@ -69,6 +70,39 @@ func volumeStreams(ctx context.Context, c *client, ch chan<- VolumeStreamsRespon
 		vopt = *opt[0]
 	default:
 		errCh <- errors.New("sampled stream: only one option is allowed")
+		return nil
+	}
+	vopt.addQuery(req)
+
+	vs := &VolumeStreams{
+		client: c.client,
+		errCh:  errCh,
+		ch:     ch,
+		done:   make(chan struct{}),
+		wg:     &sync.WaitGroup{},
+	}
+	vs.wg.Add(1)
+	go vs.retry(req)
+	return vs
+}
+
+func volumeStreams10(ctx context.Context, c *client, ch chan<- VolumeStreamsResponse, errCh chan<- error, opt ...*VolumeStreamsOption) *VolumeStreams {
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, volumeStreams10URL, nil)
+	if err != nil {
+		errCh <- fmt.Errorf("sampled stream 10%% new request with ctx: %w", err)
+		return nil
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	var vopt VolumeStreamsOption
+	switch len(opt) {
+	case 0:
+		// do nothing
+	case 1:
+		vopt = *opt[0]
+	default:
+		errCh <- errors.New("sampled stream 10%: only one option is allowed")
+		return nil
 	}
 	vopt.addQuery(req)
 


Test patch:
diff --git a/volume_stream_test.go b/volume_stream_test.go
index 8a9de02..38f67c8 100644
--- a/volume_stream_test.go
+++ b/volume_stream_test.go
@@ -75,3 +75,67 @@ func Test_sampledStream(t *testing.T) {
 		})
 	}
 }
+
+func Test_sampledStream10(t *testing.T) {
+	t.Parallel()
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		opt    []*gotwtr.VolumeStreamsOption
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.VolumeStreamsResponse
+		wantErr bool
+	}{
+		{
+			name: "200 ok default payload",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					body := `{
+						"data": {
+							"id": "1067094924124872705",
+							"text": "Just getting started with Twitter APIs? Find out what you need in order to build an app. Watch this video! https://t.co/Hg8nkfoizN"
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				opt: []*gotwtr.VolumeStreamsOption{},
+			},
+			want: &gotwtr.VolumeStreamsResponse{
+				Tweet: &gotwtr.Tweet{
+					ID:   "1067094924124872705",
+					Text: "Just getting started with Twitter APIs? Find out what you need in order to build an app. Watch this video! https://t.co/Hg8nkfoizN",
+				},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			ch := make(chan gotwtr.VolumeStreamsResponse)
+			errCh := make(chan error)
+			c := gotwtr.New("key", gotwtr.WithHTTPClient(tt.args.client))
+			c.VolumeStreams10(tt.args.ctx, ch, errCh, tt.args.opt...)
+			select {
+			case got := <-ch:
+				if diff := cmp.Diff(tt.want, &got); diff != "" {
+					t.Errorf("client.VolumeStreams10() mismatch (-want +got):\n%s", diff)
+					return
+				}
+			case err := <-errCh:
+				if (err != nil) != tt.wantErr {
+					t.Errorf("client.VolumeStreams10() error = %v, wantErr %v", err, tt.wantErr)
+					return
+				}
+			}
+		})
+	}
+}