Commit c746d70a authored by Travis Fischer's avatar Travis Fischer Committed by GitHub

Merge branch 'main' into patch-2

parents ccfd8dd7 d2770a2d
...@@ -9,5 +9,5 @@ ...@@ -9,5 +9,5 @@
# ChatGPT # ChatGPT
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# see the readme for how to find this OPENAI_EMAIL=
SESSION_TOKEN= OPENAI_PASSWORD=
name: Bug Report
description: Create a bug report
labels: ['template: bug']
body:
- type: markdown
attributes:
value: 'If you follow all of the instructions carefully, and your account / IP hasn't been permanently flagged by Cloudflare / OpenAI, then you shouldn't ever get a 403 at this point.'
- type: markdown
attributes:
value: 'My Twitter bot has been running for the past 2 days without a single 403, and others have been able to get it working on Discord. Although it can take a bit of effort to get working, once you have it working, you're set.'
- type: checkboxes
attributes:
label: Verify latest release
description: '`chatgpt@latest` is the latest version of `chatgpt`. Some issues may already be fixed in the latest version, so please verify that your issue reproduces before opening a new issue.'
options:
- label: I verified that the issue exists in the latest `chatgpt` release
required: true
- type: checkboxes
attributes:
label: Verify webapp is working
description: 'Verify that the [ChatGPT webapp](https://chat.openai.com/) is working for you locally using the same browser and account.'
options:
- label: I verify that the ChatGPT webapp is working properly for this account.
required: true
- type: checkboxes
attributes:
label: Verify [restrictions](https://github.com/transitive-bullshit/chatgpt-api#restrictions)
description: 'Verify that you\'ve double-checked all of the restrictions listed in the readme.'
options:
- label: I verify that I've double-checked all of the restrictions.
required: true
- type: textarea
attributes:
label: Provide environment information
description: Please enter `node --version`, `Chrome` version, OS, OS version.
validations:
required: true
- type: input
attributes:
label: Link to the code that reproduces this issue
description: A link to a GitHub repository you own with a minimal reproduction. Minimal reproductions should be forked from this repo and should include only changes that contribute to the issue.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken. **Is `getOpenAIAuth` returning successfully? If so, please print out its values.** If not, please describe what happens in the browser when trying to auth.
validations:
required: true
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: markdown
attributes:
value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear.
- type: markdown
attributes:
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
- type: markdown
attributes:
value: These steps are used to improve the docs to ensure the same issue does not happen again. Thanks in advance!
blank_issues_enabled: true
contact_links:
- name: Ask a question
url: https://github.com/vercel/next.js/discussions
about: Ask questions and discuss with other community members
import dotenv from 'dotenv-safe' import dotenv from 'dotenv-safe'
import { oraPromise } from 'ora' import { oraPromise } from 'ora'
import { ChatGPTAPI } from '.' import { ChatGPTAPI, getOpenAIAuth } from '../src'
dotenv.config() dotenv.config()
...@@ -13,7 +13,15 @@ dotenv.config() ...@@ -13,7 +13,15 @@ dotenv.config()
* ``` * ```
*/ */
async function main() { async function main() {
const api = new ChatGPTAPI({ sessionToken: process.env.SESSION_TOKEN }) const email = process.env.OPENAI_EMAIL
const password = process.env.OPENAI_PASSWORD
const authInfo = await getOpenAIAuth({
email,
password
})
const api = new ChatGPTAPI({ ...authInfo })
await api.ensureAuth() await api.ensureAuth()
const conversation = api.getConversation() const conversation = api.getConversation()
......
import dotenv from 'dotenv-safe' import dotenv from 'dotenv-safe'
import { oraPromise } from 'ora' import { oraPromise } from 'ora'
import { ChatGPTAPI } from '.' import { ChatGPTAPI, getOpenAIAuth } from '../src'
dotenv.config() dotenv.config()
...@@ -13,7 +13,15 @@ dotenv.config() ...@@ -13,7 +13,15 @@ dotenv.config()
* ``` * ```
*/ */
async function main() { async function main() {
const api = new ChatGPTAPI({ sessionToken: process.env.SESSION_TOKEN }) const email = process.env.OPENAI_EMAIL
const password = process.env.OPENAI_PASSWORD
const authInfo = await getOpenAIAuth({
email,
password
})
const api = new ChatGPTAPI({ ...authInfo })
await api.ensureAuth() await api.ensureAuth()
const prompt = const prompt =
......
...@@ -8,6 +8,13 @@ ...@@ -8,6 +8,13 @@
- [constructor](ChatGPTAPI.md#constructor) - [constructor](ChatGPTAPI.md#constructor)
### Accessors
- [clearanceToken](ChatGPTAPI.md#clearancetoken)
- [sessionToken](ChatGPTAPI.md#sessiontoken)
- [user](ChatGPTAPI.md#user)
- [userAgent](ChatGPTAPI.md#useragent)
### Methods ### Methods
- [ensureAuth](ChatGPTAPI.md#ensureauth) - [ensureAuth](ChatGPTAPI.md#ensureauth)
...@@ -24,21 +31,92 @@ ...@@ -24,21 +31,92 @@
Creates a new client wrapper around the unofficial ChatGPT REST API. 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`.
#### Parameters #### Parameters
| Name | Type | Description | | Name | Type | Description |
| :------ | :------ | :------ | | :------ | :------ | :------ |
| `opts` | `Object` | - | | `opts` | `Object` | - |
| `opts.accessTokenTTL?` | `number` | **`Default Value`** 60000 (60 seconds) | | `opts.accessToken?` | `string` | **`Default Value`** `undefined` * |
| `opts.accessTokenTTL?` | `number` | **`Default Value`** 1 hour * |
| `opts.apiBaseUrl?` | `string` | **`Default Value`** `'https://chat.openai.com/api'` * | | `opts.apiBaseUrl?` | `string` | **`Default Value`** `'https://chat.openai.com/api'` * |
| `opts.backendApiBaseUrl?` | `string` | **`Default Value`** `'https://chat.openai.com/backend-api'` * | | `opts.backendApiBaseUrl?` | `string` | **`Default Value`** `'https://chat.openai.com/backend-api'` * |
| `opts.clearanceToken` | `string` | = **Required** Cloudflare `cf_clearance` cookie value (see readme for instructions) |
| `opts.debug?` | `boolean` | **`Default Value`** `false` * |
| `opts.headers?` | `Record`<`string`, `string`\> | **`Default Value`** `undefined` * |
| `opts.markdown?` | `boolean` | **`Default Value`** `true` * | | `opts.markdown?` | `boolean` | **`Default Value`** `true` * |
| `opts.sessionToken` | `string` | = **Required** OpenAI session token which can be found in a valid session's cookies (see readme for instructions) | | `opts.sessionToken` | `string` | = **Required** OpenAI session token which can be found in a valid session's cookies (see readme for instructions) |
| `opts.userAgent?` | `string` | **`Default Value`** `'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36'` * | | `opts.userAgent?` | `string` | **`Default Value`** `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'` * |
#### Defined in
[src/chatgpt-api.ts:45](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L45)
## Accessors
### clearanceToken
`get` **clearanceToken**(): `string`
Gets the current Cloudflare clearance token (`cf_clearance` cookie value).
#### Returns
`string`
#### Defined in
[src/chatgpt-api.ts:136](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L136)
___
### sessionToken
`get` **sessionToken**(): `string`
Gets the current session token.
#### Returns
`string`
#### Defined in
[src/chatgpt-api.ts:131](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L131)
___
### user
`get` **user**(): [`User`](../modules.md#user)
Gets the currently signed-in user, if authenticated, `null` otherwise.
#### Returns
[`User`](../modules.md#user)
#### Defined in
[src/chatgpt-api.ts:126](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L126)
___
### userAgent
`get` **userAgent**(): `string`
Gets the current user agent.
#### Returns
`string`
#### Defined in #### Defined in
[chatgpt-api.ts:35](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-api.ts#L35) [src/chatgpt-api.ts:141](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L141)
## Methods ## Methods
...@@ -55,7 +133,7 @@ is still valid. ...@@ -55,7 +133,7 @@ is still valid.
#### Defined in #### Defined in
[chatgpt-api.ts:221](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-api.ts#L221) [src/chatgpt-api.ts:319](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L319)
___ ___
...@@ -82,7 +160,7 @@ The new conversation instance ...@@ -82,7 +160,7 @@ The new conversation instance
#### Defined in #### Defined in
[chatgpt-api.ts:285](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-api.ts#L285) [src/chatgpt-api.ts:425](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L425)
___ ___
...@@ -99,7 +177,7 @@ the token fails. ...@@ -99,7 +177,7 @@ the token fails.
#### Defined in #### Defined in
[chatgpt-api.ts:208](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-api.ts#L208) [src/chatgpt-api.ts:306](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L306)
___ ___
...@@ -125,7 +203,7 @@ A valid access token ...@@ -125,7 +203,7 @@ A valid access token
#### Defined in #### Defined in
[chatgpt-api.ts:235](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-api.ts#L235) [src/chatgpt-api.ts:333](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L333)
___ ___
...@@ -156,4 +234,4 @@ The response from ChatGPT ...@@ -156,4 +234,4 @@ The response from ChatGPT
#### Defined in #### Defined in
[chatgpt-api.ts:94](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-api.ts#L94) [src/chatgpt-api.ts:166](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-api.ts#L166)
...@@ -41,7 +41,7 @@ Creates a new conversation wrapper around the ChatGPT API. ...@@ -41,7 +41,7 @@ Creates a new conversation wrapper around the ChatGPT API.
#### Defined in #### Defined in
[chatgpt-conversation.ts:21](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-conversation.ts#L21) [src/chatgpt-conversation.ts:21](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-conversation.ts#L21)
## Properties ## Properties
...@@ -51,7 +51,7 @@ Creates a new conversation wrapper around the ChatGPT API. ...@@ -51,7 +51,7 @@ Creates a new conversation wrapper around the ChatGPT API.
#### Defined in #### Defined in
[chatgpt-conversation.ts:10](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-conversation.ts#L10) [src/chatgpt-conversation.ts:10](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-conversation.ts#L10)
___ ___
...@@ -61,7 +61,7 @@ ___ ...@@ -61,7 +61,7 @@ ___
#### Defined in #### Defined in
[chatgpt-conversation.ts:11](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-conversation.ts#L11) [src/chatgpt-conversation.ts:11](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-conversation.ts#L11)
___ ___
...@@ -71,7 +71,7 @@ ___ ...@@ -71,7 +71,7 @@ ___
#### Defined in #### Defined in
[chatgpt-conversation.ts:12](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-conversation.ts#L12) [src/chatgpt-conversation.ts:12](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-conversation.ts#L12)
## Methods ## Methods
...@@ -104,4 +104,4 @@ The response from ChatGPT ...@@ -104,4 +104,4 @@ The response from ChatGPT
#### Defined in #### Defined in
[chatgpt-conversation.ts:48](https://github.com/transitive-bullshit/chatgpt-api/blob/20c376e/src/chatgpt-conversation.ts#L48) [src/chatgpt-conversation.ts:48](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/chatgpt-conversation.ts#L48)
[chatgpt](../readme.md) / [Exports](../modules.md) / ChatGPTError
# Class: ChatGPTError
## Hierarchy
- `Error`
**`ChatGPTError`**
## Table of contents
### Constructors
- [constructor](ChatGPTError.md#constructor)
### Properties
- [originalError](ChatGPTError.md#originalerror)
- [response](ChatGPTError.md#response)
- [statusCode](ChatGPTError.md#statuscode)
- [statusText](ChatGPTError.md#statustext)
## Constructors
### constructor
**new ChatGPTError**(`message?`)
#### Parameters
| Name | Type |
| :------ | :------ |
| `message?` | `string` |
#### Inherited from
Error.constructor
#### Defined in
node_modules/.pnpm/typescript@4.9.3/node_modules/typescript/lib/lib.es5.d.ts:1059
**new ChatGPTError**(`message?`, `options?`)
#### Parameters
| Name | Type |
| :------ | :------ |
| `message?` | `string` |
| `options?` | `ErrorOptions` |
#### Inherited from
Error.constructor
#### Defined in
node_modules/.pnpm/typescript@4.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts:30
## Properties
### originalError
`Optional` **originalError**: `Error`
#### Defined in
[src/types.ts:298](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/types.ts#L298)
___
### response
`Optional` **response**: `Response`
#### Defined in
[src/types.ts:297](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/types.ts#L297)
___
### statusCode
`Optional` **statusCode**: `number`
#### Defined in
[src/types.ts:295](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/types.ts#L295)
___
### statusText
`Optional` **statusText**: `string`
#### Defined in
[src/types.ts:296](https://github.com/transitive-bullshit/chatgpt-api/blob/c257286/src/types.ts#L296)
This diff is collapsed.
This diff is collapsed.
{ {
"name": "chatgpt", "name": "chatgpt",
"version": "2.0.5", "version": "2.3.2",
"description": "Node.js client for the unofficial ChatGPT API.", "description": "Node.js client for the unofficial ChatGPT API.",
"author": "Travis Fischer <travis@transitivebullsh.it>", "author": "Travis Fischer <travis@transitivebullsh.it>",
"repository": "transitive-bullshit/chatgpt-api", "repository": "transitive-bullshit/chatgpt-api",
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
"types": "./build/index.d.ts", "types": "./build/index.d.ts",
"exports": { "exports": {
".": { ".": {
"browser": "./build/browser/index.js",
"import": "./build/index.js", "import": "./build/index.js",
"types": "./build/index.d.ts", "types": "./build/index.d.ts",
"default": "./build/index.js" "default": "./build/index.js"
...@@ -20,21 +19,19 @@ ...@@ -20,21 +19,19 @@
"build" "build"
], ],
"engines": { "engines": {
"node": ">=16.8" "node": ">=18"
}, },
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"dev": "tsup --watch", "dev": "tsup --watch",
"clean": "del build", "clean": "del build",
"prebuild": "run-s clean", "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", "predev": "run-s clean",
"pretest": "run-s build", "pretest": "run-s build",
"docs": "typedoc", "docs": "typedoc",
"prepare": "husky install", "prepare": "husky install",
"pre-commit": "lint-staged", "pre-commit": "lint-staged",
"test": "run-p test:*", "test": "run-p test:*",
"test:unit": "ava",
"test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check" "test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check"
}, },
"dependencies": { "dependencies": {
...@@ -43,7 +40,10 @@ ...@@ -43,7 +40,10 @@
"p-timeout": "^6.0.0", "p-timeout": "^6.0.0",
"remark": "^14.0.2", "remark": "^14.0.2",
"strip-markdown": "^5.0.0", "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": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.0.0", "@trivago/prettier-plugin-sort-imports": "^4.0.0",
...@@ -57,14 +57,15 @@ ...@@ -57,14 +57,15 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^6.1.2", "ora": "^6.1.2",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"puppeteer": "^19.4.0",
"tsup": "^6.5.0", "tsup": "^6.5.0",
"tsx": "^3.12.1", "tsx": "^3.12.1",
"typedoc": "^0.23.21", "typedoc": "^0.23.21",
"typedoc-plugin-markdown": "^3.13.6", "typedoc-plugin-markdown": "^3.13.6",
"typescript": "^4.9.3" "typescript": "^4.9.3"
}, },
"optionalDependencies": { "peerDependencies": {
"undici": "^5.13.0" "puppeteer": "*"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx}": [ "*.{ts,tsx}": [
......
This diff is collapsed.
This diff is collapsed.
import test from 'ava' import test from 'ava'
import dotenv from 'dotenv-safe' import dotenv from 'dotenv-safe'
import * as types from './types'
import { ChatGPTAPI } from './chatgpt-api' import { ChatGPTAPI } from './chatgpt-api'
dotenv.config() dotenv.config()
...@@ -10,16 +11,20 @@ const isCI = !!process.env.CI ...@@ -10,16 +11,20 @@ const isCI = !!process.env.CI
test('ChatGPTAPI invalid session token', async (t) => { test('ChatGPTAPI invalid session token', async (t) => {
t.timeout(30 * 1000) // 30 seconds 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' message: 'ChatGPT invalid session token'
}) })
await t.throwsAsync( await t.throwsAsync(
async () => { async () => {
const chatgpt = new ChatGPTAPI({ sessionToken: 'invalid' }) const chatgpt = new ChatGPTAPI({
sessionToken: 'invalid',
clearanceToken: 'invalid'
})
await chatgpt.ensureAuth() await chatgpt.ensureAuth()
}, },
{ {
instanceOf: types.ChatGPTError,
message: 'ChatGPT failed to refresh auth token. Error: Unauthorized' message: 'ChatGPT failed to refresh auth token. Error: Unauthorized'
} }
) )
...@@ -31,13 +36,18 @@ test('ChatGPTAPI valid session token', async (t) => { ...@@ -31,13 +36,18 @@ test('ChatGPTAPI valid session token', async (t) => {
} }
t.notThrows( t.notThrows(
() => new ChatGPTAPI({ sessionToken: 'fake valid session token' }) () =>
new ChatGPTAPI({
sessionToken: 'fake valid session token',
clearanceToken: 'invalid'
})
) )
await t.notThrowsAsync( await t.notThrowsAsync(
(async () => { (async () => {
const chatgpt = new ChatGPTAPI({ 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 // Don't make any real API calls using our session token if we're running on CI
...@@ -60,10 +70,14 @@ if (!isCI) { ...@@ -60,10 +70,14 @@ if (!isCI) {
await t.throwsAsync( await t.throwsAsync(
async () => { async () => {
const chatgpt = new ChatGPTAPI({ sessionToken: expiredSessionToken }) const chatgpt = new ChatGPTAPI({
sessionToken: expiredSessionToken,
clearanceToken: 'invalid'
})
await chatgpt.ensureAuth() await chatgpt.ensureAuth()
}, },
{ {
instanceOf: types.ChatGPTError,
message: message:
'ChatGPT failed to refresh auth token. Error: session token may have expired' 'ChatGPT failed to refresh auth token. Error: session token may have expired'
} }
...@@ -78,7 +92,8 @@ if (!isCI) { ...@@ -78,7 +92,8 @@ if (!isCI) {
await t.throwsAsync( await t.throwsAsync(
async () => { async () => {
const chatgpt = new ChatGPTAPI({ const chatgpt = new ChatGPTAPI({
sessionToken: process.env.SESSION_TOKEN sessionToken: process.env.SESSION_TOKEN,
clearanceToken: process.env.CLEARANCE_TOKEN
}) })
await chatgpt.sendMessage('test', { await chatgpt.sendMessage('test', {
...@@ -97,7 +112,8 @@ if (!isCI) { ...@@ -97,7 +112,8 @@ if (!isCI) {
await t.throwsAsync( await t.throwsAsync(
async () => { async () => {
const chatgpt = new ChatGPTAPI({ const chatgpt = new ChatGPTAPI({
sessionToken: process.env.SESSION_TOKEN sessionToken: process.env.SESSION_TOKEN,
clearanceToken: process.env.CLEARANCE_TOKEN
}) })
const abortController = new AbortController() const abortController = new AbortController()
......
This diff is collapsed.
import { createParser } from 'eventsource-parser' import { createParser } from 'eventsource-parser'
import * as types from './types'
import { fetch } from './fetch' import { fetch } from './fetch'
import { streamAsyncIterable } from './stream-async-iterable' import { streamAsyncIterable } from './stream-async-iterable'
...@@ -10,7 +11,12 @@ export async function fetchSSE( ...@@ -10,7 +11,12 @@ export async function fetchSSE(
const { onMessage, ...fetchOptions } = options const { onMessage, ...fetchOptions } = options
const res = await fetch(url, fetchOptions) const res = await fetch(url, fetchOptions)
if (!res.ok) { if (!res.ok) {
throw new Error(`ChatGPTAPI error ${res.status || res.statusText}`) const msg = `ChatGPTAPI error ${res.status || res.statusText}`
const error = new types.ChatGPTError(msg)
error.statusCode = res.status
error.statusText = res.statusText
error.response = res
throw error
} }
const parser = createParser((event) => { const parser = createParser((event) => {
...@@ -25,7 +31,7 @@ export async function fetchSSE( ...@@ -25,7 +31,7 @@ export async function fetchSSE(
const body: NodeJS.ReadableStream = res.body as any const body: NodeJS.ReadableStream = res.body as any
if (!body.on || !body.read) { if (!body.on || !body.read) {
throw new Error('unsupported "fetch" implementation') throw new types.ChatGPTError('unsupported "fetch" implementation')
} }
body.on('readable', () => { body.on('readable', () => {
......
/// <reference lib="dom" /> /// <reference lib="dom" />
let _undici: any
// Use `undici` for node.js 16 and 17
// Use `fetch` for node.js >= 18 // Use `fetch` for node.js >= 18
// Use `fetch` for all other environments, including browsers // Use `fetch` for all other environments, including browsers
// NOTE: The top-level await is removed in a `postbuild` npm script for the const fetch = globalThis.fetch
// browser build
const fetch =
globalThis.fetch ??
async function undiciFetchWrapper(
...args: Parameters<typeof globalThis.fetch>
): Promise<Response> {
if (!_undici) {
_undici = await import('undici')
}
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 } export { fetch }
...@@ -2,3 +2,4 @@ export * from './chatgpt-api' ...@@ -2,3 +2,4 @@ export * from './chatgpt-api'
export * from './chatgpt-conversation' export * from './chatgpt-conversation'
export * from './types' export * from './types'
export * from './utils' export * from './utils'
export * from './openai-auth'
import * as fs from 'node:fs'
import * as os from 'node:os'
import delay from 'delay'
import {
type Browser,
type ElementHandle,
type Page,
type Protocol,
type PuppeteerLaunchOptions
} from 'puppeteer'
import puppeteer from 'puppeteer-extra'
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
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,
timeoutMs = 2 * 60 * 1000,
isGoogleLogin = false
}: {
email?: string
password?: string
browser?: Browser
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')
await checkForChatGPTAtCapacity(page)
// 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'),
page.waitForNavigation({
waitUntil: 'networkidle0'
})
])
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 })
submitP = page.keyboard.press('Enter')
} else {
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 })
submitP = page.click('button[type="submit"]')
}
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,
executablePath: defaultChromeExecutablePath(),
...launchOptions
})
}
/**
* Gets the default path to chrome's executable for the current platform.
*/
export const defaultChromeExecutablePath = (): string => {
switch (os.platform()) {
case 'win32':
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
case 'darwin':
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
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'
}
}
async function checkForChatGPTAtCapacity(page: Page) {
let res: ElementHandle<Element> | null
try {
res = await page.$('[role="alert"]')
console.log('capacity text', res)
} catch (err) {
// ignore errors likely due to navigation
console.warn(err.toString())
}
if (res) {
const error = new types.ChatGPTError('ChatGPT is at capacity')
error.statusCode = 503
throw error
}
}
...@@ -41,7 +41,7 @@ export type User = { ...@@ -41,7 +41,7 @@ export type User = {
/** /**
* Email of the user * Email of the user
*/ */
email: string email?: string
/** /**
* Image of the user * Image of the user
...@@ -56,12 +56,12 @@ export type User = { ...@@ -56,12 +56,12 @@ export type User = {
/** /**
* Groups the user is in * Groups the user is in
*/ */
groups: string[] | [] groups: string[]
/** /**
* Features the user is in * Features the user is in
*/ */
features: string[] | [] features: string[]
} }
/** /**
...@@ -273,10 +273,13 @@ export type MessageContent = { ...@@ -273,10 +273,13 @@ export type MessageContent = {
} }
export type MessageMetadata = any export type MessageMetadata = any
export type MessageActionType = 'next' | 'variant'
export type SendMessageOptions = { export type SendMessageOptions = {
conversationId?: string conversationId?: string
parentMessageId?: string parentMessageId?: string
messageId?: string
action?: MessageActionType
timeoutMs?: number timeoutMs?: number
onProgress?: (partialResponse: string) => void onProgress?: (partialResponse: string) => void
onConversationResponse?: (response: ConversationResponseEvent) => void onConversationResponse?: (response: ConversationResponseEvent) => void
...@@ -287,3 +290,10 @@ export type SendConversationMessageOptions = Omit< ...@@ -287,3 +290,10 @@ export type SendConversationMessageOptions = Omit<
SendMessageOptions, SendMessageOptions,
'conversationId' | 'parentMessageId' 'conversationId' | 'parentMessageId'
> >
export class ChatGPTError extends Error {
statusCode?: number
statusText?: string
response?: Response
originalError?: Error
}
...@@ -11,20 +11,6 @@ export default defineConfig([ ...@@ -11,20 +11,6 @@ export default defineConfig([
sourcemap: true, sourcemap: true,
minify: false, minify: false,
shims: true, shims: true,
dts: true, dts: true
external: ['undici']
},
{
entry: ['src/index.ts'],
outDir: 'build/browser',
target: 'chrome89',
platform: 'browser',
format: ['esm'],
splitting: false,
sourcemap: true,
minify: false,
shims: true,
dts: true,
external: ['undici']
} }
]) ])
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