From 75b8b144c22ea9a9f9ebd3225b0025c82bc27c01 Mon Sep 17 00:00:00 2001 From: Owen Stubbs Date: Tue, 15 Aug 2023 22:31:06 +0100 Subject: [PATCH] initial progress toward #2 --- src/classes/errors.ts | 2 +- src/classes/timeline.ts | 76 +++++++++++++++++++++++++++++++++-------- src/classes/util.ts | 8 +---- 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/classes/errors.ts b/src/classes/errors.ts index 4f460a2..2d1439b 100644 --- a/src/classes/errors.ts +++ b/src/classes/errors.ts @@ -14,7 +14,7 @@ class ParseError extends Error { class HttpError extends Error { code: number - + constructor(message: string, statusCode: number) { super(message) this.name = 'HttpError' diff --git a/src/classes/timeline.ts b/src/classes/timeline.ts index e3ad3cd..6015b3a 100644 --- a/src/classes/timeline.ts +++ b/src/classes/timeline.ts @@ -1,6 +1,4 @@ -const puppeteer = require('puppeteer-extra') - -import { extractTimelineData, getPuppeteerContent } from "./util.js" +import { extractTimelineData, getPuppeteerContent, sendReq } from "./util.js" import { FetchError, ParseError } from "./errors.js" import User from "./user.js" @@ -15,17 +13,40 @@ const domain = 'https://twitter.com' export default class Timeline { static readonly url = 'https://syndication.twitter.com/srv/timeline-profile/screen-name/' - private static browser = null - get browser() { - return this.browser + private static puppeteer = { + use: false, + browser: null } - static async #fetchUserTimeline(url: string, cookie?: string): Promise { - if (!this.browser) { - this.browser = await puppeteer.launch({ headless: 'new' }) + /** + * Use puppeteer to get the timeline, bypassing potential Cloudflare issues. + * Unless `browser` is passed, a basic headless one is used with `Stealth` & `AdBlocker` plugins. + * + * @param browser A custom browser to use instead of the default. + */ + static async usePuppeteer(browser?: unknown) { + if (browser) { + this.puppeteer.browser = browser } + else { + const puppeteer = require('puppeteer-extra') + + const AdBlocker = require('puppeteer-extra-plugin-adblocker') + const Stealth = require('puppeteer-extra-plugin-stealth') + + puppeteer.use(AdBlocker()).use(Stealth()) + + this.puppeteer.browser = await puppeteer.launch({ headless: 'new' }) + } + + this.puppeteer.use = true + } + + static async #fetchUserTimeline(url: string, cookie?: string): Promise { + const html = this.puppeteer.use + ? await getPuppeteerContent(this.puppeteer.browser, url, cookie) + : await sendReq(url, cookie).then(body => body.text()) - const html = await getPuppeteerContent(this.browser, url, cookie) const data = extractTimelineData(html) if (!data) { @@ -38,14 +59,23 @@ export default class Timeline { } /** + * Fetches all tweets by the specified user. * - * @param username The user handle without the ``@``. + * **Default behaviour** + * - Replies and retweets are not included. + * - No proxy or cookie is used. * + * @param username The user handle without the ``@``. * @param options The options to use with the request, see {@link TweetOptions}. - * * Example: + * + * Example: * * ```js - * Timeline.get('elonmusk', { replies: true, retweets: false, cookie: process.env.TWITTER_COOKIE } + * await Timeline.get('elonmusk', { + *ㅤㅤreplies: true, + *ㅤㅤretweets: false, + *ㅤㅤcookie: process.env.TWITTER_COOKIE + * }) * ``` */ static async get( @@ -75,8 +105,24 @@ export default class Timeline { } } - static at = (username: string, index: number) => this.get(username).then(arr => arr[index]) - static latest = (username: string) => this.at(username, 0) + /** + * Works exactly the same as `.get()`, but just returns the most recent tweet. + * + * Intended to be used as shorthand for the following: + * + * ```js + * await Timeline.get().then(arr => arr[0]) + * ``` + */ + static latest( + username: string, + options: Partial = { + proxyUrl: null, + cookie: null + } + ) { + return this.get(username, options).then(arr => arr[0]) + } } class TimelineTweet { diff --git a/src/classes/util.ts b/src/classes/util.ts index bb32ee3..645655d 100644 --- a/src/classes/util.ts +++ b/src/classes/util.ts @@ -1,13 +1,6 @@ import { request } from 'undici' import { FetchError, ParseError, HttpError } from './errors.js' -const puppeteer = require('puppeteer-extra') - -const AdBlocker = require('puppeteer-extra-plugin-adblocker') -const Stealth = require('puppeteer-extra-plugin-stealth') - -puppeteer.use(AdBlocker()).use(Stealth()) - const mockAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0' const headers = (cookie?: string) => { @@ -37,6 +30,7 @@ async function sendReq(url: string, cookie?: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any async function getPuppeteerContent(browser: any, url: string, cookie?: string) { const page = await browser.newPage() + try { await page.setExtraHTTPHeaders(headers(cookie)) await page.goto(url, { waitUntil: 'load' })