Skip to content

Commit 6c3ce57

Browse files
authored
Improve Twitter client with tweet paging, including user metadata to tweet, and broading search function (#96)
1 parent bf6a906 commit 6c3ce57

File tree

1 file changed

+56
-24
lines changed

1 file changed

+56
-24
lines changed

pkg/twitter/client.go

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,17 @@ type PublicMetrics struct {
6464

6565
// Tweet represents the structure for a tweet in the Twitter API response
6666
type Tweet struct {
67-
ID string `json:"id"`
68-
Text string `json:"text"`
67+
ID string `json:"id"`
68+
Text string `json:"text"`
69+
AuthorID *string `json:"author_id"`
70+
71+
AdditionalMetadata AdditionalTweetMetadata
72+
}
73+
74+
// AdditionalTweetMetadata adds additinal metadata to a tweet that isn't directly
75+
// represented in the Twitter API response
76+
type AdditionalTweetMetadata struct {
77+
Author *User
6978
}
7079

7180
// GetUserById makes a request to the Twitter API and returns the user's information
@@ -95,39 +104,42 @@ func (c *Client) GetUserByUsername(ctx context.Context, username string) (*User,
95104
}
96105

97106
// GetUserTweets gets tweets for a given user
98-
//
99-
// todo: Doesn't support paging, so only the most recent ones are returned
100-
func (c *Client) GetUserTweets(ctx context.Context, userId string, maxResults int) ([]*Tweet, error) {
107+
func (c *Client) GetUserTweets(ctx context.Context, userId string, maxResults int, nextToken *string) ([]*Tweet, *string, error) {
101108
tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetUserTweets")
102109
defer tracer.End()
103110

104111
url := fmt.Sprintf(baseUrl+"users/"+userId+"/tweets?max_results=%d", maxResults)
112+
if nextToken != nil {
113+
url = fmt.Sprintf("%s&next_token=%s", url, *nextToken)
114+
}
105115

106-
tweets, err := c.getTweets(ctx, url)
116+
tweets, nextToken, err := c.getTweets(ctx, url)
107117
if err != nil {
108118
tracer.OnError(err)
109119
}
110-
return tweets, err
120+
return tweets, nextToken, err
111121
}
112122

113-
// SearchRecentUserTweets searches for tweets made by a user within the last 7 days
114-
//
115-
// todo: Doesn't support paging, so only the most recent ones are returned
116-
func (c *Client) SearchRecentUserTweets(ctx context.Context, userId, searchString string, maxResults int) ([]*Tweet, error) {
123+
// SearchRecentTweets searches for recent tweets within the last 7 days matching
124+
// a search string.
125+
func (c *Client) SearchRecentTweets(ctx context.Context, searchString string, maxResults int, nextToken *string) ([]*Tweet, *string, error) {
117126
tracer := metrics.TraceMethodCall(ctx, metricsStructName, "SearchUserTweets")
118127
defer tracer.End()
119128

120129
url := fmt.Sprintf(
121-
baseUrl+"tweets/search/recent?query=%s&max_results=%d",
122-
url.QueryEscape(fmt.Sprintf("from:%s %s", userId, searchString)),
130+
baseUrl+"tweets/search/recent?query=%s&expansions=author_id&user.fields=username&max_results=%d",
131+
url.QueryEscape(searchString),
123132
maxResults,
124133
)
134+
if nextToken != nil {
135+
url = fmt.Sprintf("%s&next_token=%s", url, *nextToken)
136+
}
125137

126-
tweets, err := c.getTweets(ctx, url)
138+
tweets, nextToken, err := c.getTweets(ctx, url)
127139
if err != nil {
128140
tracer.OnError(err)
129141
}
130-
return tweets, err
142+
return tweets, nextToken, err
131143
}
132144

133145
func (c *Client) getUser(ctx context.Context, fromUrl string) (*User, error) {
@@ -175,15 +187,15 @@ func (c *Client) getUser(ctx context.Context, fromUrl string) (*User, error) {
175187
return result.Data, nil
176188
}
177189

178-
func (c *Client) getTweets(ctx context.Context, fromUrl string) ([]*Tweet, error) {
190+
func (c *Client) getTweets(ctx context.Context, fromUrl string) ([]*Tweet, *string, error) {
179191
bearerToken, err := c.getBearerToken(c.clientId, c.clientSecret)
180192
if err != nil {
181-
return nil, err
193+
return nil, nil, err
182194
}
183195

184196
req, err := http.NewRequest("GET", fromUrl, nil)
185197
if err != nil {
186-
return nil, err
198+
return nil, nil, err
187199
}
188200

189201
req = req.WithContext(ctx)
@@ -192,32 +204,52 @@ func (c *Client) getTweets(ctx context.Context, fromUrl string) ([]*Tweet, error
192204

193205
resp, err := c.httpClient.Do(req)
194206
if err != nil {
195-
return nil, err
207+
return nil, nil, err
196208
}
197209
defer resp.Body.Close()
198210

199211
if resp.StatusCode != http.StatusOK {
200-
return nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
212+
return nil, nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
201213
}
202214

203215
var result struct {
204216
Data []*Tweet `json:"data"`
205217
Errors []*twitterError `json:"errors"`
218+
Meta struct {
219+
NextToken *string `json:"next_token"`
220+
} `json:"meta"`
221+
Includes struct {
222+
Users []User `json:"users"`
223+
} `json:"includes"`
206224
}
207225

208226
body, err := io.ReadAll(resp.Body)
209227
if err != nil {
210-
return nil, err
228+
return nil, nil, err
211229
}
212230

213231
if err := json.Unmarshal(body, &result); err != nil {
214-
return nil, err
232+
return nil, nil, err
215233
}
216234

217235
if len(result.Errors) > 0 {
218-
return nil, result.Errors[0].toError()
236+
return nil, nil, result.Errors[0].toError()
219237
}
220-
return result.Data, nil
238+
239+
for _, tweet := range result.Data {
240+
if tweet.AuthorID == nil {
241+
continue
242+
}
243+
244+
for _, user := range result.Includes.Users {
245+
if user.ID == *tweet.AuthorID {
246+
tweet.AdditionalMetadata.Author = &user
247+
break
248+
}
249+
}
250+
}
251+
252+
return result.Data, result.Meta.NextToken, nil
221253
}
222254

223255
func (c *Client) getBearerToken(clientId, clientSecret string) (string, error) {

0 commit comments

Comments
 (0)