Skip to content

Commit

Permalink
initial progress toward #2
Browse files Browse the repository at this point in the history
  • Loading branch information
Owen3H committed Aug 15, 2023
1 parent 442d7af commit 75b8b14
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/classes/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class ParseError extends Error {

class HttpError extends Error {
code: number

constructor(message: string, statusCode: number) {
super(message)
this.name = 'HttpError'
Expand Down
76 changes: 61 additions & 15 deletions src/classes/timeline.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<RawTimelineEntry[]> {
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<RawTimelineEntry[]> {
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) {
Expand All @@ -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(
Expand Down Expand Up @@ -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<TweetOptions> = {
proxyUrl: null,
cookie: null
}
) {
return this.get(username, options).then(arr => arr[0])
}
}

class TimelineTweet {
Expand Down
8 changes: 1 addition & 7 deletions src/classes/util.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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' })
Expand Down

0 comments on commit 75b8b14

Please sign in to comment.