/// /// /// /// declare const GM_fetch: typeof fetch; export interface Author { role: "assistant" | "user"; name: string | null; metadata: Record; } export interface AsisstantMessage { id: string; author: Author; /** UNIX TIME */ create_time: number | null; /** UNIX TIME */ update_time: number | null; content: { content_type: "text", parts: [string]; }; end_turn: null; weight: 1; metadata: { message_type: "next"; model_slug: "text-davinci-002-render-sha"; }; recipient: "all"; } export interface MessageResponse { conversation_id: string; message: AsisstantMessage; error: unknown; } export interface MakeConversationOptions { conversationId: string; parentMessageId: string; } export interface Message { id: string; role: "user"; content: { content_type: "text", parts: [string]; }; } export interface MessageRequest { action: "next"; conversation_id?: string; messages: [Message]; model: "text-davinci-002-render", parent_message_id: string; } export const makeConversation = async ( question: string, options?: MakeConversationOptions ): Promise, ErrorLike >> => { const result = await getAccessToken(); if (!result.ok) return result; const message: MessageRequest = { action: "next", messages: [ { id: uuid(), role: "user", content: { content_type: "text", parts: [question], }, }, ], model: "text-davinci-002-render", parent_message_id: options?.parentMessageId ?? uuid(), }; if (options) message.conversation_id = options.conversationId; const res = await GM_fetch("https://chat.openai.com/backend-api/conversation", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${result.value}`, Referrer: "https://chat.openai.com", }, body: JSON.stringify(message), }); if (!res.ok) { switch (res.status) { case 401: accessToken = ""; return makeUnauthorizedError(); case 429: return makeTooManyRequestsError(); } } if (!res.body) throw Error("No content in Response of https://chat.openai.com/backend-api/conversation"); return { ok: true, value: async function*() { let stack = ""; // @ts-ignore まだdomの型定義にasync iteratorが実装されていない for await (const value of res.body) { stack += String.fromCharCode(...value); const items = stack.split(/\n\n/).map((item) => item.replace(/^data: /, "")); // \n\nがなければ、読み込み継続 if (items.length < 2) continue; // まだ継続するときは、それを残す。末尾が"\n\n"のときは空文字になるので、prevResはまっさらな状態となる stack = items.pop()!; for (const item of items) { if (item === "[DONE]") return; try { yield JSON.parse(item); } catch (e: unknown) { if (!(e instanceof SyntaxError)) throw e; console.error(e); } } if (stack === "[DONE]") return; } }(), }; }; export interface Conversation { title: string; create_time: number; mapping: Record; moderation_results: []; /** 一番最後の会話のID */ current_node: string; } export const getConversation = async (conversationId: string): Promise> => { const result = await getAccessToken(); if (!result.ok) return result; const res = await GM_fetch( `https://chat.openai.com/backend-api/conversation/${conversationId}`, { headers: { "Content-Type": "application/json", Authorization: `Bearer ${result.value}`, Referrer: "https://chat.openai.com", }, } ); if (!res.ok) { switch (res.status) { case 401: accessToken = ""; return makeUnauthorizedError(); case 429: return makeTooManyRequestsError(); } } const value = await res.json(); return { ok: false, value }; }; let accessToken = ""; let expired = 0; export const getAccessToken = async (): Promise> => { if (accessToken && expired > new Date().getTime()) return { ok: true, value: accessToken, }; const res = await GM_fetch("https://chat.openai.com/api/auth/session"); const text = await res.text(); if (isBlockedByCloudflare(text)) return makeBlockedByCloudflareError(); const { accessToken: token, expires } = JSON.parse(text); if (!token) return makeUnauthorizedError(); if (expires) expired = new Date(expires).getTime(); accessToken = token; return { ok: true, value: token }; }; const uuid = (): string => { const t = [ "a", "b", "c", "d", "e", "f", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ]; const e: string[] = []; for (let n = 0; n < 36; n++) { e[n] = n === 8 || n === 13 || n === 18 || n === 23 ? "-" : t[Math.ceil(Math.random() * t.length - 1)]; } return e.join(""); }; export type Result = { ok: true; value: T } | { ok: false; value: E }; export interface ErrorLike { name: string; message: string; } export interface TooManyRequestsError { name: "TooManyRequestsError", message: string; } export interface UnauthorizedError { name: "UnauthorizedError"; message: string; } export interface BlockedByCloudflareError { name: "BlockedByCloudflareError"; message: string; } const makeUnauthorizedError = () => ({ ok: false, value: { name: "UnauthorizedError", message: "Please log in https://chat.openai.com." } }) as const; const makeTooManyRequestsError = () => ({ ok: false, value: { name: "TooManyRequestsError", message: "Too many request." } }) as const; const makeBlockedByCloudflareError = () => ({ ok: false, value: { name: "BlockedByCloudflareError", message: "Please pass Cloudflare security check at https://chat.openai.com." } }) as const; const isBlockedByCloudflare = (responseText: string): boolean => { try { const html = new DOMParser().parseFromString(responseText, "text/html"); if (!html) return false; // cloudflare html be like: https://github.com/zhengbangbo/chat-gpt-userscript/blob/512892caabef2820a3dc3ddfbcf5464fc63c405a/parse.js const title = html.querySelector("title"); if (!title) return false; return title.innerText === "Just a moment..."; } catch (_: unknown) { return false; } };