2022-12-15 04:41:43 +00:00
|
|
|
import delay from 'delay'
|
2022-12-15 06:22:31 +00:00
|
|
|
import type { Browser, HTTPRequest, HTTPResponse, Page } from 'puppeteer'
|
2022-12-16 06:28:30 +00:00
|
|
|
import { v4 as uuidv4 } from 'uuid'
|
2022-12-15 04:41:43 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
import * as types from './types'
|
2022-12-16 14:02:33 +00:00
|
|
|
import {
|
|
|
|
defaultChromeExecutablePath,
|
|
|
|
getBrowser,
|
|
|
|
getOpenAIAuth
|
|
|
|
} from './openai-auth'
|
2022-12-16 06:28:30 +00:00
|
|
|
import {
|
|
|
|
browserPostEventStream,
|
|
|
|
isRelevantRequest,
|
|
|
|
maximizePage,
|
|
|
|
minimizePage
|
|
|
|
} from './utils'
|
2022-12-15 04:41:43 +00:00
|
|
|
|
|
|
|
export class ChatGPTAPIBrowser {
|
|
|
|
protected _markdown: boolean
|
|
|
|
protected _debug: boolean
|
2022-12-16 06:28:30 +00:00
|
|
|
protected _minimize: boolean
|
2022-12-15 04:41:43 +00:00
|
|
|
protected _isGoogleLogin: boolean
|
|
|
|
protected _captchaToken: string
|
2022-12-16 06:28:30 +00:00
|
|
|
protected _accessToken: string
|
2022-12-15 04:41:43 +00:00
|
|
|
|
|
|
|
protected _email: string
|
|
|
|
protected _password: string
|
|
|
|
|
2022-12-16 14:02:33 +00:00
|
|
|
protected _browserPath: string
|
2022-12-15 04:41:43 +00:00
|
|
|
protected _browser: Browser
|
|
|
|
protected _page: Page
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new client wrapper for automating the ChatGPT webapp.
|
|
|
|
*/
|
|
|
|
constructor(opts: {
|
|
|
|
email: string
|
|
|
|
password: string
|
|
|
|
|
|
|
|
/** @defaultValue `true` **/
|
|
|
|
markdown?: boolean
|
|
|
|
|
|
|
|
/** @defaultValue `false` **/
|
|
|
|
debug?: boolean
|
|
|
|
|
2022-12-16 07:05:33 +00:00
|
|
|
/** @defaultValue `false` **/
|
2022-12-15 04:41:43 +00:00
|
|
|
isGoogleLogin?: boolean
|
2022-12-16 07:05:33 +00:00
|
|
|
|
|
|
|
/** @defaultValue `true` **/
|
2022-12-16 06:28:30 +00:00
|
|
|
minimize?: boolean
|
2022-12-16 07:05:33 +00:00
|
|
|
|
|
|
|
/** @defaultValue `undefined` **/
|
2022-12-15 04:41:43 +00:00
|
|
|
captchaToken?: string
|
2022-12-16 14:02:33 +00:00
|
|
|
|
|
|
|
/** @defaultValue `undefined` **/
|
|
|
|
browserPath?: string
|
2022-12-15 04:41:43 +00:00
|
|
|
}) {
|
|
|
|
const {
|
|
|
|
email,
|
|
|
|
password,
|
|
|
|
markdown = true,
|
|
|
|
debug = false,
|
|
|
|
isGoogleLogin = false,
|
2022-12-16 06:28:30 +00:00
|
|
|
minimize = true,
|
2022-12-16 14:02:33 +00:00
|
|
|
captchaToken,
|
|
|
|
browserPath = defaultChromeExecutablePath()
|
2022-12-15 04:41:43 +00:00
|
|
|
} = opts
|
|
|
|
|
|
|
|
this._email = email
|
|
|
|
this._password = password
|
|
|
|
|
|
|
|
this._markdown = !!markdown
|
|
|
|
this._debug = !!debug
|
|
|
|
this._isGoogleLogin = !!isGoogleLogin
|
2022-12-16 06:28:30 +00:00
|
|
|
this._minimize = !!minimize
|
2022-12-15 04:41:43 +00:00
|
|
|
this._captchaToken = captchaToken
|
2022-12-16 14:02:33 +00:00
|
|
|
this._browserPath = browserPath
|
2022-12-15 04:41:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async init() {
|
|
|
|
if (this._browser) {
|
|
|
|
await this._browser.close()
|
|
|
|
this._page = null
|
|
|
|
this._browser = null
|
|
|
|
}
|
|
|
|
|
2022-12-15 08:14:17 +00:00
|
|
|
try {
|
2022-12-16 14:02:33 +00:00
|
|
|
this._browser = await getBrowser({
|
|
|
|
captchaToken: this._captchaToken,
|
|
|
|
executablePath: this._browserPath
|
|
|
|
})
|
2022-12-15 08:14:17 +00:00
|
|
|
this._page =
|
|
|
|
(await this._browser.pages())[0] || (await this._browser.newPage())
|
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
this._page.on('request', this._onRequest.bind(this))
|
|
|
|
this._page.on('response', this._onResponse.bind(this))
|
|
|
|
|
2022-12-15 08:14:17 +00:00
|
|
|
// bypass cloudflare and login
|
|
|
|
await getOpenAIAuth({
|
|
|
|
email: this._email,
|
|
|
|
password: this._password,
|
|
|
|
browser: this._browser,
|
|
|
|
page: this._page,
|
|
|
|
isGoogleLogin: this._isGoogleLogin
|
|
|
|
})
|
|
|
|
} catch (err) {
|
|
|
|
if (this._browser) {
|
|
|
|
await this._browser.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
this._browser = null
|
|
|
|
this._page = null
|
|
|
|
|
|
|
|
throw err
|
|
|
|
}
|
2022-12-15 04:41:43 +00:00
|
|
|
|
|
|
|
const chatUrl = 'https://chat.openai.com/chat'
|
|
|
|
const url = this._page.url().replace(/\/$/, '')
|
|
|
|
|
|
|
|
if (url !== chatUrl) {
|
|
|
|
await this._page.goto(chatUrl, {
|
2022-12-16 03:33:24 +00:00
|
|
|
waitUntil: 'networkidle2'
|
2022-12-15 04:41:43 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-12-16 03:33:24 +00:00
|
|
|
// dismiss welcome modal (and other modals)
|
2022-12-15 04:41:43 +00:00
|
|
|
do {
|
|
|
|
const modalSelector = '[data-headlessui-state="open"]'
|
|
|
|
|
|
|
|
if (!(await this._page.$(modalSelector))) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this._page.click(`${modalSelector} button:last-child`)
|
|
|
|
} catch (err) {
|
|
|
|
// "next" button not found in welcome modal
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2022-12-16 03:33:24 +00:00
|
|
|
await delay(300)
|
2022-12-15 04:41:43 +00:00
|
|
|
} while (true)
|
|
|
|
|
|
|
|
if (!this.getIsAuthenticated()) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
if (this._minimize) {
|
|
|
|
await minimizePage(this._page)
|
|
|
|
}
|
2022-12-15 06:22:31 +00:00
|
|
|
|
2022-12-15 04:41:43 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-12-15 06:22:31 +00:00
|
|
|
_onRequest = (request: HTTPRequest) => {
|
|
|
|
const url = request.url()
|
|
|
|
if (!isRelevantRequest(url)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const method = request.method()
|
|
|
|
let body: any
|
|
|
|
|
|
|
|
if (method === 'POST') {
|
|
|
|
body = request.postData()
|
|
|
|
|
|
|
|
try {
|
|
|
|
body = JSON.parse(body)
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
// if (url.endsWith('/conversation') && typeof body === 'object') {
|
|
|
|
// const conversationBody: types.ConversationJSONBody = body
|
|
|
|
// const conversationId = conversationBody.conversation_id
|
|
|
|
// const parentMessageId = conversationBody.parent_message_id
|
|
|
|
// const messageId = conversationBody.messages?.[0]?.id
|
|
|
|
// const prompt = conversationBody.messages?.[0]?.content?.parts?.[0]
|
|
|
|
|
|
|
|
// // TODO: store this info for the current sendMessage request
|
|
|
|
// }
|
|
|
|
}
|
|
|
|
|
2022-12-15 06:44:13 +00:00
|
|
|
if (this._debug) {
|
|
|
|
console.log('\nrequest', {
|
|
|
|
url,
|
|
|
|
method,
|
|
|
|
headers: request.headers(),
|
|
|
|
body
|
|
|
|
})
|
|
|
|
}
|
2022-12-15 06:22:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_onResponse = async (response: HTTPResponse) => {
|
|
|
|
const request = response.request()
|
|
|
|
|
|
|
|
const url = response.url()
|
|
|
|
if (!isRelevantRequest(url)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-12-15 06:44:13 +00:00
|
|
|
const status = response.status()
|
|
|
|
|
2022-12-15 06:22:31 +00:00
|
|
|
let body: any
|
|
|
|
try {
|
|
|
|
body = await response.json()
|
|
|
|
} catch (_) {}
|
|
|
|
|
2022-12-15 06:44:13 +00:00
|
|
|
if (this._debug) {
|
|
|
|
console.log('\nresponse', {
|
|
|
|
url,
|
|
|
|
ok: response.ok(),
|
|
|
|
status,
|
|
|
|
statusText: response.statusText(),
|
|
|
|
headers: response.headers(),
|
|
|
|
body,
|
|
|
|
request: {
|
|
|
|
method: request.method(),
|
|
|
|
headers: request.headers(),
|
|
|
|
body: request.postData()
|
|
|
|
}
|
|
|
|
})
|
2022-12-15 06:22:31 +00:00
|
|
|
}
|
|
|
|
|
2022-12-15 06:44:13 +00:00
|
|
|
if (url.endsWith('/conversation')) {
|
|
|
|
if (status === 403) {
|
|
|
|
await this.handle403Error()
|
2022-12-15 06:22:31 +00:00
|
|
|
}
|
2022-12-15 06:44:13 +00:00
|
|
|
} else if (url.endsWith('api/auth/session')) {
|
|
|
|
if (status === 403) {
|
|
|
|
await this.handle403Error()
|
2022-12-16 06:28:30 +00:00
|
|
|
} else {
|
|
|
|
const session: types.SessionResult = body
|
|
|
|
|
|
|
|
if (session?.accessToken) {
|
|
|
|
this._accessToken = session.accessToken
|
|
|
|
}
|
2022-12-15 06:44:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async handle403Error() {
|
|
|
|
console.log(`ChatGPT "${this._email}" session expired; refreshing...`)
|
|
|
|
try {
|
|
|
|
await maximizePage(this._page)
|
|
|
|
await this._page.reload({
|
2022-12-16 03:33:24 +00:00
|
|
|
waitUntil: 'networkidle2',
|
2022-12-15 07:39:09 +00:00
|
|
|
timeout: 2 * 60 * 1000 // 2 minutes
|
2022-12-15 06:44:13 +00:00
|
|
|
})
|
2022-12-16 06:28:30 +00:00
|
|
|
if (this._minimize) {
|
|
|
|
await minimizePage(this._page)
|
|
|
|
}
|
2022-12-15 06:44:13 +00:00
|
|
|
} catch (err) {
|
|
|
|
console.error(
|
|
|
|
`ChatGPT "${this._email}" error refreshing session`,
|
|
|
|
err.toString()
|
|
|
|
)
|
|
|
|
}
|
2022-12-15 06:22:31 +00:00
|
|
|
}
|
2022-12-15 04:41:43 +00:00
|
|
|
|
|
|
|
async getIsAuthenticated() {
|
|
|
|
try {
|
|
|
|
const inputBox = await this._getInputBox()
|
|
|
|
return !!inputBox
|
|
|
|
} catch (err) {
|
|
|
|
// can happen when navigating during login
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
// async getLastMessage(): Promise<string | null> {
|
|
|
|
// const messages = await this.getMessages()
|
|
|
|
|
|
|
|
// if (messages) {
|
|
|
|
// return messages[messages.length - 1]
|
|
|
|
// } else {
|
|
|
|
// return null
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// async getPrompts(): Promise<string[]> {
|
|
|
|
// // Get all prompts
|
|
|
|
// const messages = await this._page.$$(
|
|
|
|
// '.text-base:has(.whitespace-pre-wrap):not(:has(button:nth-child(2))) .whitespace-pre-wrap'
|
|
|
|
// )
|
|
|
|
|
|
|
|
// // Prompts are always plaintext
|
|
|
|
// return Promise.all(messages.map((a) => a.evaluate((el) => el.textContent)))
|
|
|
|
// }
|
|
|
|
|
|
|
|
// async getMessages(): Promise<string[]> {
|
|
|
|
// // Get all complete messages
|
|
|
|
// // (in-progress messages that are being streamed back don't contain action buttons)
|
|
|
|
// const messages = await this._page.$$(
|
|
|
|
// '.text-base:has(.whitespace-pre-wrap):has(button:nth-child(2)) .whitespace-pre-wrap'
|
|
|
|
// )
|
|
|
|
|
|
|
|
// if (this._markdown) {
|
|
|
|
// const htmlMessages = await Promise.all(
|
|
|
|
// messages.map((a) => a.evaluate((el) => el.innerHTML))
|
|
|
|
// )
|
|
|
|
|
|
|
|
// const markdownMessages = htmlMessages.map((messageHtml) => {
|
|
|
|
// // parse markdown from message HTML
|
|
|
|
// messageHtml = messageHtml
|
|
|
|
// .replaceAll('Copy code</button>', '</button>')
|
|
|
|
// .replace(/Copy code\s*<\/button>/gim, '</button>')
|
|
|
|
|
|
|
|
// return html2md(messageHtml, {
|
|
|
|
// ignoreTags: [
|
|
|
|
// 'button',
|
|
|
|
// 'svg',
|
|
|
|
// 'style',
|
|
|
|
// 'form',
|
|
|
|
// 'noscript',
|
|
|
|
// 'script',
|
|
|
|
// 'meta',
|
|
|
|
// 'head'
|
|
|
|
// ],
|
|
|
|
// skipTags: ['button', 'svg']
|
|
|
|
// })
|
|
|
|
// })
|
|
|
|
|
|
|
|
// return markdownMessages
|
|
|
|
// } else {
|
|
|
|
// // plaintext
|
|
|
|
// const plaintextMessages = await Promise.all(
|
|
|
|
// messages.map((a) => a.evaluate((el) => el.textContent))
|
|
|
|
// )
|
|
|
|
// return plaintextMessages
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
async sendMessage(
|
|
|
|
message: string,
|
|
|
|
opts: types.SendMessageOptions = {}
|
|
|
|
): Promise<string> {
|
|
|
|
const {
|
|
|
|
conversationId,
|
|
|
|
parentMessageId = uuidv4(),
|
|
|
|
messageId = uuidv4(),
|
|
|
|
action = 'next',
|
|
|
|
// TODO
|
2022-12-16 07:05:33 +00:00
|
|
|
timeoutMs,
|
2022-12-16 06:28:30 +00:00
|
|
|
// onProgress,
|
|
|
|
onConversationResponse
|
|
|
|
} = opts
|
2022-12-15 04:41:43 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
const inputBox = await this._getInputBox()
|
|
|
|
if (!inputBox || !this._accessToken) {
|
|
|
|
const error = new types.ChatGPTError('Not signed in')
|
|
|
|
error.statusCode = 401
|
|
|
|
throw error
|
2022-12-15 04:41:43 +00:00
|
|
|
}
|
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
const url = `https://chat.openai.com/backend-api/conversation`
|
|
|
|
const body: types.ConversationJSONBody = {
|
|
|
|
action,
|
|
|
|
messages: [
|
|
|
|
{
|
|
|
|
id: messageId,
|
|
|
|
role: 'user',
|
|
|
|
content: {
|
|
|
|
content_type: 'text',
|
|
|
|
parts: [message]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
model: 'text-davinci-002-render',
|
|
|
|
parent_message_id: parentMessageId
|
|
|
|
}
|
2022-12-15 04:41:43 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
if (conversationId) {
|
|
|
|
body.conversation_id = conversationId
|
|
|
|
}
|
2022-12-15 04:41:43 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
// console.log('>>> EVALUATE', url, this._accessToken, body)
|
|
|
|
const result = await this._page.evaluate(
|
|
|
|
browserPostEventStream,
|
|
|
|
url,
|
|
|
|
this._accessToken,
|
2022-12-16 07:05:33 +00:00
|
|
|
body,
|
|
|
|
timeoutMs
|
2022-12-15 04:41:43 +00:00
|
|
|
)
|
2022-12-16 06:28:30 +00:00
|
|
|
// console.log('<<< EVALUATE', result)
|
2022-12-15 04:41:43 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
if (result.error) {
|
|
|
|
const error = new types.ChatGPTError(result.error.message)
|
|
|
|
error.statusCode = result.error.statusCode
|
|
|
|
error.statusText = result.error.statusText
|
2022-12-15 04:41:43 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
if (error.statusCode === 403) {
|
|
|
|
await this.handle403Error()
|
|
|
|
}
|
2022-12-15 04:41:43 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
throw error
|
2022-12-15 04:41:43 +00:00
|
|
|
}
|
2022-12-15 07:35:09 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
// TODO: support sending partial response events
|
|
|
|
if (onConversationResponse) {
|
|
|
|
onConversationResponse(result.conversationResponse)
|
2022-12-15 06:24:03 +00:00
|
|
|
}
|
2022-12-15 04:41:43 +00:00
|
|
|
|
2022-12-16 06:28:30 +00:00
|
|
|
return result.response
|
|
|
|
|
|
|
|
// const lastMessage = await this.getLastMessage()
|
|
|
|
|
|
|
|
// await inputBox.focus()
|
|
|
|
// const paragraphs = message.split('\n')
|
|
|
|
// for (let i = 0; i < paragraphs.length; i++) {
|
|
|
|
// await inputBox.type(paragraphs[i], { delay: 0 })
|
|
|
|
// if (i < paragraphs.length - 1) {
|
|
|
|
// await this._page.keyboard.down('Shift')
|
|
|
|
// await inputBox.press('Enter')
|
|
|
|
// await this._page.keyboard.up('Shift')
|
|
|
|
// } else {
|
|
|
|
// await inputBox.press('Enter')
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// const responseP = new Promise<string>(async (resolve, reject) => {
|
|
|
|
// try {
|
|
|
|
// do {
|
|
|
|
// await delay(1000)
|
|
|
|
|
|
|
|
// // TODO: this logic needs some work because we can have repeat messages...
|
|
|
|
// const newLastMessage = await this.getLastMessage()
|
|
|
|
// if (
|
|
|
|
// newLastMessage &&
|
|
|
|
// lastMessage?.toLowerCase() !== newLastMessage?.toLowerCase()
|
|
|
|
// ) {
|
|
|
|
// return resolve(newLastMessage)
|
|
|
|
// }
|
|
|
|
// } while (true)
|
|
|
|
// } catch (err) {
|
|
|
|
// return reject(err)
|
|
|
|
// }
|
|
|
|
// })
|
|
|
|
|
|
|
|
// if (timeoutMs) {
|
|
|
|
// return pTimeout(responseP, {
|
|
|
|
// milliseconds: timeoutMs
|
|
|
|
// })
|
|
|
|
// } else {
|
|
|
|
// return responseP
|
|
|
|
// }
|
2022-12-15 04:41:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async resetThread() {
|
2022-12-16 23:07:27 +00:00
|
|
|
try {
|
|
|
|
await this._page.click('nav > a:nth-child(1)')
|
|
|
|
} catch (err) {
|
|
|
|
// ignore for now
|
|
|
|
}
|
2022-12-15 04:41:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async close() {
|
|
|
|
await this._browser.close()
|
|
|
|
this._page = null
|
|
|
|
this._browser = null
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async _getInputBox() {
|
|
|
|
// [data-id="root"]
|
|
|
|
return this._page.$('textarea')
|
|
|
|
}
|
|
|
|
}
|