This repository has been archived by the owner on Aug 13, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
tweets.go
305 lines (283 loc) · 9.19 KB
/
tweets.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
// Copyright (C) 2020 Michael J. Fromberger. All Rights Reserved.
// Package tweets supports queries for tweet lookup and search.
//
// # Lookup
//
// To look up one or more tweets by ID, use tweets.Lookup. Additional IDs can
// be given in the options:
//
// single := tweets.Lookup(id, nil)
// multi := tweets.Lookup(id1, &tweets.LookupOpts{
// More: []string{id2, id3},
// })
//
// By default only the default fields are returned (see types.Tweet). To
// request additional fields or expansions, include them in the options:
//
// q := tweets.Lookup(id, &tweets.LookupOpts{
// Optional: []types.Fields{
// types.TweetFields{AuthorID: true, PublicMetrics: true},
// types.MediaFields{Duration: true},
// types.Expansions{types.Expand_AuthorID},
// },
// })
//
// Invoke the query to fetch the tweets:
//
// rsp, err := q.Invoke(ctx, cli)
//
// The Tweets field of the response contains the requested tweets. In addition,
// any attachments resulting from expansions can be fetched using methods on
// the *Reply, e.g., rsp.IncludedTweets. Note that tweet IDs that could not be
// found or accessed (e.g., for deleted or protected tweets) are not reported
// as an error. Instead. the caller should examine the ErrorDetail messages in
// the Errors field of the Reply, if requested tweets are not listed.
//
// # Search
//
// To search recent tweets, use tweets.SearchRecent:
//
// q := tweets.SearchRecent(`from:jack has:mentions -has:media`, nil)
//
// For search query syntax, see
// https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/build-a-rule
//
// Search results can be paginated. Specifically, if there are more results
// available than the requested cap (max_results), the server response will
// contain a pagination token that can be used to fetch more. Invoking a search
// query automatically updates the query with this pagination token, so
// invoking the query again will fetch the remaining results:
//
// for q.HasMorePages() {
// rsp, err := q.Invoke(ctx, cli)
// // ...
// }
//
// Use q.ResetPageToken to reset the query.
//
// # Streaming
//
// Streaming queries take a callback that receives each response sent by the
// server. Streaming continues as long as there are more results, or until the
// callback reports an error. The tweets.SearchStream and tweets.SampleStream
// functions use this interface.
//
// For example:
//
// q := tweets.SearchStream(func(rsp *tweets.Reply) error {
// handle(rsp)
// if !wantMore() {
// return jape.ErrStopStreaming
// }
// return nil
// }, nil)
//
// If the callback returns jape.ErrStopStreaming, the stream is terminated
// without error; otherwise the error returned by the callback is reported to
// the caller of the query. For the common and simple case of limiting the
// number of results, you can use the MaxResults stream option.
//
// Expansions and non-default fields can be requested using *StreamOpts:
//
// opts := &tweets.StreamOpts{
// Optional: []types.Fields{
// types.Expansions{types.Expand_MediaKeys},
// types.MediaFields{PublicMetrics: true},
// },
// }
package tweets
import (
"context"
"encoding/json"
"strconv"
"github.com/creachadair/twitter"
"github.com/creachadair/twitter/jape"
"github.com/creachadair/twitter/types"
)
// Lookup constructs a lookup query for one or more tweet IDs. To look up
// multiple IDs, add subsequent values the opts.More field.
//
// API: 2/tweets
func Lookup(id string, opts *LookupOpts) Query {
req := &jape.Request{
Method: "2/tweets",
Params: make(jape.Params),
}
req.Params.Add("ids", id)
opts.addRequestParams(req)
return Query{Request: req}
}
// LikedBy constructs a query for the tweets liked by a given user.
//
// API: 2/users/:id/liked_tweets
func LikedBy(userID string, opts *ListOpts) Query {
req := &jape.Request{
Method: "2/users/" + userID + "/liked_tweets",
Params: make(jape.Params),
}
opts.addRequestParams(req)
return Query{Request: req}
}
// Quotes consstructs a query for the quotes of a given tweet ID.
//
// API: 2/tweets/:id/quote_tweets
func Quotes(id string, opts *ListOpts) Query {
req := &jape.Request{
Method: "2/tweets/" + id + "/quote_tweets",
Params: make(jape.Params),
}
opts.addRequestParams(req)
return Query{Request: req}
}
// MentioningUser constructs a query for tweets that mention the given user ID.
//
// API: 2/users/:id/mentions
func MentioningUser(userID string, opts *ListOpts) Query {
req := &jape.Request{
Method: "2/users/" + userID + "/mentions",
Params: make(jape.Params),
}
opts.addRequestParams(req)
return Query{Request: req}
}
// FromUser constructs a query for tweets posted by the given user ID.
//
// API: 2/users/:id/tweets
func FromUser(userID string, opts *ListOpts) Query {
req := &jape.Request{
Method: "2/users/" + userID + "/tweets",
Params: make(jape.Params),
}
opts.addRequestParams(req)
return Query{Request: req}
}
// BookmarkedBy constructs a query for tweets bookmarked by the given user ID.
//
// API: 2/users/:id/bookmarks
func BookmarkedBy(userID string, opts *ListOpts) Query {
req := &jape.Request{
Method: "2/users/" + userID + "/bookmarks",
Params: make(jape.Params),
}
opts.addRequestParams(req)
return Query{Request: req}
}
// A Query performs a lookup or search query.
type Query struct {
*jape.Request
encodeErr error
}
func (q Query) nextTokenParam() string {
// N.B. For some reason the "search recent" API uses a different pagination
// token parameter the rest of the API.
if q.Request.Method == "2/tweets/search/recent" {
return "next_token"
}
return twitter.NextTokenParam
}
// Invoke executes the query on the given context and client. If the reply
// contains a pagination token, q is updated in-place so that invoking the
// query again will fetch the next page.
func (q Query) Invoke(ctx context.Context, cli *twitter.Client) (*Reply, error) {
if q.encodeErr != nil {
return nil, q.encodeErr // deferred encoding error
}
rsp, err := cli.Call(ctx, q.Request)
if err != nil {
return nil, err
}
out := &Reply{Reply: rsp}
if len(rsp.Data) == 0 {
// no results
} else if rsp.Data[0] == '{' {
out.Tweets = append(out.Tweets, new(types.Tweet))
err = json.Unmarshal(rsp.Data, out.Tweets[0])
} else {
err = json.Unmarshal(rsp.Data, &out.Tweets)
}
if err != nil {
return nil, &jape.Error{Data: rsp.Data, Message: "decoding tweet data", Err: err}
}
// Maintain the flag validity for lookup queries.
q.Request.Params.Set(q.nextTokenParam(), "")
if len(rsp.Meta) != 0 {
if err := json.Unmarshal(rsp.Meta, &out.Meta); err != nil {
return nil, &jape.Error{Data: rsp.Meta, Message: "decoding response metadata", Err: err}
}
// Update the query page token. Do this even if next_token is empty; the
// HasMorePages method uses the presence of the parameter to distinguish
// a fresh query from end-of-pages.
q.Request.Params.Set(q.nextTokenParam(), out.Meta.NextToken)
}
return out, nil
}
// HasMorePages reports whether the query has more pages to fetch. This is true
// for a freshly-constructed query, and for an invoked query where the server
// has not reported a next-page token.
func (q Query) HasMorePages() bool {
// To distinguish a fresh query from a query that has exhausted all pages,
// we use the presence of the next token parameter in the parameter map.
//
// If it's there but empty, there are no more pages.
// If it's there but nonempty, there are more pages.
// If it's not there, this is a fresh query.
v, ok := q.Request.Params[q.nextTokenParam()]
return !ok || v[0] != ""
}
// ResetPageToken clears (resets) the query's current page token. Subsequently
// invoking the query will then fetch the first page of results.
func (q Query) ResetPageToken() { q.Request.Params.Reset(q.nextTokenParam()) }
// A Reply is the response from a Query.
type Reply struct {
*twitter.Reply
Tweets types.Tweets
Meta *twitter.Pagination
}
// LookupOpts provides parameters for tweet lookup. A nil *LookupOpts provides
// empty values for all fields.
type LookupOpts struct {
More []string // additional tweet IDs to query
PageToken string // a pagination token
Optional []types.Fields // optional response fields, expansions
}
func (o *LookupOpts) addRequestParams(req *jape.Request) {
if o == nil {
return // nothing to do
}
if o.PageToken != "" {
req.Params.Set("next_token", o.PageToken)
}
req.Params.Add("ids", o.More...)
for _, fs := range o.Optional {
if vs := fs.Values(); len(vs) != 0 {
req.Params.Add(fs.Label(), vs...)
}
}
}
// ListOpts provide parameters for listing tweets. A nil *ListOpts provides
// empty values for all fields.
type ListOpts struct {
// A pagination token provided by the server.
PageToken string
// The maximum number of results to return; 0 means let the server choose.
// The service will accept values up to 100.
MaxResults int
// Optional response fields and expansions.
Optional []types.Fields
}
func (o *ListOpts) addRequestParams(req *jape.Request) {
if o == nil {
return // nothing to do
}
if o.PageToken != "" {
req.Params.Set(twitter.NextTokenParam, o.PageToken)
}
if o.MaxResults > 0 {
req.Params.Set("max_results", strconv.Itoa(o.MaxResults))
}
for _, fs := range o.Optional {
if vs := fs.Values(); len(vs) != 0 {
req.Params.Add(fs.Label(), vs...)
}
}
}