///
///
///
///
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;
}
};