diff --git a/package.json b/package.json index c7a1615..fe4fd03 100644 --- a/package.json +++ b/package.json @@ -36,15 +36,16 @@ "test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check" }, "dependencies": { + "delay": "^5.0.0", "eventsource-parser": "^0.0.5", "expiry-map": "^2.0.0", "p-timeout": "^6.0.0", + "puppeteer-extra": "^3.3.4", + "puppeteer-extra-plugin-recaptcha": "^3.6.6", + "puppeteer-extra-plugin-stealth": "^2.11.1", "remark": "^14.0.2", "strip-markdown": "^5.0.0", - "delay": "^5.0.0", - "uuid": "^9.0.0", - "puppeteer-extra": "^3.3.4", - "puppeteer-extra-plugin-stealth": "^2.11.1" + "uuid": "^9.0.0" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ef0f69..dd06dba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,7 @@ specifiers: prettier: ^2.8.0 puppeteer: ^19.4.0 puppeteer-extra: ^3.3.4 + puppeteer-extra-plugin-recaptcha: ^3.6.6 puppeteer-extra-plugin-stealth: ^2.11.1 remark: ^14.0.2 strip-markdown: ^5.0.0 @@ -34,6 +35,7 @@ dependencies: expiry-map: 2.0.0 p-timeout: 6.0.0 puppeteer-extra: 3.3.4_puppeteer@19.4.0 + puppeteer-extra-plugin-recaptcha: 3.6.6_puppeteer-extra@3.3.4 puppeteer-extra-plugin-stealth: 2.11.1_puppeteer-extra@3.3.4 remark: 14.0.2 strip-markdown: 5.0.0 @@ -553,6 +555,7 @@ packages: /arr-union/3.1.0: resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} engines: {node: '>=0.10.0'} + dev: false /array-find-index/1.0.2: resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} @@ -902,6 +905,7 @@ packages: kind-of: 3.2.2 lazy-cache: 1.0.4 shallow-clone: 0.1.2 + dev: false /clone/1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} @@ -1570,16 +1574,19 @@ packages: /for-in/0.1.8: resolution: {integrity: sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==} engines: {node: '>=0.10.0'} + dev: false /for-in/1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} engines: {node: '>=0.10.0'} + dev: false /for-own/0.1.5: resolution: {integrity: sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==} engines: {node: '>=0.10.0'} dependencies: for-in: 1.0.2 + dev: false /fs-constants/1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -1893,6 +1900,7 @@ packages: /is-buffer/1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false /is-buffer/2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} @@ -1924,6 +1932,7 @@ packages: /is-extendable/0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} + dev: false /is-extglob/2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -1994,6 +2003,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: isobject: 3.0.1 + dev: false /is-plain-object/5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} @@ -2060,6 +2070,7 @@ packages: /isobject/3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + dev: false /javascript-natural-sort/0.7.1: resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} @@ -2121,18 +2132,21 @@ packages: universalify: 2.0.0 optionalDependencies: graceful-fs: 4.2.10 + dev: false /kind-of/2.0.1: resolution: {integrity: sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==} engines: {node: '>=0.10.0'} dependencies: is-buffer: 1.1.6 + dev: false /kind-of/3.2.2: resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} engines: {node: '>=0.10.0'} dependencies: is-buffer: 1.1.6 + dev: false /kind-of/6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} @@ -2147,10 +2161,12 @@ packages: /lazy-cache/0.2.7: resolution: {integrity: sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==} engines: {node: '>=0.10.0'} + dev: false /lazy-cache/1.0.4: resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==} engines: {node: '>=0.10.0'} + dev: false /lilconfig/2.0.6: resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} @@ -2635,6 +2651,7 @@ packages: dependencies: for-in: 0.1.8 is-extendable: 0.1.1 + dev: false /mkdirp-classic/0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -3050,6 +3067,26 @@ packages: - supports-color - utf-8-validate + /puppeteer-extra-plugin-recaptcha/3.6.6_puppeteer-extra@3.3.4: + resolution: {integrity: sha512-SVbmL+igGX8m0Qg9dn85trWDghbfUCTG/QUHYscYx5XgMZVVb0/v0a6MqbPdHoKmBx5BS2kLd6rorMlncMcXdw==} + engines: {node: '>=9.11.2'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + dependencies: + debug: 4.3.4 + merge-deep: 3.0.3 + puppeteer-extra: 3.3.4_puppeteer@19.4.0 + puppeteer-extra-plugin: 3.2.2_puppeteer-extra@3.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /puppeteer-extra-plugin-stealth/2.11.1_puppeteer-extra@3.3.4: resolution: {integrity: sha512-n0wdC0Ilc9tk5L6FWLyd0P2gT8b2fp+2NuB+KB0oTSw3wXaZ0D6WNakjJsayJ4waGzIJFCUHkmK9zgx5NKMoFw==} engines: {node: '>=8'} @@ -3402,6 +3439,7 @@ packages: kind-of: 2.0.1 lazy-cache: 0.2.7 mixin-object: 2.0.1 + dev: false /shebang-command/1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} @@ -3916,6 +3954,7 @@ packages: /universalify/2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} + dev: false /update-browserslist-db/1.0.10_browserslist@4.21.4: resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} diff --git a/src/index.ts b/src/index.ts index 976d160..356a560 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,5 @@ export * from './chatgpt-conversation' export * from './types' export * from './utils' export * from './openai-auth' +export * from './openai-auth-2captcha' + diff --git a/src/openai-auth-2captcha.ts b/src/openai-auth-2captcha.ts new file mode 100644 index 0000000..b8e4e5b --- /dev/null +++ b/src/openai-auth-2captcha.ts @@ -0,0 +1,129 @@ +import delay from 'delay' +import { + type Browser, + type Page, + type Protocol, + type PuppeteerLaunchOptions +} from 'puppeteer' +import puppeteer from 'puppeteer-extra' +import RecaptchaPlugin from 'puppeteer-extra-plugin-recaptcha' +import StealthPlugin from 'puppeteer-extra-plugin-stealth' + +import { OpenAIAuth, getBrowser } from './openai-auth' + +puppeteer.use(StealthPlugin()) + +/** + * 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 getOpenAIAuth2Captcha({ + email, + password, + timeoutMs = 2 * 60 * 1000, + browser, + captchaToken +}: { + email?: string + password?: string + timeoutMs?: number + browser?: Browser + captchaToken?: string +}): Promise { + let page: Page + let origBrowser = browser + puppeteer.use( + RecaptchaPlugin({ + provider: { + id: '2captcha', + token: captchaToken + }, + visualFeedback: true // colorize reCAPTCHAs (violet = detected, green = solved) + }) + ) + + 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') + + // NOTE: this is where you may encounter a CAPTCHA + await page.solveRecaptchas() + + 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' + }) + ]) + await page.type('#username', email, { delay: 10 }) + await page.solveRecaptchas() + 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' + }) + ]) + /*var capacityLimit = await page.$('') + if (capacityLimit) { + throw `ChatGPT is at capacity right now` + }*/ + } + + 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 + } +}