{"version":3,"sources":["../src/chatgpt-api.ts","../src/utils.ts"],"sourcesContent":["import { createParser } from 'eventsource-parser'\r\nimport ExpiryMap from 'expiry-map'\r\nimport fetch from 'node-fetch'\r\nimport { v4 as uuidv4 } from 'uuid'\r\n\r\nimport * as types from './types'\r\nimport { markdownToText } from './utils'\r\n\r\nconst KEY_ACCESS_TOKEN = 'accessToken'\r\nconst USER_AGENT =\r\n  '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'\r\n\r\nexport class ChatGPTAPI {\r\n  protected _sessionToken: string\r\n  protected _markdown: boolean\r\n  protected _apiBaseUrl: string\r\n  protected _backendApiBaseUrl: string\r\n  protected _userAgent: string\r\n\r\n  // stores access tokens for up to 10 seconds before needing to refresh\r\n  protected _accessTokenCache = new ExpiryMap<string, string>(10 * 1000)\r\n\r\n  /**\r\n   * Creates a new client wrapper around the unofficial ChatGPT REST API.\r\n   *\r\n   * @param opts.sessionToken = **Required** OpenAI session token which can be found in a valid session's cookies (see readme for instructions)\r\n   * @param apiBaseUrl - Optional override; the base URL for ChatGPT webapp's API (`/api`)\r\n   * @param backendApiBaseUrl - Optional override; the base URL for the ChatGPT backend API (`/backend-api`)\r\n   * @param userAgent - Optional override; the `user-agent` header to use with ChatGPT requests\r\n   */\r\n  constructor(opts: {\r\n    sessionToken: string\r\n\r\n    /** @defaultValue `true` **/\r\n    markdown?: boolean\r\n\r\n    /** @defaultValue `'https://chat.openai.com/api'` **/\r\n    apiBaseUrl?: string\r\n\r\n    /** @defaultValue `'https://chat.openai.com/backend-api'` **/\r\n    backendApiBaseUrl?: string\r\n\r\n    /** @defaultValue `'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'` **/\r\n    userAgent?: string\r\n  }) {\r\n    const {\r\n      sessionToken,\r\n      markdown = true,\r\n      apiBaseUrl = 'https://chat.openai.com/api',\r\n      backendApiBaseUrl = 'https://chat.openai.com/backend-api',\r\n      userAgent = USER_AGENT\r\n    } = opts\r\n\r\n    this._sessionToken = sessionToken\r\n    this._markdown = !!markdown\r\n    this._apiBaseUrl = apiBaseUrl\r\n    this._backendApiBaseUrl = backendApiBaseUrl\r\n    this._userAgent = userAgent\r\n\r\n    if (!this._sessionToken) {\r\n      throw new Error('ChatGPT invalid session token')\r\n    }\r\n  }\r\n\r\n  async getIsAuthenticated() {\r\n    try {\r\n      void (await this.refreshAccessToken())\r\n      return true\r\n    } catch (err) {\r\n      return false\r\n    }\r\n  }\r\n\r\n  async ensureAuth() {\r\n    return await this.refreshAccessToken()\r\n  }\r\n\r\n  /**\r\n   * Sends a message to ChatGPT, waits for the response to resolve, and returns\r\n   * the response.\r\n   *\r\n   * @param message - The plaintext message to send.\r\n   * @param opts.conversationId - Optional ID of the previous message in a conversation\r\n   * @param opts.onProgress - Optional listener which will be called every time the partial response is updated\r\n   */\r\n  async sendMessage(\r\n    message: string,\r\n    opts: {\r\n      converstationId?: string\r\n      onProgress?: (partialResponse: string) => void\r\n    } = {}\r\n  ): Promise<string> {\r\n    const { converstationId = uuidv4(), onProgress } = opts\r\n\r\n    const accessToken = await this.refreshAccessToken()\r\n\r\n    const body: types.ConversationJSONBody = {\r\n      action: 'next',\r\n      messages: [\r\n        {\r\n          id: uuidv4(),\r\n          role: 'user',\r\n          content: {\r\n            content_type: 'text',\r\n            parts: [message]\r\n          }\r\n        }\r\n      ],\r\n      model: 'text-davinci-002-render',\r\n      parent_message_id: converstationId\r\n    }\r\n\r\n    const url = `${this._backendApiBaseUrl}/conversation`\r\n\r\n    // TODO: What's the best way to differentiate btwn wanting just the response text\r\n    // versus wanting the full response message, so you can extract the ID and other\r\n    // metadata?\r\n    // let fullResponse: types.Message = null\r\n    let response = ''\r\n\r\n    return new Promise((resolve, reject) => {\r\n      this._fetchSSE(url, {\r\n        method: 'POST',\r\n        headers: {\r\n          Authorization: `Bearer ${accessToken}`,\r\n          'Content-Type': 'application/json',\r\n          'user-agent': this._userAgent\r\n        },\r\n        body: JSON.stringify(body),\r\n        onMessage: (data: string) => {\r\n          if (data === '[DONE]') {\r\n            return resolve(response)\r\n          }\r\n\r\n          try {\r\n            const parsedData: types.ConversationResponseEvent = JSON.parse(data)\r\n            const message = parsedData.message\r\n            // console.log('event', JSON.stringify(parsedData, null, 2))\r\n\r\n            if (message) {\r\n              let text = message?.content?.parts?.[0]\r\n\r\n              if (text) {\r\n                if (!this._markdown) {\r\n                  text = markdownToText(text)\r\n                }\r\n\r\n                response = text\r\n                // fullResponse = message\r\n\r\n                if (onProgress) {\r\n                  onProgress(text)\r\n                }\r\n              }\r\n            }\r\n          } catch (err) {\r\n            console.warn('fetchSSE onMessage unexpected error', err)\r\n            reject(err)\r\n          }\r\n        }\r\n      }).catch(reject)\r\n    })\r\n  }\r\n\r\n  async refreshAccessToken(): Promise<string> {\r\n    const cachedAccessToken = this._accessTokenCache.get(KEY_ACCESS_TOKEN)\r\n    if (cachedAccessToken) {\r\n      return cachedAccessToken\r\n    }\r\n\r\n    try {\r\n      const res = await fetch('https://chat.openai.com/api/auth/session', {\r\n        headers: {\r\n          cookie: `__Secure-next-auth.session-token=${this._sessionToken}`,\r\n          'user-agent': this._userAgent\r\n        }\r\n      }).then((r) => r.json() as any as types.SessionResult)\r\n\r\n      const accessToken = res?.accessToken\r\n\r\n      if (!accessToken) {\r\n        console.warn('no auth token', res)\r\n        throw new Error('Unauthorized')\r\n      }\r\n\r\n      this._accessTokenCache.set(KEY_ACCESS_TOKEN, accessToken)\r\n      return accessToken\r\n    } catch (err: any) {\r\n      throw new Error(`ChatGPT failed to refresh auth token: ${err.toString()}`)\r\n    }\r\n  }\r\n\r\n  protected async _fetchSSE(\r\n    url: string,\r\n    options: Parameters<typeof fetch>[1] & { onMessage: (data: string) => void }\r\n  ) {\r\n    const { onMessage, ...fetchOptions } = options\r\n    const resp = await fetch(url, fetchOptions)\r\n    const parser = createParser((event) => {\r\n      if (event.type === 'event') {\r\n        onMessage(event.data)\r\n      }\r\n    })\r\n\r\n    resp.body.on('readable', () => {\r\n      let chunk: string | Buffer\r\n      while (null !== (chunk = resp.body.read())) {\r\n        parser.feed(chunk.toString())\r\n      }\r\n    })\r\n  }\r\n}\r\n","import remark from 'remark'\r\nimport stripMarkdown from 'strip-markdown'\r\n\r\nexport function markdownToText(markdown?: string): string {\r\n  return remark()\r\n    .use(stripMarkdown)\r\n    .processSync(markdown ?? '')\r\n    .toString()\r\n}\r\n"],"mappings":"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","names":["createParser","ExpiryMap","fetch","uuidv4","remark","stripMarkdown","markdownToText","markdown","KEY_ACCESS_TOKEN","USER_AGENT","ChatGPTAPI","opts","ExpiryMap","sessionToken","markdown","apiBaseUrl","backendApiBaseUrl","userAgent","message","converstationId","uuidv4","onProgress","accessToken","body","url","response","resolve","reject","data","_a","_b","text","markdownToText","err","cachedAccessToken","res","fetch","options","onMessage","fetchOptions","resp","parser","createParser","event","chunk"]}