(
query: string,
opts: {
pages?: number;
maxResults?: number;
sortOrder?: 'relevancy' | 'recency';
since?: string;
} = {},
)
| 119 | } |
| 120 | |
| 121 | async function searchTweets( |
| 122 | query: string, |
| 123 | opts: { |
| 124 | pages?: number; |
| 125 | maxResults?: number; |
| 126 | sortOrder?: 'relevancy' | 'recency'; |
| 127 | since?: string; |
| 128 | } = {}, |
| 129 | ): Promise<XTweet[]> { |
| 130 | const pages = Math.min(opts.pages ?? 1, 5); |
| 131 | const maxResults = Math.max(Math.min(opts.maxResults ?? 100, 100), 10); |
| 132 | const sort = opts.sortOrder ?? 'relevancy'; |
| 133 | const encoded = encodeURIComponent(query); |
| 134 | |
| 135 | let timeFilter = ''; |
| 136 | if (opts.since) { |
| 137 | const startTime = parseSince(opts.since); |
| 138 | if (startTime) timeFilter = `&start_time=${startTime}`; |
| 139 | } |
| 140 | |
| 141 | const allTweets: XTweet[] = []; |
| 142 | let nextToken: string | undefined; |
| 143 | |
| 144 | for (let page = 0; page < pages; page++) { |
| 145 | const pagination = nextToken ? `&pagination_token=${nextToken}` : ''; |
| 146 | const url = |
| 147 | `${X_API_BASE}/tweets/search/recent?query=${encoded}` + |
| 148 | `&max_results=${maxResults}&${TWEET_FIELDS}` + |
| 149 | `&sort_order=${sort}${timeFilter}${pagination}`; |
| 150 | |
| 151 | const raw = await xApiGet(url); |
| 152 | allTweets.push(...parseTweets(raw)); |
| 153 | nextToken = raw.meta?.next_token; |
| 154 | if (!nextToken) break; |
| 155 | if (page < pages - 1) await sleep(RATE_DELAY_MS); |
| 156 | } |
| 157 | |
| 158 | // Deduplicate |
| 159 | const seen = new Set<string>(); |
| 160 | return allTweets.filter((t) => { |
| 161 | if (seen.has(t.id)) return false; |
| 162 | seen.add(t.id); |
| 163 | return true; |
| 164 | }); |
| 165 | } |
| 166 | |
| 167 | async function getProfile( |
| 168 | username: string, |
no test coverage detected