Commit 70fd95c8 authored by Travis Fischer's avatar Travis Fischer

feat: add demos for puppeteer automated CF workaround

parent 74a9b928
......@@ -9,5 +9,5 @@
# ChatGPT
# -----------------------------------------------------------------------------
# see the readme for how to find this
SESSION_TOKEN=
EMAIL=
PASSWORD=
import dotenv from 'dotenv-safe'
import { oraPromise } from 'ora'
import { ChatGPTAPI } from '.'
import { ChatGPTAPI } from '../src'
import { getOpenAIAuthInfo } from './openai-auth-puppeteer'
dotenv.config()
......@@ -13,7 +14,15 @@ dotenv.config()
* ```
*/
async function main() {
const api = new ChatGPTAPI({ sessionToken: process.env.SESSION_TOKEN })
const email = process.env.EMAIL
const password = process.env.PASSWORD
const authInfo = await getOpenAIAuthInfo({
email,
password
})
const api = new ChatGPTAPI({ ...authInfo })
await api.ensureAuth()
const conversation = api.getConversation()
......
import dotenv from 'dotenv-safe'
import { oraPromise } from 'ora'
import { ChatGPTAPI } from '.'
import { ChatGPTAPI } from '../src'
import { getOpenAIAuthInfo } from './openai-auth-puppeteer'
dotenv.config()
......@@ -13,7 +14,15 @@ dotenv.config()
* ```
*/
async function main() {
const api = new ChatGPTAPI({ sessionToken: process.env.SESSION_TOKEN })
const email = process.env.EMAIL
const password = process.env.PASSWORD
const authInfo = await getOpenAIAuthInfo({
email,
password
})
const api = new ChatGPTAPI({ ...authInfo })
await api.ensureAuth()
const prompt =
......
import delay from 'delay'
import {
type Browser,
type Page,
type Protocol,
type PuppeteerLaunchOptions
} from 'puppeteer'
import puppeteer from 'puppeteer-extra'
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
puppeteer.use(StealthPlugin())
export type OpenAIAuthInfo = {
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.
*/
export async function getOpenAIAuthInfo({
email,
password,
timeout = 2 * 60 * 1000,
browser
}: {
email: string
password: string
timeout?: number
browser?: Browser
}): Promise<OpenAIAuthInfo> {
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(timeout)
await page.goto('https://chat.openai.com/auth/login')
await page.waitForSelector('#__next .btn-primary', { timeout })
await delay(1000)
if (email && password) {
await Promise.all([
page.click('#__next .btn-primary'),
page.waitForNavigation({
waitUntil: 'networkidle0'
})
])
await page.type('#username', email, { delay: 10 })
await page.click('button[type="submit"]')
await page.waitForSelector('#password')
await page.type('#password', password, { delay: 10 })
await Promise.all([
page.click('button[type="submit"]'),
page.waitForNavigation({
waitUntil: 'networkidle0'
})
])
}
const pageCookies = await page.cookies()
const cookies = pageCookies.reduce(
(map, cookie) => ({ ...map, [cookie.name]: cookie }),
{}
)
const authInfo: OpenAIAuthInfo = {
userAgent,
clearanceToken: cookies['cf_clearance']?.value,
sessionToken: cookies['__Secure-next-auth.session-token']?.value,
cookies
}
return authInfo
} catch (err) {
console.error(err)
throw null
} finally {
if (origBrowser) {
if (page) {
await page.close()
}
} else if (browser) {
await browser.close()
}
page = null
browser = null
}
}
export async function getBrowser(launchOptions?: PuppeteerLaunchOptions) {
const macChromePath =
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
return puppeteer.launch({
headless: false,
args: ['--no-sandbox', '--exclude-switches', 'enable-automation'],
ignoreHTTPSErrors: true,
// executablePath: executablePath()
executablePath: macChromePath,
...launchOptions
})
}
This diff is collapsed.
......@@ -162,23 +162,21 @@ See the [auto-generated docs](./docs/classes/ChatGPTAPI.md) for more info on met
### Demos
A [basic demo](./src/demo.ts) is included for testing purposes:
To run the included demos:
1. clone repo
2. install node deps
3. set `EMAIL` and `PASSWORD` in .env
A [basic demo](./demos/demo.ts) is included for testing purposes:
```bash
# 1. clone repo
# 2. install node deps
# 3. set `SESSION_TOKEN` in .env
# 4. run:
npx tsx src/demo.ts
```
A [conversation demo](./src/demo-conversation.ts) is also included:
A [conversation demo](./demos/demo-conversation.ts) is also included:
```bash
# 1. clone repo
# 2. install node deps
# 3. set `SESSION_TOKEN` in .env
# 4. run:
npx tsx src/demo-conversation.ts
```
......@@ -186,15 +184,18 @@ npx tsx src/demo-conversation.ts
**This package requires a valid session token from ChatGPT to access it's unofficial REST API.**
To get a session token:
As of December 11, 2021, it also requires a valid Cloudflare clearance token.
There are two options to get these; either manually, or automated. For the automated way, see the `demos/` folder using Puppeteer.
To get a session token manually:
1. Go to https://chat.openai.com/chat and log in or sign up.
2. Open dev tools.
3. Open `Application` > `Cookies`.
![ChatGPT cookies](./media/session-token.png)
4. Copy the value for `__Secure-next-auth.session-token` and save it to your environment.
If you want to run the built-in demo, store this value as `SESSION_TOKEN` in a local `.env` file.
5. Copy the value for `cf_clearance` and save it to your environment.
> **Note**
> This package will switch to using the official API once it's released.
......
......@@ -11,13 +11,16 @@ const isCI = !!process.env.CI
test('ChatGPTAPI invalid session token', async (t) => {
t.timeout(30 * 1000) // 30 seconds
t.throws(() => new ChatGPTAPI({ sessionToken: null }), {
t.throws(() => new ChatGPTAPI({ sessionToken: null, clearanceToken: null }), {
message: 'ChatGPT invalid session token'
})
await t.throwsAsync(
async () => {
const chatgpt = new ChatGPTAPI({ sessionToken: 'invalid' })
const chatgpt = new ChatGPTAPI({
sessionToken: 'invalid',
clearanceToken: 'invalid'
})
await chatgpt.ensureAuth()
},
{
......@@ -33,13 +36,18 @@ test('ChatGPTAPI valid session token', async (t) => {
}
t.notThrows(
() => new ChatGPTAPI({ sessionToken: 'fake valid session token' })
() =>
new ChatGPTAPI({
sessionToken: 'fake valid session token',
clearanceToken: 'invalid'
})
)
await t.notThrowsAsync(
(async () => {
const chatgpt = new ChatGPTAPI({
sessionToken: process.env.SESSION_TOKEN
sessionToken: process.env.SESSION_TOKEN,
clearanceToken: process.env.CLEARANCE_TOKEN
})
// Don't make any real API calls using our session token if we're running on CI
......@@ -62,7 +70,10 @@ if (!isCI) {
await t.throwsAsync(
async () => {
const chatgpt = new ChatGPTAPI({ sessionToken: expiredSessionToken })
const chatgpt = new ChatGPTAPI({
sessionToken: expiredSessionToken,
clearanceToken: 'invalid'
})
await chatgpt.ensureAuth()
},
{
......@@ -81,7 +92,8 @@ if (!isCI) {
await t.throwsAsync(
async () => {
const chatgpt = new ChatGPTAPI({
sessionToken: process.env.SESSION_TOKEN
sessionToken: process.env.SESSION_TOKEN,
clearanceToken: process.env.CLEARANCE_TOKEN
})
await chatgpt.sendMessage('test', {
......@@ -100,7 +112,8 @@ if (!isCI) {
await t.throwsAsync(
async () => {
const chatgpt = new ChatGPTAPI({
sessionToken: process.env.SESSION_TOKEN
sessionToken: process.env.SESSION_TOKEN,
clearanceToken: process.env.CLEARANCE_TOKEN
})
const abortController = new AbortController()
......
......@@ -99,6 +99,10 @@ export class ChatGPTAPI {
if (!this._sessionToken) {
throw new types.ChatGPTError('ChatGPT invalid session token')
}
if (!this._clearanceToken) {
throw new types.ChatGPTError('ChatGPT invalid clearance token')
}
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment