You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

294 lines
7.4 KiB

import Chat from "./Chat.ts";
import { wrapFetch } from "https://deno.land/x/another_cookiejar@v5.0.2/mod.ts";
interface FetchOptions extends RequestInit {
client?: Deno.HttpClient;
}
interface Category {
name: string;
description: string;
}
interface Featured {
characters: string[];
name: string;
}
interface CharacterInfo {
external_id: string;
title: string;
name: string;
visibility: string;
copyable: false;
greeting: string;
description: string;
identifier: string;
avatar_file_name: string;
songs: unknown[];
img_gen_enabled: false;
base_img_prompt: string;
img_prompt_regex: string;
strip_img_prompt_from_msg: boolean;
user__username: string;
participant__name: string;
participant__num_interactions: number;
participant__user__username: string;
voice_id: number;
usage: string;
}
interface SearchResult {
external_id: string;
title: string;
greeting: string;
description: string;
avatar_file_name: string;
visibility: string;
copyable: boolean;
participant__name: string;
participant__num_interactions: number;
user__id: number;
user__username: string;
img_gen_enabled: boolean;
priority: number;
search_score: number;
}
export default class Client {
public token?: string;
#client?: Deno.HttpClient;
#cookieFetch = wrapFetch();
/**
* Initalizes a HTTP(S) proxy for each request (Requires --unstable)
* @param proxyUrl URL of the proxy
* @param certPath Path to a certificate file
*/
public async intitalizeProxy(proxyUrl: string, certPath: string | URL) {
this.#client = Deno.createHttpClient({
caCerts: [await Deno.readTextFile(certPath)],
proxy: { url: proxyUrl },
});
}
async fetch(
input: string | URL | Request,
options?: FetchOptions | undefined,
retryUntilSuccess = true,
) {
if (this.#client) {
options = {
client: this.#client,
...options,
};
}
let res = await this.#cookieFetch(input, options);
let resText = await res.clone().text();
while (
!resText.startsWith("{") && !resText.startsWith("[") && retryUntilSuccess
) {
console.log("Failed request! Retrying...");
await new Promise((resolve) => setTimeout(resolve, 1000));
res = await this.#cookieFetch(input, options);
resText = await res.clone().text();
}
return {
body: res.body,
text: () => res,
json: () => JSON.parse(resText),
};
}
/**
* Authenticates with character.ai
* @param authToken The authorization token
*/
public async authenticate(authToken: string) {
const res = await this.fetch(
"https://beta.character.ai/dj-rest-auth/auth0/",
{
body: JSON.stringify({
access_token: authToken,
}),
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
},
);
const json = await res.json();
this.token = json.key;
if (!this.token) {
throw new Error("Authentication failed, your token might be invalid");
}
}
/**
* Fetches a list of categories
* @returns List of categories
*/
public async fetchCategories(): Promise<Category[]> {
const res = await this.fetch(
"https://beta.character.ai/chat/character/categories/",
);
const { categories } = await res.json();
if (!Array.isArray(categories)) {
throw new Error("API provided invalid category data");
}
return categories;
}
/**
* Fetches a list of featured characters
* @returns List of featured characters
*/
public async fetchFeaturedCharacters(): Promise<Featured[]> {
const res = await this.fetch(
"https://beta.character.ai/chat/characters/featured/",
{
headers: {
Authorization: `Token ${this.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
},
);
const { featured_characters } = await res.json();
if (!Array.isArray(featured_characters)) {
throw new Error("API provided invalid featured characters data");
}
return featured_characters;
}
/**
* Fetches information for a character
* @param id Id of the character
* @returns Character information
*/
public async fetchCharacterInfo(id: string): Promise<CharacterInfo> {
const res = await this.fetch(
"https://beta.character.ai/chat/character/",
{
body: JSON.stringify({
external_id: id,
}),
method: "POST",
headers: {
Authorization: `Token ${this.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
},
);
const { character } = res.json();
return character;
}
/**
* Search for characters
* @param query Search query
* @returns Array of characters
*/
public async search(query: string): Promise<SearchResult[]> {
const url = new URL(
"https://beta.character.ai/chat/characters/search/",
);
url.searchParams.set("query", query);
const res = await this.fetch(url, {
headers: {
Authorization: `Token ${this.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
});
const resText = await res.text();
const { characters } = await res.json();
if (!Array.isArray(characters)) {
throw new Error("API provided invalid search response");
}
return characters;
}
/**
* Creates a new chat history
* @param id Character id
* @returns The raw history to be used with the Chat class
*/
public async createChat(id: string) {
const res = await this.fetch(
"https://beta.character.ai/chat/history/create/",
{
body: JSON.stringify({
character_external_id: id,
history_external_id: null,
}),
method: "POST",
headers: {
Authorization: `Token ${this.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
},
);
const history = await res.json();
return history;
}
/**
* Continues a chat
* @param id Character id
* @returns The raw history to be used with the Chat class or false if the chat doesn't exist
*/
public async continueChat(id: string): Promise<Chat | false> {
const res = await this.fetch(
"https://beta.character.ai/chat/history/continue/",
{
body: JSON.stringify({
character_external_id: id,
history_external_id: null,
}),
method: "POST",
headers: {
Authorization: `Token ${this.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
},
);
const history = await res.json();
if (history.status === "No Such History") return false;
return history;
}
/**
* Continues a chat or creates a new one if it doesn't exist
* @param id Character id
* @returns The Chat class
*/
public async continueOrCreateChat(id: string): Promise<Chat> {
let history = await this.continueChat(id);
if (!history) history = await this.createChat(id);
return new Chat(this, id, history);
}
}