scrapebadger 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -124,6 +124,19 @@ var BaseClient = class {
124
124
  * Make an HTTP request to the API.
125
125
  */
126
126
  async request(path, options = {}) {
127
+ const { data } = await this.requestRaw(path, options);
128
+ return data;
129
+ }
130
+ /**
131
+ * Make an HTTP request and return both data and rate limit headers.
132
+ */
133
+ async requestWithHeaders(path, options = {}) {
134
+ return this.requestRaw(path, options);
135
+ }
136
+ /**
137
+ * Internal method that builds the request and executes it, returning data and rate limit info.
138
+ */
139
+ async requestRaw(path, options = {}) {
127
140
  const { method = "GET", params, body, headers = {} } = options;
128
141
  const url = new URL(path, this.config.baseUrl);
129
142
  if (params) {
@@ -137,7 +150,7 @@ var BaseClient = class {
137
150
  "Content-Type": "application/json",
138
151
  Accept: "application/json",
139
152
  "X-API-Key": this.config.apiKey,
140
- "User-Agent": "scrapebadger-node/0.1.0",
153
+ "User-Agent": "scrapebadger-node/0.3.1",
141
154
  ...headers
142
155
  };
143
156
  const fetchOptions = {
@@ -156,8 +169,10 @@ var BaseClient = class {
156
169
  let lastError;
157
170
  for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
158
171
  try {
159
- const response = await this.fetchWithTimeout(url, options);
160
- return await this.handleResponse(response);
172
+ const httpResponse = await this.fetchWithTimeout(url, options);
173
+ const data = await this.handleResponse(httpResponse);
174
+ const rateLimit = this.parseRateLimitHeaders(httpResponse.headers);
175
+ return { data, rateLimit };
161
176
  } catch (error) {
162
177
  lastError = error;
163
178
  if (error instanceof ScrapeBadgerError && !(error instanceof RateLimitError)) {
@@ -167,18 +182,56 @@ var BaseClient = class {
167
182
  break;
168
183
  }
169
184
  const delay = this.config.retryDelay * Math.pow(2, attempt);
170
- if (error instanceof RateLimitError && error.retryAfter) {
171
- const retryDelay = (error.retryAfter - Date.now() / 1e3) * 1e3;
172
- if (retryDelay > 0 && retryDelay < 6e4) {
173
- await this.sleep(retryDelay);
174
- continue;
185
+ const delaySec = Math.round(delay / 1e3);
186
+ const attemptNum = attempt + 1;
187
+ const maxRetries = this.config.maxRetries;
188
+ if (error instanceof RateLimitError) {
189
+ console.warn(
190
+ `\x1B[33m\u26A0 ScrapeBadger: 429 Rate Limited \u2014 retrying in ${delaySec}s (attempt ${attemptNum}/${maxRetries})\x1B[0m`
191
+ );
192
+ if (error.retryAfter) {
193
+ const retryDelay = (error.retryAfter - Date.now() / 1e3) * 1e3;
194
+ if (retryDelay > 0 && retryDelay < 6e4) {
195
+ await this.sleep(retryDelay);
196
+ continue;
197
+ }
175
198
  }
199
+ } else if (error instanceof TimeoutError) {
200
+ console.warn(
201
+ `\x1B[33m\u26A0 ScrapeBadger: TimeoutError \u2014 retrying in ${delaySec}s (attempt ${attemptNum}/${maxRetries})\x1B[0m`
202
+ );
203
+ } else if (error instanceof ServerError) {
204
+ console.warn(
205
+ `\x1B[33m\u26A0 ScrapeBadger: ${error.statusCode} ${error.message} \u2014 retrying in ${delaySec}s (attempt ${attemptNum}/${maxRetries})\x1B[0m`
206
+ );
207
+ } else {
208
+ console.warn(
209
+ `\x1B[33m\u26A0 ScrapeBadger: ${error.name} \u2014 retrying in ${delaySec}s (attempt ${attemptNum}/${maxRetries})\x1B[0m`
210
+ );
176
211
  }
177
212
  await this.sleep(delay);
178
213
  }
179
214
  }
180
215
  throw lastError ?? new ScrapeBadgerError("Request failed after retries");
181
216
  }
217
+ /**
218
+ * Parse rate limit headers from an HTTP response.
219
+ */
220
+ parseRateLimitHeaders(headers) {
221
+ const limit = headers.get("X-RateLimit-Limit");
222
+ const remaining = headers.get("X-RateLimit-Remaining");
223
+ const reset = headers.get("X-RateLimit-Reset");
224
+ if (limit === null || remaining === null || reset === null) {
225
+ return void 0;
226
+ }
227
+ const parsedLimit = parseInt(limit, 10);
228
+ const parsedRemaining = parseInt(remaining, 10);
229
+ const parsedReset = parseInt(reset, 10);
230
+ if (isNaN(parsedLimit) || isNaN(parsedRemaining) || isNaN(parsedReset)) {
231
+ return void 0;
232
+ }
233
+ return { limit: parsedLimit, remaining: parsedRemaining, reset: parsedReset };
234
+ }
182
235
  /**
183
236
  * Fetch with timeout support.
184
237
  */
@@ -260,7 +313,7 @@ var BaseClient = class {
260
313
  // src/internal/config.ts
261
314
  var DEFAULT_BASE_URL = "https://scrapebadger.com";
262
315
  var DEFAULT_TIMEOUT = 3e4;
263
- var DEFAULT_MAX_RETRIES = 3;
316
+ var DEFAULT_MAX_RETRIES = 10;
264
317
  var DEFAULT_RETRY_DELAY = 1e3;
265
318
  function resolveConfig(config) {
266
319
  if (!config.apiKey) {
@@ -289,12 +342,26 @@ function createPaginatedResponse(data, cursor) {
289
342
  hasMore: !!cursor
290
343
  };
291
344
  }
345
+ var RATE_LIMIT_WARN_THRESHOLD = 0.2;
292
346
  async function* paginate(fetchPage, options = {}) {
293
347
  const { maxItems } = options;
294
348
  let cursor;
295
349
  let totalYielded = 0;
296
350
  do {
297
- const response = await fetchPage(cursor);
351
+ const { response, rateLimit } = await fetchPage(cursor);
352
+ if (rateLimit) {
353
+ const { limit, remaining, reset } = rateLimit;
354
+ if (limit > 0 && remaining / limit < RATE_LIMIT_WARN_THRESHOLD) {
355
+ const nowSec = Date.now() / 1e3;
356
+ const windowRemainingSec = Math.max(reset - nowSec, 1);
357
+ const delayMs = remaining > 0 ? windowRemainingSec / remaining * 1e3 : windowRemainingSec * 1e3;
358
+ const resetInSec = Math.round(windowRemainingSec);
359
+ console.warn(
360
+ `\x1B[33m\u26A0 ScrapeBadger: Rate limit: ${remaining}/${limit} remaining (resets in ${resetInSec}s), throttling pagination\x1B[0m`
361
+ );
362
+ await sleep(delayMs);
363
+ }
364
+ }
298
365
  for (const item of response.data) {
299
366
  yield item;
300
367
  totalYielded++;
@@ -312,6 +379,9 @@ async function collectAll(generator) {
312
379
  }
313
380
  return items;
314
381
  }
382
+ function sleep(ms) {
383
+ return new Promise((resolve) => setTimeout(resolve, ms));
384
+ }
315
385
 
316
386
  // src/twitter/tweets.ts
317
387
  var TweetsClient = class {
@@ -509,7 +579,8 @@ var TweetsClient = class {
509
579
  */
510
580
  async *getQuotesAll(tweetId, options = {}) {
511
581
  const fetchPage = async (cursor) => {
512
- return this.getQuotes(tweetId, { ...options, cursor });
582
+ const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/tweets/tweet/${tweetId}/quotes`, { params: { cursor } });
583
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
513
584
  };
514
585
  yield* paginate(fetchPage, options);
515
586
  }
@@ -576,7 +647,15 @@ var TweetsClient = class {
576
647
  */
577
648
  async *searchAll(query, options = {}) {
578
649
  const fetchPage = async (cursor) => {
579
- return this.search(query, { ...options, cursor });
650
+ const { data, rateLimit } = await this.client.requestWithHeaders("/v1/twitter/tweets/advanced_search", {
651
+ params: {
652
+ query,
653
+ query_type: options.queryType ?? "Top",
654
+ count: options.count,
655
+ cursor
656
+ }
657
+ });
658
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
580
659
  };
581
660
  yield* paginate(fetchPage, options);
582
661
  }
@@ -620,10 +699,64 @@ var TweetsClient = class {
620
699
  */
621
700
  async *getUserTweetsAll(username, options = {}) {
622
701
  const fetchPage = async (cursor) => {
623
- return this.getUserTweets(username, { ...options, cursor });
702
+ const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/users/${username}/latest_tweets`, { params: { cursor } });
703
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
624
704
  };
625
705
  yield* paginate(fetchPage, options);
626
706
  }
707
+ /**
708
+ * Get the edit history of a tweet.
709
+ *
710
+ * @param tweetId - The tweet ID to get edit history for.
711
+ * @returns Paginated response containing tweet versions.
712
+ *
713
+ * @example
714
+ * ```typescript
715
+ * const history = await client.twitter.tweets.getEditHistory("1234567890");
716
+ * console.log(`${history.data.length} version(s) of this tweet`);
717
+ * ```
718
+ */
719
+ async getEditHistory(tweetId) {
720
+ const response = await this.client.request(
721
+ `/v1/twitter/tweets/tweet/${tweetId}/edit_history`
722
+ );
723
+ return createPaginatedResponse(response.data ?? [], void 0);
724
+ }
725
+ /**
726
+ * Get community notes (Birdwatch) attached to a tweet.
727
+ *
728
+ * @param tweetId - The tweet ID to get community notes for.
729
+ * @returns Paginated response containing community notes.
730
+ *
731
+ * @example
732
+ * ```typescript
733
+ * const notes = await client.twitter.tweets.getCommunityNotes("1234567890");
734
+ * for (const note of notes.data) {
735
+ * console.log(note.text);
736
+ * }
737
+ * ```
738
+ */
739
+ async getCommunityNotes(tweetId) {
740
+ const response = await this.client.request(
741
+ `/v1/twitter/tweets/tweet/${tweetId}/community_notes`
742
+ );
743
+ return createPaginatedResponse(response.data ?? [], void 0);
744
+ }
745
+ /**
746
+ * Get a long-form article by its ID.
747
+ *
748
+ * @param articleId - The article ID to fetch.
749
+ * @returns The article data.
750
+ *
751
+ * @example
752
+ * ```typescript
753
+ * const article = await client.twitter.tweets.getArticle("abc123");
754
+ * console.log(`${article.title}: ${article.text?.slice(0, 100)}...`);
755
+ * ```
756
+ */
757
+ async getArticle(articleId) {
758
+ return this.client.request(`/v1/twitter/tweets/article/${articleId}`);
759
+ }
627
760
  };
628
761
 
629
762
  // src/twitter/users.ts
@@ -730,7 +863,8 @@ var UsersClient = class {
730
863
  */
731
864
  async *getFollowersAll(username, options = {}) {
732
865
  const fetchPage = async (cursor) => {
733
- return this.getFollowers(username, { ...options, cursor });
866
+ const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/users/${username}/followers`, { params: { cursor } });
867
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
734
868
  };
735
869
  yield* paginate(fetchPage, options);
736
870
  }
@@ -765,7 +899,8 @@ var UsersClient = class {
765
899
  */
766
900
  async *getFollowingAll(username, options = {}) {
767
901
  const fetchPage = async (cursor) => {
768
- return this.getFollowing(username, { ...options, cursor });
902
+ const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/users/${username}/followings`, { params: { cursor } });
903
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
769
904
  };
770
905
  yield* paginate(fetchPage, options);
771
906
  }
@@ -928,10 +1063,97 @@ var UsersClient = class {
928
1063
  */
929
1064
  async *searchAll(query, options = {}) {
930
1065
  const fetchPage = async (cursor) => {
931
- return this.search(query, { ...options, cursor });
1066
+ const { data, rateLimit } = await this.client.requestWithHeaders("/v1/twitter/users/search_users", { params: { query, cursor } });
1067
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
932
1068
  };
933
1069
  yield* paginate(fetchPage, options);
934
1070
  }
1071
+ /**
1072
+ * Get multiple users by their numeric IDs in a single request.
1073
+ *
1074
+ * @param userIds - List of user IDs to fetch.
1075
+ * @returns Paginated response containing the matching users.
1076
+ *
1077
+ * @example
1078
+ * ```typescript
1079
+ * const users = await client.twitter.users.getByIds(["44196397", "783214"]);
1080
+ * for (const user of users.data) {
1081
+ * console.log(`@${user.username}`);
1082
+ * }
1083
+ * ```
1084
+ */
1085
+ async getByIds(userIds) {
1086
+ const response = await this.client.request(
1087
+ "/v1/twitter/users/batch_by_ids",
1088
+ { params: { user_ids: userIds.join(",") } }
1089
+ );
1090
+ return createPaginatedResponse(response.data ?? [], void 0);
1091
+ }
1092
+ /**
1093
+ * Get multiple users by their usernames in a single request.
1094
+ *
1095
+ * @param usernames - List of usernames (without @) to fetch.
1096
+ * @returns Paginated response containing the matching users.
1097
+ *
1098
+ * @example
1099
+ * ```typescript
1100
+ * const users = await client.twitter.users.getByUsernames(["elonmusk", "twitter"]);
1101
+ * for (const user of users.data) {
1102
+ * console.log(`${user.name}: ${user.followers_count?.toLocaleString()} followers`);
1103
+ * }
1104
+ * ```
1105
+ */
1106
+ async getByUsernames(usernames) {
1107
+ const response = await this.client.request(
1108
+ "/v1/twitter/users/batch_by_usernames",
1109
+ { params: { usernames: usernames.join(",") } }
1110
+ );
1111
+ return createPaginatedResponse(response.data ?? [], void 0);
1112
+ }
1113
+ /**
1114
+ * Get tweets that mention a user.
1115
+ *
1116
+ * @param username - The user's username (without @).
1117
+ * @param options - Pagination options with optional count.
1118
+ * @returns Paginated response containing tweets mentioning the user.
1119
+ *
1120
+ * @example
1121
+ * ```typescript
1122
+ * const mentions = await client.twitter.users.getMentions("elonmusk");
1123
+ * for (const tweet of mentions.data) {
1124
+ * console.log(`@${tweet.username}: ${tweet.text.slice(0, 100)}...`);
1125
+ * }
1126
+ * ```
1127
+ */
1128
+ async getMentions(username, options = {}) {
1129
+ const response = await this.client.request(
1130
+ `/v1/twitter/users/${username}/mentions`,
1131
+ { params: { count: options.count, cursor: options.cursor } }
1132
+ );
1133
+ return createPaginatedResponse(response.data ?? [], response.next_cursor);
1134
+ }
1135
+ /**
1136
+ * Get long-form articles authored by a user.
1137
+ *
1138
+ * @param userId - The user's numeric ID.
1139
+ * @param options - Pagination options with optional count.
1140
+ * @returns Paginated response containing the user's articles as tweets.
1141
+ *
1142
+ * @example
1143
+ * ```typescript
1144
+ * const articles = await client.twitter.users.getArticles("44196397");
1145
+ * for (const article of articles.data) {
1146
+ * console.log(article.text?.slice(0, 100));
1147
+ * }
1148
+ * ```
1149
+ */
1150
+ async getArticles(userId, options = {}) {
1151
+ const response = await this.client.request(
1152
+ `/v1/twitter/users/${userId}/articles`,
1153
+ { params: { count: options.count, cursor: options.cursor } }
1154
+ );
1155
+ return createPaginatedResponse(response.data ?? [], response.next_cursor);
1156
+ }
935
1157
  };
936
1158
 
937
1159
  // src/twitter/lists.ts
@@ -988,7 +1210,8 @@ var ListsClient = class {
988
1210
  */
989
1211
  async *getTweetsAll(listId, options = {}) {
990
1212
  const fetchPage = async (cursor) => {
991
- return this.getTweets(listId, { ...options, cursor });
1213
+ const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/lists/${listId}/tweets`, { params: { cursor } });
1214
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
992
1215
  };
993
1216
  yield* paginate(fetchPage, options);
994
1217
  }
@@ -1023,7 +1246,8 @@ var ListsClient = class {
1023
1246
  */
1024
1247
  async *getMembersAll(listId, options = {}) {
1025
1248
  const fetchPage = async (cursor) => {
1026
- return this.getMembers(listId, { ...options, cursor });
1249
+ const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/lists/${listId}/members`, { params: { cursor } });
1250
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
1027
1251
  };
1028
1252
  yield* paginate(fetchPage, options);
1029
1253
  }
@@ -1076,6 +1300,29 @@ var ListsClient = class {
1076
1300
  );
1077
1301
  return createPaginatedResponse(response.data ?? [], response.next_cursor);
1078
1302
  }
1303
+ /**
1304
+ * Search tweets within a specific list.
1305
+ *
1306
+ * @param listId - The list ID to search within.
1307
+ * @param query - Search query string.
1308
+ * @param options - Pagination options with optional count.
1309
+ * @returns Paginated response containing matching tweets from the list.
1310
+ *
1311
+ * @example
1312
+ * ```typescript
1313
+ * const results = await client.twitter.lists.searchTweets("123456", "python");
1314
+ * for (const tweet of results.data) {
1315
+ * console.log(`@${tweet.username}: ${tweet.text.slice(0, 100)}...`);
1316
+ * }
1317
+ * ```
1318
+ */
1319
+ async searchTweets(listId, query, options = {}) {
1320
+ const response = await this.client.request(
1321
+ `/v1/twitter/lists/${listId}/search_tweets`,
1322
+ { params: { query, count: options.count, cursor: options.cursor } }
1323
+ );
1324
+ return createPaginatedResponse(response.data ?? [], response.next_cursor);
1325
+ }
1079
1326
  };
1080
1327
 
1081
1328
  // src/twitter/communities.ts
@@ -1171,7 +1418,14 @@ var CommunitiesClient = class {
1171
1418
  */
1172
1419
  async *getTweetsAll(communityId, options = {}) {
1173
1420
  const fetchPage = async (cursor) => {
1174
- return this.getTweets(communityId, { ...options, cursor });
1421
+ const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/communities/${communityId}/tweets`, {
1422
+ params: {
1423
+ tweet_type: options.tweetType ?? "Top",
1424
+ count: options.count ?? 40,
1425
+ cursor
1426
+ }
1427
+ });
1428
+ return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
1175
1429
  };
1176
1430
  yield* paginate(fetchPage, options);
1177
1431
  }
@@ -2090,6 +2344,46 @@ function verifyWebhookSignature(secret, body, signatureHeader) {
2090
2344
  }
2091
2345
  }
2092
2346
 
2347
+ // src/twitter/spaces.ts
2348
+ var SpacesClient = class {
2349
+ client;
2350
+ constructor(client) {
2351
+ this.client = client;
2352
+ }
2353
+ /**
2354
+ * Get details for a specific Twitter Space.
2355
+ *
2356
+ * @param spaceId - The Space ID to fetch.
2357
+ * @returns The Space data.
2358
+ * @throws NotFoundError - If the Space doesn't exist.
2359
+ *
2360
+ * @example
2361
+ * ```typescript
2362
+ * const space = await client.twitter.spaces.getDetail("1eaKbrPPbPwKX");
2363
+ * console.log(`${space.title} — ${space.participant_count} participants`);
2364
+ * ```
2365
+ */
2366
+ async getDetail(spaceId) {
2367
+ return this.client.request(`/v1/twitter/spaces/${spaceId}`);
2368
+ }
2369
+ /**
2370
+ * Get details for a live video broadcast.
2371
+ *
2372
+ * @param broadcastId - The broadcast ID to fetch.
2373
+ * @returns The broadcast data.
2374
+ * @throws NotFoundError - If the broadcast doesn't exist.
2375
+ *
2376
+ * @example
2377
+ * ```typescript
2378
+ * const broadcast = await client.twitter.spaces.getBroadcast("broadcast123");
2379
+ * console.log(`${broadcast.title}: ${broadcast.total_viewers} viewers`);
2380
+ * ```
2381
+ */
2382
+ async getBroadcast(broadcastId) {
2383
+ return this.client.request(`/v1/twitter/spaces/broadcast/${broadcastId}`);
2384
+ }
2385
+ };
2386
+
2093
2387
  // src/twitter/client.ts
2094
2388
  var TwitterClient = class {
2095
2389
  /** Client for tweet operations */
@@ -2106,6 +2400,8 @@ var TwitterClient = class {
2106
2400
  geo;
2107
2401
  /** Client for real-time stream monitor management and WebSocket streaming */
2108
2402
  stream;
2403
+ /** Client for Twitter Spaces and live broadcast operations */
2404
+ spaces;
2109
2405
  /**
2110
2406
  * Create a new Twitter client.
2111
2407
  *
@@ -2119,6 +2415,7 @@ var TwitterClient = class {
2119
2415
  this.trends = new TrendsClient(client);
2120
2416
  this.geo = new GeoClient(client);
2121
2417
  this.stream = new StreamClient(client);
2418
+ this.spaces = new SpacesClient(client);
2122
2419
  }
2123
2420
  };
2124
2421
 
@@ -2267,6 +2564,6 @@ var ScrapeBadger = class {
2267
2564
  }
2268
2565
  };
2269
2566
 
2270
- export { AccountRestrictedError, AuthenticationError, CommunitiesClient, ConflictError, GeoClient, InsufficientCreditsError, ListsClient, NotFoundError, RateLimitError, ScrapeBadger, ScrapeBadgerError, ServerError, StreamClient, TimeoutError, TrendsClient, TweetsClient, TwitterClient, UsersClient, ValidationError, WebClient, WebSocketStreamError, collectAll, verifyWebhookSignature };
2567
+ export { AccountRestrictedError, AuthenticationError, CommunitiesClient, ConflictError, GeoClient, InsufficientCreditsError, ListsClient, NotFoundError, RateLimitError, ScrapeBadger, ScrapeBadgerError, ServerError, SpacesClient, StreamClient, TimeoutError, TrendsClient, TweetsClient, TwitterClient, UsersClient, ValidationError, WebClient, WebSocketStreamError, collectAll, verifyWebhookSignature };
2271
2568
  //# sourceMappingURL=index.mjs.map
2272
2569
  //# sourceMappingURL=index.mjs.map