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.

268 lines
6.7 KiB

import Chat from "./Chat.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;
/**
* 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
) {
if (this.#client)
options = {
client: this.#client,
...options,
};
return await fetch(input, options);
}
/**
* 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/info/",
{
body: JSON.stringify({
external_id: id,
}),
method: "POST",
headers: {
Authorization: `Token ${this.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
}
);
const { character } = await 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 { 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);
}
}