Initial commit

master
Tymon 1 year ago
commit 1aeb95dc16

2
.gitignore vendored

@ -0,0 +1,2 @@
cert.pem
test.ts

@ -0,0 +1,8 @@
{
"deno.enable": true,
"deno.unstable": true,
"[typescript]": {
"editor.tabSize": 2,
"editor.detectIndentation": false
}
}

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

@ -0,0 +1,3 @@
# chaideno (name pending (probably forever))
The most blazingly good (not really) character.ai library for Deno!

@ -0,0 +1,8 @@
{
"fmt": {
"options": {
"indentWidth": 2,
"useTabs": false
}
}
}

@ -0,0 +1,4 @@
import Client from "./src/Client.ts";
import Chat from "./src/Chat.ts";
export { Client, Chat };

@ -0,0 +1,139 @@
// deno-lint-ignore-file no-explicit-any
import Client from "./Client.ts";
interface MessageHistory {
messages: {
id: number;
text: string;
src__name: string;
src__user__username: string;
src__user__id: number;
tgt__user__id: number;
src__is_human: boolean;
src__character__avatar_file_name: string | null;
is_alternative: boolean;
image_rel_path: string;
image_prompt_text: string;
image_description_type: string;
responsible_user__username: string | null;
deleted: boolean | null; // This is probably a boolean???
src_char: {
participant: { name: string };
avatar_file_name: string | null;
};
}[];
has_more: boolean;
next_page: number;
}
interface Reply {
replies: {
text: string;
id: number;
}[];
src_char: {
participant: { name: string };
avatar_file_name: string;
};
is_final_chunk: boolean;
last_user_msg_id: number;
}
export default class Chat {
#client: Client;
public characterId: string;
public characterInternalId: string;
public historyId: string;
constructor(client: Client, id: string, history: any) {
this.#client = client;
this.characterId = id;
this.historyId = history.external_id;
this.characterInternalId = history.participants.find(
(participant: any) => participant.is_human === false
).user.username;
}
/**
* Fetches message history
* @returns Message history
*/
public async fetchMessageHistory(): Promise<MessageHistory> {
const url = new URL("https://beta.character.ai/chat/history/msgs/user/");
url.searchParams.set("history_external_id", this.historyId);
const res = await this.#client.fetch(url, {
headers: {
Authorization: `Token ${this.#client.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
});
const messageHistory = await res.json();
return messageHistory;
}
/**
* Sends a message to the character
* @param message The message you want to send
* @returns Object with replies
*/
public async sendMessage(message: string): Promise<Reply> {
const payload = {
history_external_id: this.historyId,
character_external_id: this.characterId,
text: message,
tgt: this.characterInternalId,
ranking_method: "random",
faux_chat: false,
staging: false,
model_server_address: null,
override_prefix: null,
override_rank: null,
rank_candidates: null,
filter_candidates: null,
prefix_limit: null,
prefix_token_limit: null,
livetune_coeff: null,
stream_params: null,
enable_tti: true,
initial_timeout: null,
insert_beginning: null,
translate_candidates: null,
stream_every_n_steps: 16,
chunks_to_pad: 8,
is_proactive: false,
};
const res = await this.#client.fetch(
"https://beta.character.ai/chat/streaming/",
{
body: JSON.stringify(payload),
method: "POST",
headers: {
Authorization: `Token ${this.#client.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
}
);
const reader = res.body?.getReader();
if (!reader) throw new Error("How."); // Let's hope no one ever sees this
let reply = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
reply = new TextDecoder("utf-8").decode(value);
}
return JSON.parse(reply);
}
}

@ -0,0 +1,267 @@
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);
}
}
Loading…
Cancel
Save