Skip to content

Commit 284eeb6

Browse files
author
Kaiwen Li
committed
feat: add pagination & field-override flags to search, timeline, mentions
Add PaginationOptions struct with --since-id, --until-id, --start-time, --end-time, --next-token, --tweet-fields, --user-fields, --expansions flags to the search, timeline, and mentions shortcut commands. Timeline also gets --exclude (replies,retweets). These flags enable incremental polling (via since_id) and pagination (via next_token) without falling back to raw API mode. - api/shortcuts.go: Add PaginationOptions, update SearchPosts/GetTimeline/GetMentions signatures - cli/shortcuts.go: Wire new flags for searchCmd, timelineCmd, mentionsCmd - api/shortcuts_test.go: Update TestSearchPosts for new signature - SKILL.md: Document new flags with examples and reference table
1 parent 4a56f8e commit 284eeb6

4 files changed

Lines changed: 234 additions & 13 deletions

File tree

SKILL.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ xurl read https://x.com/user/status/1234567890
140140
xurl search "golang"
141141
xurl search "from:elonmusk" -n 20
142142
xurl search "#buildinpublic lang:en" -n 15
143+
144+
# Search with pagination / incremental polling
145+
xurl search "from:elonmusk" -n 100 --since-id 1234567890
146+
xurl search "crypto" --start-time 2025-01-01T00:00:00Z -n 50
147+
xurl search "AI" --next-token TOKEN_FROM_PREVIOUS_RESPONSE
148+
149+
# Override default fields / expansions
150+
xurl search "golang" --tweet-fields created_at,public_metrics,author_id --user-fields username,name --expansions author_id
143151
```
144152

145153
### User Info
@@ -160,9 +168,20 @@ xurl user @XDevelopers
160168
xurl timeline
161169
xurl timeline -n 25
162170

171+
# Timeline with pagination / filtering
172+
xurl timeline --since-id 1234567890 -n 50
173+
xurl timeline --exclude replies,retweets
174+
xurl timeline --start-time 2025-01-01T00:00:00Z -n 100
175+
xurl timeline --next-token TOKEN_FROM_PREVIOUS_RESPONSE
176+
163177
# Your mentions
164178
xurl mentions
165179
xurl mentions -n 20
180+
181+
# Mentions with incremental polling
182+
xurl mentions --since-id 1234567890 -n 50
183+
xurl mentions --start-time 2025-03-01T00:00:00Z
184+
xurl mentions --next-token TOKEN_FROM_PREVIOUS_RESPONSE
166185
```
167186

168187
### Engagement
@@ -241,6 +260,24 @@ xurl post "lol" --media-id MEDIA_ID
241260

242261
---
243262

263+
## Pagination & Polling Flags
264+
265+
The `search`, `timeline`, and `mentions` commands accept these optional flags for incremental polling and field customisation:
266+
267+
| Flag | Applies to | Description |
268+
|---|---|---|
269+
| `--since-id ID` | search, timeline, mentions | Only return results with a tweet ID greater than this (exclusive). Ideal for incremental polling. |
270+
| `--until-id ID` | search, timeline, mentions | Only return results with a tweet ID less than this (exclusive). |
271+
| `--start-time TIME` | search, timeline, mentions | Oldest UTC datetime, ISO 8601 format (`YYYY-MM-DDTHH:mm:ssZ`). |
272+
| `--end-time TIME` | search, timeline, mentions | Newest UTC datetime, ISO 8601 format. |
273+
| `--next-token TOKEN` | search, timeline, mentions | Pagination token from a previous response's `meta.next_token`. |
274+
| `--tweet-fields FIELDS` | search, timeline, mentions | Comma-separated tweet fields (overrides defaults: `created_at,public_metrics,conversation_id,entities`). |
275+
| `--user-fields FIELDS` | search, timeline, mentions | Comma-separated user fields (overrides defaults: `username,name,verified`). |
276+
| `--expansions FIELDS` | search, timeline, mentions | Comma-separated expansions (overrides default: `author_id`). |
277+
| `--exclude TYPES` | timeline only | Comma-separated exclusions: `replies`, `retweets`, or both. |
278+
279+
---
280+
244281
## Global Flags
245282

