@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.
@@ -26,6 +26,12 @@ class ApiError extends Error {
26
26
  return new ApiError(response, data, `Response status: ${response.status}`);
27
27
  }
28
28
  }
29
+ class AuthenticationError extends Error {
30
+ constructor(message) {
31
+ super(message || "Authentication failed");
32
+ this.name = "AuthenticationError";
33
+ }
34
+ }
29
35
 
30
36
  class WaitingRateLimitStrategy {
31
37
  async onRateLimit({ response: res }) {
@@ -254,7 +260,9 @@ class TwitterGuestAuth {
254
260
  }
255
261
  const token = this.guestToken;
256
262
  if (token == null) {
257
- throw new Error("Authentication token is null or undefined.");
263
+ throw new AuthenticationError(
264
+ "Authentication token is null or undefined."
265
+ );
258
266
  }
259
267
  headers.set("authorization", `Bearer ${this.bearerToken}`);
260
268
  headers.set("x-guest-token", token);
@@ -301,15 +309,15 @@ class TwitterGuestAuth {
301
309
  });
302
310
  await updateCookieJar(this.jar, res.headers);
303
311
  if (!res.ok) {
304
- throw new Error(await res.text());
312
+ throw new AuthenticationError(await res.text());
305
313
  }
306
314
  const o = await res.json();
307
315
  if (o == null || o["guest_token"] == null) {
308
- throw new Error("guest_token not found.");
316
+ throw new AuthenticationError("guest_token not found.");
309
317
  }
310
318
  const newGuestToken = o["guest_token"];
311
319
  if (typeof newGuestToken !== "string") {
312
- throw new Error("guest_token was not a string.");
320
+ throw new AuthenticationError("guest_token was not a string.");
313
321
  }
314
322
  this.guestToken = newGuestToken;
315
323
  this.guestCreatedAt = /* @__PURE__ */ new Date();
@@ -330,6 +338,47 @@ const TwitterUserAuthSubtask = Type.Object({
330
338
  class TwitterUserAuth extends TwitterGuestAuth {
331
339
  constructor(bearerToken, options) {
332
340
  super(bearerToken, options);
341
+ this.subtaskHandlers = /* @__PURE__ */ new Map();
342
+ this.initializeDefaultHandlers();
343
+ }
344
+ /**
345
+ * Register a custom subtask handler or override an existing one
346
+ * @param subtaskId The ID of the subtask to handle
347
+ * @param handler The handler function that processes the subtask
348
+ */
349
+ registerSubtaskHandler(subtaskId, handler) {
350
+ this.subtaskHandlers.set(subtaskId, handler);
351
+ }
352
+ initializeDefaultHandlers() {
353
+ this.subtaskHandlers.set(
354
+ "LoginJsInstrumentationSubtask",
355
+ this.handleJsInstrumentationSubtask.bind(this)
356
+ );
357
+ this.subtaskHandlers.set(
358
+ "LoginEnterUserIdentifierSSO",
359
+ this.handleEnterUserIdentifierSSO.bind(this)
360
+ );
361
+ this.subtaskHandlers.set(
362
+ "LoginEnterAlternateIdentifierSubtask",
363
+ this.handleEnterAlternateIdentifierSubtask.bind(this)
364
+ );
365
+ this.subtaskHandlers.set(
366
+ "LoginEnterPassword",
367
+ this.handleEnterPassword.bind(this)
368
+ );
369
+ this.subtaskHandlers.set(
370
+ "AccountDuplicationCheck",
371
+ this.handleAccountDuplicationCheck.bind(this)
372
+ );
373
+ this.subtaskHandlers.set(
374
+ "LoginTwoFactorAuthChallenge",
375
+ this.handleTwoFactorAuthChallenge.bind(this)
376
+ );
377
+ this.subtaskHandlers.set("LoginAcid", this.handleAcid.bind(this));
378
+ this.subtaskHandlers.set(
379
+ "LoginSuccessSubtask",
380
+ this.handleSuccessSubtask.bind(this)
381
+ );
333
382
  }
334
383
  async isLoggedIn() {
335
384
  const res = await requestApi(
@@ -344,52 +393,49 @@ class TwitterUserAuth extends TwitterGuestAuth {
344
393
  }
345
394
  async login(username, password, email, twoFactorSecret) {
346
395
  await this.updateGuestToken();
396
+ const credentials = {
397
+ username,
398
+ password,
399
+ email,
400
+ twoFactorSecret
401
+ };
347
402
  let next = await this.initLogin();
348
- while ("subtask" in next && next.subtask) {
349
- if (next.subtask.subtask_id === "LoginJsInstrumentationSubtask") {
350
- next = await this.handleJsInstrumentationSubtask(next);
351
- } else if (next.subtask.subtask_id === "LoginEnterUserIdentifierSSO") {
352
- next = await this.handleEnterUserIdentifierSSO(next, username);
353
- } else if (next.subtask.subtask_id === "LoginEnterAlternateIdentifierSubtask") {
354
- next = await this.handleEnterAlternateIdentifierSubtask(
355
- next,
356
- email
357
- );
358
- } else if (next.subtask.subtask_id === "LoginEnterPassword") {
359
- next = await this.handleEnterPassword(next, password);
360
- } else if (next.subtask.subtask_id === "AccountDuplicationCheck") {
361
- next = await this.handleAccountDuplicationCheck(next);
362
- } else if (next.subtask.subtask_id === "LoginTwoFactorAuthChallenge") {
363
- if (twoFactorSecret) {
364
- next = await this.handleTwoFactorAuthChallenge(next, twoFactorSecret);
365
- } else {
366
- throw new Error(
367
- "Requested two factor authentication code but no secret provided"
368
- );
369
- }
370
- } else if (next.subtask.subtask_id === "LoginAcid") {
371
- next = await this.handleAcid(next, email);
372
- } else if (next.subtask.subtask_id === "LoginSuccessSubtask") {
373
- next = await this.handleSuccessSubtask(next);
403
+ while (next.status === "success" && next.response.subtasks?.length) {
404
+ const flowToken = next.response.flow_token;
405
+ if (flowToken == null) {
406
+ throw new Error("flow_token not found.");
407
+ }
408
+ const subtaskId = next.response.subtasks[0].subtask_id;
409
+ const handler = this.subtaskHandlers.get(subtaskId);
410
+ if (handler) {
411
+ next = await handler(subtaskId, next.response, credentials, {
412
+ sendFlowRequest: this.executeFlowTask.bind(this),
413
+ getFlowToken: () => flowToken
414
+ });
374
415
  } else {
375
- throw new Error(`Unknown subtask ${next.subtask.subtask_id}`);
416
+ throw new Error(`Unknown subtask ${subtaskId}`);
376
417
  }
377
418
  }
378
- if ("err" in next) {
419
+ if (next.status === "error") {
379
420
  throw next.err;
380
421
  }
381
422
  }
382
423
  async logout() {
383
- if (!this.isLoggedIn()) {
424
+ if (!this.hasToken()) {
384
425
  return;
385
426
  }
386
- await requestApi(
387
- "https://api.twitter.com/1.1/account/logout.json",
388
- this,
389
- "POST"
390
- );
391
- this.deleteToken();
392
- this.jar = new CookieJar();
427
+ try {
428
+ await requestApi(
429
+ "https://api.twitter.com/1.1/account/logout.json",
430
+ this,
431
+ "POST"
432
+ );
433
+ } catch (error) {
434
+ console.warn("Error during logout:", error);
435
+ } finally {
436
+ this.deleteToken();
437
+ this.jar = new CookieJar();
438
+ }
393
439
  }
394
440
  async installCsrfToken(headers) {
395
441
  const cookies = await this.getCookies();
@@ -428,12 +474,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
428
474
  }
429
475
  });
430
476
  }
431
- async handleJsInstrumentationSubtask(prev) {
432
- return await this.executeFlowTask({
433
- flow_token: prev.flowToken,
477
+ async handleJsInstrumentationSubtask(subtaskId, _prev, _credentials, api) {
478
+ return await api.sendFlowRequest({
479
+ flow_token: api.getFlowToken(),
434
480
  subtask_inputs: [
435
481
  {
436
- subtask_id: "LoginJsInstrumentationSubtask",
482
+ subtask_id: subtaskId,
437
483
  js_instrumentation: {
438
484
  response: "{}",
439
485
  link: "next_link"
@@ -442,32 +488,32 @@ class TwitterUserAuth extends TwitterGuestAuth {
442
488
  ]
443
489
  });
444
490
  }
445
- async handleEnterAlternateIdentifierSubtask(prev, email) {
491
+ async handleEnterAlternateIdentifierSubtask(subtaskId, _prev, credentials, api) {
446
492
  return await this.executeFlowTask({
447
- flow_token: prev.flowToken,
493
+ flow_token: api.getFlowToken(),
448
494
  subtask_inputs: [
449
495
  {
450
- subtask_id: "LoginEnterAlternateIdentifierSubtask",
496
+ subtask_id: subtaskId,
451
497
  enter_text: {
452
- text: email,
498
+ text: credentials.email,
453
499
  link: "next_link"
454
500
  }
455
501
  }
456
502
  ]
457
503
  });
458
504
  }
459
- async handleEnterUserIdentifierSSO(prev, username) {
505
+ async handleEnterUserIdentifierSSO(subtaskId, _prev, credentials, api) {
460
506
  return await this.executeFlowTask({
461
- flow_token: prev.flowToken,
507
+ flow_token: api.getFlowToken(),
462
508
  subtask_inputs: [
463
509
  {
464
- subtask_id: "LoginEnterUserIdentifierSSO",
510
+ subtask_id: subtaskId,
465
511
  settings_list: {
466
512
  setting_responses: [
467
513
  {
468
514
  key: "user_identifier",
469
515
  response_data: {
470
- text_data: { result: username }
516
+ text_data: { result: credentials.username }
471
517
  }
472
518
  }
473
519
  ],
@@ -477,26 +523,26 @@ class TwitterUserAuth extends TwitterGuestAuth {
477
523
  ]
478
524
  });
479
525
  }
480
- async handleEnterPassword(prev, password) {
526
+ async handleEnterPassword(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: "LoginEnterPassword",
531
+ subtask_id: subtaskId,
486
532
  enter_password: {
487
- password,
533
+ password: credentials.password,
488
534
  link: "next_link"
489
535
  }
490
536
  }
491
537
  ]
492
538
  });
493
539
  }
494
- async handleAccountDuplicationCheck(prev) {
540
+ async handleAccountDuplicationCheck(subtaskId, _prev, _credentials, api) {
495
541
  return await this.executeFlowTask({
496
- flow_token: prev.flowToken,
542
+ flow_token: api.getFlowToken(),
497
543
  subtask_inputs: [
498
544
  {
499
- subtask_id: "AccountDuplicationCheck",
545
+ subtask_id: subtaskId,
500
546
  check_logged_in_account: {
501
547
  link: "AccountDuplicationCheck_false"
502
548
  }
@@ -504,16 +550,24 @@ class TwitterUserAuth extends TwitterGuestAuth {
504
550
  ]
505
551
  });
506
552
  }
507
- async handleTwoFactorAuthChallenge(prev, secret) {
508
- const totp = new OTPAuth.TOTP({ secret });
553
+ async handleTwoFactorAuthChallenge(subtaskId, _prev, credentials, api) {
554
+ if (!credentials.twoFactorSecret) {
555
+ return {
556
+ status: "error",
557
+ err: new AuthenticationError(
558
+ "Two-factor authentication is required but no secret was provided"
559
+ )
560
+ };
561
+ }
562
+ const totp = new OTPAuth.TOTP({ secret: credentials.twoFactorSecret });
509
563
  let error;
510
564
  for (let attempts = 1; attempts < 4; attempts += 1) {
511
565
  try {
512
- return await this.executeFlowTask({
513
- flow_token: prev.flowToken,
566
+ return await api.sendFlowRequest({
567
+ flow_token: api.getFlowToken(),
514
568
  subtask_inputs: [
515
569
  {
516
- subtask_id: "LoginTwoFactorAuthChallenge",
570
+ subtask_id: subtaskId,
517
571
  enter_text: {
518
572
  link: "next_link",
519
573
  text: totp.generate()
@@ -528,23 +582,23 @@ class TwitterUserAuth extends TwitterGuestAuth {
528
582
  }
529
583
  throw error;
530
584
  }
531
- async handleAcid(prev, email) {
585
+ async handleAcid(subtaskId, _prev, credentials, api) {
532
586
  return await this.executeFlowTask({
533
- flow_token: prev.flowToken,
587
+ flow_token: api.getFlowToken(),
534
588
  subtask_inputs: [
535
589
  {
536
- subtask_id: "LoginAcid",
590
+ subtask_id: subtaskId,
537
591
  enter_text: {
538
- text: email,
592
+ text: credentials.email,
539
593
  link: "next_link"
540
594
  }
541
595
  }
542
596
  ]
543
597
  });
544
598
  }
545
- async handleSuccessSubtask(prev) {
599
+ async handleSuccessSubtask(_subtaskId, _prev, _credentials, api) {
546
600
  return await this.executeFlowTask({
547
- flow_token: prev.flowToken,
601
+ flow_token: api.getFlowToken(),
548
602
  subtask_inputs: []
549
603
  });
550
604
  }
@@ -552,7 +606,9 @@ class TwitterUserAuth extends TwitterGuestAuth {
552
606
  const onboardingTaskUrl = "https://api.twitter.com/1.1/onboarding/task.json";
553
607
  const token = this.guestToken;
554
608
  if (token == null) {
555
- throw new Error("Authentication token is null or undefined.");
609
+ throw new AuthenticationError(
610
+ "Authentication token is null or undefined."
611
+ );
556
612
  }
557
613
  const headers = new Headers({
558
614
  authorization: `Bearer ${this.bearerToken}`,
@@ -577,12 +633,15 @@ class TwitterUserAuth extends TwitterGuestAuth {
577
633
  }
578
634
  const flow = await res.json();
579
635
  if (flow?.flow_token == null) {
580
- return { status: "error", err: new Error("flow_token not found.") };
636
+ return {
637
+ status: "error",
638
+ err: new AuthenticationError("flow_token not found.")
639
+ };
581
640
  }
582
641
  if (flow.errors?.length) {
583
642
  return {
584
643
  status: "error",
585
- err: new Error(
644
+ err: new AuthenticationError(
586
645
  `Authentication error (${flow.errors[0].code}): ${flow.errors[0].message}`
587
646
  )
588
647
  };
@@ -590,7 +649,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
590
649
  if (typeof flow.flow_token !== "string") {
591
650
  return {
592
651
  status: "error",
593
- err: new Error("flow_token was not a string.")
652
+ err: new AuthenticationError("flow_token was not a string.")
594
653
  };
595
654
  }
596
655
  const subtask = flow.subtasks?.length ? flow.subtasks[0] : void 0;
@@ -598,13 +657,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
598
657
  if (subtask && subtask.subtask_id === "DenyLoginSubtask") {
599
658
  return {
600
659
  status: "error",
601
- err: new Error("Authentication error: DenyLoginSubtask")
660
+ err: new AuthenticationError("Authentication error: DenyLoginSubtask")
602
661
  };
603
662
  }
604
663
  return {
605
664
  status: "success",
606
- subtask,
607
- flowToken: flow.flow_token
665
+ response: flow
608
666
  };
609
667
  }
610
668
  }
@@ -1267,8 +1325,8 @@ async function fetchSearchProfiles(query, maxProfiles, auth, cursor) {
1267
1325
  return parseSearchTimelineUsers(timeline);
1268
1326
  }
1269
1327
  async function getSearchTimeline(query, maxItems, searchMode, auth, cursor) {
1270
- if (!auth.isLoggedIn()) {
1271
- throw new Error("Scraper is not logged-in for search.");
1328
+ if (!await auth.isLoggedIn()) {
1329
+ throw new AuthenticationError("Scraper is not logged-in for search.");
1272
1330
  }
1273
1331
  if (maxItems > 50) {
1274
1332
  maxItems = 50;
@@ -1375,6 +1433,11 @@ function getFollowers(userId, maxProfiles, auth) {
1375
1433
  });
1376
1434
  }
1377
1435
  async function fetchProfileFollowing(userId, maxProfiles, auth, cursor) {
1436
+ if (!await auth.isLoggedIn()) {
1437
+ throw new AuthenticationError(
1438
+ "Scraper is not logged-in for profile following."
1439
+ );
1440
+ }
1378
1441
  const timeline = await getFollowingTimeline(
1379
1442
  userId,
1380
1443
  maxProfiles,
@@ -1384,6 +1447,11 @@ async function fetchProfileFollowing(userId, maxProfiles, auth, cursor) {
1384
1447
  return parseRelationshipTimeline(timeline);
1385
1448
  }
1386
1449
  async function fetchProfileFollowers(userId, maxProfiles, auth, cursor) {
1450
+ if (!await auth.isLoggedIn()) {
1451
+ throw new AuthenticationError(
1452
+ "Scraper is not logged-in for profile followers."
1453
+ );
1454
+ }
1387
1455
  const timeline = await getFollowersTimeline(
1388
1456
  userId,
1389
1457
  maxProfiles,
@@ -1394,7 +1462,9 @@ async function fetchProfileFollowers(userId, maxProfiles, auth, cursor) {
1394
1462
  }
1395
1463
  async function getFollowingTimeline(userId, maxItems, auth, cursor) {
1396
1464
  if (!auth.isLoggedIn()) {
1397
- throw new Error("Scraper is not logged-in for profile following.");
1465
+ throw new AuthenticationError(
1466
+ "Scraper is not logged-in for profile following."
1467
+ );
1398
1468
  }
1399
1469
  if (maxItems > 50) {
1400
1470
  maxItems = 50;
@@ -1427,7 +1497,9 @@ async function getFollowingTimeline(userId, maxItems, auth, cursor) {
1427
1497
  }
1428
1498
  async function getFollowersTimeline(userId, maxItems, auth, cursor) {
1429
1499
  if (!auth.isLoggedIn()) {
1430
- throw new Error("Scraper is not logged-in for profile followers.");
1500
+ throw new AuthenticationError(
1501
+ "Scraper is not logged-in for profile followers."
1502
+ );
1431
1503
  }
1432
1504
  if (maxItems > 50) {
1433
1505
  maxItems = 50;
@@ -1684,8 +1756,10 @@ function getTweetsAndRepliesByUserId(userId, maxTweets, auth) {
1684
1756
  });
1685
1757
  }
1686
1758
  async function fetchLikedTweets(userId, maxTweets, cursor, auth) {
1687
- if (!auth.isLoggedIn()) {
1688
- throw new Error("Scraper is not logged-in for fetching liked tweets.");
1759
+ if (!await auth.isLoggedIn()) {
1760
+ throw new AuthenticationError(
1761
+ "Scraper is not logged-in for fetching liked tweets."
1762
+ );
1689
1763
  }
1690
1764
  if (maxTweets > 200) {
1691
1765
  maxTweets = 200;
@@ -1790,6 +1864,20 @@ class Scraper {
1790
1864
  this.token = bearerToken;
1791
1865
  this.useGuestAuth();
1792
1866
  }
1867
+ /**
1868
+ * Registers a subtask handler for the given subtask ID. This
1869
+ * will override any existing handler for the same subtask.
1870
+ * @param subtaskId The ID of the subtask to register the handler for.
1871
+ * @param subtaskHandler The handler function to register.
1872
+ */
1873
+ registerAuthSubtaskHandler(subtaskId, subtaskHandler) {
1874
+ if (this.auth instanceof TwitterUserAuth) {
1875
+ this.auth.registerSubtaskHandler(subtaskId, subtaskHandler);
1876
+ }
1877
+ if (this.authTrends instanceof TwitterUserAuth) {
1878
+ this.authTrends.registerSubtaskHandler(subtaskId, subtaskHandler);
1879
+ }
1880
+ }
1793
1881
  /**
1794
1882
  * Initializes auth properties using a guest token.
1795
1883
  * Used when creating a new instance of this class, and when logging out.
@@ -2123,5 +2211,5 @@ class Scraper {
2123
2211
  }
2124
2212
  }
2125
2213
 
2126
- export { ApiError, ErrorRateLimitStrategy, Scraper, SearchMode, WaitingRateLimitStrategy };
2214
+ export { ApiError, AuthenticationError, ErrorRateLimitStrategy, Scraper, SearchMode, WaitingRateLimitStrategy };
2127
2215
  //# sourceMappingURL=index.mjs.map