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