diff --git a/v2/_examples/tweets/README.md b/v2/_examples/tweets/README.md index d6b80cb..9434353 100644 --- a/v2/_examples/tweets/README.md +++ b/v2/_examples/tweets/README.md @@ -17,6 +17,7 @@ The examples can be run my providing some options, including the authorization t * [Returns most recent tweets composed by a user](./timeline/user-tweet-timeline/main.go) * [Returns most recent tweet mentioning a user](./timeline/user-mention-timeline/main.go) +* [Allows you to retrieve a collection of the most recent Tweets and Retweets posted by you and users you follow.](./timeline/user-tweet-reverse-chronological-timeline/main.go) ### [Search Tweets](https://developer.twitter.com/en/docs/twitter-api/tweets/search/introduction) diff --git a/v2/_examples/tweets/timeline/user-tweet-reverse-chronological-timeline/main.go b/v2/_examples/tweets/timeline/user-tweet-reverse-chronological-timeline/main.go new file mode 100644 index 0000000..4bf41b5 --- /dev/null +++ b/v2/_examples/tweets/timeline/user-tweet-reverse-chronological-timeline/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + + twitter "github.com/g8rswimmer/go-twitter/v2" +) + +type authorize struct { + Token string +} + +func (a authorize) Add(req *http.Request) { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.Token)) +} + +/** + In order to run, the user will need to provide the bearer token and the list of tweet ids. +**/ +func main() { + token := flag.String("token", "", "twitter API token") + userID := flag.String("user_id", "", "user id") + flag.Parse() + + client := &twitter.Client{ + Authorizer: authorize{ + Token: *token, + }, + Client: http.DefaultClient, + Host: "https://api.twitter.com", + } + opts := twitter.UserTweetReverseChronologicalTimelineOpts{ + TweetFields: []twitter.TweetField{twitter.TweetFieldCreatedAt, twitter.TweetFieldAuthorID, twitter.TweetFieldConversationID, twitter.TweetFieldPublicMetrics, twitter.TweetFieldContextAnnotations}, + UserFields: []twitter.UserField{twitter.UserFieldUserName}, + Expansions: []twitter.Expansion{twitter.ExpansionAuthorID}, + MaxResults: 5, + } + + fmt.Println("Callout to tweet user reverse chronological timeline callout") + + timeline, err := client.UserTweetReverseChronologicalTimeline(context.Background(), *userID, opts) + if err != nil { + log.Panicf("user reverse chronological timeline error: %v", err) + } + + dictionaries := timeline.Raw.TweetDictionaries() + + enc, err := json.MarshalIndent(dictionaries, "", " ") + if err != nil { + log.Panic(err) + } + fmt.Println(string(enc)) + + metaBytes, err := json.MarshalIndent(timeline.Meta, "", " ") + if err != nil { + log.Panic(err) + } + fmt.Println(string(metaBytes)) +} diff --git a/v2/client.go b/v2/client.go index 2ef9b91..c3d0f26 100644 --- a/v2/client.go +++ b/v2/client.go @@ -11,34 +11,36 @@ import ( ) const ( - tweetMaxIDs = 100 - userMaxIDs = 100 - spaceMaxIDs = 100 - spaceByCreatorMaxIDs = 100 - userMaxNames = 100 - tweetRecentSearchQueryLength = 512 - tweetSearchQueryLength = 1024 - tweetRecentCountsQueryLength = 512 - tweetAllCountsQueryLength = 1024 - userBlocksMaxResults = 1000 - userMutesMaxResults = 1000 - likesMaxResults = 100 - likesMinResults = 10 - sampleStreamMaxBackoffMin = 5 - userListMaxResults = 100 - listTweetMaxResults = 100 - userListMembershipMaxResults = 100 - listUserMemberMaxResults = 100 - userListFollowedMaxResults = 100 - listuserFollowersMaxResults = 100 - quoteTweetMaxResults = 100 - quoteTweetMinResults = 10 - tweetBookmarksMaxResults = 100 - userTweetTimelineMinResults = 5 - userTweetTimelineMaxResults = 100 - userMentionTimelineMinResults = 5 - userMentionTimelineMaxResults = 100 - userRetweetLookupMaxResults = 100 + tweetMaxIDs = 100 + userMaxIDs = 100 + spaceMaxIDs = 100 + spaceByCreatorMaxIDs = 100 + userMaxNames = 100 + tweetRecentSearchQueryLength = 512 + tweetSearchQueryLength = 1024 + tweetRecentCountsQueryLength = 512 + tweetAllCountsQueryLength = 1024 + userBlocksMaxResults = 1000 + userMutesMaxResults = 1000 + likesMaxResults = 100 + likesMinResults = 10 + sampleStreamMaxBackOffMin = 5 + userListMaxResults = 100 + listTweetMaxResults = 100 + userListMembershipMaxResults = 100 + listUserMemberMaxResults = 100 + userListFollowedMaxResults = 100 + listUserFollowersMaxResults = 100 + quoteTweetMaxResults = 100 + quoteTweetMinResults = 10 + tweetBookmarksMaxResults = 100 + userTweetTimelineMinResults = 5 + userTweetTimelineMaxResults = 100 + userMentionTimelineMinResults = 5 + userMentionTimelineMaxResults = 100 + userRetweetLookupMaxResults = 100 + userTweetReverseChronologicalTimelineMinResults = 1 + userTweetReverseChronologicalTimelineMaxResults = 100 ) // Client is used to make twitter v2 API callouts. @@ -937,8 +939,8 @@ func (c *Client) TweetSearchStreamRules(ctx context.Context, ruleIDs []TweetSear func (c *Client) TweetSearchStream(ctx context.Context, opts TweetSearchStreamOpts) (*TweetStream, error) { switch { case opts.BackfillMinutes == 0: - case opts.BackfillMinutes > sampleStreamMaxBackoffMin: - return nil, fmt.Errorf("tweet search stream: a max backoff minutes [%d] is [current: %d]: %w", sampleStreamMaxBackoffMin, opts.BackfillMinutes, ErrParameter) + case opts.BackfillMinutes > sampleStreamMaxBackOffMin: + return nil, fmt.Errorf("tweet search stream: a max backoff minutes [%d] is [current: %d]: %w", sampleStreamMaxBackOffMin, opts.BackfillMinutes, ErrParameter) default: } @@ -1501,6 +1503,73 @@ func (c *Client) UserMentionTimeline(ctx context.Context, userID string, opts Us return timeline, nil } +// UserTweetReverseChronologicalTimeline allows you to retrieve a collection of the most recent Tweets and Retweets posted by you and users you follow. +// This endpoint returns up to the last 3200 Tweets. +func (c *Client) UserTweetReverseChronologicalTimeline(ctx context.Context, userID string, opts UserTweetReverseChronologicalTimelineOpts) (*UserTweetReverseChronologicalTimelineResponse, error) { + switch { + case len(userID) == 0: + return nil, fmt.Errorf("user tweet reverse chronological timeline: a query is required: %w", ErrParameter) + case opts.MaxResults == 0: + case opts.MaxResults < userTweetReverseChronologicalTimelineMinResults: + return nil, fmt.Errorf("user tweet reverse chronological timeline: max results [%d] have a min[%d] %w", opts.MaxResults, userTweetReverseChronologicalTimelineMinResults, ErrParameter) + case opts.MaxResults > userTweetReverseChronologicalTimelineMaxResults: + return nil, fmt.Errorf("user tweet reverse chronological timeline: max results [%d] have a max[%d] %w", opts.MaxResults, userTweetReverseChronologicalTimelineMaxResults, ErrParameter) + default: + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, userTweetReverseChronologicalTimelineEndpoint.urlID(c.Host, userID), nil) + if err != nil { + return nil, fmt.Errorf("user tweet reverse chronological timeline request: %w", err) + } + req.Header.Add("Accept", "application/json") + c.Authorizer.Add(req) + opts.addQuery(req) + + resp, err := c.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("user tweet reverse chronological timeline response: %w", err) + } + defer resp.Body.Close() + + rl := rateFromHeader(resp.Header) + + decoder := json.NewDecoder(resp.Body) + + if resp.StatusCode != http.StatusOK { + e := &ErrorResponse{} + if err := decoder.Decode(e); err != nil { + return nil, &HTTPError{ + Status: resp.Status, + StatusCode: resp.StatusCode, + URL: resp.Request.URL.String(), + RateLimit: rl, + } + } + e.StatusCode = resp.StatusCode + e.RateLimit = rl + return nil, e + } + + timeline := struct { + TweetRaw + Meta UserReverseChronologicalTimelineMeta `json:"meta"` + }{} + + if err := decoder.Decode(&timeline); err != nil { + return nil, &ResponseDecodeError{ + Name: "user tweet reverse chronological timeline", + Err: err, + RateLimit: rl, + } + } + + return &UserTweetReverseChronologicalTimelineResponse{ + Raw: &timeline.TweetRaw, + Meta: &timeline.Meta, + RateLimit: rl, + }, nil +} + // TweetHideReplies will hide the replies for a given tweet func (c Client) TweetHideReplies(ctx context.Context, id string, hide bool) (*TweetHideReplyResponse, error) { if len(id) == 0 { @@ -2318,8 +2387,8 @@ func (c *Client) DeleteUserLikes(ctx context.Context, userID, tweetID string) (* func (c *Client) TweetSampleStream(ctx context.Context, opts TweetSampleStreamOpts) (*TweetStream, error) { switch { case opts.BackfillMinutes == 0: - case opts.BackfillMinutes > sampleStreamMaxBackoffMin: - return nil, fmt.Errorf("tweet sample stream: a max backoff minutes [%d] is [current: %d]: %w", sampleStreamMaxBackoffMin, opts.BackfillMinutes, ErrParameter) + case opts.BackfillMinutes > sampleStreamMaxBackOffMin: + return nil, fmt.Errorf("tweet sample stream: a max backoff minutes [%d] is [current: %d]: %w", sampleStreamMaxBackOffMin, opts.BackfillMinutes, ErrParameter) default: } @@ -3365,8 +3434,8 @@ func (c *Client) ListUserFollowers(ctx context.Context, listID string, opts List case len(listID) == 0: return nil, fmt.Errorf("list user followers: an id is required: %w", ErrParameter) case opts.MaxResults == 0: - case opts.MaxResults > listuserFollowersMaxResults: - return nil, fmt.Errorf("list user followers: max results [%d] is greater thanmax [%d]: %w", opts.MaxResults, listuserFollowersMaxResults, ErrParameter) + case opts.MaxResults > listUserFollowersMaxResults: + return nil, fmt.Errorf("list user followers: max results [%d] is greater thanmax [%d]: %w", opts.MaxResults, listUserFollowersMaxResults, ErrParameter) default: } @@ -3774,7 +3843,7 @@ func (c *Client) CreateComplianceBatchJob(ctx context.Context, jobType Complianc return nil, fmt.Errorf("create compliance batch job request encode: %w", err) } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, complianceJobsEndpiont.url(c.Host), bytes.NewReader(enc)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, complianceJobsEndpoint.url(c.Host), bytes.NewReader(enc)) if err != nil { return nil, fmt.Errorf("create compliance batch job request: %w", err) } @@ -3831,7 +3900,7 @@ func (c *Client) ComplianceBatchJob(ctx context.Context, id string) (*Compliance default: } - ep := complianceJobsEndpiont.url(c.Host) + "/" + id + ep := complianceJobsEndpoint.url(c.Host) + "/" + id req, err := http.NewRequestWithContext(ctx, http.MethodGet, ep, nil) if err != nil { return nil, fmt.Errorf("compliance batch job request: %w", err) @@ -3888,7 +3957,7 @@ func (c *Client) ComplianceBatchJobLookup(ctx context.Context, jobType Complianc default: } - ep := complianceJobsEndpiont.url(c.Host) + ep := complianceJobsEndpoint.url(c.Host) req, err := http.NewRequestWithContext(ctx, http.MethodGet, ep, nil) if err != nil { return nil, fmt.Errorf("compliance batch job lookup request: %w", err) diff --git a/v2/client_compliance_jobs_test.go b/v2/client_compliance_jobs_test.go index da646ea..67648bc 100644 --- a/v2/client_compliance_jobs_test.go +++ b/v2/client_compliance_jobs_test.go @@ -36,8 +36,8 @@ func TestClient_CreateComplianceBatchJob(t *testing.T) { if req.Method != http.MethodPost { log.Panicf("the method is not correct %s %s", req.Method, http.MethodPost) } - if strings.Contains(req.URL.String(), string(complianceJobsEndpiont)) == false { - log.Panicf("the url is not correct %s %s", req.URL.String(), complianceJobsEndpiont) + if strings.Contains(req.URL.String(), string(complianceJobsEndpoint)) == false { + log.Panicf("the url is not correct %s %s", req.URL.String(), complianceJobsEndpoint) } body := `{ "data": { @@ -140,8 +140,8 @@ func TestClient_ComplianceBatchJob(t *testing.T) { if req.Method != http.MethodGet { log.Panicf("the method is not correct %s %s", req.Method, http.MethodGet) } - if strings.Contains(req.URL.String(), string(complianceJobsEndpiont)+"/job-id") == false { - log.Panicf("the url is not correct %s %s", req.URL.String(), complianceJobsEndpiont) + if strings.Contains(req.URL.String(), string(complianceJobsEndpoint)+"/job-id") == false { + log.Panicf("the url is not correct %s %s", req.URL.String(), complianceJobsEndpoint) } body := `{ "data": { @@ -243,8 +243,8 @@ func TestClient_ComplianceBatchJobLookup(t *testing.T) { if req.Method != http.MethodGet { log.Panicf("the method is not correct %s %s", req.Method, http.MethodGet) } - if strings.Contains(req.URL.String(), string(complianceJobsEndpiont)) == false { - log.Panicf("the url is not correct %s %s", req.URL.String(), complianceJobsEndpiont) + if strings.Contains(req.URL.String(), string(complianceJobsEndpoint)) == false { + log.Panicf("the url is not correct %s %s", req.URL.String(), complianceJobsEndpoint) } body := `{ "data": [ diff --git a/v2/client_user_tweet_timeline_test.go b/v2/client_user_tweet_timeline_test.go index 41ac897..28a95ce 100644 --- a/v2/client_user_tweet_timeline_test.go +++ b/v2/client_user_tweet_timeline_test.go @@ -292,3 +292,220 @@ func TestClient_UserTweetTimeline(t *testing.T) { }) } } + +func TestClient_UserTweetReverseChronologicalTimeline(t *testing.T) { + type fields struct { + Authorizer Authorizer + Client *http.Client + Host string + } + type args struct { + userID string + opts UserTweetReverseChronologicalTimelineOpts + } + tests := []struct { + name string + fields fields + args args + want *UserTweetReverseChronologicalTimelineResponse + wantErr bool + }{ + { + name: "success", + fields: fields{ + Authorizer: &mockAuth{}, + Host: "https://www.test.com", + Client: mockHTTPClient(func(req *http.Request) *http.Response { + if req.Method != http.MethodGet { + log.Panicf("the method is not correct %s %s", req.Method, http.MethodGet) + } + if strings.Contains(req.URL.String(), userTweetReverseChronologicalTimelineEndpoint.urlID("", "2244994945")) == false { + log.Panicf("the url is not correct %s %s", req.URL.String(), userTweetReverseChronologicalTimelineEndpoint) + } + body := `{ + "data": [ + { + "id": "1524796546306478083", + "text": "Today marks the launch of Devs in the Details, a technical video series made for developers by developers building with the Twitter API. 🚀nnIn this premiere episode, @jessicagarson walks us through how she built @FactualCat #WelcomeToOurTechTalkn⬇️nnhttps://t.co/nGa8JTQVBJ" + }, + { + "id": "1522642323535847424", + "text": "We’ve gone into more detail on each Insider in our forum post. nnJoin us in congratulating the new additions! 🥳nnhttps://t.co/0r5maYEjPJ" + } + ], + "meta": { + "result_count": 5, + "newest_id": "1524796546306478083", + "oldest_id": "1522642323535847424", + "next_token": "7140dibdnow9c7btw421dyz6jism75z99gyxd8egarsc4" + } + }` + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(body)), + Header: func() http.Header { + h := http.Header{} + h.Add(rateLimit, "15") + h.Add(rateRemaining, "12") + h.Add(rateReset, "1644461060") + return h + }(), + } + }), + }, + args: args{ + userID: "2244994945", + }, + want: &UserTweetReverseChronologicalTimelineResponse{ + Raw: &TweetRaw{ + Tweets: []*TweetObj{ + { + ID: "1524796546306478083", + Text: "Today marks the launch of Devs in the Details, a technical video series made for developers by developers building with the Twitter API. 🚀nnIn this premiere episode, @jessicagarson walks us through how she built @FactualCat #WelcomeToOurTechTalkn⬇️nnhttps://t.co/nGa8JTQVBJ", + }, + { + ID: "1522642323535847424", + Text: "We’ve gone into more detail on each Insider in our forum post. nnJoin us in congratulating the new additions! 🥳nnhttps://t.co/0r5maYEjPJ", + }, + }, + }, + Meta: &UserReverseChronologicalTimelineMeta{ + ResultCount: 5, + NewestID: "1524796546306478083", + OldestID: "1522642323535847424", + NextToken: "7140dibdnow9c7btw421dyz6jism75z99gyxd8egarsc4", + }, + RateLimit: &RateLimit{ + Limit: 15, + Remaining: 12, + Reset: Epoch(1644461060), + }, + }, + wantErr: false, + }, + { + name: "success with options", + fields: fields{ + Authorizer: &mockAuth{}, + Host: "https://www.test.com", + Client: mockHTTPClient(func(req *http.Request) *http.Response { + if req.Method != http.MethodGet { + log.Panicf("the method is not correct %s %s", req.Method, http.MethodGet) + } + if strings.Contains(req.URL.String(), userTweetReverseChronologicalTimelineEndpoint.urlID("", "2244994945")) == false { + log.Panicf("the url is not correct %s %s", req.URL.String(), userTweetReverseChronologicalTimelineEndpoint) + } + body := `{ + "data": [ + { + "created_at": "2022-05-12T17:00:00.000Z", + "text": "Today marks the launch of Devs in the Details, a technical video series made for developers by developers building with the Twitter API. 🚀nnIn this premiere episode, @jessicagarson walks us through how she built @FactualCat #WelcomeToOurTechTalkn⬇️nnhttps://t.co/nGa8JTQVBJ", + "author_id": "2244994945", + "id": "1524796546306478083" + }, + { + "created_at": "2022-05-06T18:19:53.000Z", + "text": "We’ve gone into more detail on each Insider in our forum post. nnJoin us in congratulating the new additions! 🥳nnhttps://t.co/0r5maYEjPJ", + "author_id": "2244994945", + "id": "1522642323535847424" + } + ], + "includes": { + "users": [ + { + "created_at": "2013-12-14T04:35:55.000Z", + "name": "Twitter Dev", + "username": "TwitterDev", + "id": "2244994945" + } + ] + }, + "meta": { + "result_count": 5, + "newest_id": "1524796546306478083", + "oldest_id": "1522642323535847424", + "next_token": "7140dibdnow9c7btw421dyz6jism75z99gyxd8egarsc4" + } + }` + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(body)), + Header: func() http.Header { + h := http.Header{} + h.Add(rateLimit, "15") + h.Add(rateRemaining, "12") + h.Add(rateReset, "1644461060") + return h + }(), + } + }), + }, + args: args{ + userID: "2244994945", + opts: UserTweetReverseChronologicalTimelineOpts{ + Expansions: []Expansion{ExpansionAuthorID}, + TweetFields: []TweetField{TweetFieldCreatedAt}, + UserFields: []UserField{UserFieldCreatedAt, UserFieldName}, + MaxResults: 5, + }, + }, + want: &UserTweetReverseChronologicalTimelineResponse{ + Raw: &TweetRaw{ + Tweets: []*TweetObj{ + { + CreatedAt: "2022-05-12T17:00:00.000Z", + AuthorID: "2244994945", + ID: "1524796546306478083", + Text: "Today marks the launch of Devs in the Details, a technical video series made for developers by developers building with the Twitter API. 🚀nnIn this premiere episode, @jessicagarson walks us through how she built @FactualCat #WelcomeToOurTechTalkn⬇️nnhttps://t.co/nGa8JTQVBJ", + }, + { + CreatedAt: "2022-05-06T18:19:53.000Z", + AuthorID: "2244994945", + ID: "1522642323535847424", + Text: "We’ve gone into more detail on each Insider in our forum post. nnJoin us in congratulating the new additions! 🥳nnhttps://t.co/0r5maYEjPJ", + }, + }, + Includes: &TweetRawIncludes{ + Users: []*UserObj{ + { + CreatedAt: "2013-12-14T04:35:55.000Z", + Name: "Twitter Dev", + UserName: "TwitterDev", + ID: "2244994945", + }, + }, + }, + }, + Meta: &UserReverseChronologicalTimelineMeta{ + ResultCount: 5, + NewestID: "1524796546306478083", + OldestID: "1522642323535847424", + NextToken: "7140dibdnow9c7btw421dyz6jism75z99gyxd8egarsc4", + }, + RateLimit: &RateLimit{ + Limit: 15, + Remaining: 12, + Reset: Epoch(1644461060), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + Authorizer: tt.fields.Authorizer, + Client: tt.fields.Client, + Host: tt.fields.Host, + } + got, err := c.UserTweetReverseChronologicalTimeline(context.Background(), tt.args.userID, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("Client.UserTweetReverseChronologicalTimeline() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.UserTweetReverseChronologicalTimeline() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/endpoints.go b/v2/endpoints.go index b10c356..eb153cb 100644 --- a/v2/endpoints.go +++ b/v2/endpoints.go @@ -8,50 +8,51 @@ import ( type endpoint string const ( - tweetLookupEndpoint endpoint = "2/tweets" - tweetCreateEndpoint endpoint = "2/tweets" - tweetDeleteEndpoint endpoint = "2/tweets/{id}" - userLookupEndpoint endpoint = "2/users" - userNameLookupEndpoint endpoint = "2/users/by" - userAuthLookupEndpoint endpoint = "2/users/me" - userManageRetweetEndpoint endpoint = "2/users/{id}/retweets" - userBlocksEndpoint endpoint = "2/users/{id}/blocking" - userMutesEndpoint endpoint = "2/users/{id}/muting" - userRetweetLookupEndpoint endpoint = "2/tweets/{id}/retweeted_by" - tweetRecentSearchEndpoint endpoint = "2/tweets/search/recent" - tweetSearchEndpoint endpoint = "2/tweets/search/all" - tweetRecentCountsEndpoint endpoint = "2/tweets/counts/recent" - tweetAllCountsEndpoint endpoint = "2/tweets/counts/all" - userFollowingEndpoint endpoint = "2/users/{id}/following" - userFollowersEndpoint endpoint = "2/users/{id}/followers" - userTweetTimelineEndpoint endpoint = "2/users/{id}/tweets" - userMentionTimelineEndpoint endpoint = "2/users/{id}/mentions" - tweetHideRepliesEndpoint endpoint = "2/tweets/{id}/hidden" - tweetLikesEndpoint endpoint = "2/tweets/{id}/liking_users" - userLikedTweetEndpoint endpoint = "2/users/{id}/liked_tweets" - userLikesEndpoint endpoint = "2/users/{id}/likes" - tweetSampleStreamEndpoint endpoint = "2/tweets/sample/stream" - tweetSearchStreamRulesEndpoint endpoint = "2/tweets/search/stream/rules" - tweetSearchStreamEndpoint endpoint = "2/tweets/search/stream" - listLookupEndpoint endpoint = "2/lists/{id}" - userListLookupEndpoint endpoint = "2/users/{id}/owned_lists" - listTweetLookupEndpoint endpoint = "2/lists/{id}/tweets" - listCreateEndpoint endpoint = "2/lists" - listUpdateEndpoint endpoint = "2/lists/{id}" - listDeleteEndpoint endpoint = "2/lists/{id}" - listMemberEndpoint endpoint = "2/lists/{id}/members" - userListMemberEndpoint endpoint = "2/users/{id}/list_memberships" - userPinnedListEndpoint endpoint = "2/users/{id}/pinned_lists" - userFollowedListEndpoint endpoint = "2/users/{id}/followed_lists" - listUserFollowersEndpoint endpoint = "2/lists/{id}/followers" - spaceLookupEndpoint endpoint = "2/spaces" - spaceByCreatorLookupEndpoint endpoint = "2/spaces/by/creator_ids" - spaceBuyersLookupEndpoint endpoint = "2/spaces/{id}/buyers" - spaceTweetsLookupEndpoint endpoint = "2/spaces/{id}/tweets" - spaceSearchEndpoint endpoint = "2/spaces/search" - complianceJobsEndpiont endpoint = "2/compliance/jobs" - quoteTweetLookupEndpoint endpoint = "2/tweets/{id}/quote_tweets" - tweetBookmarksEndpoint endpoint = "2/users/{id}/bookmarks" + tweetLookupEndpoint endpoint = "2/tweets" + tweetCreateEndpoint endpoint = "2/tweets" + tweetDeleteEndpoint endpoint = "2/tweets/{id}" + userLookupEndpoint endpoint = "2/users" + userNameLookupEndpoint endpoint = "2/users/by" + userAuthLookupEndpoint endpoint = "2/users/me" + userManageRetweetEndpoint endpoint = "2/users/{id}/retweets" + userBlocksEndpoint endpoint = "2/users/{id}/blocking" + userMutesEndpoint endpoint = "2/users/{id}/muting" + userRetweetLookupEndpoint endpoint = "2/tweets/{id}/retweeted_by" + tweetRecentSearchEndpoint endpoint = "2/tweets/search/recent" + tweetSearchEndpoint endpoint = "2/tweets/search/all" + tweetRecentCountsEndpoint endpoint = "2/tweets/counts/recent" + tweetAllCountsEndpoint endpoint = "2/tweets/counts/all" + userFollowingEndpoint endpoint = "2/users/{id}/following" + userFollowersEndpoint endpoint = "2/users/{id}/followers" + userTweetTimelineEndpoint endpoint = "2/users/{id}/tweets" + userMentionTimelineEndpoint endpoint = "2/users/{id}/mentions" + userTweetReverseChronologicalTimelineEndpoint endpoint = "2/users/{id}/timelines/reverse_chronological" + tweetHideRepliesEndpoint endpoint = "2/tweets/{id}/hidden" + tweetLikesEndpoint endpoint = "2/tweets/{id}/liking_users" + userLikedTweetEndpoint endpoint = "2/users/{id}/liked_tweets" + userLikesEndpoint endpoint = "2/users/{id}/likes" + tweetSampleStreamEndpoint endpoint = "2/tweets/sample/stream" + tweetSearchStreamRulesEndpoint endpoint = "2/tweets/search/stream/rules" + tweetSearchStreamEndpoint endpoint = "2/tweets/search/stream" + listLookupEndpoint endpoint = "2/lists/{id}" + userListLookupEndpoint endpoint = "2/users/{id}/owned_lists" + listTweetLookupEndpoint endpoint = "2/lists/{id}/tweets" + listCreateEndpoint endpoint = "2/lists" + listUpdateEndpoint endpoint = "2/lists/{id}" + listDeleteEndpoint endpoint = "2/lists/{id}" + listMemberEndpoint endpoint = "2/lists/{id}/members" + userListMemberEndpoint endpoint = "2/users/{id}/list_memberships" + userPinnedListEndpoint endpoint = "2/users/{id}/pinned_lists" + userFollowedListEndpoint endpoint = "2/users/{id}/followed_lists" + listUserFollowersEndpoint endpoint = "2/lists/{id}/followers" + spaceLookupEndpoint endpoint = "2/spaces" + spaceByCreatorLookupEndpoint endpoint = "2/spaces/by/creator_ids" + spaceBuyersLookupEndpoint endpoint = "2/spaces/{id}/buyers" + spaceTweetsLookupEndpoint endpoint = "2/spaces/{id}/tweets" + spaceSearchEndpoint endpoint = "2/spaces/search" + complianceJobsEndpoint endpoint = "2/compliance/jobs" + quoteTweetLookupEndpoint endpoint = "2/tweets/{id}/quote_tweets" + tweetBookmarksEndpoint endpoint = "2/users/{id}/bookmarks" idTag = "{id}" ) diff --git a/v2/tweet_options.go b/v2/tweet_options.go index fb088a6..d878d97 100644 --- a/v2/tweet_options.go +++ b/v2/tweet_options.go @@ -163,3 +163,66 @@ func (t UserMentionTimelineOpts) addQuery(req *http.Request) { req.URL.RawQuery = q.Encode() } } + +// UserTweetReverseChronologicalTimelineOpts are the options for the user tweet reverse chronological timeline +type UserTweetReverseChronologicalTimelineOpts struct { + Expansions []Expansion + MediaFields []MediaField + PlaceFields []PlaceField + PollFields []PollField + TweetFields []TweetField + UserFields []UserField + Excludes []Exclude + StartTime time.Time + EndTime time.Time + MaxResults int + PaginationToken string + SinceID string + UntilID string +} + +func (t UserTweetReverseChronologicalTimelineOpts) addQuery(req *http.Request) { + q := req.URL.Query() + if len(t.Expansions) > 0 { + q.Add("expansions", strings.Join(expansionStringArray(t.Expansions), ",")) + } + if len(t.MediaFields) > 0 { + q.Add("media.fields", strings.Join(mediaFieldStringArray(t.MediaFields), ",")) + } + if len(t.PlaceFields) > 0 { + q.Add("place.fields", strings.Join(placeFieldStringArray(t.PlaceFields), ",")) + } + if len(t.PollFields) > 0 { + q.Add("poll.fields", strings.Join(pollFieldStringArray(t.PollFields), ",")) + } + if len(t.TweetFields) > 0 { + q.Add("tweet.fields", strings.Join(tweetFieldStringArray(t.TweetFields), ",")) + } + if len(t.UserFields) > 0 { + q.Add("user.fields", strings.Join(userFieldStringArray(t.UserFields), ",")) + } + if len(t.Excludes) > 0 { + q.Add("exclude", strings.Join(excludeStringArray(t.Excludes), ",")) + } + if !t.StartTime.IsZero() { + q.Add("start_time", t.StartTime.Format(time.RFC3339)) + } + if !t.EndTime.IsZero() { + q.Add("end_time", t.EndTime.Format(time.RFC3339)) + } + if t.MaxResults > 0 { + q.Add("max_results", strconv.Itoa(t.MaxResults)) + } + if len(t.PaginationToken) > 0 { + q.Add("pagination_token", t.PaginationToken) + } + if len(t.SinceID) > 0 { + q.Add("since_id", t.SinceID) + } + if len(t.UntilID) > 0 { + q.Add("until_id", t.UntilID) + } + if len(q) > 0 { + req.URL.RawQuery = q.Encode() + } +} diff --git a/v2/tweet_raw.go b/v2/tweet_raw.go index d57d06f..e332582 100644 --- a/v2/tweet_raw.go +++ b/v2/tweet_raw.go @@ -17,13 +17,29 @@ type TweetLookupResponse struct { RateLimit *RateLimit } -// UserMentionTimelineResponse contains the information from the user mention timelint callout +// UserMentionTimelineResponse contains the information from the user mention timeline callout type UserMentionTimelineResponse struct { Raw *TweetRaw Meta *UserTimelineMeta `json:"meta"` RateLimit *RateLimit } +// UserTweetReverseChronologicalTimelineResponse contains the tweet timeline for the reverse chronological timeline +type UserTweetReverseChronologicalTimelineResponse struct { + Raw *TweetRaw + Meta *UserReverseChronologicalTimelineMeta `json:"meta"` + RateLimit *RateLimit +} + +// UserReverseChronologicalTimelineMeta has the meta data from the reverse chronological timeline +type UserReverseChronologicalTimelineMeta struct { + ResultCount int `json:"result_count"` + NewestID string `json:"newest_id"` + OldestID string `json:"oldest_id"` + NextToken string `json:"next_token"` + PreviousToken string `json:"previous_token"` +} + // UserTweetTimelineResponse contains the information from the user tweet timeline callout type UserTweetTimelineResponse struct { Raw *TweetRaw