2022-12-14 02:13:31 +00:00
|
|
|
import * as fs from 'node:fs'
|
|
|
|
import * as os from 'node:os'
|
2022-12-14 02:05:29 +00:00
|
|
|
|
2022-12-12 11:28:53 +00:00
|
|
|
import delay from 'delay'
|
|
|
|
import {
|
|
|
|
type Browser,
|
2022-12-14 02:05:29 +00:00
|
|
|
type ElementHandle,
|
2022-12-12 11:28:53 +00:00
|
|
|
type Page,
|
|
|
|
type Protocol,
|
|
|
|
type PuppeteerLaunchOptions
|
|
|
|
} from 'puppeteer'
|
|
|
|
import puppeteer from 'puppeteer-extra'
|
|
|
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
|
|
|
|
|
2022-12-14 02:05:29 +00:00
|
|
|
import * as types from './types'
|
|
|
|
|
2022-12-12 11:28:53 +00:00
|
|
|
puppeteer.use(StealthPlugin())
|
|
|
|
|
2022-12-12 17:23:03 +00:00
|
|
|
/**
|
|
|
|
* Represents everything that's required to pass into `ChatGPTAPI` in order
|
|
|
|
* to authenticate with the unofficial ChatGPT API.
|
|
|
|
*/
|
|
|
|
export type OpenAIAuth = {
|
2022-12-12 11:28:53 +00:00
|
|
|
userAgent: string
|
|
|
|
clearanceToken: string
|
|
|
|
sessionToken: string
|
|
|
|
cookies?: Record<string, Protocol.Network.Cookie>
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bypasses OpenAI's use of Cloudflare to get the cookies required to use
|
|
|
|
* ChatGPT. Uses Puppeteer with a stealth plugin under the hood.
|
2022-12-12 17:23:03 +00:00
|
|
|
*
|
|
|
|
* If you pass `email` and `password`, then it will log into the account and
|
|
|
|
* include a `sessionToken` in the response.
|
|
|
|
*
|
|
|
|
* If you don't pass `email` and `password`, then it will just return a valid
|
|
|
|
* `clearanceToken`.
|
|
|
|
*
|
|
|
|
* This can be useful because `clearanceToken` expires after ~2 hours, whereas
|
|
|
|
* `sessionToken` generally lasts much longer. We recommend renewing your
|
|
|
|
* `clearanceToken` every hour or so and creating a new instance of `ChatGPTAPI`
|
|
|
|
* with your updated credentials.
|
2022-12-12 11:28:53 +00:00
|
|
|
*/
|
2022-12-12 17:23:03 +00:00
|
|
|
export async function getOpenAIAuth({
|
2022-12-12 11:28:53 +00:00
|
|
|
email,
|
|
|
|
password,
|
2022-12-14 00:01:32 +00:00
|
|
|
browser,
|
2022-12-14 02:05:29 +00:00
|
|
|
timeoutMs = 2 * 60 * 1000,
|
|
|
|
isGoogleLogin = false
|
2022-12-12 11:28:53 +00:00
|
|
|
}: {
|
2022-12-12 17:23:03 +00:00
|
|
|
email?: string
|
|
|
|
password?: string
|
2022-12-12 11:28:53 +00:00
|
|
|
browser?: Browser
|
2022-12-14 02:05:29 +00:00
|
|
|
timeoutMs?: number
|
2022-12-14 00:01:32 +00:00
|
|
|
isGoogleLogin?: boolean
|
2022-12-12 17:23:03 +00:00
|
|
|
}): Promise<OpenAIAuth> {
|
2022-12-12 11:28:53 +00:00
|
|
|
let page: Page
|
|
|
|
let origBrowser = browser
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (!browser) {
|
|
|
|
browser = await getBrowser()
|
|
|
|
}
|
|
|
|
|
|
|
|
const userAgent = await browser.userAgent()
|
|
|
|
page = (await browser.pages())[0] || (await browser.newPage())
|
2022-12-12 17:23:03 +00:00
|
|
|
page.setDefaultTimeout(timeoutMs)
|
2022-12-12 11:28:53 +00:00
|
|
|
|
|
|
|
await page.goto('https://chat.openai.com/auth/login')
|
2022-12-12 17:23:03 +00:00
|
|
|
|
|
|
|
// NOTE: this is where you may encounter a CAPTCHA
|
2022-12-15 03:24:19 +00:00
|
|
|
await checkForChatGPTAtCapacity(page)
|
2022-12-12 17:23:03 +00:00
|
|
|
|
|
|
|
await page.waitForSelector('#__next .btn-primary', { timeout: timeoutMs })
|
|
|
|
|
|
|
|
// once we get to this point, the Cloudflare cookies are available
|
2022-12-12 11:28:53 +00:00
|
|
|
await delay(1000)
|
|
|
|
|
2022-12-12 17:23:03 +00:00
|
|
|
// login as well (optional)
|
2022-12-12 11:28:53 +00:00
|
|
|
if (email && password) {
|
|
|
|
await Promise.all([
|
|
|
|
page.click('#__next .btn-primary'),
|
|
|
|
page.waitForNavigation({
|
|
|
|
waitUntil: 'networkidle0'
|
|
|
|
})
|
|
|
|
])
|
2022-12-14 02:05:29 +00:00
|
|
|
|
|
|
|
let submitP: Promise<void>
|
|
|
|
|
2022-12-14 00:01:32 +00:00
|
|
|
if (isGoogleLogin) {
|
|
|
|
await page.click('button[data-provider="google"]')
|
|
|
|
await page.waitForSelector('input[type="email"]')
|
|
|
|
await page.type('input[type="email"]', email, { delay: 10 })
|
|
|
|
await Promise.all([
|
|
|
|
page.waitForNavigation(),
|
|
|
|
await page.keyboard.press('Enter')
|
|
|
|
])
|
|
|
|
await page.waitForSelector('input[type="password"]', { visible: true })
|
|
|
|
await page.type('input[type="password"]', password, { delay: 10 })
|
2022-12-14 02:05:29 +00:00
|
|
|
submitP = page.keyboard.press('Enter')
|
2022-12-14 00:01:32 +00:00
|
|
|
} else {
|
2022-12-14 02:05:29 +00:00
|
|
|
await page.waitForSelector('#username')
|
2022-12-14 00:01:32 +00:00
|
|
|
await page.type('#username', email, { delay: 10 })
|
|
|
|
await page.click('button[type="submit"]')
|
|
|
|
await page.waitForSelector('#password')
|
|
|
|
await page.type('#password', password, { delay: 10 })
|
2022-12-14 02:05:29 +00:00
|
|
|
submitP = page.click('button[type="submit"]')
|
2022-12-14 00:01:32 +00:00
|
|
|
}
|
2022-12-14 02:05:29 +00:00
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
submitP,
|
|
|
|
|
|
|
|
new Promise<void>((resolve, reject) => {
|
|
|
|
let resolved = false
|
|
|
|
|
|
|
|
async function waitForCapacityText() {
|
|
|
|
if (resolved) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await checkForChatGPTAtCapacity(page)
|
|
|
|
|
|
|
|
if (!resolved) {
|
|
|
|
setTimeout(waitForCapacityText, 500)
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
if (!resolved) {
|
|
|
|
resolved = true
|
|
|
|
return reject(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
page
|
|
|
|
.waitForNavigation({
|
|
|
|
waitUntil: 'networkidle0'
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
if (!resolved) {
|
|
|
|
resolved = true
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
if (!resolved) {
|
|
|
|
resolved = true
|
|
|
|
reject(err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
setTimeout(waitForCapacityText, 500)
|
|
|
|
})
|
|
|
|
])
|
2022-12-12 11:28:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const pageCookies = await page.cookies()
|
|
|
|
const cookies = pageCookies.reduce(
|
|
|
|
(map, cookie) => ({ ...map, [cookie.name]: cookie }),
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
|
2022-12-12 17:23:03 +00:00
|
|
|
const authInfo: OpenAIAuth = {
|
2022-12-12 11:28:53 +00:00
|
|
|
userAgent,
|
|
|
|
clearanceToken: cookies['cf_clearance']?.value,
|
|
|
|
sessionToken: cookies['__Secure-next-auth.session-token']?.value,
|
|
|
|
cookies
|
|
|
|
}
|
|
|
|
|
|
|
|
return authInfo
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
2022-12-12 17:23:03 +00:00
|
|
|
throw err
|
2022-12-12 11:28:53 +00:00
|
|
|
} finally {
|
|
|
|
if (origBrowser) {
|
|
|
|
if (page) {
|
|
|
|
await page.close()
|
|
|
|
}
|
|
|
|
} else if (browser) {
|
|
|
|
await browser.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
page = null
|
|
|
|
browser = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-12 17:23:03 +00:00
|
|
|
/**
|
|
|
|
* Launches a non-puppeteer instance of Chrome. Note that in my testing, I wasn't
|
|
|
|
* able to use the built-in `puppeteer` version of Chromium because Cloudflare
|
|
|
|
* recognizes it and blocks access.
|
|
|
|
*/
|
2022-12-12 11:28:53 +00:00
|
|
|
export async function getBrowser(launchOptions?: PuppeteerLaunchOptions) {
|
|
|
|
return puppeteer.launch({
|
|
|
|
headless: false,
|
|
|
|
args: ['--no-sandbox', '--exclude-switches', 'enable-automation'],
|
|
|
|
ignoreHTTPSErrors: true,
|
2022-12-14 02:05:29 +00:00
|
|
|
executablePath: defaultChromeExecutablePath(),
|
2022-12-12 11:28:53 +00:00
|
|
|
...launchOptions
|
|
|
|
})
|
|
|
|
}
|
2022-12-13 14:39:54 +00:00
|
|
|
|
|
|
|
/**
|
2022-12-14 02:05:29 +00:00
|
|
|
* Gets the default path to chrome's executable for the current platform.
|
2022-12-13 14:39:54 +00:00
|
|
|
*/
|
2022-12-14 02:05:29 +00:00
|
|
|
export const defaultChromeExecutablePath = (): string => {
|
2022-12-13 19:56:34 +00:00
|
|
|
switch (os.platform()) {
|
2022-12-13 14:39:54 +00:00
|
|
|
case 'win32':
|
2022-12-14 08:36:45 +00:00
|
|
|
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
2022-12-14 02:05:29 +00:00
|
|
|
|
2022-12-13 19:56:34 +00:00
|
|
|
case 'darwin':
|
2022-12-13 14:39:54 +00:00
|
|
|
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
2022-12-14 02:05:29 +00:00
|
|
|
|
2022-12-13 19:56:34 +00:00
|
|
|
default:
|
|
|
|
/**
|
|
|
|
* Since two (2) separate chrome releases exists on linux
|
|
|
|
* we first do a check to ensure we're executing the right one.
|
|
|
|
*/
|
|
|
|
const chromeExists = fs.existsSync('/usr/bin/google-chrome')
|
|
|
|
|
|
|
|
return chromeExists
|
|
|
|
? '/usr/bin/google-chrome'
|
|
|
|
: '/usr/bin/google-chrome-stable'
|
2022-12-13 14:39:54 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-14 02:05:29 +00:00
|
|
|
|
|
|
|
async function checkForChatGPTAtCapacity(page: Page) {
|
2022-12-15 03:24:19 +00:00
|
|
|
let res: ElementHandle<Node>[]
|
2022-12-14 02:05:29 +00:00
|
|
|
|
|
|
|
try {
|
2022-12-14 22:14:35 +00:00
|
|
|
// res = await page.$('[role="alert"]')
|
2022-12-15 03:24:19 +00:00
|
|
|
res = await page.$x("//div[contains(., 'ChatGPT is at capacity')]")
|
2022-12-14 02:05:29 +00:00
|
|
|
} catch (err) {
|
|
|
|
// ignore errors likely due to navigation
|
|
|
|
}
|
|
|
|
|
2022-12-15 03:24:19 +00:00
|
|
|
if (res?.length) {
|
2022-12-14 22:14:35 +00:00
|
|
|
const error = new types.ChatGPTError('ChatGPT is at capacity')
|
2022-12-14 02:05:29 +00:00
|
|
|
error.statusCode = 503
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
}
|