246283
These flags work on every command:

api/shortcuts.go

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,22 @@ func ReadPost(client Client, postID string, opts RequestOptions) (json.RawMessag
158158
return client.SendRequest(opts)
159159
}
160160

161+
// PaginationOptions holds optional pagination and field-override flags
162+
// shared by search, timeline, and mentions commands.
163+
type PaginationOptions struct {
164+
SinceID string
165+
UntilID string
166+
StartTime string
167+
EndTime string
168+
NextToken string
169+
TweetFields string
170+
UserFields string
171+
Expansions string
172+
Exclude string // timeline/mentions only: "replies,retweets"
173+
}
174+
161175
// SearchPosts searches recent posts.
162-
func SearchPosts(client Client, query string, maxResults int, opts RequestOptions) (json.RawMessage, error) {
176+
func SearchPosts(client Client, query string, maxResults int, pag PaginationOptions, opts RequestOptions) (json.RawMessage, error) {
163177
q := url.QueryEscape(query)
164178

165179
// X API enforces min 10 / max 100 for search
@@ -169,8 +183,41 @@ func SearchPosts(client Client, query string, maxResults int, opts RequestOption
169183
maxResults = 100
170184
}
171185

186+
// Defaults
187+
tweetFields := "created_at,public_metrics,conversation_id,entities"
188+
userFields := "username,name,verified"
189+
expansions := "author_id"
190+
if pag.TweetFields != "" {
191+
tweetFields = pag.TweetFields
192+
}
193+
if pag.UserFields != "" {
194+
userFields = pag.UserFields
195+
}
196+
if pag.Expansions != "" {
197+
expansions = pag.Expansions
198+
}
199+
200+
endpoint := fmt.Sprintf("/2/tweets/search/recent?query=%s&max_results=%d&tweet.fields=%s&expansions=%s&user.fields=%s",
201+
q, maxResults, tweetFields, expansions, userFields)
202+
203+
if pag.SinceID != "" {
204+
endpoint += "&since_id=" + pag.SinceID
205+
}
206+
if pag.UntilID != "" {
207+
endpoint += "&until_id=" + pag.UntilID
208+
}
209+
if pag.StartTime != "" {
210+
endpoint += "&start_time=" + url.QueryEscape(pag.StartTime)
211+
}
212+
if pag.EndTime != "" {
213+
endpoint += "&end_time=" + url.QueryEscape(pag.EndTime)
214+
}
215+
if pag.NextToken != "" {
216+
endpoint += "&next_token=" + pag.NextToken
217+
}
218+
172219
opts.Method = "GET"
173-
opts.Endpoint = fmt.Sprintf("/2/tweets/search/recent?query=%s&max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name,verified", q, maxResults)
220+
opts.Endpoint = endpoint
174221
opts.Data = ""
175222

176223
return client.SendRequest(opts)
@@ -207,18 +254,88 @@ func GetUserPosts(client Client, userID string, maxResults int, opts RequestOpti
207254

208255
// GetTimeline fetches the authenticated user's reverse‑chronological timeline.
209256
// Route: GET /2/users/{id}/timelines/reverse_chronological
210-
func GetTimeline(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) {
257+
func GetTimeline(client Client, userID string, maxResults int, pag PaginationOptions, opts RequestOptions) (json.RawMessage, error) {
258+
tweetFields := "created_at,public_metrics,conversation_id,entities"
259+
userFields := "username,name"
260+
expansions := "author_id"
261+
if pag.TweetFields != "" {
262+
tweetFields = pag.TweetFields
263+
}
264+
if pag.UserFields != "" {
265+
userFields = pag.UserFields
266+
}
267+
if pag.Expansions != "" {
268+
expansions = pag.Expansions
269+
}
270+
271+
endpoint := fmt.Sprintf("/2/users/%s/timelines/reverse_chronological?max_results=%d&tweet.fields=%s&expansions=%s&user.fields=%s",
272+
userID, maxResults, tweetFields, expansions, userFields)
273+
274+
if pag.SinceID != "" {
275+
endpoint += "&since_id=" + pag.SinceID
276+
}
277+
if pag.UntilID != "" {
278+
endpoint += "&until_id=" + pag.UntilID
279+
}
280+
if pag.StartTime != "" {
281+
endpoint += "&start_time=" + url.QueryEscape(pag.StartTime)
282+
}
283+
if pag.EndTime != "" {
284+
endpoint += "&end_time=" + url.QueryEscape(pag.EndTime)
285+
}
286+
if pag.NextToken != "" {
287+
endpoint += "&pagination_token=" + pag.NextToken
288+
}
289+
if pag.Exclude != "" {
290+
endpoint += "&exclude=" + pag.Exclude
291+
}
292+
211293
opts.Method = "GET"
212-
opts.Endpoint = fmt.Sprintf("/2/users/%s/timelines/reverse_chronological?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name", userID, maxResults)
294+
opts.Endpoint = endpoint
213295
opts.Data = ""
214296

215297
return client.SendRequest(opts)
216298
}
217299

218300
// GetMentions fetches recent mentions for a user.
219-
func GetMentions(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) {
301+
func GetMentions(client Client, userID string, maxResults int, pag PaginationOptions, opts RequestOptions) (json.RawMessage, error) {
302+
tweetFields := "created_at,public_metrics,conversation_id,entities"
303+
userFields := "username,name"
304+
expansions := "author_id"
305+
if pag.TweetFields != "" {
306+
tweetFields = pag.TweetFields
307+
}
308+
if pag.UserFields != "" {
309+
userFields = pag.UserFields
310+
}
311+
if pag.Expansions != "" {
312+
expansions = pag.Expansions
313+
}
314+
315+
endpoint := fmt.Sprintf("/2/users/%s/mentions?max_results=%d&tweet.fields=%s&expansions=%s&user.fields=%s",
316+
userID, maxResults, tweetFields, expansions, userFields)
317+
318+
if pag.SinceID != "" {
319+
endpoint += "&since_id=" + pag.SinceID
320+
}
321+
if pag.UntilID != "" {
322+
endpoint += "&until_id=" + pag.UntilID
323+
}
324+
if pag.StartTime != "" {
325+
endpoint += "&start_time=" + url.QueryEscape(pag.StartTime)
326+
}
327+
if pag.EndTime != "" {
328+
endpoint += "&end_time=" + url.QueryEscape(pag.EndTime)
329+
}
330+
if pag.NextToken != "" {
331+
endpoint += "&pagination_token=" + pag.NextToken
332+
}
333+
if pag.Exclude != "" {
334+
endpoint += "&exclude=" + pag.Exclude
335+
}
336+
220337
opts.Method = "GET"
221-
opts.Endpoint = fmt.Sprintf("/2/users/%s/mentions?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name", userID, maxResults)
338+
opts.Endpoint = endpoint
222339
opts.Data = ""
223340

224341
return client.SendRequest(opts)

api/shortcuts_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ func TestSearchPosts(t *testing.T) {
228228
defer server.Close()
229229
client := shortcutClient(t, server)
230230

231-
resp, err := SearchPosts(client, "golang", 10, baseTestOpts())
231+
resp, err := SearchPosts(client, "golang", 10, PaginationOptions{}, baseTestOpts())
232232
require.NoError(t, err)
233233

234234
var result struct {

cli/shortcuts.go

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ Examples:
254254

255255
func searchCmd(a *auth.Auth) *cobra.Command {
256256
var maxResults int
257+
var sinceID, untilID, startTime, endTime, nextToken string
258+
var tweetFields, userFields, expansions string
257259
cmd := &cobra.Command{
258260
Use: `search "QUERY"`,
259261
Short: "Search recent posts",
@@ -262,15 +264,35 @@ func searchCmd(a *auth.Auth) *cobra.Command {
262264
Examples:
263265
xurl search "golang"
264266
xurl search "from:elonmusk" -n 20
265-
xurl search "#buildinpublic" -n 15`,
267+
xurl search "#buildinpublic" -n 15
268+
xurl search "from:user1 OR from:user2" -n 100 --since-id 1234567890
269+
xurl search "crypto" --start-time 2025-01-01T00:00:00Z -n 50`,
266270
Args: cobra.ExactArgs(1),
267271
Run: func(cmd *cobra.Command, args []string) {
268272
client := newClient(a)
269273
opts := baseOpts(cmd)
270-
printResult(api.SearchPosts(client, args[0], maxResults, opts))
274+
pag := api.PaginationOptions{
275+
SinceID: sinceID,
276+
UntilID: untilID,
277+
StartTime: startTime,
278+
EndTime: endTime,
279+
NextToken: nextToken,
280+
TweetFields: tweetFields,
281+
UserFields: userFields,
282+
Expansions: expansions,
283+
}
284+
printResult(api.SearchPosts(client, args[0], maxResults, pag, opts))
271285
},
272286
}
273287
cmd.Flags().IntVarP(&maxResults, "max-results", "n", 10, "Number of results (min 10, max 100)")
288+
cmd.Flags().StringVar(&sinceID, "since-id", "", "Only return results with an ID greater than this")
289+
cmd.Flags().StringVar(&untilID, "until-id", "", "Only return results with an ID less than this")
290+
cmd.Flags().StringVar(&startTime, "start-time", "", "Oldest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
291+
cmd.Flags().StringVar(&endTime, "end-time", "", "Newest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
292+
cmd.Flags().StringVar(&nextToken, "next-token", "", "Pagination token for next page of results")
293+
cmd.Flags().StringVar(&tweetFields, "tweet-fields", "", "Comma-separated tweet fields (overrides defaults)")
294+
cmd.Flags().StringVar(&userFields, "user-fields", "", "Comma-separated user fields (overrides defaults)")
295+
cmd.Flags().StringVar(&expansions, "expansions", "", "Comma-separated expansions (overrides defaults)")
274296
addCommonFlags(cmd)
275297
return cmd
276298
}
@@ -324,14 +346,18 @@ Examples:
324346

325347
func timelineCmd(a *auth.Auth) *cobra.Command {
326348
var maxResults int
349+
var sinceID, untilID, startTime, endTime, nextToken string
350+
var tweetFields, userFields, expansions, exclude string
327351
cmd := &cobra.Command{
328352
Use: "timeline",
329353
Short: "Show your home timeline",
330354
Long: `Fetch your reverse‑chronological home timeline.
331355
332356
Examples:
333357
xurl timeline
334-
xurl timeline -n 25`,
358+
xurl timeline -n 25
359+
xurl timeline --since-id 1234567890 -n 50
360+
xurl timeline --exclude replies,retweets`,
335361
Args: cobra.NoArgs,
336362
Run: func(cmd *cobra.Command, args []string) {
337363
client := newClient(a)
@@ -341,24 +367,47 @@ Examples:
341367
fmt.Fprintf(os.Stderr, "\033[31mError: %v\033[0m\n", err)
342368
os.Exit(1)
343369
}
344-
printResult(api.GetTimeline(client, userID, maxResults, opts))
370+
pag := api.PaginationOptions{
371+
SinceID: sinceID,
372+
UntilID: untilID,
373+
StartTime: startTime,
374+
EndTime: endTime,
375+
NextToken: nextToken,
376+
TweetFields: tweetFields,
377+
UserFields: userFields,
378+
Expansions: expansions,
379+
Exclude: exclude,
380+
}
381+
printResult(api.GetTimeline(client, userID, maxResults, pag, opts))
345382
},
346383
}
347384
cmd.Flags().IntVarP(&maxResults, "max-results", "n", 10, "Number of results (1–100)")
385+
cmd.Flags().StringVar(&sinceID, "since-id", "", "Only return results with an ID greater than this")
386+
cmd.Flags().StringVar(&untilID, "until-id", "", "Only return results with an ID less than this")
387+
cmd.Flags().StringVar(&startTime, "start-time", "", "Oldest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
388+
cmd.Flags().StringVar(&endTime, "end-time", "", "Newest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
389+
cmd.Flags().StringVar(&nextToken, "next-token", "", "Pagination token for next page of results")
390+
cmd.Flags().StringVar(&tweetFields, "tweet-fields", "", "Comma-separated tweet fields (overrides defaults)")
391+
cmd.Flags().StringVar(&userFields, "user-fields", "", "Comma-separated user fields (overrides defaults)")
392+
cmd.Flags().StringVar(&expansions, "expansions", "", "Comma-separated expansions (overrides defaults)")
393+
cmd.Flags().StringVar(&exclude, "exclude", "", "Comma-separated exclusions: replies,retweets")
348394
addCommonFlags(cmd)
349395
return cmd
350396
}
351397

352398
func mentionsCmd(a *auth.Auth) *cobra.Command {
353399
var maxResults int
400+
var sinceID, untilID, startTime, endTime, nextToken string
401+
var tweetFields, userFields, expansions string
354402
cmd := &cobra.Command{
355403
Use: "mentions",
356404
Short: "Show your recent mentions",
357405
Long: `Fetch posts that mention the authenticated user.
358406
359407
Examples:
360408
xurl mentions
361-
xurl mentions -n 25`,
409+
xurl mentions -n 25
410+
xurl mentions --since-id 1234567890`,
362411
Args: cobra.NoArgs,
363412
Run: func(cmd *cobra.Command, args []string) {
364413
client := newClient(a)
@@ -368,10 +417,28 @@ Examples:
368417
fmt.Fprintf(os.Stderr, "\033[31mError: %v\033[0m\n", err)
369418
os.Exit(1)
370419
}
371-
printResult(api.GetMentions(client, userID, maxResults, opts))
420+
pag := api.PaginationOptions{
421+
SinceID: sinceID,
422+
UntilID: untilID,
423+
StartTime: startTime,
424+
EndTime: endTime,
425+
NextToken: nextToken,
426+
TweetFields: tweetFields,
427+
UserFields: userFields,
428+
Expansions: expansions,
429+
}
430+
printResult(api.GetMentions(client, userID, maxResults, pag, opts))
372431
},
373432
}
374433
cmd.Flags().IntVarP(&maxResults, "max-results", "n", 10, "Number of results (5–100)")
434+
cmd.Flags().StringVar(&sinceID, "since-id", "", "Only return results with an ID greater than this")
435+
cmd.Flags().StringVar(&untilID, "until-id", "", "Only return results with an ID less than this")
436+
cmd.Flags().StringVar(&startTime, "start-time", "", "Oldest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
437+
cmd.Flags().StringVar(&endTime, "end-time", "", "Newest UTC datetime (YYYY-MM-DDTHH:mm:ssZ)")
438+
cmd.Flags().StringVar(&nextToken, "next-token", "", "Pagination token for next page of results")
439+
cmd.Flags().StringVar(&tweetFields, "tweet-fields", "", "Comma-separated tweet fields (overrides defaults)")
440+
cmd.Flags().StringVar(&userFields, "user-fields", "", "Comma-separated user fields (overrides defaults)")
441+
cmd.Flags().StringVar(&expansions, "expansions", "", "Comma-separated expansions (overrides defaults)")
375442
addCommonFlags(cmd)
376443
return cmd
377444
}

0 commit comments

Comments
 (0)