diff --git a/README.md b/README.md index 9df813a..69914ee 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,8 @@ api.SetUnmarshalHook(func(bytes []byte) (interface{}, error) { ##### Start Stream Start your stream. This is a long-running HTTP GET request. -You can get specific data you want by adding [query params](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream). -Additionally, [view an example of query params here](https://developer.twitter.com/en/docs/twitter-api/expansions), or in the [examples](https://github.com/fallenstedt/twitter-stream/tree/master/example) +You can request additional tweet data by adding [query params](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream). +Use the `twitterstream.NewStreamQueryParamsBuilder()` to start a stream with the data you want. ```go @@ -142,7 +142,15 @@ func fetchTweets() stream.IStream { } return data, err }) - err = api.StartStream("?expansions=author_id&tweet.fields=created_at") + + // https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream + streamExpansions := twitterstream.NewStreamQueryParamsBuilder(). + AddExpansion("author_id"). + AddTweetField("created_at"). + Build() + + // StartStream will start the stream + err = api.StartStream(streamExpansions) if err != nil { panic(err) @@ -162,7 +170,7 @@ func initiateStream() { // When the loop below ends, restart the stream defer initiateStream() - // Start processing data from twitter + // Start processing data from twitter after starting the stream for tweet := range api.GetMessages() { // Handle disconnections from twitter diff --git a/VERSION b/VERSION index 60a2d3e..44bb5d1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.0 \ No newline at end of file +0.4.1 \ No newline at end of file diff --git a/example/create_rules_example.go b/example/create_rules_example.go index cbc9299..1c138e3 100644 --- a/example/create_rules_example.go +++ b/example/create_rules_example.go @@ -68,7 +68,7 @@ func deleteRules() { api := twitterstream.NewTwitterStream(tok.AccessToken) // use api.Rules.Get to find the ID number for an existing rule - res, err := api.Rules.Delete(rules.NewDeleteRulesRequest(1469776000158363653, 1469776000158363654), false) + res, err := api.Rules.Delete(rules.NewDeleteRulesRequest(1469777072675450881, 74893274932), false) if err != nil { panic(err) @@ -89,4 +89,4 @@ func printRules(data []rules.DataRule) { fmt.Printf("Tag: %v\n",datum.Tag) fmt.Printf("Value: %v\n\n", datum.Value) } -} \ No newline at end of file +} diff --git a/example/main.go b/example/main.go index 0cfe9ea..2952e62 100644 --- a/example/main.go +++ b/example/main.go @@ -5,7 +5,6 @@ const SECRET = "SECRET" func main() { // Run an example function - addRules() getRules() initiateStream() diff --git a/example/stream_forever.go b/example/stream_forever.go index 888932d..7389031 100644 --- a/example/stream_forever.go +++ b/example/stream_forever.go @@ -75,12 +75,16 @@ func initiateStream() { } func fetchTweets() stream.IStream { + // Get Bearer Token using API keys tok, err := getTwitterToken() if err != nil { panic(err) } + // Instantiate an instance of twitter stream using the bearer token api := getTwitterStreamApi(tok) + + // On Each tweet, decode the bytes into a StreamDataExample struct api.SetUnmarshalHook(func(bytes []byte) (interface{}, error) { data := StreamDataExample{} if err := json.Unmarshal(bytes, &data); err != nil { @@ -88,11 +92,20 @@ func fetchTweets() stream.IStream { } return data, err }) - err = api.StartStream("?expansions=author_id&tweet.fields=created_at") + + // Request additional data from teach tweet + streamExpansions := twitterstream.NewStreamQueryParamsBuilder(). + AddExpansion("author_id"). + AddTweetField("created_at"). + Build() + + // Start the Stream + err = api.StartStream(streamExpansions) if err != nil { panic(err) } + // Return the twitter stream api instance return api } diff --git a/httpclient/http_client_mock.go b/httpclient/http_client_mock.go index 6250708..e295198 100644 --- a/httpclient/http_client_mock.go +++ b/httpclient/http_client_mock.go @@ -1,21 +1,24 @@ package httpclient -import "net/http" +import ( + "net/http" + "net/url" +) type mockHttpClient struct { token string MockNewHttpRequest func(opts *RequestOpts) (*http.Response, error) - MockGetSearchStream func(queryParams string) (*http.Response, error) + MockGetSearchStream func(queryParams *url.Values) (*http.Response, error) MockGetRules func() (*http.Response, error) - MockAddRules func(queryParams string, body string) (*http.Response, error) - MockGenerateUrl func(name string, queryParams string) (string, error) + MockAddRules func(queryParams *url.Values, body string) (*http.Response, error) + MockGenerateUrl func(name string, queryParams *url.Values) (string, error) } func NewHttpClientMock(token string) *mockHttpClient { return &mockHttpClient{token: token} } -func (t *mockHttpClient) GenerateUrl(name string, queryParams string) (string, error) { +func (t *mockHttpClient) GenerateUrl(name string, queryParams *url.Values) (string, error) { return t.MockGenerateUrl(name, queryParams) } @@ -23,11 +26,11 @@ func (t *mockHttpClient) GetRules() (*http.Response, error) { return t.MockGetRules() } -func (t *mockHttpClient) AddRules(queryParams string, body string) (*http.Response, error) { +func (t *mockHttpClient) AddRules(queryParams *url.Values, body string) (*http.Response, error) { return t.MockAddRules(queryParams, body) } -func (t *mockHttpClient) GetSearchStream(queryParams string) (*http.Response, error) { +func (t *mockHttpClient) GetSearchStream(queryParams *url.Values) (*http.Response, error) { return t.MockGetSearchStream(queryParams) } diff --git a/httpclient/httpclient.go b/httpclient/httpclient.go index dd0af7e..0677d18 100644 --- a/httpclient/httpclient.go +++ b/httpclient/httpclient.go @@ -3,8 +3,10 @@ package httpclient import ( "bytes" "errors" + "fmt" "log" "net/http" + "net/url" "strings" ) @@ -18,9 +20,9 @@ type ( IHttpClient interface { NewHttpRequest(opts *RequestOpts) (*http.Response, error) GetRules() (*http.Response, error) - GetSearchStream(queryParams string) (*http.Response, error) - AddRules(queryParams string, body string) (*http.Response, error) - GenerateUrl(name string, queryParams string) (string, error) + GetSearchStream(queryParams *url.Values) (*http.Response, error) + AddRules(queryParams *url.Values, body string) (*http.Response, error) + GenerateUrl(name string, queryParams *url.Values) (string, error) } httpClient struct { @@ -48,7 +50,7 @@ func (t *httpClient) GetRules() (*http.Response, error) { } // AddRules will add rules for you to stream with. -func (t *httpClient) AddRules(queryParams string, body string) (*http.Response, error) { +func (t *httpClient) AddRules(queryParams *url.Values, body string) (*http.Response, error) { url, err := t.GenerateUrl("rules", queryParams) if err != nil { @@ -69,7 +71,7 @@ func (t *httpClient) AddRules(queryParams string, body string) (*http.Response, } // GetSearchStream will start the stream with twitter. -func (t *httpClient) GetSearchStream(queryParams string) (*http.Response, error) { +func (t *httpClient) GetSearchStream(queryParams *url.Values) (*http.Response, error) { // Make an HTTP GET request to GET /2/tweets/search/stream url, err := t.GenerateUrl("stream", queryParams) @@ -90,10 +92,10 @@ func (t *httpClient) GetSearchStream(queryParams string) (*http.Response, error) } // GenerateUrl is a utility function for httpclient package to generate a valid url for api.twitter. -func (t *httpClient) GenerateUrl(name string, queryParams string) (string, error) { +func (t *httpClient) GenerateUrl(name string, queryParams *url.Values) (string, error) { var url string - if len(queryParams) > 0 { - url = Endpoints[name] + queryParams + if queryParams != nil { + url = Endpoints[name] + fmt.Sprintf("?%v", queryParams.Encode()) } else { url = Endpoints[name] } diff --git a/rules/rule_builder.go b/rules/rule_builder.go index a21e89c..8d23828 100644 --- a/rules/rule_builder.go +++ b/rules/rule_builder.go @@ -1,24 +1,30 @@ package rules type ( + // IRuleBuilder is an interface that describers how to implement a RuleBuilder. IRuleBuilder interface { AddRule(value string, tag string) *RuleBuilder Build() CreateRulesRequest } + // RuleValue is a struct used to help create twitter stream rules. + // It takes in a value and a tag. RuleValue struct { Value *string `json:"value,omitempty"` Tag *string `json:"tag,omitempty"` } + // RuleBuilder is struct used to help create twitter stream rules. RuleBuilder struct { rules []*RuleValue } + // CreateRulesRequest is a struct used to create the payload for creating rules. CreateRulesRequest struct { Add []*RuleValue `json:"add"` } + // DeleteRulesRequest is a struct used to create the payload for deleting rules. DeleteRulesRequest struct { Delete struct { Ids []int `json:"ids"` @@ -27,12 +33,14 @@ type ( ) +// NewDeleteRulesRequest will create an instance of DeleteRulesRequest. func NewDeleteRulesRequest(ids ...int) DeleteRulesRequest { return DeleteRulesRequest{Delete: struct { Ids []int `json:"ids"` }(struct{ Ids []int }{Ids: ids})} } +// NewRuleBuilder will create an instance of `RuleBuilder`. func NewRuleBuilder() *RuleBuilder { return &RuleBuilder{ rules: []*RuleValue{}, @@ -64,4 +72,3 @@ func (r *RuleValue) setValueTag(value string, tag string) *RuleValue { r.Tag = &tag return r } - diff --git a/rules/rule_builder_test.go b/rules/rule_builder_test.go index 603aef3..1e01f48 100644 --- a/rules/rule_builder_test.go +++ b/rules/rule_builder_test.go @@ -44,4 +44,4 @@ func TestNewRuleBuilderBuildsManyRules(t *testing.T) { if string(body) != "{\"add\":[{\"value\":\"cats\",\"tag\":\"cat tweets\"},{\"value\":\"dogs\",\"tag\":\"dog tweets\"}]}" { t.Errorf("Expected %v to equal %v", string(body), "{\"add\":[{\"value\":\"cats\",\"tag\":\"cat tweets\"},{\"value\":\"dogs\",\"tag\":\"dog tweets\"}]}") } -} \ No newline at end of file +} diff --git a/rules/rules.go b/rules/rules.go index dce53ea..1d3ac96 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -3,6 +3,7 @@ package rules import ( "encoding/json" "github.com/fallenstedt/twitter-stream/httpclient" + "net/url" ) type ( @@ -68,13 +69,7 @@ func (t *rules) Create(rules CreateRulesRequest, dryRun bool) (*TwitterRuleRespo return nil, err } - res, err := t.httpClient.AddRules(func() string { - if dryRun { - return "?dry_run=true" - } else { - return "" - } - }(), string(body)) + res, err := t.httpClient.AddRules(t.addDryRun(dryRun), string(body)) if err != nil { return nil, err @@ -95,13 +90,7 @@ func (t *rules) Delete(req DeleteRulesRequest, dryRun bool) (*TwitterRuleRespons return nil, err } - res, err := t.httpClient.AddRules(func() string { - if dryRun { - return "?dry_run=true" - } else { - return "" - } - }(), string(body)) + res, err := t.httpClient.AddRules(t.addDryRun(dryRun), string(body)) defer res.Body.Close() @@ -127,3 +116,14 @@ func (t *rules) Get() (*TwitterRuleResponse, error) { return data, nil } + + +func (t *rules) addDryRun(dryRun bool) *url.Values { + if dryRun { + query := new(url.URL).Query() + query.Add("dry_run", "true") + return &query + } else { + return nil + } +} diff --git a/rules/rules_test.go b/rules/rules_test.go index 6708783..03e592e 100644 --- a/rules/rules_test.go +++ b/rules/rules_test.go @@ -6,6 +6,7 @@ import ( "github.com/fallenstedt/twitter-stream/httpclient" "io/ioutil" "net/http" + "net/url" "testing" ) @@ -13,12 +14,12 @@ func TestCreate(t *testing.T) { var tests = []struct { body CreateRulesRequest - mockRequest func(queryParams string, body string) (*http.Response, error) + mockRequest func(queryParams *url.Values, body string) (*http.Response, error) result *TwitterRuleResponse }{ { NewRuleBuilder().AddRule("cat has:images", "cat tweets with images").Build(), - func(queryParams string, bodyRequest string) (*http.Response, error) { + func(queryParams *url.Values, bodyRequest string) (*http.Response, error) { json := `{ "data": [{ "Value": "cat has:images", @@ -108,12 +109,12 @@ func TestDelete(t *testing.T) { var tests = []struct { body DeleteRulesRequest - mockRequest func(queryParams string, body string) (*http.Response, error) + mockRequest func(queryParams *url.Values, body string) (*http.Response, error) result *TwitterRuleResponse }{ { NewDeleteRulesRequest(123), - func(queryParams string, bodyRequest string) (*http.Response, error) { + func(queryParams *url.Values, bodyRequest string) (*http.Response, error) { json := `{ "data": [{ "Value": "cat has:images", diff --git a/stream/stream.go b/stream/stream.go index 0f0d2c7..c66d238 100644 --- a/stream/stream.go +++ b/stream/stream.go @@ -3,6 +3,7 @@ package stream import ( "github.com/fallenstedt/twitter-stream/httpclient" "net/http" + "net/url" ) type ( @@ -11,7 +12,7 @@ type ( // IStream is the interface that the stream struct implements. IStream interface { - StartStream(queryParams string) error + StartStream(queryParams *url.Values) error StopStream() GetMessages() <-chan StreamMessage SetUnmarshalHook(hook UnmarshalHook) @@ -36,6 +37,7 @@ type ( } ) +// NewStream creates an instance of `Stream`. This is used to manage the stream with Twitter. func NewStream(httpClient httpclient.IHttpClient, reader IStreamResponseBodyReader) IStream { return &Stream{ unmarshalHook: func(bytes []byte) (interface{}, error) { @@ -69,7 +71,7 @@ func (s *Stream) StopStream() { // Accepts query params described in GET /2/tweets/search/stream to expand the payload that is returned. Query params string must begin with a ?. // See available query params here https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. // See an example here: https://developer.twitter.com/en/docs/twitter-api/expansions. -func (s *Stream) StartStream(optionalQueryParams string) error { +func (s *Stream) StartStream(optionalQueryParams *url.Values) error { res, err := s.httpClient.GetSearchStream(optionalQueryParams) if err != nil { diff --git a/stream/stream_expansion_builder.go b/stream/stream_expansion_builder.go new file mode 100644 index 0000000..b88fb02 --- /dev/null +++ b/stream/stream_expansion_builder.go @@ -0,0 +1,151 @@ +package stream + +import ( + "fmt" + "net/url" + "strconv" + "strings" +) + +type ( + //IStreamQueryParamsBuilder is the interface for StreamQueryParamBuilder. + IStreamQueryParamsBuilder interface { + AddBackFillMinutes(minutes uint) *StreamQueryParamBuilder + AddExpansion(expansion string) *StreamQueryParamBuilder + AddMediaField(mediaField string) *StreamQueryParamBuilder + AddPlaceField(placeField string) *StreamQueryParamBuilder + AddPollField(pollField string) *StreamQueryParamBuilder + AddTweetField(tweetField string) *StreamQueryParamBuilder + AddUserField(userField string) *StreamQueryParamBuilder + Build() *url.Values + } + + // StreamQueryParamBuilder is a struct used for requesting additional data from a tweet. + // Read more at https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. + StreamQueryParamBuilder struct { + backFillMinutes uint + expansions []*string + mediaFields []*string + placeFields []*string + pollFields []*string + tweetFields []*string + userFields []*string + } + +) + +// NewStreamQueryParamsBuilder creeates a struct that implements IStreamQueryParamsBuilder. +// It is used to request additional data from a tweet. +// Read more at https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. +func NewStreamQueryParamsBuilder() IStreamQueryParamsBuilder { + return &StreamQueryParamBuilder{ + backFillMinutes: 0, + expansions: []*string{}, + mediaFields: []*string{}, + placeFields: []*string{}, + pollFields: []*string{}, + tweetFields: []*string{}, + userFields: []*string{}, + } +} + +// Build will build and encode the required query params. +func (s *StreamQueryParamBuilder) Build() *url.Values { + query := new(url.URL).Query() + + s.addQuery(&query, &s.expansions, "expansions") + s.addQuery(&query, &s.mediaFields, "media.fields") + s.addQuery(&query, &s.placeFields, "place.fields") + s.addQuery(&query, &s.pollFields, "poll.fields") + s.addQuery(&query, &s.tweetFields, "tweet.fields") + s.addQuery(&query, &s.userFields, "user.fields") + + if s.backFillMinutes > 0 { + query.Add("backfill_minutes", strconv.Itoa(int(s.backFillMinutes))) + } + + return &query +} + + +// AddExpansion adds an expansion defined in https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. +// With expansions, developers can expand objects referenced in the payload. Objects available for expansion are referenced by ID. +// Add a single expansion for each invoke of `AddExpansion`. +func (s *StreamQueryParamBuilder) AddExpansion(expansion string) *StreamQueryParamBuilder { + s.expansions = append(s.expansions, &expansion) + return s +} + +// AddMediaField adds a media field which enables you to select which specific media fields will deliver in each returned tweet. +// The Tweet will only return media fields if the Tweet contains media and if you've also included `AddExpansion("attachments.media_keys")`. +// Learn more about media fields on twitter docs https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. +// Add a single media field for each invoke of `AddMediaField`. +func (s *StreamQueryParamBuilder) AddMediaField(mediaField string) *StreamQueryParamBuilder { + s.mediaFields = append(s.mediaFields, &mediaField) + return s +} + +// AddPlaceField adds a place field which enables you to select which specific place fields will deliver in each returned tweet. +// The Tweet will only return place fields if the Tweet contains a place and if you've also included `AddExpansion("geo.place_id")`. +// Learn more about place fields on twitter docs https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. +// Add a single place field for each invoke of `AddPlaceField`. +func (s *StreamQueryParamBuilder) AddPlaceField(placeField string) *StreamQueryParamBuilder { + s.placeFields = append(s.placeFields, &placeField) + return s +} + +// AddPollField adds a poll field which enables you to select which specific poll fields will deliver in each returned tweet. +// The Tweet will only return poll fields if the Tweet contains a place and if you've also included `AddExpansion("attachments.poll_ids")`. +// Learn more about poll fields on twitter docs https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. +// Add a single poll field for each invoke of `AddPollField`. +func (s *StreamQueryParamBuilder) AddPollField(pollField string) *StreamQueryParamBuilder { + s.pollFields = append(s.pollFields, &pollField) + return s +} + +// AddTweetField This fields parameter enables you to select which specific Tweet fields will deliver in each returned Tweet object. +// Specify the desired fields in a comma-separated list without spaces between commas and fields. +// You can also include `AddExpansion("referenced_tweets.id")` to return the specified fields for both the original Tweet and any included referenced Tweets. +// The requested Tweet fields will display in both the original Tweet data object, as well as in the referenced Tweet expanded data object that will be located in the includes data object. +// Learn more about tweet fields on twitter docs https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. +func (s *StreamQueryParamBuilder) AddTweetField(tweetField string) *StreamQueryParamBuilder { + s.tweetFields = append(s.tweetFields, &tweetField) + return s +} + +// AddUserField This fields parameter enables you to select which specific user fields will deliver in each returned Tweet. +// Specify the desired fields in a comma-separated list without spaces between commas and fields. +// While the user ID will be located in the original Tweet object, you will find this ID and all additional user fields in the includes data object. +// You must also pass one of the user expansions to return the desired user field. +// `AddExpansion("author_id")` +// `AddExpansion("entities.mentions.username")` +// `AddExpansion("in_reply_to_user_id")` +// `AddExpansion("referenced_tweets.id.author_id")` +func (s *StreamQueryParamBuilder) AddUserField(userField string) *StreamQueryParamBuilder { + s.userFields = append(s.userFields, &userField) + return s +} + + +// AddBackFillMinutes will allow you to recover up to 5 minutes worth of data that might have been missed during a disconnection. +// This feature is currently only available to the academic research product track! +// Learn more about media fields on twitter docs https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. +func (s *StreamQueryParamBuilder) AddBackFillMinutes(backFillMinutes uint) *StreamQueryParamBuilder { + s.backFillMinutes = backFillMinutes + return s +} + +func (s StreamQueryParamBuilder) addQuery(qb *url.Values, fields *[]*string, param string) { + if len(*fields) > 0 { + var sb strings.Builder + for i, expansion := range *fields { + if i == len(*fields) - 1 { + sb.WriteString(fmt.Sprintf("%v", *expansion)) + } else { + sb.WriteString(fmt.Sprintf("%v,", *expansion)) + } + } + value := sb.String() + qb.Add(param, value) + } +} diff --git a/stream/stream_expansion_builder_test.go b/stream/stream_expansion_builder_test.go new file mode 100644 index 0000000..9ef2872 --- /dev/null +++ b/stream/stream_expansion_builder_test.go @@ -0,0 +1,28 @@ +package stream + +import "testing" + +func TestStreamQueryParamsBuilderBuildsQueryParams(t *testing.T) { + builder := NewStreamQueryParamsBuilder() + + result := builder. + AddExpansion("expansion1"). + AddExpansion("expansion2"). + AddBackFillMinutes(1). + AddMediaField("mediaField1"). + AddMediaField("mediaField2"). + AddPlaceField("placeField1"). + AddPlaceField("placeField2"). + AddPollField("pollField1"). + AddPollField("pollField2"). + AddTweetField("tweetField1"). + AddTweetField("tweetField2"). + AddUserField("userField1"). + AddUserField("userField2"). + Build().Encode() + expected := "backfill_minutes=1&expansions=expansion1%2Cexpansion2&media.fields=mediaField1%2CmediaField2&place.fields=placeField1%2CplaceField2&poll.fields=pollField1%2CpollField2&tweet.fields=tweetField1%2CtweetField2&user.fields=userField1%2CuserField2" + if result != expected { + t.Errorf("ahh") + } + +} \ No newline at end of file diff --git a/stream/stream_test.go b/stream/stream_test.go index 76a18c5..038ae22 100644 --- a/stream/stream_test.go +++ b/stream/stream_test.go @@ -7,6 +7,7 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "testing" ) @@ -52,7 +53,7 @@ func TestStartStream(t *testing.T) { { func() httpclient.IHttpClient { mockClient := httpclient.NewHttpClientMock("foobar") - mockClient.MockGetSearchStream = func(queryParams string) (*http.Response, error) { + mockClient.MockGetSearchStream = func(queryParams *url.Values) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewReader([]byte("hello"))), @@ -84,7 +85,7 @@ func TestStartStream(t *testing.T) { tt.givenMockStreamResponseBodyReader(), ) - err := instance.StartStream("") + err := instance.StartStream(nil) if err != nil { t.Errorf("got err when starting stream %v", err) } diff --git a/twitterstream.go b/twitterstream.go index 0b018d7..47ab426 100644 --- a/twitterstream.go +++ b/twitterstream.go @@ -20,10 +20,25 @@ func NewTokenGenerator() token_generator.ITokenGenerator { return tokenGenerator } +// NewRuleBuilder creates a rule builder for creating rules. +// It is used in `rules.Create`. func NewRuleBuilder() rules.IRuleBuilder { return rules.NewRuleBuilder() } +// NewRuleDelete creates a delete rules request. +// It is used in `rules.Delete`. +func NewRuleDelete(ids ...int) rules.DeleteRulesRequest { + return rules.NewDeleteRulesRequest(ids...) +} + +// NewStreamQueryParamsBuilder creates a stream query param builder. +// It is used with `stream.StartStream()` to include tweets with extra metadata. +func NewStreamQueryParamsBuilder() stream.IStreamQueryParamsBuilder { + return stream.NewStreamQueryParamsBuilder() +} + + // NewTwitterStream consumes a twitter Bearer token. // It is used to interact with Twitter's v2 filtered streaming API func NewTwitterStream(token string) *TwitterApi {