From d85b4dd04818aa2689d6aa69aa2d5310ceb4e36c Mon Sep 17 00:00:00 2001 From: Alex Fallenstedt <13971824+Fallenstedt@users.noreply.github.com> Date: Mon, 13 Dec 2021 21:46:25 -0800 Subject: [PATCH 1/8] add some expansions --- stream/stream_expansion_builder.go | 56 ++++++++++++++++++++++++++++++ twitterstream.go | 9 +++++ 2 files changed, 65 insertions(+) create mode 100644 stream/stream_expansion_builder.go diff --git a/stream/stream_expansion_builder.go b/stream/stream_expansion_builder.go new file mode 100644 index 0000000..36caea3 --- /dev/null +++ b/stream/stream_expansion_builder.go @@ -0,0 +1,56 @@ +package stream + + +type ( + IStreamQueryParamsBuilder interface { + AddExpansion(expansion string) *StreamQueryParamBuilder + AddMediaField(mediaField string) *StreamQueryParamBuilder + AddPlaceField(placeField string) *StreamQueryParamBuilder + Build() string + } + + StreamQueryParamBuilder struct { + expansions []*string + mediaFields []*string + placeFields []*string + } + +) + +func NewStreamQueryParamsBuilder() IStreamQueryParamsBuilder { + return &StreamQueryParamBuilder{ + expansions: []*string{}, + mediaFields: []*string{}, + placeFields: []*string{}, + } +} + +func (s *StreamQueryParamBuilder) Build() string { + return "?expansions=lol" +} + +// 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 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 place field for each invoke of `AddPlaceField`. +func (s *StreamQueryParamBuilder) AddPlaceField(placeField string) *StreamQueryParamBuilder { + s.placeFields = append(s.placeFields, &placeField) + return s +} \ No newline at end of file diff --git a/twitterstream.go b/twitterstream.go index 0b018d7..b443724 100644 --- a/twitterstream.go +++ b/twitterstream.go @@ -20,10 +20,19 @@ 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...) +} + + // NewTwitterStream consumes a twitter Bearer token. // It is used to interact with Twitter's v2 filtered streaming API func NewTwitterStream(token string) *TwitterApi { From dda464597135355126760f86604cad4700c70345 Mon Sep 17 00:00:00 2001 From: Alex Fallenstedt <13971824+Fallenstedt@users.noreply.github.com> Date: Mon, 13 Dec 2021 21:47:02 -0800 Subject: [PATCH 2/8] 0.4.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 29f729caa20ba5923bdd6a5b9d9b485497d7c275 Mon Sep 17 00:00:00 2001 From: Alex Fallenstedt <13971824+Fallenstedt@users.noreply.github.com> Date: Sat, 18 Dec 2021 16:47:19 -0800 Subject: [PATCH 3/8] start building query param string --- stream/stream_expansion_builder.go | 74 +++++++++++++++++++++++++++++- twitterstream.go | 6 +++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/stream/stream_expansion_builder.go b/stream/stream_expansion_builder.go index 36caea3..c85506d 100644 --- a/stream/stream_expansion_builder.go +++ b/stream/stream_expansion_builder.go @@ -1,32 +1,61 @@ package stream +import ( + "fmt" + "net/url" + "strings" +) type ( IStreamQueryParamsBuilder interface { + AddBackFillMinutes(minutes uint8) *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() string } StreamQueryParamBuilder struct { + backFillMinutes *uint8 expansions []*string mediaFields []*string placeFields []*string + pollFields []*string + tweetFields []*string + userFields []*string } ) func NewStreamQueryParamsBuilder() IStreamQueryParamsBuilder { return &StreamQueryParamBuilder{ + backFillMinutes: nil, expansions: []*string{}, mediaFields: []*string{}, placeFields: []*string{}, + pollFields: []*string{}, + tweetFields: []*string{}, + userFields: []*string{}, } } +// TODO finish building query param string and document. func (s *StreamQueryParamBuilder) Build() string { - return "?expansions=lol" + query := new(url.URL).Query() + + if len(s.expansions) > 0 { + var sb strings.Builder + for _, expansion := range s.expansions { + sb.WriteString(fmt.Sprintf("%v,", expansion)) + } + value := sb.String() + query.Add("expansions", value) + } + + return query.Encode() } // AddExpansion adds an expansion defined in https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. @@ -48,9 +77,50 @@ func (s *StreamQueryParamBuilder) AddMediaField(mediaField string) *StreamQueryP // 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 media fields on twitter docs https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. +// 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 uint8) *StreamQueryParamBuilder { + s.backFillMinutes = &backFillMinutes + return s } \ No newline at end of file diff --git a/twitterstream.go b/twitterstream.go index b443724..47ab426 100644 --- a/twitterstream.go +++ b/twitterstream.go @@ -32,6 +32,12 @@ 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 From 5afbd0524f6823cf1a365c924bc4125b33748a6c Mon Sep 17 00:00:00 2001 From: Alex Fallenstedt <13971824+Fallenstedt@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:54:52 -0800 Subject: [PATCH 4/8] stream query param builder update --- example/create_rules_example.go | 2 +- example/main.go | 3 +- example/stream_forever.go | 7 +++- httpclient/httpclient.go | 18 +++++----- rules/rules.go | 28 +++++++-------- stream/stream.go | 5 +-- stream/stream_expansion_builder.go | 56 +++++++++++++++++++++--------- 7 files changed, 74 insertions(+), 45 deletions(-) diff --git a/example/create_rules_example.go b/example/create_rules_example.go index cbc9299..be117fd 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) diff --git a/example/main.go b/example/main.go index 0cfe9ea..fd73447 100644 --- a/example/main.go +++ b/example/main.go @@ -5,9 +5,8 @@ const SECRET = "SECRET" func main() { // Run an example function - addRules() getRules() initiateStream() //deleteRules() -} +} \ No newline at end of file diff --git a/example/stream_forever.go b/example/stream_forever.go index 888932d..8d40118 100644 --- a/example/stream_forever.go +++ b/example/stream_forever.go @@ -88,7 +88,12 @@ func fetchTweets() stream.IStream { } return data, err }) - err = api.StartStream("?expansions=author_id&tweet.fields=created_at") + streamExpansions := twitterstream.NewStreamQueryParamsBuilder(). + AddExpansion("author_id"). + AddTweetField("created_at"). + Build() + + err = api.StartStream(streamExpansions) if err != nil { panic(err) } 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/rules.go b/rules/rules.go index dce53ea..802ee86 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 + } +} \ No newline at end of file diff --git a/stream/stream.go b/stream/stream.go index 0f0d2c7..b18b5ea 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) @@ -69,7 +70,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 index c85506d..e77f730 100644 --- a/stream/stream_expansion_builder.go +++ b/stream/stream_expansion_builder.go @@ -3,23 +3,27 @@ package stream import ( "fmt" "net/url" + "strconv" "strings" ) type ( + //IStreamQueryParamsBuilder is the interface for StreamQueryParamBuilder. IStreamQueryParamsBuilder interface { - AddBackFillMinutes(minutes uint8) *StreamQueryParamBuilder + 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() string + 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 *uint8 + backFillMinutes uint expansions []*string mediaFields []*string placeFields []*string @@ -32,7 +36,7 @@ type ( func NewStreamQueryParamsBuilder() IStreamQueryParamsBuilder { return &StreamQueryParamBuilder{ - backFillMinutes: nil, + backFillMinutes: 0, expansions: []*string{}, mediaFields: []*string{}, placeFields: []*string{}, @@ -42,22 +46,25 @@ func NewStreamQueryParamsBuilder() IStreamQueryParamsBuilder { } } -// TODO finish building query param string and document. -func (s *StreamQueryParamBuilder) Build() string { +// Build will build and encode the required query params +func (s *StreamQueryParamBuilder) Build() *url.Values { query := new(url.URL).Query() - if len(s.expansions) > 0 { - var sb strings.Builder - for _, expansion := range s.expansions { - sb.WriteString(fmt.Sprintf("%v,", expansion)) - } - value := sb.String() - query.Add("expansions", value) + 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.Encode() + 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`. @@ -120,7 +127,22 @@ func (s *StreamQueryParamBuilder) AddUserField(userField string) *StreamQueryPar // 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 uint8) *StreamQueryParamBuilder { - s.backFillMinutes = &backFillMinutes +func (s *StreamQueryParamBuilder) AddBackFillMinutes(backFillMinutes uint) *StreamQueryParamBuilder { + s.backFillMinutes = backFillMinutes return s -} \ No newline at end of file +} + +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) + } +} From 55643fd9d6f32b51d30fc85274b353620de2936c Mon Sep 17 00:00:00 2001 From: Alex Fallenstedt <13971824+Fallenstedt@users.noreply.github.com> Date: Mon, 20 Dec 2021 22:07:23 -0800 Subject: [PATCH 5/8] update readme --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 From 4006516822ec452225f4a21c075766a6a2188d49 Mon Sep 17 00:00:00 2001 From: Alex Fallenstedt <13971824+Fallenstedt@users.noreply.github.com> Date: Tue, 21 Dec 2021 20:40:48 -0800 Subject: [PATCH 6/8] update spec --- httpclient/http_client_mock.go | 17 ++++++++------- rules/rules_test.go | 9 ++++---- stream/stream_expansion_builder.go | 5 ++++- stream/stream_expansion_builder_test.go | 28 +++++++++++++++++++++++++ stream/stream_test.go | 5 +++-- 5 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 stream/stream_expansion_builder_test.go 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/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_expansion_builder.go b/stream/stream_expansion_builder.go index e77f730..b88fb02 100644 --- a/stream/stream_expansion_builder.go +++ b/stream/stream_expansion_builder.go @@ -34,6 +34,9 @@ type ( ) +// 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, @@ -46,7 +49,7 @@ func NewStreamQueryParamsBuilder() IStreamQueryParamsBuilder { } } -// Build will build and encode the required query params +// Build will build and encode the required query params. func (s *StreamQueryParamBuilder) Build() *url.Values { query := new(url.URL).Query() 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) } From 61f7cf03c0c9b878bbfbad2681102079cb6a8809 Mon Sep 17 00:00:00 2001 From: Alex Fallenstedt <13971824+Fallenstedt@users.noreply.github.com> Date: Tue, 21 Dec 2021 21:09:15 -0800 Subject: [PATCH 7/8] add comments to examples --- example/stream_forever.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/example/stream_forever.go b/example/stream_forever.go index 8d40118..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,16 +92,20 @@ func fetchTweets() stream.IStream { } return data, err }) + + // 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 } From de2d3ae323a551d556af5dcd0b00c63017b1ba0e Mon Sep 17 00:00:00 2001 From: Alex Fallenstedt <13971824+Fallenstedt@users.noreply.github.com> Date: Wed, 22 Dec 2021 17:10:37 -0800 Subject: [PATCH 8/8] add comments --- example/create_rules_example.go | 2 +- example/main.go | 2 +- rules/rule_builder.go | 9 ++++++++- rules/rule_builder_test.go | 2 +- rules/rules.go | 2 +- stream/stream.go | 1 + 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/example/create_rules_example.go b/example/create_rules_example.go index be117fd..1c138e3 100644 --- a/example/create_rules_example.go +++ b/example/create_rules_example.go @@ -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 fd73447..2952e62 100644 --- a/example/main.go +++ b/example/main.go @@ -9,4 +9,4 @@ func main() { getRules() initiateStream() //deleteRules() -} \ No newline at end of file +} 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 802ee86..1d3ac96 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -126,4 +126,4 @@ func (t *rules) addDryRun(dryRun bool) *url.Values { } else { return nil } -} \ No newline at end of file +} diff --git a/stream/stream.go b/stream/stream.go index b18b5ea..c66d238 100644 --- a/stream/stream.go +++ b/stream/stream.go @@ -37,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) {