diff --git a/demos/demo-conversation.ts b/demos/demo-conversation.ts index 35967f9..2f61706 100644 --- a/demos/demo-conversation.ts +++ b/demos/demo-conversation.ts @@ -1,8 +1,7 @@ import dotenv from 'dotenv-safe' import { oraPromise } from 'ora' -import { ChatGPTAPI } from '../src' -import { getOpenAIAuthInfo } from './openai-auth-puppeteer' +import { ChatGPTAPI, getOpenAIAuth } from '../src' dotenv.config() @@ -17,7 +16,7 @@ async function main() { const email = process.env.EMAIL const password = process.env.PASSWORD - const authInfo = await getOpenAIAuthInfo({ + const authInfo = await getOpenAIAuth({ email, password }) diff --git a/demos/demo.ts b/demos/demo.ts index 10568ca..e1dfab4 100644 --- a/demos/demo.ts +++ b/demos/demo.ts @@ -1,8 +1,7 @@ import dotenv from 'dotenv-safe' import { oraPromise } from 'ora' -import { ChatGPTAPI } from '../src' -import { getOpenAIAuthInfo } from './openai-auth-puppeteer' +import { ChatGPTAPI, getOpenAIAuth } from '../src' dotenv.config() @@ -17,7 +16,7 @@ async function main() { const email = process.env.EMAIL const password = process.env.PASSWORD - const authInfo = await getOpenAIAuthInfo({ + const authInfo = await getOpenAIAuth({ email, password }) diff --git a/package.json b/package.json index 1491445..ea3d656 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,13 @@ "build" ], "engines": { - "node": ">=16.8" + "node": ">=18" }, "scripts": { "build": "tsup", "dev": "tsup --watch", "clean": "del build", "prebuild": "run-s clean", - "postbuild": "[ -n CI ] && sed -i '' 's/await import(\"undici\")/null/' build/browser/index.js || echo 'skipping postbuild on CI'", "predev": "run-s clean", "pretest": "run-s build", "docs": "typedoc", @@ -42,7 +41,10 @@ "p-timeout": "^6.0.0", "remark": "^14.0.2", "strip-markdown": "^5.0.0", - "uuid": "^9.0.0" + "delay": "^5.0.0", + "uuid": "^9.0.0", + "puppeteer-extra": "^3.3.4", + "puppeteer-extra-plugin-stealth": "^2.11.1" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.0.0", @@ -50,7 +52,6 @@ "@types/uuid": "^9.0.0", "ava": "^5.1.0", "del-cli": "^5.0.0", - "delay": "^5.0.0", "dotenv-safe": "^8.2.0", "husky": "^8.0.2", "lint-staged": "^13.0.3", @@ -58,16 +59,14 @@ "ora": "^6.1.2", "prettier": "^2.8.0", "puppeteer": "^19.4.0", - "puppeteer-extra": "^3.3.4", - "puppeteer-extra-plugin-stealth": "^2.11.1", "tsup": "^6.5.0", "tsx": "^3.12.1", "typedoc": "^0.23.21", "typedoc-plugin-markdown": "^3.13.6", "typescript": "^4.9.3" }, - "optionalDependencies": { - "undici": "^5.13.0" + "peerDependencies": { + "puppeteer": "*" }, "lint-staged": { "*.{ts,tsx}": [ diff --git a/readme.md b/readme.md index 90631e7..86f3fce 100644 --- a/readme.md +++ b/readme.md @@ -4,28 +4,9 @@ Yesterday, OpenAI added additional Cloudflare protections that make it more diff The demos have been updated to use Puppeteer to log in to ChatGPT and extract the Cloudflare `cf_clearance` cookie and OpenAI session token. 🔥 -To use the updated version, first make sure you're using the latest version of this package and Node.js >= 18: +To use the updated version, make sure you're using the latest version of this package and Node.js >= 18. Then update your code to use the examples below, paying special attention to the sections on [Authentication](#authentication) and [Restrictions](#restrictions). -```ts -const api = new ChatGPTAPI({ - sessionToken: process.env.SESSION_TOKEN, - clearanceToken: process.env.CLEARANCE_TOKEN, - userAgent: '' // needs to match your browser's user agent -}) - -await api.ensureAuth() -``` - -Restrictions on this method: - -- Cloudflare `cf_clearance` **tokens expire after 2 hours**, so right now we recommend that you refresh your `cf_clearance` token every ~45 minutes or so. -- Your `user-agent` and `IP address` **must match** from the real browser window you're logged in with to the one you're using for `ChatGPTAPI`. - - This means that you currently can't log in with your laptop and then run the bot on a server or proxy somewhere. -- Cloudflare will still sometimes ask you to complete a CAPTCHA, so you may need to keep an eye on it and manually resolve the CAPTCHA. Automated CAPTCHA bypass is a WIP. -- You must use `node >= 18`. I'm using `v19.2.0` in my testing, but for some reason, all `fetch` requests using Node.js `v16` and `v17` fail at the moment (these use `undici` under the hood, whereas Node.js v18 and above use a built-in `fetch` based on `undici`). -- You should not be using this account while the bot is using it, because that browser window may refresh one of your tokens and invalidate the bot's session. - -We're working hard in [this issue](https://github.com/transitive-bullshit/chatgpt-api/issues/96) to make this process easier and more automated. +We're working hard in [this issue](https://github.com/transitive-bullshit/chatgpt-api/issues/96) to improve this process. Keep in mind that this package will be updated to use the official API as soon as it's released. 💪 Cheers, Travis @@ -48,7 +29,8 @@ Travis - [Usage](#usage) - [Docs](#docs) - [Demos](#demos) - - [Session Tokens](#session-tokens) + - [Authentication](#authentication) + - [Restrictions](#restrictions) - [Projects](#projects) - [Compatibility](#compatibility) - [Credits](#credits) @@ -69,15 +51,17 @@ npm install chatgpt ## Usage ```ts -import { ChatGPTAPI } from 'chatgpt' +import { ChatGPTAPI, getOpenAIAuth } from 'chatgpt' async function example() { - const api = new ChatGPTAPI({ - sessionToken: process.env.SESSION_TOKEN, - clearanceToken: process.env.CLEARANCE_TOKEN, - userAgent: 'TODO' + // uses puppeteer to bypass cloudflare (headful because you may have to solve + // a captcha) + const openAIAuth = await getOpenAIAuth({ + email: process.env.EMAIL, + password: process.env.EMAIL }) + const api = new ChatGPTAPI({ ...openAIAuth }) await api.ensureAuth() // send a message and wait for the response @@ -93,32 +77,23 @@ async function example() { ChatGPT responses are formatted as markdown by default. If you want to work with plaintext instead, you can use: ```ts -const api = new ChatGPTAPI({ - sessionToken: process.env.SESSION_TOKEN, - clearanceToken: process.env.CLEARANCE_TOKEN, - userAgent: 'TODO', - markdown: false -}) +const api = new ChatGPTAPI({ ...openAIAuth, markdown: false }) ``` If you want to automatically track the conversation, you can use `ChatGPTAPI.getConversation()`: ```ts -const api = new ChatGPTAPI({ - sessionToken: process.env.SESSION_TOKEN, - clearanceToken: process.env.CLEARANCE_TOKEN, - userAgent: 'TODO' -}) +const api = new ChatGPTAPI({ ...openAIAuth, markdown: false }) const conversation = api.getConversation() // send a message and wait for the response const response0 = await conversation.sendMessage('What is OpenAI?') -// send a follow-up prompt to the previous message and wait for the response +// send a follow-up const response1 = await conversation.sendMessage('Can you expand on that?') -// send another follow-up to the same conversation +// send another follow-up const response2 = await conversation.sendMessage('Oh cool; thank you') ``` @@ -141,13 +116,14 @@ You can stream responses using the `onProgress` or `onConversationResponse` call ```js async function example() { // To use ESM in CommonJS, you can use a dynamic import - const { ChatGPTAPI } = await import('chatgpt') + const { ChatGPTAPI, getOpenAIAuth } = await import('chatgpt') - const api = new ChatGPTAPI({ - sessionToken: process.env.SESSION_TOKEN, - clearanceToken: process.env.CLEARANCE_TOKEN, - userAgent: 'TODO' + const openAIAuth = await getOpenAIAuth({ + email: process.env.EMAIL, + password: process.env.EMAIL }) + + const api = new ChatGPTAPI({ ...openAIAuth }) await api.ensureAuth() const response = await api.sendMessage('Hello World!') @@ -181,13 +157,21 @@ A [conversation demo](./demos/demo-conversation.ts) is also included: npx tsx src/demo-conversation.ts ``` -### Session Tokens +### Authentication -**This package requires a valid session token from ChatGPT to access it's unofficial REST API.** +#### Restrictions -As of December 11, 2021, it also requires a valid Cloudflare clearance token. +**Please read carefully** -There are two options to get these; either manually, or automated. For the automated way, see the `demos/` folder using Puppeteer. +- You must use `node >= 18`. I'm using `v19.2.0` in my testing, but for some reason, all `fetch` requests using Node.js `v16` and `v17` fail at the moment (these use `undici` under the hood, whereas Node.js v18 and above use a built-in `fetch` based on `undici`). +- Cloudflare `cf_clearance` **tokens expire after 2 hours**, so right now we recommend that you refresh your `cf_clearance` token every hour or so. +- Your `user-agent` and `IP address` **must match** from the real browser window you're logged in with to the one you're using for `ChatGPTAPI`. + - This means that you currently can't log in with your laptop and then run the bot on a server or proxy somewhere. +- Cloudflare will still sometimes ask you to complete a CAPTCHA, so you may need to keep an eye on it and manually resolve the CAPTCHA. Automated CAPTCHA bypass is coming soon. +- You should not be using this account while the bot is using it, because that browser window may refresh one of your tokens and invalidate the bot's session. + +
+Getting tokens manually To get a session token manually: @@ -195,8 +179,10 @@ To get a session token manually: 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. -5. Copy the value for `cf_clearance` and save it to your environment. +4. Copy the value for `__Secure-next-auth.session-token` and save it to your environment. This will be your `sessionToken`. +5. Copy the value for `cf_clearance` and save it to your environment. This will be your `clearanceToken`. + +
> **Note** > This package will switch to using the official API once it's released. @@ -255,11 +241,8 @@ If you create a cool integration, feel free to open a PR and add it to the list. This package is ESM-only. It supports: -- Node.js >= 16.8 - - If you need Node.js 14 support, use [`v1.4.0`](https://github.com/transitive-bullshit/chatgpt-api/releases/tag/v1.4.0) -- Edge runtimes like CF workers and Vercel edge functions -- Modern browsers - - Mainly meant for chrome extensions where your code is protected to a degree +- Node.js >= 18 + - Node.js 17, 16, and 14 were supported in earlier versions, but OpenAI's Cloudflare update caused a bug with `undici` on v17 and v16 that we need to debug. So for now, use `node >= 18` - We recommend against using `chatgpt` from client-side browser code because it would expose your private session token - If you want to build a website using `chatgpt`, we recommend using it only from your backend API diff --git a/src/chatgpt-api.ts b/src/chatgpt-api.ts index 8022b4f..fea5af6 100644 --- a/src/chatgpt-api.ts +++ b/src/chatgpt-api.ts @@ -29,6 +29,9 @@ export class ChatGPTAPI { /** * Creates a new client wrapper around the unofficial ChatGPT REST API. * + * Note that your IP address and `userAgent` must match the same values that you used + * to obtain your `clearanceToken`. + * * @param opts.sessionToken = **Required** OpenAI session token which can be found in a valid session's cookies (see readme for instructions) * @param opts.clearanceToken = **Required** Cloudflare `cf_clearance` cookie value (see readme for instructions) * @param apiBaseUrl - Optional override; the base URL for ChatGPT webapp's API (`/api`) @@ -124,6 +127,21 @@ export class ChatGPTAPI { return this._user } + /** Gets the current session token. */ + get sessionToken() { + return this._sessionToken + } + + /** Gets the current Cloudflare clearance token (`cf_clearance` cookie value). */ + get clearanceToken() { + return this._clearanceToken + } + + /** Gets the current user agent. */ + get userAgent() { + return this._userAgent + } + /** * Sends a message to ChatGPT, waits for the response to resolve, and returns * the response. @@ -244,7 +262,23 @@ export class ChatGPTAPI { reject(err) } } - }).catch(reject) + }).catch((err) => { + const errMessageL = err.toString().toLowerCase() + + if ( + response && + (errMessageL === 'error: typeerror: terminated' || + errMessageL === 'typeerror: terminated') + ) { + // OpenAI sometimes forcefully terminates the socket from their end before + // the HTTP request has resolved cleanly. In my testing, these cases tend to + // happen when OpenAI has already send the last `response`, so we can ignore + // the `fetch` error in this case. + return resolve(response) + } else { + return reject(err) + } + }) }) if (timeoutMs) { diff --git a/src/fetch.ts b/src/fetch.ts index cb63786..6722f4f 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -1,28 +1,13 @@ /// -let _undici: any - -// Use `undici` for node.js 16 and 17 // Use `fetch` for node.js >= 18 // Use `fetch` for all other environments, including browsers -// NOTE: The top-level await is removed in a `postbuild` npm script for the -// browser build -const fetch = - globalThis.fetch ?? - async function undiciFetchWrapper( - ...args: Parameters - ): Promise { - if (!_undici) { - _undici = await import('undici') - } +const fetch = globalThis.fetch - if (typeof _undici?.fetch !== 'function') { - throw new Error( - 'Invalid undici installation; please make sure undici is installed correctly in your node_modules. Note that this package requires Node.js >= 16.8' - ) - } - - return _undici.fetch(...args) - } +if (typeof fetch !== 'function') { + throw new Error( + 'Invalid environment: global fetch not defined; `chatgpt` requires Node.js >= 18 at the moment due to Cloudflare protections' + ) +} export { fetch } diff --git a/src/index.ts b/src/index.ts index ed6a4b5..976d160 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * from './chatgpt-api' export * from './chatgpt-conversation' export * from './types' export * from './utils' +export * from './openai-auth' diff --git a/demos/openai-auth-puppeteer.ts b/src/openai-auth.ts similarity index 66% rename from demos/openai-auth-puppeteer.ts rename to src/openai-auth.ts index 55f9255..74a2151 100644 --- a/demos/openai-auth-puppeteer.ts +++ b/src/openai-auth.ts @@ -10,7 +10,11 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth' puppeteer.use(StealthPlugin()) -export type OpenAIAuthInfo = { +/** + * 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 @@ -20,18 +24,29 @@ export type OpenAIAuthInfo = { /** * 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 getOpenAIAuthInfo({ +export async function getOpenAIAuth({ email, password, - timeout = 2 * 60 * 1000, + timeoutMs = 2 * 60 * 1000, browser }: { - email: string - password: string - timeout?: number + email?: string + password?: string + timeoutMs?: number browser?: Browser -}): Promise { +}): Promise { let page: Page let origBrowser = browser @@ -42,12 +57,18 @@ export async function getOpenAIAuthInfo({ const userAgent = await browser.userAgent() page = (await browser.pages())[0] || (await browser.newPage()) - page.setDefaultTimeout(timeout) + page.setDefaultTimeout(timeoutMs) await page.goto('https://chat.openai.com/auth/login') - await page.waitForSelector('#__next .btn-primary', { timeout }) + + // NOTE: this is where you may encounter a CAPTCHA + + 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'), @@ -73,7 +94,7 @@ export async function getOpenAIAuthInfo({ {} ) - const authInfo: OpenAIAuthInfo = { + const authInfo: OpenAIAuth = { userAgent, clearanceToken: cookies['cf_clearance']?.value, sessionToken: cookies['__Secure-next-auth.session-token']?.value, @@ -83,7 +104,7 @@ export async function getOpenAIAuthInfo({ return authInfo } catch (err) { console.error(err) - throw null + throw err } finally { if (origBrowser) { if (page) { @@ -98,6 +119,11 @@ export async function getOpenAIAuthInfo({ } } +/** + * 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) { const macChromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'