scrapebadger 0.1.8 → 0.2.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.
@@ -1,5 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ var crypto = require('crypto');
4
+ var events = require('events');
5
+ var WebSocket = require('ws');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
10
+
3
11
  // src/internal/pagination.ts
4
12
  function createPaginatedResponse(data, cursor) {
5
13
  return {
@@ -253,6 +261,7 @@ var TweetsClient = class {
253
261
  params: {
254
262
  query,
255
263
  query_type: options.queryType ?? "Top",
264
+ count: options.count,
256
265
  cursor: options.cursor
257
266
  }
258
267
  }
@@ -811,11 +820,7 @@ var CommunitiesClient = class {
811
820
  name: item.name ?? "",
812
821
  profile_image_url: item.profile_image_url,
813
822
  verified: item.verified ?? false,
814
- is_blue_verified: item.is_blue_verified,
815
- followers_count: 0,
816
- following_count: 0,
817
- tweet_count: 0,
818
- listed_count: 0
823
+ is_blue_verified: item.is_blue_verified
819
824
  },
820
825
  role: item.role,
821
826
  joined_at: item.joined_at
@@ -1144,6 +1149,688 @@ var GeoClient = class {
1144
1149
  }
1145
1150
  };
1146
1151
 
1152
+ // src/internal/exceptions.ts
1153
+ var ScrapeBadgerError = class _ScrapeBadgerError extends Error {
1154
+ constructor(message) {
1155
+ super(message);
1156
+ this.name = "ScrapeBadgerError";
1157
+ Object.setPrototypeOf(this, _ScrapeBadgerError.prototype);
1158
+ }
1159
+ };
1160
+ var WebSocketStreamError = class _WebSocketStreamError extends ScrapeBadgerError {
1161
+ /** WebSocket close code or server error code */
1162
+ code;
1163
+ constructor(message = "WebSocket stream error", code) {
1164
+ super(message);
1165
+ this.name = "WebSocketStreamError";
1166
+ this.code = code;
1167
+ Object.setPrototypeOf(this, _WebSocketStreamError.prototype);
1168
+ }
1169
+ };
1170
+
1171
+ // src/twitter/stream.ts
1172
+ var MIN_RECONNECT_DELAY_SECONDS = 5;
1173
+ function wsUrlFromBase(baseUrl) {
1174
+ if (baseUrl.startsWith("https://")) {
1175
+ return baseUrl.replace("https://", "wss://") + "/v1/twitter/stream";
1176
+ }
1177
+ if (baseUrl.startsWith("http://")) {
1178
+ return baseUrl.replace("http://", "ws://") + "/v1/twitter/stream";
1179
+ }
1180
+ return baseUrl + "/v1/twitter/stream";
1181
+ }
1182
+ function parseEvent(raw) {
1183
+ const type = raw["type"];
1184
+ switch (type) {
1185
+ case "connected":
1186
+ return {
1187
+ type: "connected",
1188
+ connectionId: raw["connection_id"],
1189
+ apiKeyId: raw["api_key_id"]
1190
+ };
1191
+ case "ping":
1192
+ return {
1193
+ type: "ping",
1194
+ timestamp: raw["timestamp"]
1195
+ };
1196
+ case "tweet":
1197
+ return {
1198
+ type: "tweet",
1199
+ monitorId: raw["monitor_id"],
1200
+ tweetId: raw["tweet_id"],
1201
+ authorUsername: raw["author_username"],
1202
+ tweetPublishedAt: raw["tweet_published_at"],
1203
+ detectedAt: raw["detected_at"],
1204
+ latencyMs: raw["latency_ms"],
1205
+ tweet: raw["tweet"]
1206
+ };
1207
+ case "error":
1208
+ return {
1209
+ type: "error",
1210
+ code: raw["code"],
1211
+ message: raw["message"]
1212
+ };
1213
+ default:
1214
+ return {
1215
+ type: "error",
1216
+ code: 0,
1217
+ message: `Unknown event type: ${String(type)}`
1218
+ };
1219
+ }
1220
+ }
1221
+ var StreamClient = class {
1222
+ client;
1223
+ constructor(client) {
1224
+ this.client = client;
1225
+ }
1226
+ // ===========================================================================
1227
+ // Monitor CRUD
1228
+ // ===========================================================================
1229
+ /**
1230
+ * Create a new stream monitor.
1231
+ *
1232
+ * @param params - Monitor configuration.
1233
+ * @returns The created StreamMonitor.
1234
+ * @throws InsufficientCreditsError - Credit balance below tier threshold (402).
1235
+ * @throws ValidationError - Invalid username or interval (422).
1236
+ * @throws ScrapeBadgerError - Name conflict (409).
1237
+ * @throws AuthenticationError - Invalid API key (401).
1238
+ *
1239
+ * @example
1240
+ * ```typescript
1241
+ * const monitor = await client.twitter.stream.createMonitor({
1242
+ * name: "Breaking News",
1243
+ * usernames: ["cnnbrk", "bbcbreaking"],
1244
+ * pollIntervalSeconds: 5,
1245
+ * });
1246
+ * ```
1247
+ */
1248
+ async createMonitor(params) {
1249
+ const body = {
1250
+ name: params.name,
1251
+ usernames: params.usernames,
1252
+ poll_interval_seconds: params.pollIntervalSeconds
1253
+ };
1254
+ if (params.webhookUrl !== void 0) body["webhook_url"] = params.webhookUrl;
1255
+ if (params.webhookSecret !== void 0) body["webhook_secret"] = params.webhookSecret;
1256
+ return this.client.request("/v1/twitter/stream/monitors", {
1257
+ method: "POST",
1258
+ body
1259
+ });
1260
+ }
1261
+ /**
1262
+ * List stream monitors for the authenticated API key.
1263
+ *
1264
+ * @param options - Filter and pagination options.
1265
+ * @returns StreamMonitorList with pagination metadata.
1266
+ *
1267
+ * @example
1268
+ * ```typescript
1269
+ * const { monitors, total } = await client.twitter.stream.listMonitors({
1270
+ * status: "active",
1271
+ * });
1272
+ * console.log(`${total} active monitors`);
1273
+ * ```
1274
+ */
1275
+ async listMonitors(options) {
1276
+ const params = {
1277
+ page: options?.page ?? 1,
1278
+ page_size: options?.pageSize ?? 20,
1279
+ status: options?.status
1280
+ };
1281
+ return this.client.request("/v1/twitter/stream/monitors", {
1282
+ params
1283
+ });
1284
+ }
1285
+ /**
1286
+ * Get a single stream monitor by ID.
1287
+ *
1288
+ * @param monitorId - UUID of the monitor.
1289
+ * @returns The StreamMonitor.
1290
+ * @throws NotFoundError - No monitor with that ID for this API key.
1291
+ *
1292
+ * @example
1293
+ * ```typescript
1294
+ * const monitor = await client.twitter.stream.getMonitor("550e8400-...");
1295
+ * console.log(`${monitor.name}: ${monitor.status}`);
1296
+ * ```
1297
+ */
1298
+ async getMonitor(monitorId) {
1299
+ return this.client.request(`/v1/twitter/stream/monitors/${monitorId}`);
1300
+ }
1301
+ /**
1302
+ * Partially update a stream monitor.
1303
+ *
1304
+ * Only fields that are explicitly set in params are sent to the server.
1305
+ *
1306
+ * @param monitorId - UUID of the monitor.
1307
+ * @param params - Fields to update (all optional).
1308
+ * @returns The updated StreamMonitor.
1309
+ * @throws NotFoundError - Monitor not found for this API key.
1310
+ * @throws InsufficientCreditsError - When resuming with insufficient credits.
1311
+ *
1312
+ * @example
1313
+ * ```typescript
1314
+ * const monitor = await client.twitter.stream.updateMonitor("550e8400-...", {
1315
+ * pollIntervalSeconds: 60,
1316
+ * });
1317
+ * ```
1318
+ */
1319
+ async updateMonitor(monitorId, params) {
1320
+ const body = {};
1321
+ if (params.name !== void 0) body["name"] = params.name;
1322
+ if (params.usernames !== void 0) body["usernames"] = params.usernames;
1323
+ if (params.pollIntervalSeconds !== void 0)
1324
+ body["poll_interval_seconds"] = params.pollIntervalSeconds;
1325
+ if (params.status !== void 0) body["status"] = params.status;
1326
+ if (params.webhookUrl !== void 0) body["webhook_url"] = params.webhookUrl;
1327
+ if (params.webhookSecret !== void 0) body["webhook_secret"] = params.webhookSecret;
1328
+ return this.client.request(`/v1/twitter/stream/monitors/${monitorId}`, {
1329
+ method: "PATCH",
1330
+ body
1331
+ });
1332
+ }
1333
+ /**
1334
+ * Pause an active stream monitor.
1335
+ *
1336
+ * Convenience wrapper around updateMonitor({ status: "paused" }).
1337
+ *
1338
+ * @param monitorId - UUID of the monitor.
1339
+ * @returns The updated StreamMonitor with status="paused".
1340
+ */
1341
+ async pauseMonitor(monitorId) {
1342
+ return this.updateMonitor(monitorId, { status: "paused" });
1343
+ }
1344
+ /**
1345
+ * Resume a paused stream monitor.
1346
+ *
1347
+ * Convenience wrapper around updateMonitor({ status: "active" }).
1348
+ *
1349
+ * @param monitorId - UUID of the monitor.
1350
+ * @returns The updated StreamMonitor with status="active".
1351
+ * @throws InsufficientCreditsError - If credits are below the tier threshold.
1352
+ */
1353
+ async resumeMonitor(monitorId) {
1354
+ return this.updateMonitor(monitorId, { status: "active" });
1355
+ }
1356
+ /**
1357
+ * Delete a stream monitor and all its associated logs. Irreversible.
1358
+ *
1359
+ * @param monitorId - UUID of the monitor.
1360
+ * @throws NotFoundError - Monitor not found for this API key.
1361
+ *
1362
+ * @example
1363
+ * ```typescript
1364
+ * await client.twitter.stream.deleteMonitor("550e8400-...");
1365
+ * ```
1366
+ */
1367
+ async deleteMonitor(monitorId) {
1368
+ await this.client.request(`/v1/twitter/stream/monitors/${monitorId}`, {
1369
+ method: "DELETE"
1370
+ });
1371
+ }
1372
+ // ===========================================================================
1373
+ // Delivery and Billing Logs
1374
+ // ===========================================================================
1375
+ /**
1376
+ * List tweet delivery logs.
1377
+ *
1378
+ * @param options - Filter and pagination options.
1379
+ * @returns DeliveryLogList with pagination metadata.
1380
+ */
1381
+ async listDeliveryLogs(options) {
1382
+ const params = {
1383
+ page: options?.page ?? 1,
1384
+ page_size: options?.pageSize ?? 20,
1385
+ sort: options?.sort ?? "desc",
1386
+ monitor_id: options?.monitorId,
1387
+ author_username: options?.authorUsername,
1388
+ delivery_status: options?.deliveryStatus
1389
+ };
1390
+ return this.client.request("/v1/twitter/stream/logs", { params });
1391
+ }
1392
+ /**
1393
+ * List billing activity logs.
1394
+ *
1395
+ * @param options - Filter and pagination options.
1396
+ * @returns BillingLogList with pagination metadata.
1397
+ */
1398
+ async listBillingLogs(options) {
1399
+ const params = {
1400
+ page: options?.page ?? 1,
1401
+ page_size: options?.pageSize ?? 20,
1402
+ monitor_id: options?.monitorId
1403
+ };
1404
+ return this.client.request("/v1/twitter/stream/billing-logs", { params });
1405
+ }
1406
+ // ===========================================================================
1407
+ // Filter Rules CRUD
1408
+ // ===========================================================================
1409
+ /**
1410
+ * Create a new tweet filter rule.
1411
+ *
1412
+ * @param params - Filter rule configuration.
1413
+ * @returns The created FilterRuleResponse.
1414
+ * @throws ValidationError - Invalid query or interval (422).
1415
+ * @throws InsufficientCreditsError - Credit balance below tier threshold (402).
1416
+ * @throws AuthenticationError - Invalid API key (401).
1417
+ *
1418
+ * @example
1419
+ * ```typescript
1420
+ * const rule = await client.twitter.stream.createFilterRule({
1421
+ * tag: "python news",
1422
+ * query: "#python lang:en -is:retweet",
1423
+ * interval_seconds: 60,
1424
+ * });
1425
+ * console.log(`Created: ${rule.id}, tier: ${rule.pricing_tier}`);
1426
+ * ```
1427
+ */
1428
+ async createFilterRule(params) {
1429
+ const body = {
1430
+ tag: params.tag,
1431
+ query: params.query,
1432
+ interval_seconds: params.interval_seconds
1433
+ };
1434
+ if (params.webhook_url !== void 0) body["webhook_url"] = params.webhook_url;
1435
+ if (params.webhook_secret !== void 0) body["webhook_secret"] = params.webhook_secret;
1436
+ if (params.max_results_per_poll !== void 0)
1437
+ body["max_results_per_poll"] = params.max_results_per_poll;
1438
+ return this.client.request("/v1/twitter/stream/filter-rules", {
1439
+ method: "POST",
1440
+ body
1441
+ });
1442
+ }
1443
+ /**
1444
+ * List filter rules for the authenticated API key.
1445
+ *
1446
+ * @param options - Filter and pagination options.
1447
+ * @returns FilterRuleListResponse with pagination metadata.
1448
+ *
1449
+ * @example
1450
+ * ```typescript
1451
+ * const { rules, total } = await client.twitter.stream.listFilterRules({
1452
+ * status: "active",
1453
+ * });
1454
+ * console.log(`${total} active rules`);
1455
+ * ```
1456
+ */
1457
+ async listFilterRules(options) {
1458
+ const params = {
1459
+ limit: options?.limit ?? 20,
1460
+ offset: options?.offset ?? 0,
1461
+ status: options?.status
1462
+ };
1463
+ return this.client.request("/v1/twitter/stream/filter-rules", {
1464
+ params
1465
+ });
1466
+ }
1467
+ /**
1468
+ * Get a single filter rule by ID.
1469
+ *
1470
+ * @param ruleId - UUID of the filter rule.
1471
+ * @returns The FilterRuleResponse.
1472
+ * @throws NotFoundError - No rule with that ID for this API key.
1473
+ *
1474
+ * @example
1475
+ * ```typescript
1476
+ * const rule = await client.twitter.stream.getFilterRule("550e8400-...");
1477
+ * console.log(`${rule.tag}: ${rule.status}`);
1478
+ * ```
1479
+ */
1480
+ async getFilterRule(ruleId) {
1481
+ return this.client.request(`/v1/twitter/stream/filter-rules/${ruleId}`);
1482
+ }
1483
+ /**
1484
+ * Partially update a filter rule.
1485
+ *
1486
+ * Only fields that are explicitly set in params are sent to the server.
1487
+ *
1488
+ * @param ruleId - UUID of the filter rule.
1489
+ * @param params - Fields to update (all optional).
1490
+ * @returns The updated FilterRuleResponse.
1491
+ * @throws NotFoundError - Rule not found for this API key.
1492
+ * @throws ValidationError - Invalid field values (422).
1493
+ *
1494
+ * @example
1495
+ * ```typescript
1496
+ * const rule = await client.twitter.stream.updateFilterRule("550e8400-...", {
1497
+ * interval_seconds: 120,
1498
+ * });
1499
+ * ```
1500
+ */
1501
+ async updateFilterRule(ruleId, params) {
1502
+ const body = {};
1503
+ if (params.tag !== void 0) body["tag"] = params.tag;
1504
+ if (params.query !== void 0) body["query"] = params.query;
1505
+ if (params.interval_seconds !== void 0) body["interval_seconds"] = params.interval_seconds;
1506
+ if (params.status !== void 0) body["status"] = params.status;
1507
+ if (params.webhook_url !== void 0) body["webhook_url"] = params.webhook_url;
1508
+ if (params.webhook_secret !== void 0) body["webhook_secret"] = params.webhook_secret;
1509
+ if (params.max_results_per_poll !== void 0)
1510
+ body["max_results_per_poll"] = params.max_results_per_poll;
1511
+ return this.client.request(`/v1/twitter/stream/filter-rules/${ruleId}`, {
1512
+ method: "PATCH",
1513
+ body
1514
+ });
1515
+ }
1516
+ /**
1517
+ * Delete a filter rule and all its associated logs. Irreversible.
1518
+ *
1519
+ * @param ruleId - UUID of the filter rule.
1520
+ * @throws NotFoundError - Rule not found for this API key.
1521
+ *
1522
+ * @example
1523
+ * ```typescript
1524
+ * await client.twitter.stream.deleteFilterRule("550e8400-...");
1525
+ * ```
1526
+ */
1527
+ async deleteFilterRule(ruleId) {
1528
+ await this.client.request(`/v1/twitter/stream/filter-rules/${ruleId}`, {
1529
+ method: "DELETE"
1530
+ });
1531
+ }
1532
+ // ===========================================================================
1533
+ // Filter Rules Utility
1534
+ // ===========================================================================
1535
+ /**
1536
+ * Validate a Twitter search query before creating a rule.
1537
+ *
1538
+ * @param query - The Twitter search query to validate.
1539
+ * @returns FilterRuleValidateResponse with validity and sample result count.
1540
+ *
1541
+ * @example
1542
+ * ```typescript
1543
+ * const result = await client.twitter.stream.validateFilterRuleQuery(
1544
+ * "#python lang:en -is:retweet"
1545
+ * );
1546
+ * if (!result.valid) {
1547
+ * console.error("Invalid query:", result.error);
1548
+ * }
1549
+ * ```
1550
+ */
1551
+ async validateFilterRuleQuery(query) {
1552
+ return this.client.request(
1553
+ "/v1/twitter/stream/filter-rules/validate",
1554
+ { method: "POST", body: { query } }
1555
+ );
1556
+ }
1557
+ /**
1558
+ * List tweet delivery logs for a specific filter rule.
1559
+ *
1560
+ * @param ruleId - UUID of the filter rule.
1561
+ * @param options - Filter and pagination options.
1562
+ * @returns FilterRuleDeliveryLogListResponse with pagination metadata.
1563
+ *
1564
+ * @example
1565
+ * ```typescript
1566
+ * const { logs, total } = await client.twitter.stream.listFilterRuleLogs(
1567
+ * "550e8400-...",
1568
+ * { limit: 50, deliveryStatus: "webhook_delivered" }
1569
+ * );
1570
+ * ```
1571
+ */
1572
+ async listFilterRuleLogs(ruleId, options) {
1573
+ const params = {
1574
+ limit: options?.limit ?? 20,
1575
+ offset: options?.offset ?? 0,
1576
+ sort: options?.sort ?? "desc",
1577
+ delivery_status: options?.deliveryStatus
1578
+ };
1579
+ return this.client.request(
1580
+ `/v1/twitter/stream/filter-rules/${ruleId}/logs`,
1581
+ { params }
1582
+ );
1583
+ }
1584
+ /**
1585
+ * Get all available filter rule pricing tiers.
1586
+ *
1587
+ * @returns FilterRulePricingTiersResponse listing all tiers.
1588
+ *
1589
+ * @example
1590
+ * ```typescript
1591
+ * const { tiers } = await client.twitter.stream.getFilterRulePricingTiers();
1592
+ * tiers.forEach((t) => console.log(t.tier_label, t.credits_per_rule_per_day));
1593
+ * ```
1594
+ */
1595
+ async getFilterRulePricingTiers() {
1596
+ return this.client.request(
1597
+ "/v1/twitter/stream/filter-rules-pricing"
1598
+ );
1599
+ }
1600
+ // ===========================================================================
1601
+ // WebSocket Streaming -- EventEmitter style
1602
+ // ===========================================================================
1603
+ /**
1604
+ * Connect to the WebSocket stream and return an EventEmitter.
1605
+ *
1606
+ * The caller subscribes to events via `.on("tweet", handler)`.
1607
+ * The SDK handles pong replies to server pings automatically.
1608
+ * Call `.close()` on the emitter to disconnect cleanly.
1609
+ *
1610
+ * If reconnect is true, the emitter automatically reconnects after
1611
+ * disconnects (other than auth failures). A new "connected" event is
1612
+ * emitted on each reconnect.
1613
+ *
1614
+ * @param options - Connection options (reconnect, delay, maxReconnects).
1615
+ * @returns StreamEmitter -- an EventEmitter subclass.
1616
+ *
1617
+ * @example
1618
+ * ```typescript
1619
+ * const stream = client.twitter.stream.connect();
1620
+ * stream.on("connected", (e) => console.log("Connected:", e.connectionId));
1621
+ * stream.on("tweet", (event) => {
1622
+ * console.log(`@${event.authorUsername}: ${event.tweet.text}`);
1623
+ * console.log(` latency: ${event.latencyMs}ms`);
1624
+ * });
1625
+ * stream.on("error", (err) => console.error("Stream error:", err));
1626
+ * stream.on("close", () => console.log("Stream closed"));
1627
+ *
1628
+ * // Later:
1629
+ * stream.close();
1630
+ * ```
1631
+ */
1632
+ connect(options = {}) {
1633
+ const { reconnect = false, reconnectDelaySeconds = 90, maxReconnects } = options;
1634
+ const delay = Math.max(MIN_RECONNECT_DELAY_SECONDS, reconnectDelaySeconds) * 1e3;
1635
+ const wsUrl = wsUrlFromBase(this.client.config.baseUrl);
1636
+ const apiKey = this.client.config.apiKey;
1637
+ const emitter = new events.EventEmitter();
1638
+ let ws = null;
1639
+ let closed = false;
1640
+ let reconnectCount = 0;
1641
+ const connectOnce = () => {
1642
+ ws = new WebSocket__default.default(wsUrl, { headers: { "x-api-key": apiKey } });
1643
+ ws.on("message", (data) => {
1644
+ let raw;
1645
+ try {
1646
+ raw = JSON.parse(String(data));
1647
+ } catch {
1648
+ return;
1649
+ }
1650
+ const event = parseEvent(raw);
1651
+ if (event.type === "ping") {
1652
+ ws?.send(JSON.stringify({ type: "pong" }));
1653
+ emitter.emit("ping", event);
1654
+ return;
1655
+ }
1656
+ if (event.type === "error") {
1657
+ const code = event.code;
1658
+ const err = new WebSocketStreamError(event.message, code);
1659
+ emitter.emit("error", err);
1660
+ if (code === 4001 || code === 4003) {
1661
+ closed = true;
1662
+ ws?.close();
1663
+ }
1664
+ return;
1665
+ }
1666
+ emitter.emit(event.type, event);
1667
+ });
1668
+ ws.on("open", () => {
1669
+ });
1670
+ ws.on("close", (code, reason) => {
1671
+ if (closed) {
1672
+ emitter.emit("close");
1673
+ return;
1674
+ }
1675
+ if (!reconnect) {
1676
+ const reasonStr = reason instanceof Buffer ? reason.toString() : String(reason ?? "");
1677
+ emitter.emit(
1678
+ "error",
1679
+ new WebSocketStreamError(`WebSocket closed: ${reasonStr || String(code)}`)
1680
+ );
1681
+ emitter.emit("close");
1682
+ return;
1683
+ }
1684
+ if (maxReconnects !== void 0 && reconnectCount >= maxReconnects) {
1685
+ emitter.emit(
1686
+ "error",
1687
+ new WebSocketStreamError(`Max reconnects (${maxReconnects}) exhausted`)
1688
+ );
1689
+ emitter.emit("close");
1690
+ return;
1691
+ }
1692
+ reconnectCount++;
1693
+ setTimeout(connectOnce, delay);
1694
+ });
1695
+ ws.on("error", (err) => {
1696
+ const message = err instanceof Error ? err.message : String(err);
1697
+ emitter.emit("error", new WebSocketStreamError(message));
1698
+ });
1699
+ };
1700
+ emitter.close = () => {
1701
+ closed = true;
1702
+ ws?.close(1e3, "Client closed");
1703
+ };
1704
+ connectOnce();
1705
+ return emitter;
1706
+ }
1707
+ // ===========================================================================
1708
+ // WebSocket Streaming -- AsyncIterator style
1709
+ // ===========================================================================
1710
+ /**
1711
+ * Connect to the WebSocket stream and return an AsyncIterator.
1712
+ *
1713
+ * Iterates over StreamEvent objects. The SDK handles pong replies
1714
+ * automatically but still yields PingEvent to the caller (the caller
1715
+ * may ignore ping events).
1716
+ *
1717
+ * @param options - Connection options (reconnect, delay, maxReconnects).
1718
+ * @yields StreamEvent
1719
+ *
1720
+ * @example
1721
+ * ```typescript
1722
+ * for await (const event of client.twitter.stream.connectIter()) {
1723
+ * if (event.type === "tweet") {
1724
+ * console.log(`@${event.authorUsername}: ${event.latencyMs}ms`);
1725
+ * }
1726
+ * }
1727
+ * ```
1728
+ *
1729
+ * @example With auto-reconnect
1730
+ * ```typescript
1731
+ * for await (const event of client.twitter.stream.connectIter({
1732
+ * reconnect: true,
1733
+ * reconnectDelaySeconds: 90,
1734
+ * })) {
1735
+ * if (event.type === "tweet") {
1736
+ * // process(event);
1737
+ * }
1738
+ * }
1739
+ * ```
1740
+ */
1741
+ async *connectIter(options = {}) {
1742
+ const { reconnect = false, reconnectDelaySeconds = 90, maxReconnects } = options;
1743
+ const delay = Math.max(MIN_RECONNECT_DELAY_SECONDS, reconnectDelaySeconds) * 1e3;
1744
+ const wsUrl = wsUrlFromBase(this.client.config.baseUrl);
1745
+ const apiKey = this.client.config.apiKey;
1746
+ let reconnectCount = 0;
1747
+ while (true) {
1748
+ const events = [];
1749
+ let resolveWait = null;
1750
+ let rejectWait = null;
1751
+ let done = false;
1752
+ const ws = new WebSocket__default.default(wsUrl, { headers: { "x-api-key": apiKey } });
1753
+ const waitForEvent = () => new Promise((res, rej) => {
1754
+ resolveWait = res;
1755
+ rejectWait = rej;
1756
+ });
1757
+ ws.on("message", (data) => {
1758
+ let raw;
1759
+ try {
1760
+ raw = JSON.parse(String(data));
1761
+ } catch {
1762
+ return;
1763
+ }
1764
+ const event = parseEvent(raw);
1765
+ if (event.type === "ping") {
1766
+ ws.send(JSON.stringify({ type: "pong" }));
1767
+ }
1768
+ if (event.type === "error") {
1769
+ const code = event.code;
1770
+ if (code === 4001 || code === 4003) {
1771
+ rejectWait?.(new WebSocketStreamError(event.message, code));
1772
+ rejectWait = null;
1773
+ resolveWait = null;
1774
+ return;
1775
+ }
1776
+ }
1777
+ events.push(event);
1778
+ resolveWait?.();
1779
+ resolveWait = null;
1780
+ rejectWait = null;
1781
+ });
1782
+ ws.on("close", () => {
1783
+ done = true;
1784
+ resolveWait?.();
1785
+ resolveWait = null;
1786
+ rejectWait = null;
1787
+ });
1788
+ ws.on("error", (err) => {
1789
+ const message = err instanceof Error ? err.message : String(err);
1790
+ rejectWait?.(new WebSocketStreamError(message));
1791
+ rejectWait = null;
1792
+ resolveWait = null;
1793
+ });
1794
+ try {
1795
+ while (!done || events.length > 0) {
1796
+ if (events.length === 0) {
1797
+ await waitForEvent();
1798
+ }
1799
+ while (events.length > 0) {
1800
+ yield events.shift();
1801
+ }
1802
+ }
1803
+ } catch (err) {
1804
+ ws.close();
1805
+ throw err;
1806
+ } finally {
1807
+ ws.close();
1808
+ }
1809
+ if (!reconnect) {
1810
+ return;
1811
+ }
1812
+ if (maxReconnects !== void 0 && reconnectCount >= maxReconnects) {
1813
+ throw new WebSocketStreamError(`Max reconnects (${maxReconnects}) exhausted`);
1814
+ }
1815
+ reconnectCount++;
1816
+ await new Promise((res) => setTimeout(res, delay));
1817
+ }
1818
+ }
1819
+ };
1820
+ function verifyWebhookSignature(secret, body, signatureHeader) {
1821
+ if (!signatureHeader.startsWith("sha256=")) {
1822
+ return false;
1823
+ }
1824
+ const expectedHex = signatureHeader.slice("sha256=".length);
1825
+ const bodyBuffer = typeof body === "string" ? Buffer.from(body, "utf-8") : body;
1826
+ const actualHex = crypto.createHmac("sha256", secret).update(bodyBuffer).digest("hex");
1827
+ try {
1828
+ return crypto.timingSafeEqual(Buffer.from(expectedHex, "hex"), Buffer.from(actualHex, "hex"));
1829
+ } catch {
1830
+ return false;
1831
+ }
1832
+ }
1833
+
1147
1834
  // src/twitter/client.ts
1148
1835
  var TwitterClient = class {
1149
1836
  /** Client for tweet operations */
@@ -1158,6 +1845,8 @@ var TwitterClient = class {
1158
1845
  trends;
1159
1846
  /** Client for geo/places operations */
1160
1847
  geo;
1848
+ /** Client for real-time stream monitor management and WebSocket streaming */
1849
+ stream;
1161
1850
  /**
1162
1851
  * Create a new Twitter client.
1163
1852
  *
@@ -1170,15 +1859,18 @@ var TwitterClient = class {
1170
1859
  this.communities = new CommunitiesClient(client);
1171
1860
  this.trends = new TrendsClient(client);
1172
1861
  this.geo = new GeoClient(client);
1862
+ this.stream = new StreamClient(client);
1173
1863
  }
1174
1864
  };
1175
1865
 
1176
1866
  exports.CommunitiesClient = CommunitiesClient;
1177
1867
  exports.GeoClient = GeoClient;
1178
1868
  exports.ListsClient = ListsClient;
1869
+ exports.StreamClient = StreamClient;
1179
1870
  exports.TrendsClient = TrendsClient;
1180
1871
  exports.TweetsClient = TweetsClient;
1181
1872
  exports.TwitterClient = TwitterClient;
1182
1873
  exports.UsersClient = UsersClient;
1874
+ exports.verifyWebhookSignature = verifyWebhookSignature;
1183
1875
  //# sourceMappingURL=index.js.map
1184
1876
  //# sourceMappingURL=index.js.map