Initial commit
commit
1aeb95dc16
@ -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…
Reference in new issue