@the-convocation/twitter-scraper 0.15.1 → 0.16.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.
@@ -47,6 +47,12 @@ class ApiError extends Error {
47
47
  return new ApiError(response, data, `Response status: ${response.status}`);
48
48
  }
49
49
  }
50
+ class AuthenticationError extends Error {
51
+ constructor(message) {
52
+ super(message || "Authentication failed");
53
+ this.name = "AuthenticationError";
54
+ }
55
+ }
50
56
 
51
57
  class WaitingRateLimitStrategy {
52
58
  async onRateLimit({ response: res }) {
@@ -275,7 +281,9 @@ class TwitterGuestAuth {
275
281
  }
276
282
  const token = this.guestToken;
277
283
  if (token == null) {
278
- throw new Error("Authentication token is null or undefined.");
284
+ throw new AuthenticationError(
285
+ "Authentication token is null or undefined."
286
+ );
279
287
  }
280
288
  headers.set("authorization", `Bearer ${this.bearerToken}`);
281
289
  headers.set("x-guest-token", token);
@@ -322,15 +330,15 @@ class TwitterGuestAuth {
322
330
  });
323
331
  await updateCookieJar(this.jar, res.headers);
324
332
  if (!res.ok) {
325
- throw new Error(await res.text());
333
+ throw new AuthenticationError(await res.text());
326
334
  }
327
335
  const o = await res.json();
328
336
  if (o == null || o["guest_token"] == null) {
329
- throw new Error("guest_token not found.");
337
+ throw new AuthenticationError("guest_token not found.");
330
338
  }
331
339
  const newGuestToken = o["guest_token"];
332
340
  if (typeof newGuestToken !== "string") {
333
- throw new Error("guest_token was not a string.");
341
+ throw new AuthenticationError("guest_token was not a string.");
334
342
  }
335
343
  this.guestToken = newGuestToken;
336
344
  this.guestCreatedAt = /* @__PURE__ */ new Date();
@@ -351,6 +359,47 @@ const TwitterUserAuthSubtask = typebox.Type.Object({
351
359
  class TwitterUserAuth extends TwitterGuestAuth {
352
360
  constructor(bearerToken, options) {
353
361
  super(bearerToken, options);
362
+ this.subtaskHandlers = /* @__PURE__ */ new Map();
363
+ this.initializeDefaultHandlers();
364
+ }
365
+ /**
366
+ * Register a custom subtask handler or override an existing one
367
+ * @param subtaskId The ID of the subtask to handle
368
+ * @param handler The handler function that processes the subtask
369
+ */
370
+ registerSubtaskHandler(subtaskId, handler) {
371
+ this.subtaskHandlers.set(subtaskId, handler);
372
+ }
373
+ initializeDefaultHandlers() {
374
+ this.subtaskHandlers.set(
375
+ "LoginJsInstrumentationSubtask",
376
+ this.handleJsInstrumentationSubtask.bind(this)
377
+ );
378
+ this.subtaskHandlers.set(
379
+ "LoginEnterUserIdentifierSSO",
380
+ this.handleEnterUserIdentifierSSO.bind(this)
381
+ );
382
+ this.subtaskHandlers.set(
383
+ "LoginEnterAlternateIdentifierSubtask",
384
+ this.handleEnterAlternateIdentifierSubtask.bind(this)
385
+ );
386
+ this.subtaskHandlers.set(
387
+ "LoginEnterPassword",
388
+ this.handleEnterPassword.bind(this)
389
+ );
390
+ this.subtaskHandlers.set(
391
+ "AccountDuplicationCheck",
392
+ this.handleAccountDuplicationCheck.bind(this)
393
+ );
394
+ this.subtaskHandlers.set(
395
+ "LoginTwoFactorAuthChallenge",
396
+ this.handleTwoFactorAuthChallenge.bind(this)
397
+ );
398
+ this.subtaskHandlers.set("LoginAcid", this.handleAcid.bind(this));
399
+ this.subtaskHandlers.set(
400
+ "LoginSuccessSubtask",
401
+ this.handleSuccessSubtask.bind(this)
402
+ );
354
403
  }
355
404
  async isLoggedIn() {
356
405
  const res = await requestApi(
@@ -365,52 +414,49 @@ class TwitterUserAuth extends TwitterGuestAuth {
365
414
  }
366
415
  async login(username, password, email, twoFactorSecret) {
367
416
  await this.updateGuestToken();
417
+ const credentials = {
418
+ username,
419
+ password,
420
+ email,
421
+ twoFactorSecret
422
+ };
368
423
  let next = await this.initLogin();
369
- while ("subtask" in next && next.subtask) {
370
- if (next.subtask.subtask_id === "LoginJsInstrumentationSubtask") {
371
- next = await this.handleJsInstrumentationSubtask(next);
372
- } else if (next.subtask.subtask_id === "LoginEnterUserIdentifierSSO") {
373
- next = await this.handleEnterUserIdentifierSSO(next, username);
374
- } else if (next.subtask.subtask_id === "LoginEnterAlternateIdentifierSubtask") {
375
- next = await this.handleEnterAlternateIdentifierSubtask(
376
- next,
377
- email
378
- );
379
- } else if (next.subtask.subtask_id === "LoginEnterPassword") {
380
- next = await this.handleEnterPassword(next, password);
381
- } else if (next.subtask.subtask_id === "AccountDuplicationCheck") {
382
- next = await this.handleAccountDuplicationCheck(next);
383
- } else if (next.subtask.subtask_id === "LoginTwoFactorAuthChallenge") {
384
- if (twoFactorSecret) {
385
- next = await this.handleTwoFactorAuthChallenge(next, twoFactorSecret);
386
- } else {
387
- throw new Error(
388
- "Requested two factor authentication code but no secret provided"
389
- );
390
- }
391
- } else if (next.subtask.subtask_id === "LoginAcid") {
392
- next = await this.handleAcid(next, email);
393
- } else if (next.subtask.subtask_id === "LoginSuccessSubtask") {
394
- next = await this.handleSuccessSubtask(next);
424
+ while (next.status === "success" && next.response.subtasks?.length) {
425
+ const flowToken = next.response.flow_token;
426
+ if (flowToken == null) {
427
+ throw new Error("flow_token not found.");
428
+ }
429
+ const subtaskId = next.response.subtasks[0].subtask_id;
430
+ const handler = this.subtaskHandlers.get(subtaskId);
431
+ if (handler) {
432
+ next = await handler(subtaskId, next.response, credentials, {
433
+ sendFlowRequest: this.executeFlowTask.bind(this),
434
+ getFlowToken: () => flowToken
435
+ });
395
436
  } else {
396
- throw new Error(`Unknown subtask ${next.subtask.subtask_id}`);
437
+ throw new Error(`Unknown subtask ${subtaskId}`);
397
438
  }
398
439
  }
399
- if ("err" in next) {
440
+ if (next.status === "error") {
400
441
  throw next.err;
401
442
  }
402
443
  }
403
444
  async logout() {
404
- if (!this.isLoggedIn()) {
445
+ if (!this.hasToken()) {
405
446
  return;
406
447
  }
407
- await requestApi(
408
- "https://api.twitter.com/1.1/account/logout.json",
409
- this,
410
- "POST"
411
- );
412
- this.deleteToken();
413
- this.jar = new toughCookie.CookieJar();
448
+ try {
449
+ await requestApi(
450
+ "https://api.twitter.com/1.1/account/logout.json",
451
+ this,
452
+ "POST"
453
+ );
454
+ } catch (error) {
455
+ console.warn("Error during logout:", error);
456
+ } finally {
457
+ this.deleteToken();
458
+ this.jar = new toughCookie.CookieJar();
459
+ }
414
460
  }
415
461
  async installCsrfToken(headers) {
416
462
  const cookies = await this.getCookies();
@@ -449,12 +495,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
449
495
  }
450
496
  });
451
497
  }
452
- async handleJsInstrumentationSubtask(prev) {
453
- return await this.executeFlowTask({
454
- flow_token: prev.flowToken,
498
+ async handleJsInstrumentationSubtask(subtaskId, _prev, _credentials, api) {
499
+ return await api.sendFlowRequest({
500
+ flow_token: api.getFlowToken(),
455
501
  subtask_inputs: [
456
502
  {
457
- subtask_id: "LoginJsInstrumentationSubtask",
503
+ subtask_id: subtaskId,
458
504
  js_instrumentation: {
459
505
  response: "{}",
460
506
  link: "next_link"
@@ -463,32 +509,32 @@ class TwitterUserAuth extends TwitterGuestAuth {
463
509
  ]
464
510
  });
465
511
  }
466
- async handleEnterAlternateIdentifierSubtask(prev, email) {
512
+ async handleEnterAlternateIdentifierSubtask(subtaskId, _prev, credentials, api) {
467
513
  return await this.executeFlowTask({
468
- flow_token: prev.flowToken,
514
+ flow_token: api.getFlowToken(),
469
515
  subtask_inputs: [
470
516
  {
471
- subtask_id: "LoginEnterAlternateIdentifierSubtask",
517
+ subtask_id: subtaskId,
472
518
  enter_text: {
473
- text: email,
519
+ text: credentials.email,
474
520
  link: "next_link"
475
521
  }
476
522
  }
477
523
  ]
478
524
  });
479
525
  }
480
- async handleEnterUserIdentifierSSO(prev, username) {
526
+ async handleEnterUserIdentifierSSO(subtaskId, _prev, credentials, api) {
481
527
  return await this.executeFlowTask({
482
- flow_token: prev.flowToken,
528
+ flow_token: api.getFlowToken(),
483
529
  subtask_inputs: [
484
530
  {
485
- subtask_id: "LoginEnterUserIdentifierSSO",
531
+ subtask_id: subtaskId,
486
532
  settings_list: {
487
533
  setting_responses: [
488
534
  {
489
535
  key: "user_identifier",
490
536
  response_data: {
491
- text_data: { result: username }
537
+ text_data: { result: credentials.username }
492
538
  }
493
539
  }
494
540
  ],
@@ -498,26 +544,26 @@ class TwitterUserAuth extends TwitterGuestAuth {
498
544
  ]
499
545
  });
500
546
  }
501
- async handleEnterPassword(prev, password) {
547
+ async handleEnterPassword(subtaskId, _prev, credentials, api) {
502
548
  return await this.executeFlowTask({
503
- flow_token: prev.flowToken,
549
+ flow_token: api.getFlowToken(),
504
550
  subtask_inputs: [
505
551
  {
506
- subtask_id: "LoginEnterPassword",
552
+ subtask_id: subtaskId,
507
553
  enter_password: {
508
- password,
554
+ password: credentials.password,
509
555
  link: "next_link"
510
556
  }
511
557
  }
512
558
  ]
513
559
  });
514
560
  }
515
- async handleAccountDuplicationCheck(prev) {
561
+ async handleAccountDuplicationCheck(subtaskId, _prev, _credentials, api) {
516
562
  return await this.executeFlowTask({
517
- flow_token: prev.flowToken,
563
+ flow_token: api.getFlowToken(),
518
564
  subtask_inputs: [
519
565
  {
520
- subtask_id: "AccountDuplicationCheck",
566
+ subtask_id: subtaskId,
521
567
  check_logged_in_account: {
522
568
  link: "AccountDuplicationCheck_false"
523
569
  }
@@ -525,16 +571,24 @@ class TwitterUserAuth extends TwitterGuestAuth {
525
571
  ]
526
572
  });
527
573
  }
528
- async handleTwoFactorAuthChallenge(prev, secret) {
529
- const totp = new OTPAuth__namespace.TOTP({ secret });
574
+ async handleTwoFactorAuthChallenge(subtaskId, _prev, credentials, api) {
575
+ if (!credentials.twoFactorSecret) {
576
+ return {
577
+ status: "error",
578
+ err: new AuthenticationError(
579
+ "Two-factor authentication is required but no secret was provided"
580
+ )
581
+ };
582
+ }
583
+ const totp = new OTPAuth__namespace.TOTP({ secret: credentials.twoFactorSecret });
530
584
  let error;
531
585
  for (let attempts = 1; attempts < 4; attempts += 1) {
532
586
  try {
533
- return await this.executeFlowTask({
534
- flow_token: prev.flowToken,
587
+ return await api.sendFlowRequest({
588
+ flow_token: api.getFlowToken(),
535
589
  subtask_inputs: [
536
590
  {
537
- subtask_id: "LoginTwoFactorAuthChallenge",
591
+ subtask_id: subtaskId,
538
592
  enter_text: {
539
593
  link: "next_link",
540
594
  text: totp.generate()
@@ -549,23 +603,23 @@ class TwitterUserAuth extends TwitterGuestAuth {
549
603
  }
550
604
  throw error;
551
605
  }
552
- async handleAcid(prev, email) {
606
+ async handleAcid(subtaskId, _prev, credentials, api) {
553
607
  return await this.executeFlowTask({
554
- flow_token: prev.flowToken,
608
+ flow_token: api.getFlowToken(),
555
609
  subtask_inputs: [
556
610
  {
557
- subtask_id: "LoginAcid",
611
+ subtask_id: subtaskId,
558
612
  enter_text: {
559
- text: email,
613
+ text: credentials.email,
560
614
  link: "next_link"
561
615
  }
562
616
  }
563
617
  ]
564
618
  });
565
619
  }
566
- async handleSuccessSubtask(prev) {
620
+ async handleSuccessSubtask(_subtaskId, _prev, _credentials, api) {
567
621
  return await this.executeFlowTask({
568
- flow_token: prev.flowToken,
622
+ flow_token: api.getFlowToken(),
569
623
  subtask_inputs: []
570
624
  });
571
625
  }
@@ -573,7 +627,9 @@ class TwitterUserAuth extends TwitterGuestAuth {
573
627
  const onboardingTaskUrl = "https://api.twitter.com/1.1/onboarding/task.json";
574
628
  const token = this.guestToken;
575
629
  if (token == null) {
576
- throw new Error("Authentication token is null or undefined.");
630
+ throw new AuthenticationError(
631
+ "Authentication token is null or undefined."
632
+ );
577
633
  }
578
634
  const headers = new headersPolyfill.Headers({
579
635
  authorization: `Bearer ${this.bearerToken}`,
@@ -598,12 +654,15 @@ class TwitterUserAuth extends TwitterGuestAuth {
598
654
  }
599
655
  const flow = await res.json();
600
656
  if (flow?.flow_token == null) {
601
- return { status: "error", err: new Error("flow_token not found.") };
657
+ return {
658
+ status: "error",
659
+ err: new AuthenticationError("flow_token not found.")
660
+ };
602
661
  }
603
662
  if (flow.errors?.length) {
604
663
  return {
605
664
  status: "error",
606
- err: new Error(
665
+ err: new AuthenticationError(
607
666
  `Authentication error (${flow.errors[0].code}): ${flow.errors[0].message}`
608
667
  )
609
668
  };
@@ -611,7 +670,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
611
670
  if (typeof flow.flow_token !== "string") {
612
671
  return {
613
672
  status: "error",
614
- err: new Error("flow_token was not a string.")
673
+ err: new AuthenticationError("flow_token was not a string.")
615
674
  };
616
675
  }
617
676
  const subtask = flow.subtasks?.length ? flow.subtasks[0] : void 0;
@@ -619,13 +678,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
619
678
  if (subtask && subtask.subtask_id === "DenyLoginSubtask") {
620
679
  return {
621
680
  status: "error",
622
- err: new Error("Authentication error: DenyLoginSubtask")
681
+ err: new AuthenticationError("Authentication error: DenyLoginSubtask")
623
682
  };
624
683
  }
625
684
  return {
626
685
  status: "success",
627
- subtask,
628
- flowToken: flow.flow_token
686
+ response: flow
629
687
  };
630
688
  }
631
689
  }
@@ -1288,8 +1346,8 @@ async function fetchSearchProfiles(query, maxProfiles, auth, cursor) {
1288
1346
  return parseSearchTimelineUsers(timeline);
1289
1347
  }
1290
1348
  async function getSearchTimeline(query, maxItems, searchMode, auth, cursor) {
1291
- if (!auth.isLoggedIn()) {
1292
- throw new Error("Scraper is not logged-in for search.");
1349
+ if (!await auth.isLoggedIn()) {
1350
+ throw new AuthenticationError("Scraper is not logged-in for search.");
1293
1351
  }
1294
1352
  if (maxItems > 50) {
1295
1353
  maxItems = 50;
@@ -1396,6 +1454,11 @@ function getFollowers(userId, maxProfiles, auth) {
1396
1454
  });
1397
1455
  }
1398
1456
  async function fetchProfileFollowing(userId, maxProfiles, auth, cursor) {
1457
+ if (!await auth.isLoggedIn()) {
1458
+ throw new AuthenticationError(
1459
+ "Scraper is not logged-in for profile following."
1460
+ );
1461
+ }
1399
1462
  const timeline = await getFollowingTimeline(
1400
1463
  userId,
1401
1464
  maxProfiles,
@@ -1405,6 +1468,11 @@ async function fetchProfileFollowing(userId, maxProfiles, auth, cursor) {
1405
1468
  return parseRelationshipTimeline(timeline);
1406
1469
  }
1407
1470
  async function fetchProfileFollowers(userId, maxProfiles, auth, cursor) {
1471
+ if (!await auth.isLoggedIn()) {
1472
+ throw new AuthenticationError(
1473
+ "Scraper is not logged-in for profile followers."
1474
+ );
1475
+ }
1408
1476
  const timeline = await getFollowersTimeline(
1409
1477
  userId,
1410
1478
  maxProfiles,
@@ -1415,7 +1483,9 @@ async function fetchProfileFollowers(userId, maxProfiles, auth, cursor) {
1415
1483
  }
1416
1484
  async function getFollowingTimeline(userId, maxItems, auth, cursor) {
1417
1485
  if (!auth.isLoggedIn()) {
1418
- throw new Error("Scraper is not logged-in for profile following.");
1486
+ throw new AuthenticationError(
1487
+ "Scraper is not logged-in for profile following."
1488
+ );
1419
1489
  }
1420
1490
  if (maxItems > 50) {
1421
1491
  maxItems = 50;
@@ -1448,7 +1518,9 @@ async function getFollowingTimeline(userId, maxItems, auth, cursor) {
1448
1518
  }
1449
1519
  async function getFollowersTimeline(userId, maxItems, auth, cursor) {
1450
1520
  if (!auth.isLoggedIn()) {
1451
- throw new Error("Scraper is not logged-in for profile followers.");
1521
+ throw new AuthenticationError(
1522
+ "Scraper is not logged-in for profile followers."
1523
+ );
1452
1524
  }
1453
1525
  if (maxItems > 50) {
1454
1526
  maxItems = 50;
@@ -1705,8 +1777,10 @@ function getTweetsAndRepliesByUserId(userId, maxTweets, auth) {
1705
1777
  });
1706
1778
  }
1707
1779
  async function fetchLikedTweets(userId, maxTweets, cursor, auth) {
1708
- if (!auth.isLoggedIn()) {
1709
- throw new Error("Scraper is not logged-in for fetching liked tweets.");
1780
+ if (!await auth.isLoggedIn()) {
1781
+ throw new AuthenticationError(
1782
+ "Scraper is not logged-in for fetching liked tweets."
1783
+ );
1710
1784
  }
1711
1785
  if (maxTweets > 200) {
1712
1786
  maxTweets = 200;
@@ -1811,6 +1885,20 @@ class Scraper {
1811
1885
  this.token = bearerToken;
1812
1886
  this.useGuestAuth();
1813
1887
  }
1888
+ /**
1889
+ * Registers a subtask handler for the given subtask ID. This
1890
+ * will override any existing handler for the same subtask.
1891
+ * @param subtaskId The ID of the subtask to register the handler for.
1892
+ * @param subtaskHandler The handler function to register.
1893
+ */
1894
+ registerAuthSubtaskHandler(subtaskId, subtaskHandler) {
1895
+ if (this.auth instanceof TwitterUserAuth) {
1896
+ this.auth.registerSubtaskHandler(subtaskId, subtaskHandler);
1897
+ }
1898
+ if (this.authTrends instanceof TwitterUserAuth) {
1899
+ this.authTrends.registerSubtaskHandler(subtaskId, subtaskHandler);
1900
+ }
1901
+ }
1814
1902
  /**
1815
1903
  * Initializes auth properties using a guest token.
1816
1904
  * Used when creating a new instance of this class, and when logging out.
@@ -2145,6 +2233,7 @@ class Scraper {
2145
2233
  }
2146
2234
 
2147
2235
  exports.ApiError = ApiError;
2236
+ exports.AuthenticationError = AuthenticationError;
2148
2237
  exports.ErrorRateLimitStrategy = ErrorRateLimitStrategy;
2149
2238
  exports.Scraper = Scraper;
2150
2239
  exports.SearchMode = SearchMode;