chatgpt-api/src/openai-auth.ts

248 wiersze
6.5 KiB
TypeScript
Czysty Zwykły widok Historia

import * as fs from 'node:fs'
import * as os from 'node:os'
2022-12-14 02:05:29 +00:00
import delay from 'delay'
import {
type Browser,
2022-12-14 02:05:29 +00:00
type ElementHandle,
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'
puppeteer.use(StealthPlugin())
/**
* Represents everything that's required to pass into `ChatGPTAPI` in order
* to authenticate with the unofficial ChatGPT API.
*/
export type OpenAIAuth = {
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.
*
* 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.
*/
export async function getOpenAIAuth({
email,
password,
browser,
2022-12-14 02:05:29 +00:00
timeoutMs = 2 * 60 * 1000,
isGoogleLogin = false
}: {
email?: string
password?: string
browser?: Browser
2022-12-14 02:05:29 +00:00
timeoutMs?: number
isGoogleLogin?: boolean
}): Promise<OpenAIAuth> {
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())
page.setDefaultTimeout(timeoutMs)
await page.goto('https://chat.openai.com/auth/login')
2022-12-14 02:05:29 +00:00
await checkForChatGPTAtCapacity(page)
// NOTE: this is where you may encounter a CAPTCHA
2022-12-12 19:49:57 +00:00
var capacityLimit = await page.$('[role="alert"]')
if (capacityLimit) {
throw `ChatGPT is at capacity right now`
}
await page.waitForSelector('#__next .btn-primary', { timeout: timeoutMs })
// once we get to this point, the Cloudflare cookies are available
await delay(1000)
// login as well (optional)
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>
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')
} else {
2022-12-14 02:05:29 +00:00
await page.waitForSelector('#username')
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 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)
})
])
}
const pageCookies = await page.cookies()
const cookies = pageCookies.reduce(
(map, cookie) => ({ ...map, [cookie.name]: cookie }),
{}
)
const authInfo: OpenAIAuth = {
userAgent,
clearanceToken: cookies['cf_clearance']?.value,
sessionToken: cookies['__Secure-next-auth.session-token']?.value,
cookies
}
return authInfo
} catch (err) {
console.error(err)
throw err
} finally {
if (origBrowser) {
if (page) {
await page.close()
}
} else if (browser) {
await browser.close()
}
page = null
browser = null
}
}
/**
* 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.
*/
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(),
...launchOptions
})
}
/**
2022-12-14 02:05:29 +00:00
* Gets the default path to chrome's executable for the current platform.
*/
2022-12-14 02:05:29 +00:00
export const defaultChromeExecutablePath = (): string => {
2022-12-13 19:56:34 +00:00
switch (os.platform()) {
case 'win32':
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':
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-14 02:05:29 +00:00
async function checkForChatGPTAtCapacity(page: Page) {
2022-12-14 19:05:16 +00:00
let res: ElementHandle<Element> | null
2022-12-14 02:05:29 +00:00
try {
2022-12-14 19:05:16 +00:00
res = await page.$('[role="alert"]')
2022-12-14 02:05:29 +00:00
} catch (err) {
// ignore errors likely due to navigation
}
2022-12-14 19:05:16 +00:00
if (res) {
const error = new types.ChatGPTError(`ChatGPT is at capacity: ${res}`)
2022-12-14 02:05:29 +00:00
error.statusCode = 503
throw error
}
}