mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
2584 lines
86 KiB
JavaScript
2584 lines
86 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
const process = require('process')
|
||
const yargs = require('yargs/yargs');
|
||
const { hideBin } = require('yargs/helpers');
|
||
|
||
const cliArguments = yargs(hideBin(process.argv))
|
||
.option('ssl', {
|
||
type: 'boolean',
|
||
default: false,
|
||
describe: 'Enables SSL'
|
||
}).option('certPath', {
|
||
type: 'string',
|
||
default: 'certs/cert.pem',
|
||
describe: 'Path to your certificate file.'
|
||
}).option('keyPath', {
|
||
type: 'string',
|
||
default: 'certs/privkey.pem',
|
||
describe: 'Path to your private key file.'
|
||
}).argv;
|
||
|
||
// change all relative paths
|
||
process.chdir(__dirname)
|
||
|
||
const express = require('express');
|
||
const compression = require('compression');
|
||
const app = express();
|
||
app.use(compression());
|
||
|
||
const fs = require('fs');
|
||
const readline = require('readline');
|
||
const open = require('open');
|
||
|
||
const rimraf = require("rimraf");
|
||
const multer = require("multer");
|
||
const http = require("http");
|
||
const https = require('https');
|
||
const basicAuthMiddleware = require('./src/middleware/basicAuthMiddleware');
|
||
//const PNG = require('pngjs').PNG;
|
||
const extract = require('png-chunks-extract');
|
||
const encode = require('png-chunks-encode');
|
||
const PNGtext = require('png-chunk-text');
|
||
|
||
const jimp = require('jimp');
|
||
const path = require('path');
|
||
const sanitize = require('sanitize-filename');
|
||
const mime = require('mime-types');
|
||
|
||
const cookieParser = require('cookie-parser');
|
||
const crypto = require('crypto');
|
||
const ipaddr = require('ipaddr.js');
|
||
const json5 = require('json5');
|
||
|
||
const ExifReader = require('exifreader');
|
||
const exif = require('piexifjs');
|
||
const webp = require('webp-converter');
|
||
const DeviceDetector = require("device-detector-js");
|
||
const { TextEncoder, TextDecoder } = require('util');
|
||
const utf8Encode = new TextEncoder();
|
||
const utf8Decode = new TextDecoder('utf-8', { ignoreBOM: true });
|
||
|
||
const config = require(path.join(__dirname, './config.conf'));
|
||
const server_port = process.env.SILLY_TAVERN_PORT || config.port;
|
||
|
||
const whitelistPath = path.join(__dirname, "./whitelist.txt");
|
||
let whitelist = config.whitelist;
|
||
|
||
if (fs.existsSync(whitelistPath)) {
|
||
try {
|
||
let whitelistTxt = fs.readFileSync(whitelistPath, 'utf-8');
|
||
whitelist = whitelistTxt.split("\n").filter(ip => ip).map(ip => ip.trim());
|
||
} catch (e) { }
|
||
}
|
||
|
||
const whitelistMode = config.whitelistMode;
|
||
const autorun = config.autorun && !cliArguments.ssl;
|
||
const enableExtensions = config.enableExtensions;
|
||
const listen = config.listen;
|
||
|
||
const axios = require('axios');
|
||
const tiktoken = require('@dqbd/tiktoken');
|
||
const WebSocket = require('ws');
|
||
|
||
var Client = require('node-rest-client').Client;
|
||
var client = new Client();
|
||
|
||
client.on('error', (err) => {
|
||
console.error('An error occurred:', err);
|
||
});
|
||
|
||
let poe = require('./poe-client');
|
||
|
||
var api_server = "http://0.0.0.0:5000";
|
||
var api_novelai = "https://api.novelai.net";
|
||
let api_openai = "https://api.openai.com/v1";
|
||
var main_api = "kobold";
|
||
|
||
var response_get_story;
|
||
var response_generate;
|
||
var response_generate_novel;
|
||
var request_promt;
|
||
var response_promt;
|
||
var characters = {};
|
||
var character_i = 0;
|
||
var response_create;
|
||
var response_edit;
|
||
var response_dw_bg;
|
||
var response_getstatus;
|
||
var response_getstatus_novel;
|
||
var response_getlastversion;
|
||
var api_key_novel;
|
||
|
||
let response_generate_openai;
|
||
let response_getstatus_openai;
|
||
let api_key_openai;
|
||
|
||
//RossAscends: Added function to format dates used in files and chat timestamps to a humanized format.
|
||
//Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected.
|
||
//During testing, this performs the same as previous date.now() structure.
|
||
//It also does not break old characters/chats, as the code just uses whatever timestamp exists in the chat.
|
||
//New chats made with characters will use this new formatting.
|
||
//Useable variable is (( humanizedISO8601Datetime ))
|
||
|
||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||
|
||
const { SentencePieceProcessor, cleanText } = require("./src/sentencepiece/sentencepiece.min.js");
|
||
let spp = new SentencePieceProcessor();
|
||
|
||
async function countTokensLlama(text) {
|
||
let cleaned = cleanText(text);
|
||
|
||
let ids = spp.encodeIds(cleaned);
|
||
return ids.length;
|
||
}
|
||
|
||
function humanizedISO8601DateTime() {
|
||
let baseDate = new Date(Date.now());
|
||
let humanYear = baseDate.getFullYear();
|
||
let humanMonth = (baseDate.getMonth() + 1);
|
||
let humanDate = baseDate.getDate();
|
||
let humanHour = (baseDate.getHours() < 10 ? '0' : '') + baseDate.getHours();
|
||
let humanMinute = (baseDate.getMinutes() < 10 ? '0' : '') + baseDate.getMinutes();
|
||
let humanSecond = (baseDate.getSeconds() < 10 ? '0' : '') + baseDate.getSeconds();
|
||
let humanMillisecond = (baseDate.getMilliseconds() < 10 ? '0' : '') + baseDate.getMilliseconds();
|
||
let HumanizedDateTime = (humanYear + "-" + humanMonth + "-" + humanDate + " @" + humanHour + "h " + humanMinute + "m " + humanSecond + "s " + humanMillisecond + "ms");
|
||
return HumanizedDateTime;
|
||
};
|
||
|
||
var is_colab = process.env.colaburl !== undefined;
|
||
var charactersPath = 'public/characters/';
|
||
var chatsPath = 'public/chats/';
|
||
const jsonParser = express.json({ limit: '100mb' });
|
||
const urlencodedParser = express.urlencoded({ extended: true, limit: '100mb' });
|
||
const baseRequestArgs = { headers: { "Content-Type": "application/json" } };
|
||
const directories = {
|
||
worlds: 'public/worlds/',
|
||
avatars: 'public/User Avatars',
|
||
groups: 'public/groups/',
|
||
groupChats: 'public/group chats',
|
||
chats: 'public/chats/',
|
||
characters: 'public/characters/',
|
||
backgrounds: 'public/backgrounds',
|
||
novelAI_Settings: 'public/NovelAI Settings',
|
||
koboldAI_Settings: 'public/KoboldAI Settings',
|
||
openAI_Settings: 'public/OpenAI Settings',
|
||
textGen_Settings: 'public/TextGen Settings',
|
||
thumbnails: 'thumbnails/',
|
||
thumbnailsBg: 'thumbnails/bg/',
|
||
thumbnailsAvatar: 'thumbnails/avatar/',
|
||
themes: 'public/themes',
|
||
extensions: 'public/scripts/extensions'
|
||
};
|
||
|
||
// CSRF Protection //
|
||
const doubleCsrf = require('csrf-csrf').doubleCsrf;
|
||
|
||
const CSRF_SECRET = crypto.randomBytes(8).toString('hex');
|
||
const COOKIES_SECRET = crypto.randomBytes(8).toString('hex');
|
||
|
||
const { invalidCsrfTokenError, generateToken, doubleCsrfProtection } = doubleCsrf({
|
||
getSecret: () => CSRF_SECRET,
|
||
cookieName: "X-CSRF-Token",
|
||
cookieOptions: {
|
||
httpOnly: true,
|
||
sameSite: "strict",
|
||
secure: false
|
||
},
|
||
size: 64,
|
||
getTokenFromRequest: (req) => req.headers["x-csrf-token"]
|
||
});
|
||
|
||
app.get("/csrf-token", (req, res) => {
|
||
res.json({
|
||
"token": generateToken(res)
|
||
});
|
||
});
|
||
|
||
app.use(cookieParser(COOKIES_SECRET));
|
||
app.use(doubleCsrfProtection);
|
||
|
||
// CORS Settings //
|
||
const cors = require('cors');
|
||
const CORS = cors({
|
||
origin: 'null',
|
||
methods: ['OPTIONS']
|
||
});
|
||
|
||
app.use(CORS);
|
||
|
||
if (listen && config.basicAuthMode) app.use(basicAuthMiddleware);
|
||
|
||
app.use(function (req, res, next) { //Security
|
||
let clientIp = req.connection.remoteAddress;
|
||
let ip = ipaddr.parse(clientIp);
|
||
// Check if the IP address is IPv4-mapped IPv6 address
|
||
if (ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) {
|
||
const ipv4 = ip.toIPv4Address().toString();
|
||
clientIp = ipv4;
|
||
} else {
|
||
clientIp = ip;
|
||
clientIp = clientIp.toString();
|
||
}
|
||
|
||
//clientIp = req.connection.remoteAddress.split(':').pop();
|
||
if (whitelistMode === true && !whitelist.includes(clientIp)) {
|
||
console.log('Forbidden: Connection attempt from ' + clientIp + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.conf in root of SillyTavern folder.\n');
|
||
return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + clientIp + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.conf in root of SillyTavern folder.');
|
||
}
|
||
next();
|
||
});
|
||
|
||
app.use((req, res, next) => {
|
||
if (req.url.startsWith('/characters/') && is_colab && process.env.googledrive == 2) {
|
||
|
||
const filePath = path.join(charactersPath, decodeURIComponent(req.url.substr('/characters'.length)));
|
||
console.log('req.url: ' + req.url);
|
||
console.log(filePath);
|
||
fs.access(filePath, fs.constants.R_OK, (err) => {
|
||
if (!err) {
|
||
res.sendFile(filePath, { root: __dirname });
|
||
} else {
|
||
res.send('Character not found: ' + filePath);
|
||
//next();
|
||
}
|
||
});
|
||
} else {
|
||
next();
|
||
}
|
||
});
|
||
|
||
app.use(express.static(__dirname + "/public", { refresh: true }));
|
||
|
||
app.use('/backgrounds', (req, res) => {
|
||
const filePath = decodeURIComponent(path.join(__dirname, 'public/backgrounds', req.url.replace(/%20/g, ' ')));
|
||
fs.readFile(filePath, (err, data) => {
|
||
if (err) {
|
||
res.status(404).send('File not found');
|
||
return;
|
||
}
|
||
//res.contentType('image/jpeg');
|
||
res.send(data);
|
||
});
|
||
});
|
||
|
||
app.use('/characters', (req, res) => {
|
||
const filePath = decodeURIComponent(path.join(__dirname, charactersPath, req.url.replace(/%20/g, ' ')));
|
||
fs.readFile(filePath, (err, data) => {
|
||
if (err) {
|
||
res.status(404).send('File not found');
|
||
return;
|
||
}
|
||
res.send(data);
|
||
});
|
||
});
|
||
app.use(multer({ dest: "uploads" }).single("avatar"));
|
||
app.get("/", function (request, response) {
|
||
response.sendFile(__dirname + "/public/index.html");
|
||
});
|
||
app.get("/notes/*", function (request, response) {
|
||
response.sendFile(__dirname + "/public" + request.url + ".html");
|
||
});
|
||
app.get('/get_faq', function (_, response) {
|
||
response.sendFile(__dirname + "/faq.md");
|
||
});
|
||
app.get('/get_readme', function (_, response) {
|
||
response.sendFile(__dirname + "/readme.md");
|
||
});
|
||
app.get('/deviceinfo', function (request, response) {
|
||
const userAgent = request.header('user-agent');
|
||
const deviceDetector = new DeviceDetector();
|
||
const deviceInfo = deviceDetector.parse(userAgent);
|
||
return response.send(deviceInfo);
|
||
});
|
||
|
||
//**************Kobold api
|
||
app.post("/generate", jsonParser, async function (request, response_generate = response) {
|
||
if (!request.body) return response_generate.sendStatus(400);
|
||
//console.log(request.body.prompt);
|
||
//const dataJson = JSON.parse(request.body);
|
||
request_promt = request.body.prompt;
|
||
|
||
//console.log(request.body);
|
||
var this_settings = {
|
||
prompt: request_promt,
|
||
use_story: false,
|
||
use_memory: false,
|
||
use_authors_note: false,
|
||
use_world_info: false,
|
||
max_context_length: request.body.max_context_length,
|
||
singleline: !!request.body.singleline,
|
||
//temperature: request.body.temperature,
|
||
//max_length: request.body.max_length
|
||
};
|
||
|
||
if (request.body.gui_settings == false) {
|
||
var sampler_order = [request.body.s1, request.body.s2, request.body.s3, request.body.s4, request.body.s5, request.body.s6, request.body.s7];
|
||
this_settings = {
|
||
prompt: request_promt,
|
||
use_story: false,
|
||
use_memory: false,
|
||
use_authors_note: false,
|
||
use_world_info: false,
|
||
max_context_length: request.body.max_context_length,
|
||
max_length: request.body.max_length,
|
||
rep_pen: request.body.rep_pen,
|
||
rep_pen_range: request.body.rep_pen_range,
|
||
rep_pen_slope: request.body.rep_pen_slope,
|
||
temperature: request.body.temperature,
|
||
tfs: request.body.tfs,
|
||
top_a: request.body.top_a,
|
||
top_k: request.body.top_k,
|
||
top_p: request.body.top_p,
|
||
typical: request.body.typical,
|
||
sampler_order: sampler_order,
|
||
singleline: !!request.body.singleline,
|
||
};
|
||
if (!!request.body.stop_sequence) {
|
||
this_settings['stop_sequence'] = request.body.stop_sequence;
|
||
}
|
||
}
|
||
|
||
console.log(this_settings);
|
||
var args = {
|
||
data: this_settings,
|
||
headers: { "Content-Type": "application/json" }
|
||
};
|
||
|
||
const MAX_RETRIES = 10;
|
||
const delayAmount = 3000;
|
||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||
try {
|
||
const data = await postAsync(api_server + "/v1/generate", args);
|
||
console.log(data);
|
||
return response_generate.send(data);
|
||
}
|
||
catch (error) {
|
||
// data
|
||
console.log(error[0]);
|
||
|
||
// response
|
||
if (error[1]) {
|
||
switch (error[1].statusCode) {
|
||
case 503:
|
||
await delay(delayAmount);
|
||
break;
|
||
default:
|
||
return response_generate.send({ error: true });
|
||
}
|
||
} else {
|
||
return response_generate.send({ error: true });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
//************** Text generation web UI
|
||
app.post("/generate_textgenerationwebui", jsonParser, async function (request, response_generate = response) {
|
||
if (!request.body) return response_generate.sendStatus(400);
|
||
|
||
console.log(request.body);
|
||
|
||
if (!!request.header('X-Response-Streaming')) {
|
||
let isStreamingStopped = false;
|
||
request.socket.on('close', function () {
|
||
isStreamingStopped = true;
|
||
});
|
||
|
||
response_generate.writeHead(200, {
|
||
'Content-Type': 'text/plain;charset=utf-8',
|
||
'Transfer-Encoding': 'chunked',
|
||
'Cache-Control': 'no-transform',
|
||
});
|
||
|
||
async function* readWebsocket() {
|
||
const streamingUrl = request.header('X-Streaming-URL');
|
||
const websocket = new WebSocket(streamingUrl);
|
||
|
||
websocket.on('open', async function () {
|
||
console.log('websocket open');
|
||
websocket.send(JSON.stringify(request.body));
|
||
});
|
||
|
||
websocket.on('error', (err) => {
|
||
console.error(err);
|
||
websocket.close();
|
||
});
|
||
|
||
websocket.on('close', (code, buffer) => {
|
||
const reason = new TextDecoder().decode(buffer)
|
||
console.log(reason);
|
||
});
|
||
|
||
while (true) {
|
||
if (isStreamingStopped) {
|
||
console.error('Streaming stopped by user. Closing websocket...');
|
||
websocket.close();
|
||
return;
|
||
}
|
||
|
||
const rawMessage = await new Promise(resolve => websocket.once('message', resolve));
|
||
const message = json5.parse(rawMessage);
|
||
|
||
switch (message.event) {
|
||
case 'text_stream':
|
||
yield message.text;
|
||
break;
|
||
case 'stream_end':
|
||
websocket.close();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
let reply = '';
|
||
|
||
try {
|
||
for await (const text of readWebsocket()) {
|
||
if (typeof text !== 'string') {
|
||
break;
|
||
}
|
||
|
||
let newText = text;
|
||
|
||
if (!newText) {
|
||
continue;
|
||
}
|
||
|
||
reply += text;
|
||
response_generate.write(newText);
|
||
}
|
||
|
||
console.log(reply);
|
||
}
|
||
finally {
|
||
response_generate.end();
|
||
}
|
||
}
|
||
else {
|
||
var args = {
|
||
data: request.body,
|
||
headers: { "Content-Type": "application/json" }
|
||
};
|
||
client.post(api_server + "/v1/generate", args, function (data, response) {
|
||
console.log("####", data);
|
||
if (response.statusCode == 200) {
|
||
console.log(data);
|
||
response_generate.send(data);
|
||
}
|
||
if (response.statusCode == 422) {
|
||
console.log('Validation error');
|
||
response_generate.send({ error: true });
|
||
}
|
||
if (response.statusCode == 501 || response.statusCode == 503 || response.statusCode == 507) {
|
||
console.log(data);
|
||
response_generate.send({ error: true });
|
||
}
|
||
}).on('error', function (err) {
|
||
console.log(err);
|
||
//console.log('something went wrong on the request', err.request.options);
|
||
response_generate.send({ error: true });
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
app.post("/savechat", jsonParser, function (request, response) {
|
||
var dir_name = String(request.body.avatar_url).replace('.png', '');
|
||
let chat_data = request.body.chat;
|
||
let jsonlData = chat_data.map(JSON.stringify).join('\n');
|
||
fs.writeFile(chatsPath + dir_name + "/" + request.body.file_name + '.jsonl', jsonlData, 'utf8', function (err) {
|
||
if (err) {
|
||
response.send(err);
|
||
return console.log(err);
|
||
} else {
|
||
response.send({ result: "ok" });
|
||
}
|
||
});
|
||
|
||
});
|
||
app.post("/getchat", jsonParser, function (request, response) {
|
||
var dir_name = String(request.body.avatar_url).replace('.png', '');
|
||
|
||
fs.stat(chatsPath + dir_name, function (err, stat) {
|
||
|
||
if (stat === undefined) { //if no chat dir for the character is found, make one with the character name
|
||
|
||
fs.mkdirSync(chatsPath + dir_name);
|
||
response.send({});
|
||
return;
|
||
} else {
|
||
|
||
if (err === null) { //if there is a dir, then read the requested file from the JSON call
|
||
|
||
fs.stat(chatsPath + dir_name + "/" + request.body.file_name + ".jsonl", function (err, stat) {
|
||
|
||
if (err === null) { //if no error (the file exists), read the file
|
||
if (stat !== undefined) {
|
||
fs.readFile(chatsPath + dir_name + "/" + request.body.file_name + ".jsonl", 'utf8', (err, data) => {
|
||
if (err) {
|
||
console.error(err);
|
||
response.send(err);
|
||
return;
|
||
}
|
||
//console.log(data);
|
||
const lines = data.split('\n');
|
||
|
||
// Iterate through the array of strings and parse each line as JSON
|
||
const jsonData = lines.map(json5.parse);
|
||
response.send(jsonData);
|
||
//console.log('read the requested file')
|
||
|
||
});
|
||
}
|
||
} else {
|
||
response.send({});
|
||
//return console.log(err);
|
||
return;
|
||
}
|
||
});
|
||
} else {
|
||
console.error(err);
|
||
response.send({});
|
||
return;
|
||
}
|
||
}
|
||
});
|
||
|
||
|
||
});
|
||
app.post("/getstatus", jsonParser, async function (request, response_getstatus = response) {
|
||
if (!request.body) return response_getstatus.sendStatus(400);
|
||
api_server = request.body.api_server;
|
||
main_api = request.body.main_api;
|
||
if (api_server.indexOf('localhost') != -1) {
|
||
api_server = api_server.replace('localhost', '127.0.0.1');
|
||
}
|
||
var args = {
|
||
headers: { "Content-Type": "application/json" }
|
||
};
|
||
var url = api_server + "/v1/model";
|
||
let version = '';
|
||
if (main_api == "kobold") {
|
||
try {
|
||
version = (await getAsync(api_server + "/v1/info/version")).result;
|
||
}
|
||
catch {
|
||
version = '0.0.0';
|
||
}
|
||
}
|
||
client.get(url, args, function (data, response) {
|
||
if (response.statusCode == 200) {
|
||
data.version = version;
|
||
if (data.result != "ReadOnly") {
|
||
} else {
|
||
data.result = "no_connection";
|
||
}
|
||
} else {
|
||
data.result = "no_connection";
|
||
}
|
||
response_getstatus.send(data);
|
||
}).on('error', function (err) {
|
||
response_getstatus.send({ result: "no_connection" });
|
||
});
|
||
});
|
||
|
||
const formatApiUrl = (url) => (url.indexOf('localhost') !== -1)
|
||
? url.replace('localhost', '127.0.0.1')
|
||
: url;
|
||
|
||
app.post('/getsoftprompts', jsonParser, async function (request, response) {
|
||
if (!request.body || !request.body.api_server) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const baseUrl = formatApiUrl(request.body.api_server);
|
||
let soft_prompts = [];
|
||
|
||
try {
|
||
const softPromptsList = (await getAsync(`${baseUrl}/v1/config/soft_prompts_list`, baseRequestArgs)).values.map(x => x.value);
|
||
const softPromptSelected = (await getAsync(`${baseUrl}/v1/config/soft_prompt`, baseRequestArgs)).value;
|
||
soft_prompts = softPromptsList.map(x => ({ name: x, selected: x === softPromptSelected }));
|
||
} catch (err) {
|
||
soft_prompts = [];
|
||
}
|
||
|
||
return response.send({ soft_prompts });
|
||
});
|
||
|
||
app.post("/setsoftprompt", jsonParser, async function (request, response) {
|
||
if (!request.body || !request.body.api_server) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const baseUrl = formatApiUrl(request.body.api_server);
|
||
const args = {
|
||
headers: { "Content-Type": "application/json" },
|
||
data: { value: request.body.name ?? '' },
|
||
};
|
||
|
||
try {
|
||
await putAsync(`${baseUrl}/v1/config/soft_prompt`, args);
|
||
} catch {
|
||
return response.sendStatus(500);
|
||
}
|
||
|
||
return response.sendStatus(200);
|
||
});
|
||
|
||
function checkServer() {
|
||
api_server = 'http://127.0.0.1:5000';
|
||
var args = {
|
||
headers: { "Content-Type": "application/json" }
|
||
};
|
||
client.get(api_server + "/v1/model", args, function (data, response) {
|
||
console.log(data.result);
|
||
console.log(data);
|
||
}).on('error', function (err) {
|
||
console.log(err);
|
||
});
|
||
}
|
||
|
||
//***************** Main functions
|
||
function charaFormatData(data) {
|
||
var char = { "name": data.ch_name, "description": data.description, "personality": data.personality, "first_mes": data.first_mes, "avatar": 'none', "chat": data.ch_name + ' - ' + humanizedISO8601DateTime(), "mes_example": data.mes_example, "scenario": data.scenario, "create_date": humanizedISO8601DateTime(), "talkativeness": data.talkativeness, "fav": data.fav };
|
||
return char;
|
||
}
|
||
|
||
app.post("/createcharacter", urlencodedParser, function (request, response) {
|
||
if (!request.body) return response.sendStatus(400);
|
||
|
||
request.body.ch_name = sanitize(request.body.ch_name);
|
||
|
||
const char = JSON.stringify(charaFormatData(request.body));
|
||
const internalName = getPngName(request.body.ch_name);
|
||
const avatarName = `${internalName}.png`;
|
||
const defaultAvatar = './public/img/ai4.png';
|
||
const chatsPath = directories.chats + internalName; //path.join(chatsPath, internalName);
|
||
|
||
if (!fs.existsSync(chatsPath)) fs.mkdirSync(chatsPath);
|
||
|
||
if (!request.file) {
|
||
charaWrite(defaultAvatar, char, internalName, response, avatarName);
|
||
} else {
|
||
const uploadPath = path.join("./uploads/", request.file.filename);
|
||
charaWrite(uploadPath, char, internalName, response, avatarName);
|
||
}
|
||
});
|
||
|
||
app.post('/renamechat', jsonParser, async function (request, response) {
|
||
if (!request.body || !request.body.original_file || !request.body.renamed_file) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const pathToFolder = request.body.is_group
|
||
? directories.groupChats
|
||
: path.join(directories.chats, String(request.body.avatar_url).replace('.png', ''));
|
||
const pathToOriginalFile = path.join(pathToFolder, request.body.original_file);
|
||
const pathToRenamedFile = path.join(pathToFolder, request.body.renamed_file);
|
||
console.log('Old chat name', pathToOriginalFile);
|
||
console.log('New chat name', pathToRenamedFile);
|
||
|
||
if (!fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile)) {
|
||
console.log('Either Source or Destination files are not available');
|
||
return response.status(400).send({ error: true });
|
||
}
|
||
|
||
console.log('Successfully renamed.');
|
||
fs.renameSync(pathToOriginalFile, pathToRenamedFile);
|
||
return response.send({ ok: true });
|
||
});
|
||
|
||
app.post("/renamecharacter", jsonParser, async function (request, response) {
|
||
if (!request.body.avatar_url || !request.body.new_name) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const oldAvatarName = request.body.avatar_url;
|
||
const newName = sanitize(request.body.new_name);
|
||
const oldInternalName = path.parse(request.body.avatar_url).name;
|
||
const newInternalName = getPngName(newName);
|
||
const newAvatarName = `${newInternalName}.png`;
|
||
|
||
const oldAvatarPath = path.join(charactersPath, oldAvatarName);
|
||
const newAvatarPath = path.join(charactersPath, newAvatarName);
|
||
|
||
const oldChatsPath = path.join(chatsPath, oldInternalName);
|
||
const newChatsPath = path.join(chatsPath, newInternalName);
|
||
|
||
try {
|
||
// Read old file, replace name int it
|
||
const rawOldData = await charaRead(oldAvatarPath);
|
||
const oldData = json5.parse(rawOldData);
|
||
oldData['name'] = newName;
|
||
const newData = JSON.stringify(oldData);
|
||
|
||
// Write data to new location
|
||
await charaWrite(oldAvatarPath, newData, newInternalName);
|
||
|
||
// Rename chats folder
|
||
if (fs.existsSync(oldChatsPath) && !fs.existsSync(newChatsPath)) {
|
||
fs.renameSync(oldChatsPath, newChatsPath);
|
||
}
|
||
|
||
// Remove the old character file
|
||
fs.rmSync(oldAvatarPath);
|
||
|
||
// Return new avatar name to ST
|
||
return response.send({ 'avatar': newAvatarName });
|
||
}
|
||
catch (err) {
|
||
console.error(err);
|
||
return response.sendStatus(500);
|
||
}
|
||
});
|
||
|
||
app.post("/editcharacter", urlencodedParser, async function (request, response) {
|
||
if (!request.body) {
|
||
console.error('Error: no response body detected');
|
||
response.status(400).send('Error: no response body detected');
|
||
return;
|
||
}
|
||
|
||
if (request.body.ch_name === '' || request.body.ch_name === undefined || request.body.ch_name === '.') {
|
||
console.error('Error: invalid name.');
|
||
response.status(400).send('Error: invalid name.');
|
||
return;
|
||
}
|
||
|
||
let char = charaFormatData(request.body);
|
||
char.chat = request.body.chat;
|
||
char.create_date = request.body.create_date;
|
||
char = JSON.stringify(char);
|
||
let target_img = (request.body.avatar_url).replace('.png', '');
|
||
|
||
try {
|
||
if (!request.file) {
|
||
const avatarPath = path.join(charactersPath, request.body.avatar_url);
|
||
await charaWrite(avatarPath, char, target_img, response, 'Character saved');
|
||
} else {
|
||
const newAvatarPath = path.join("./uploads/", request.file.filename);
|
||
invalidateThumbnail('avatar', request.body.avatar_url);
|
||
await charaWrite(newAvatarPath, char, target_img, response, 'Character saved');
|
||
}
|
||
}
|
||
catch {
|
||
console.error('An error occured, character edit invalidated.');
|
||
}
|
||
});
|
||
|
||
app.post("/deletecharacter", urlencodedParser, function (request, response) {
|
||
if (!request.body || !request.body.avatar_url) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
if (request.body.avatar_url !== sanitize(request.body.avatar_url)) {
|
||
console.error('Malicious filename prevented');
|
||
return response.sendStatus(403);
|
||
}
|
||
|
||
const avatarPath = charactersPath + request.body.avatar_url;
|
||
if (!fs.existsSync(avatarPath)) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
fs.rmSync(avatarPath);
|
||
invalidateThumbnail('avatar', request.body.avatar_url);
|
||
let dir_name = (request.body.avatar_url.replace('.png', ''));
|
||
|
||
if (!dir_name.length) {
|
||
console.error('Malicious dirname prevented');
|
||
return response.sendStatus(403);
|
||
}
|
||
|
||
rimraf(path.join(chatsPath, sanitize(dir_name)), (err) => {
|
||
if (err) {
|
||
response.send(err);
|
||
return console.log(err);
|
||
} else {
|
||
//response.redirect("/");
|
||
|
||
response.send('ok');
|
||
}
|
||
});
|
||
});
|
||
|
||
async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok') {
|
||
try {
|
||
// Read the image, resize, and save it as a PNG into the buffer
|
||
const rawImg = await jimp.read(img_url);
|
||
const image = await rawImg.cover(400, 600).getBufferAsync(jimp.MIME_PNG);
|
||
|
||
// Get the chunks
|
||
const chunks = extract(image);
|
||
const tEXtChunks = chunks.filter(chunk => chunk.create_date === 'tEXt');
|
||
|
||
// Remove all existing tEXt chunks
|
||
for (let tEXtChunk of tEXtChunks) {
|
||
chunks.splice(chunks.indexOf(tEXtChunk), 1);
|
||
}
|
||
// Add new chunks before the IEND chunk
|
||
const base64EncodedData = Buffer.from(data, 'utf8').toString('base64');
|
||
chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData));
|
||
//chunks.splice(-1, 0, text.encode('lorem', 'ipsum'));
|
||
|
||
fs.writeFileSync(charactersPath + target_img + '.png', new Buffer.from(encode(chunks)));
|
||
if (response !== undefined) response.send(mes);
|
||
return true;
|
||
|
||
|
||
} catch (err) {
|
||
console.log(err);
|
||
if (response !== undefined) response.status(500).send(err);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function charaRead(img_url, input_format) {
|
||
let format;
|
||
if (input_format === undefined) {
|
||
if (img_url.indexOf('.webp') !== -1) {
|
||
format = 'webp';
|
||
} else {
|
||
format = 'png';
|
||
}
|
||
} else {
|
||
format = input_format;
|
||
}
|
||
|
||
switch (format) {
|
||
case 'webp':
|
||
try {
|
||
const exif_data = await ExifReader.load(fs.readFileSync(img_url));
|
||
let char_data;
|
||
|
||
if (exif_data['UserComment']['description']) {
|
||
let description = exif_data['UserComment']['description'];
|
||
if (description === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) {
|
||
description = exif_data['UserComment'].value[0];
|
||
}
|
||
try {
|
||
json5.parse(description);
|
||
char_data = description;
|
||
} catch {
|
||
const byteArr = description.split(",").map(Number);
|
||
const uint8Array = new Uint8Array(byteArr);
|
||
const char_data_string = utf8Decode.decode(uint8Array);
|
||
char_data = char_data_string;
|
||
}
|
||
} else {
|
||
console.log('No description found in EXIF data.');
|
||
return false;
|
||
}
|
||
return char_data;
|
||
}
|
||
catch (err) {
|
||
console.log(err);
|
||
return false;
|
||
}
|
||
case 'png':
|
||
const buffer = fs.readFileSync(img_url);
|
||
const chunks = extract(buffer);
|
||
|
||
const textChunks = chunks.filter(function (chunk) {
|
||
return chunk.name === 'tEXt';
|
||
}).map(function (chunk) {
|
||
return PNGtext.decode(chunk.data);
|
||
});
|
||
var base64DecodedData = Buffer.from(textChunks[0].text, 'base64').toString('utf8');
|
||
return base64DecodedData;//textChunks[0].text;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
app.post("/getcharacters", jsonParser, function (request, response) {
|
||
fs.readdir(charactersPath, async (err, files) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return;
|
||
}
|
||
|
||
const pngFiles = files.filter(file => file.endsWith('.png'));
|
||
|
||
//console.log(pngFiles);
|
||
characters = {};
|
||
var i = 0;
|
||
for (const item of pngFiles) {
|
||
try {
|
||
var img_data = await charaRead(charactersPath + item);
|
||
let jsonObject = json5.parse(img_data);
|
||
jsonObject.avatar = item;
|
||
//console.log(jsonObject);
|
||
characters[i] = {};
|
||
characters[i] = jsonObject;
|
||
|
||
try {
|
||
const charStat = fs.statSync(path.join(charactersPath, item));
|
||
characters[i]['date_added'] = charStat.birthtimeMs;
|
||
const char_dir = path.join(chatsPath, item.replace('.png', ''));
|
||
|
||
let chat_size = 0;
|
||
let date_last_chat = 0;
|
||
|
||
if (fs.existsSync(char_dir)) {
|
||
const chats = fs.readdirSync(char_dir);
|
||
|
||
if (Array.isArray(chats) && chats.length) {
|
||
for (const chat of chats) {
|
||
const chatStat = fs.statSync(path.join(char_dir, chat));
|
||
chat_size += chatStat.size;
|
||
date_last_chat = Math.max(date_last_chat, chatStat.mtimeMs);
|
||
}
|
||
}
|
||
}
|
||
|
||
characters[i]['date_last_chat'] = date_last_chat;
|
||
characters[i]['chat_size'] = chat_size;
|
||
}
|
||
catch {
|
||
characters[i]['date_added'] = 0;
|
||
characters[i]['date_last_chat'] = 0;
|
||
characters[i]['chat_size'] = 0;
|
||
}
|
||
|
||
i++;
|
||
} catch (error) {
|
||
console.log(`Could not read character: ${item}`);
|
||
if (error instanceof SyntaxError) {
|
||
console.log("String [" + (i) + "] is not valid JSON!");
|
||
} else {
|
||
console.log("An unexpected error occurred: ", error);
|
||
}
|
||
}
|
||
};
|
||
//console.log(characters);
|
||
response.send(JSON.stringify(characters));
|
||
});
|
||
|
||
});
|
||
app.post("/getbackgrounds", jsonParser, function (request, response) {
|
||
var images = getImages("public/backgrounds");
|
||
response.send(JSON.stringify(images));
|
||
|
||
});
|
||
app.post("/iscolab", jsonParser, function (request, response) {
|
||
let send_data = false;
|
||
if (is_colab) {
|
||
send_data = String(process.env.colaburl).trim();
|
||
}
|
||
response.send({ colaburl: send_data });
|
||
|
||
});
|
||
app.post("/getuseravatars", jsonParser, function (request, response) {
|
||
var images = getImages("public/User Avatars");
|
||
response.send(JSON.stringify(images));
|
||
|
||
});
|
||
app.post("/setbackground", jsonParser, function (request, response) {
|
||
var bg = "#bg1 {background-image: url('../backgrounds/" + request.body.bg + "');}";
|
||
fs.writeFile('public/css/bg_load.css', bg, 'utf8', function (err) {
|
||
if (err) {
|
||
response.send(err);
|
||
return console.log(err);
|
||
} else {
|
||
//response.redirect("/");
|
||
response.send({ result: 'ok' });
|
||
}
|
||
});
|
||
|
||
});
|
||
app.post("/delbackground", jsonParser, function (request, response) {
|
||
if (!request.body) return response.sendStatus(400);
|
||
|
||
if (request.body.bg !== sanitize(request.body.bg)) {
|
||
console.error('Malicious bg name prevented');
|
||
return response.sendStatus(403);
|
||
}
|
||
|
||
const fileName = path.join('public/backgrounds/', sanitize(request.body.bg));
|
||
|
||
if (!fs.existsSync(fileName)) {
|
||
console.log('BG file not found');
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
fs.rmSync(fileName);
|
||
invalidateThumbnail('bg', request.body.bg);
|
||
return response.send('ok');
|
||
});
|
||
|
||
app.post("/delchat", jsonParser, function (request, response) {
|
||
console.log('/delchat entered');
|
||
if (!request.body) {
|
||
console.log('no request body seen');
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
if (request.body.chatfile !== sanitize(request.body.chatfile)) {
|
||
console.error('Malicious chat name prevented');
|
||
return response.sendStatus(403);
|
||
}
|
||
|
||
const fileName = path.join(directories.chats, '/', sanitize(request.body.id), '/', sanitize(request.body.chatfile));
|
||
if (!fs.existsSync(fileName)) {
|
||
console.log('Chat file not found');
|
||
return response.sendStatus(400);
|
||
} else {
|
||
console.log('found the chat file: ' + fileName);
|
||
/* fs.unlinkSync(fileName); */
|
||
fs.rmSync(fileName);
|
||
console.log('deleted chat file: ' + fileName);
|
||
|
||
}
|
||
|
||
|
||
return response.send('ok');
|
||
});
|
||
|
||
|
||
app.post("/downloadbackground", urlencodedParser, function (request, response) {
|
||
response_dw_bg = response;
|
||
if (!request.body) return response.sendStatus(400);
|
||
|
||
let filedata = request.file;
|
||
//console.log(filedata.mimetype);
|
||
var fileType = ".png";
|
||
var img_file = "ai";
|
||
var img_path = "public/img/";
|
||
|
||
img_path = "uploads/";
|
||
img_file = filedata.filename;
|
||
if (filedata.mimetype == "image/jpeg") fileType = ".jpeg";
|
||
if (filedata.mimetype == "image/png") fileType = ".png";
|
||
if (filedata.mimetype == "image/gif") fileType = ".gif";
|
||
if (filedata.mimetype == "image/bmp") fileType = ".bmp";
|
||
fs.copyFile(img_path + img_file, 'public/backgrounds/' + img_file + fileType, (err) => {
|
||
invalidateThumbnail('bg', img_file + fileType);
|
||
if (err) {
|
||
|
||
return console.log(err);
|
||
} else {
|
||
//console.log(img_file+fileType);
|
||
response_dw_bg.send(img_file + fileType);
|
||
}
|
||
//console.log('The image was copied from temp directory.');
|
||
});
|
||
|
||
|
||
});
|
||
|
||
app.post("/savesettings", jsonParser, function (request, response) {
|
||
fs.writeFile('public/settings.json', JSON.stringify(request.body), 'utf8', function (err) {
|
||
if (err) {
|
||
response.send(err);
|
||
return console.log(err);
|
||
//response.send(err);
|
||
} else {
|
||
//response.redirect("/");
|
||
response.send({ result: "ok" });
|
||
}
|
||
});
|
||
});
|
||
|
||
app.post('/getsettings', jsonParser, (request, response) => { //Wintermute's code
|
||
const koboldai_settings = [];
|
||
const koboldai_setting_names = [];
|
||
const novelai_settings = [];
|
||
const novelai_setting_names = [];
|
||
const openai_settings = [];
|
||
const openai_setting_names = [];
|
||
const textgenerationwebui_presets = [];
|
||
const textgenerationwebui_preset_names = [];
|
||
const themes = [];
|
||
const settings = fs.readFileSync('public/settings.json', 'utf8', (err, data) => {
|
||
if (err) return response.sendStatus(500);
|
||
|
||
return data;
|
||
});
|
||
//Kobold
|
||
const files = fs
|
||
.readdirSync('public/KoboldAI Settings')
|
||
.sort(
|
||
(a, b) =>
|
||
new Date(fs.statSync(`public/KoboldAI Settings/${b}`).mtime) -
|
||
new Date(fs.statSync(`public/KoboldAI Settings/${a}`).mtime)
|
||
);
|
||
|
||
const worldFiles = fs
|
||
.readdirSync(directories.worlds)
|
||
.filter(file => path.extname(file).toLowerCase() === '.json')
|
||
.sort((a, b) => a < b);
|
||
const world_names = worldFiles.map(item => path.parse(item).name);
|
||
|
||
files.forEach(item => {
|
||
const file = fs.readFileSync(
|
||
`public/KoboldAI Settings/${item}`,
|
||
'utf8',
|
||
(err, data) => {
|
||
if (err) return response.sendStatus(500)
|
||
|
||
return data;
|
||
}
|
||
);
|
||
koboldai_settings.push(file);
|
||
koboldai_setting_names.push(item.replace(/\.[^/.]+$/, ''));
|
||
});
|
||
|
||
//Novel
|
||
const files2 = fs
|
||
.readdirSync('public/NovelAI Settings')
|
||
.sort(
|
||
(a, b) =>
|
||
new Date(fs.statSync(`public/NovelAI Settings/${b}`).mtime) -
|
||
new Date(fs.statSync(`public/NovelAI Settings/${a}`).mtime)
|
||
);
|
||
|
||
files2.forEach(item => {
|
||
const file2 = fs.readFileSync(
|
||
`public/NovelAI Settings/${item}`,
|
||
'utf8',
|
||
(err, data) => {
|
||
if (err) return response.sendStatus(500);
|
||
|
||
return data;
|
||
}
|
||
);
|
||
|
||
novelai_settings.push(file2);
|
||
novelai_setting_names.push(item.replace(/\.[^/.]+$/, ''));
|
||
});
|
||
|
||
//OpenAI
|
||
const files3 = fs
|
||
.readdirSync('public/OpenAI Settings')
|
||
.sort(
|
||
(a, b) =>
|
||
new Date(fs.statSync(`public/OpenAI Settings/${b}`).mtime) -
|
||
new Date(fs.statSync(`public/OpenAI Settings/${a}`).mtime)
|
||
);
|
||
|
||
files3.forEach(item => {
|
||
const file3 = fs.readFileSync(
|
||
`public/OpenAI Settings/${item}`,
|
||
'utf8',
|
||
(err, data) => {
|
||
if (err) return response.sendStatus(500);
|
||
|
||
return data;
|
||
}
|
||
);
|
||
|
||
openai_settings.push(file3);
|
||
openai_setting_names.push(item.replace(/\.[^/.]+$/, ''));
|
||
});
|
||
|
||
// TextGenerationWebUI
|
||
const textGenFiles = fs
|
||
.readdirSync(directories.textGen_Settings)
|
||
.sort();
|
||
|
||
textGenFiles.forEach(item => {
|
||
const file = fs.readFileSync(
|
||
path.join(directories.textGen_Settings, item),
|
||
'utf8',
|
||
(err, data) => {
|
||
if (err) return response.sendStatus(500);
|
||
|
||
return data;
|
||
}
|
||
);
|
||
|
||
textgenerationwebui_presets.push(file);
|
||
textgenerationwebui_preset_names.push(item.replace(/\.[^/.]+$/, ''));
|
||
});
|
||
|
||
// Theme files
|
||
const themeFiles = fs
|
||
.readdirSync(directories.themes)
|
||
.filter(x => path.parse(x).ext == '.json')
|
||
.sort();
|
||
|
||
themeFiles.forEach(item => {
|
||
const file = fs.readFileSync(
|
||
path.join(directories.themes, item),
|
||
'utf-8',
|
||
(err, data) => {
|
||
if (err) return response.sendStatus(500);
|
||
return data;
|
||
}
|
||
);
|
||
|
||
try {
|
||
themes.push(json5.parse(file));
|
||
}
|
||
catch {
|
||
// skip
|
||
}
|
||
})
|
||
|
||
response.send({
|
||
settings,
|
||
koboldai_settings,
|
||
koboldai_setting_names,
|
||
world_names,
|
||
novelai_settings,
|
||
novelai_setting_names,
|
||
openai_settings,
|
||
openai_setting_names,
|
||
textgenerationwebui_presets,
|
||
textgenerationwebui_preset_names,
|
||
themes,
|
||
enable_extensions: enableExtensions,
|
||
});
|
||
});
|
||
|
||
app.post('/getworldinfo', jsonParser, (request, response) => {
|
||
if (!request.body?.name) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const file = readWorldInfoFile(request.body.name);
|
||
|
||
return response.send(file);
|
||
});
|
||
|
||
app.post('/deleteworldinfo', jsonParser, (request, response) => {
|
||
if (!request.body?.name) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const worldInfoName = request.body.name;
|
||
const filename = sanitize(`${worldInfoName}.json`);
|
||
const pathToWorldInfo = path.join(directories.worlds, filename);
|
||
|
||
if (!fs.existsSync(pathToWorldInfo)) {
|
||
throw new Error(`World info file ${filename} doesn't exist.`);
|
||
}
|
||
|
||
fs.rmSync(pathToWorldInfo);
|
||
|
||
return response.sendStatus(200);
|
||
});
|
||
|
||
app.post('/savetheme', jsonParser, (request, response) => {
|
||
if (!request.body || !request.body.name) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const filename = path.join(directories.themes, sanitize(request.body.name) + '.json');
|
||
fs.writeFileSync(filename, JSON.stringify(request.body), 'utf8');
|
||
|
||
return response.sendStatus(200);
|
||
});
|
||
|
||
function readWorldInfoFile(worldInfoName) {
|
||
if (!worldInfoName) {
|
||
return { entries: {} };
|
||
}
|
||
|
||
const filename = `${worldInfoName}.json`;
|
||
const pathToWorldInfo = path.join(directories.worlds, filename);
|
||
|
||
if (!fs.existsSync(pathToWorldInfo)) {
|
||
throw new Error(`World info file ${filename} doesn't exist.`);
|
||
}
|
||
|
||
const worldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8');
|
||
const worldInfo = json5.parse(worldInfoText);
|
||
return worldInfo;
|
||
}
|
||
|
||
|
||
function getImages(path) {
|
||
return fs
|
||
.readdirSync(path)
|
||
.filter(file => {
|
||
const type = mime.lookup(file);
|
||
return type && type.startsWith('image/');
|
||
})
|
||
.sort(Intl.Collator().compare);
|
||
}
|
||
|
||
//***********Novel.ai API
|
||
|
||
app.post("/getstatus_novelai", jsonParser, function (request, response_getstatus_novel = response) {
|
||
|
||
if (!request.body) return response_getstatus_novel.sendStatus(400);
|
||
api_key_novel = request.body.key;
|
||
var data = {};
|
||
var args = {
|
||
data: data,
|
||
|
||
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + api_key_novel }
|
||
};
|
||
client.get(api_novelai + "/user/subscription", args, function (data, response) {
|
||
if (response.statusCode == 200) {
|
||
//console.log(data);
|
||
response_getstatus_novel.send(data);//data);
|
||
}
|
||
if (response.statusCode == 401) {
|
||
console.log('Access Token is incorrect.');
|
||
response_getstatus_novel.send({ error: true });
|
||
}
|
||
if (response.statusCode == 500 || response.statusCode == 501 || response.statusCode == 501 || response.statusCode == 503 || response.statusCode == 507) {
|
||
console.log(data);
|
||
response_getstatus_novel.send({ error: true });
|
||
}
|
||
}).on('error', function (err) {
|
||
//console.log('');
|
||
//console.log('something went wrong on the request', err.request.options);
|
||
response_getstatus_novel.send({ error: true });
|
||
});
|
||
});
|
||
|
||
app.post("/generate_novelai", jsonParser, function (request, response_generate_novel = response) {
|
||
if (!request.body) return response_generate_novel.sendStatus(400);
|
||
|
||
console.log(request.body);
|
||
var data = {
|
||
"input": request.body.input,
|
||
"model": request.body.model,
|
||
"parameters": {
|
||
"use_string": request.body.use_string,
|
||
"temperature": request.body.temperature,
|
||
"max_length": request.body.max_length,
|
||
"min_length": request.body.min_length,
|
||
"tail_free_sampling": request.body.tail_free_sampling,
|
||
"repetition_penalty": request.body.repetition_penalty,
|
||
"repetition_penalty_range": request.body.repetition_penalty_range,
|
||
"repetition_penalty_frequency": request.body.repetition_penalty_frequency,
|
||
"repetition_penalty_presence": request.body.repetition_penalty_presence,
|
||
//"stop_sequences": {{187}},
|
||
//bad_words_ids = {{50256}, {0}, {1}};
|
||
//generate_until_sentence = true;
|
||
"use_cache": request.body.use_cache,
|
||
//use_string = true;
|
||
"return_full_text": request.body.return_full_text,
|
||
"prefix": request.body.prefix,
|
||
"order": request.body.order
|
||
}
|
||
};
|
||
|
||
var args = {
|
||
data: data,
|
||
|
||
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + api_key_novel }
|
||
};
|
||
client.post(api_novelai + "/ai/generate", args, function (data, response) {
|
||
if (response.statusCode == 201) {
|
||
console.log(data);
|
||
response_generate_novel.send(data);
|
||
}
|
||
if (response.statusCode == 400) {
|
||
console.log('Validation error');
|
||
response_generate_novel.send({ error: true });
|
||
}
|
||
if (response.statusCode == 401) {
|
||
console.log('Access Token is incorrect');
|
||
response_generate_novel.send({ error: true });
|
||
}
|
||
if (response.statusCode == 402) {
|
||
console.log('An active subscription is required to access this endpoint');
|
||
response_generate_novel.send({ error: true });
|
||
}
|
||
if (response.statusCode == 500 || response.statusCode == 409) {
|
||
console.log(data);
|
||
response_generate_novel.send({ error: true });
|
||
}
|
||
}).on('error', function (err) {
|
||
//console.log('');
|
||
//console.log('something went wrong on the request', err.request.options);
|
||
response_getstatus.send({ error: true });
|
||
});
|
||
});
|
||
|
||
app.post("/getallchatsofcharacter", jsonParser, function (request, response) {
|
||
if (!request.body) return response.sendStatus(400);
|
||
|
||
var char_dir = (request.body.avatar_url).replace('.png', '')
|
||
fs.readdir(chatsPath + char_dir, (err, files) => {
|
||
if (err) {
|
||
console.log('found error in history loading');
|
||
console.error(err);
|
||
response.send({ error: true });
|
||
return;
|
||
}
|
||
|
||
// filter for JSON files
|
||
console.log('looking for JSONL files');
|
||
const jsonFiles = files.filter(file => path.extname(file) === '.jsonl');
|
||
|
||
// sort the files by name
|
||
//jsonFiles.sort().reverse();
|
||
// print the sorted file names
|
||
var chatData = {};
|
||
let ii = jsonFiles.length; //this is the number of files belonging to the character
|
||
if (ii !== 0) {
|
||
//console.log('found '+ii+' chat logs to load');
|
||
for (let i = jsonFiles.length - 1; i >= 0; i--) {
|
||
const file = jsonFiles[i];
|
||
const fileStream = fs.createReadStream(chatsPath + char_dir + '/' + file);
|
||
|
||
const fullPathAndFile = chatsPath + char_dir + '/' + file
|
||
const stats = fs.statSync(fullPathAndFile);
|
||
const fileSizeInKB = (stats.size / 1024).toFixed(2) + "kb";
|
||
|
||
//console.log(fileSizeInKB);
|
||
|
||
const rl = readline.createInterface({
|
||
input: fileStream,
|
||
crlfDelay: Infinity
|
||
});
|
||
|
||
let lastLine;
|
||
let itemCounter = 0;
|
||
rl.on('line', (line) => {
|
||
itemCounter++;
|
||
lastLine = line;
|
||
});
|
||
rl.on('close', () => {
|
||
ii--;
|
||
if (lastLine) {
|
||
|
||
let jsonData = json5.parse(lastLine);
|
||
if (jsonData.name !== undefined || jsonData.character_name !== undefined) {
|
||
chatData[i] = {};
|
||
chatData[i]['file_name'] = file;
|
||
chatData[i]['file_size'] = fileSizeInKB;
|
||
chatData[i]['chat_items'] = itemCounter - 1;
|
||
chatData[i]['mes'] = jsonData['mes'] || '[The chat is empty]';
|
||
}
|
||
}
|
||
if (ii === 0) {
|
||
//console.log('ii count went to zero, responding with chatData');
|
||
response.send(chatData);
|
||
}
|
||
//console.log('successfully closing getallchatsofcharacter');
|
||
rl.close();
|
||
});
|
||
};
|
||
} else {
|
||
//console.log('Found No Chats. Exiting Load Routine.');
|
||
response.send({ error: true });
|
||
};
|
||
})
|
||
});
|
||
|
||
function getPngName(file) {
|
||
let i = 1;
|
||
let base_name = file;
|
||
while (fs.existsSync(charactersPath + file + '.png')) {
|
||
file = base_name + i;
|
||
i++;
|
||
}
|
||
return file;
|
||
}
|
||
|
||
app.post("/importcharacter", urlencodedParser, async function (request, response) {
|
||
|
||
if (!request.body) return response.sendStatus(400);
|
||
|
||
let png_name = '';
|
||
let filedata = request.file;
|
||
let uploadPath = path.join('./uploads', filedata.filename);
|
||
var format = request.body.file_type;
|
||
//console.log(format);
|
||
if (filedata) {
|
||
if (format == 'json') {
|
||
fs.readFile(uploadPath, 'utf8', async (err, data) => {
|
||
if (err) {
|
||
console.log(err);
|
||
response.send({ error: true });
|
||
}
|
||
const jsonData = json5.parse(data);
|
||
|
||
if (jsonData.name !== undefined) {
|
||
jsonData.name = sanitize(jsonData.name);
|
||
|
||
png_name = getPngName(jsonData.name);
|
||
let char = { "name": jsonData.name, "description": jsonData.description ?? '', "personality": jsonData.personality ?? '', "first_mes": jsonData.first_mes ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.mes_example ?? '', "scenario": jsonData.scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 };
|
||
char = JSON.stringify(char);
|
||
charaWrite('./public/img/ai4.png', char, png_name, response, { file_name: png_name });
|
||
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
|
||
jsonData.char_name = sanitize(jsonData.char_name);
|
||
|
||
png_name = getPngName(jsonData.char_name);
|
||
let char = { "name": jsonData.char_name, "description": jsonData.char_persona ?? '', "personality": '', "first_mes": jsonData.char_greeting ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.example_dialogue ?? '', "scenario": jsonData.world_scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 };
|
||
char = JSON.stringify(char);
|
||
charaWrite('./public/img/ai4.png', char, png_name, response, { file_name: png_name });
|
||
} else {
|
||
console.log('Incorrect character format .json');
|
||
response.send({ error: true });
|
||
}
|
||
});
|
||
} else {
|
||
try {
|
||
var img_data = await charaRead(uploadPath, format);
|
||
let jsonData = json5.parse(img_data);
|
||
jsonData.name = sanitize(jsonData.name);
|
||
|
||
if (format == 'webp') {
|
||
let convertedPath = path.join('./uploads', path.basename(uploadPath, ".webp") + ".png")
|
||
await webp.dwebp(uploadPath, convertedPath, "-o");
|
||
uploadPath = convertedPath;
|
||
}
|
||
|
||
png_name = getPngName(jsonData.name);
|
||
|
||
if (jsonData.name !== undefined) {
|
||
let char = { "name": jsonData.name, "description": jsonData.description ?? '', "personality": jsonData.personality ?? '', "first_mes": jsonData.first_mes ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.mes_example ?? '', "scenario": jsonData.scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 };
|
||
char = JSON.stringify(char);
|
||
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
|
||
}
|
||
} catch (err) {
|
||
console.log(err);
|
||
response.send({ error: true });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
app.post("/exportcharacter", jsonParser, async function (request, response) {
|
||
if (!request.body.format || !request.body.avatar_url) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
let filename = path.join(directories.characters, sanitize(request.body.avatar_url));
|
||
|
||
if (!fs.existsSync(filename)) {
|
||
return response.sendStatus(404);
|
||
}
|
||
|
||
switch (request.body.format) {
|
||
case 'png':
|
||
return response.sendFile(filename, { root: __dirname });
|
||
case 'json': {
|
||
try {
|
||
let json = await charaRead(filename);
|
||
let jsonObject = json5.parse(json);
|
||
return response.type('json').send(jsonObject)
|
||
}
|
||
catch {
|
||
return response.sendStatus(400);
|
||
}
|
||
}
|
||
case 'webp': {
|
||
try {
|
||
let json = await charaRead(filename);
|
||
let stringByteArray = utf8Encode.encode(json).toString();
|
||
let inputWebpPath = `./uploads/${Date.now()}_input.webp`;
|
||
let outputWebpPath = `./uploads/${Date.now()}_output.webp`;
|
||
let metadataPath = `./uploads/${Date.now()}_metadata.exif`;
|
||
let metadata =
|
||
{
|
||
"Exif": {
|
||
[exif.ExifIFD.UserComment]: stringByteArray,
|
||
},
|
||
};
|
||
const exifString = exif.dump(metadata);
|
||
fs.writeFileSync(metadataPath, exifString, 'binary');
|
||
|
||
await webp.cwebp(filename, inputWebpPath, '-q 95');
|
||
await webp.webpmux_add(inputWebpPath, outputWebpPath, metadataPath, 'exif');
|
||
|
||
response.sendFile(outputWebpPath, { root: __dirname }, () => {
|
||
fs.rmSync(inputWebpPath);
|
||
fs.rmSync(metadataPath);
|
||
fs.rmSync(outputWebpPath);
|
||
});
|
||
|
||
return;
|
||
}
|
||
catch (err) {
|
||
console.log(err);
|
||
return response.sendStatus(400);
|
||
}
|
||
}
|
||
}
|
||
|
||
return response.sendStatus(400);
|
||
});
|
||
|
||
|
||
app.post("/importchat", urlencodedParser, function (request, response) {
|
||
if (!request.body) return response.sendStatus(400);
|
||
|
||
var format = request.body.file_type;
|
||
let filedata = request.file;
|
||
let avatar_url = (request.body.avatar_url).replace('.png', '');
|
||
let ch_name = request.body.character_name;
|
||
if (filedata) {
|
||
|
||
if (format === 'json') {
|
||
fs.readFile('./uploads/' + filedata.filename, 'utf8', (err, data) => {
|
||
|
||
if (err) {
|
||
console.log(err);
|
||
response.send({ error: true });
|
||
}
|
||
|
||
const jsonData = json5.parse(data);
|
||
if (jsonData.histories !== undefined) {
|
||
//console.log('/importchat confirms JSON histories are defined');
|
||
const chat = {
|
||
from(history) {
|
||
return [
|
||
{
|
||
user_name: 'You',
|
||
character_name: ch_name,
|
||
create_date: humanizedISO8601DateTime(),
|
||
|
||
},
|
||
...history.msgs.map(
|
||
(message) => ({
|
||
name: message.src.is_human ? 'You' : ch_name,
|
||
is_user: message.src.is_human,
|
||
is_name: true,
|
||
send_date: humanizedISO8601DateTime(),
|
||
mes: message.text,
|
||
})
|
||
)];
|
||
}
|
||
}
|
||
|
||
const newChats = [];
|
||
(jsonData.histories.histories ?? []).forEach((history) => {
|
||
newChats.push(chat.from(history));
|
||
});
|
||
|
||
const errors = [];
|
||
newChats.forEach(chat => fs.writeFile(
|
||
chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + ' imported.jsonl',
|
||
chat.map(JSON.stringify).join('\n'),
|
||
'utf8',
|
||
(err) => err ?? errors.push(err)
|
||
)
|
||
);
|
||
|
||
if (0 < errors.length) {
|
||
response.send('Errors occurred while writing character files. Errors: ' + JSON.stringify(errors));
|
||
}
|
||
|
||
response.send({ res: true });
|
||
} else {
|
||
response.send({ error: true });
|
||
}
|
||
});
|
||
}
|
||
if (format === 'jsonl') {
|
||
//console.log(humanizedISO8601DateTime()+':imported chat format is JSONL');
|
||
const fileStream = fs.createReadStream('./uploads/' + filedata.filename);
|
||
const rl = readline.createInterface({
|
||
input: fileStream,
|
||
crlfDelay: Infinity
|
||
});
|
||
|
||
rl.once('line', (line) => {
|
||
let jsonData = json5.parse(line);
|
||
|
||
if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
|
||
//console.log(humanizedISO8601DateTime()+':/importchat copying chat as '+ch_name+' - '+humanizedISO8601DateTime()+'.jsonl');
|
||
fs.copyFile('./uploads/' + filedata.filename, chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + '.jsonl', (err) => { //added character name and replaced Date.now() with humanizedISO8601DateTime
|
||
if (err) {
|
||
response.send({ error: true });
|
||
return console.log(err);
|
||
} else {
|
||
response.send({ res: true });
|
||
return;
|
||
}
|
||
});
|
||
} else {
|
||
response.send({ error: true });
|
||
return;
|
||
}
|
||
rl.close();
|
||
});
|
||
}
|
||
|
||
}
|
||
|
||
});
|
||
|
||
app.post('/importworldinfo', urlencodedParser, (request, response) => {
|
||
if (!request.file) return response.sendStatus(400);
|
||
|
||
const filename = sanitize(request.file.originalname);
|
||
|
||
if (path.parse(filename).ext.toLowerCase() !== '.json') {
|
||
return response.status(400).send('Only JSON files are supported.')
|
||
}
|
||
|
||
const pathToUpload = path.join('./uploads/', request.file.filename);
|
||
const fileContents = fs.readFileSync(pathToUpload, 'utf8');
|
||
|
||
try {
|
||
const worldContent = json5.parse(fileContents);
|
||
if (!('entries' in worldContent)) {
|
||
throw new Error('File must contain a world info entries list');
|
||
}
|
||
} catch (err) {
|
||
return response.status(400).send('Is not a valid world info file');
|
||
}
|
||
|
||
const pathToNewFile = path.join(directories.worlds, filename);
|
||
const worldName = path.parse(pathToNewFile).name;
|
||
|
||
if (!worldName) {
|
||
return response.status(400).send('World file must have a name');
|
||
}
|
||
|
||
fs.writeFileSync(pathToNewFile, fileContents);
|
||
return response.send({ name: worldName });
|
||
});
|
||
|
||
app.post('/editworldinfo', jsonParser, (request, response) => {
|
||
if (!request.body) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
if (!request.body.name) {
|
||
return response.status(400).send('World file must have a name');
|
||
}
|
||
|
||
try {
|
||
if (!('entries' in request.body.data)) {
|
||
throw new Error('World info must contain an entries list');
|
||
}
|
||
} catch (err) {
|
||
return response.status(400).send('Is not a valid world info file');
|
||
}
|
||
|
||
const filename = `${request.body.name}.json`;
|
||
const pathToFile = path.join(directories.worlds, filename);
|
||
|
||
fs.writeFileSync(pathToFile, JSON.stringify(request.body.data));
|
||
|
||
return response.send({ ok: true });
|
||
});
|
||
|
||
app.post('/uploaduseravatar', urlencodedParser, async (request, response) => {
|
||
if (!request.file) return response.sendStatus(400);
|
||
|
||
try {
|
||
const pathToUpload = path.join('./uploads/' + request.file.filename);
|
||
const rawImg = await jimp.read(pathToUpload);
|
||
const image = await rawImg.cover(400, 400).getBufferAsync(jimp.MIME_PNG);
|
||
|
||
const filename = `${Date.now()}.png`;
|
||
const pathToNewFile = path.join(directories.avatars, filename);
|
||
fs.writeFileSync(pathToNewFile, image);
|
||
fs.rmSync(pathToUpload);
|
||
return response.send({ path: filename });
|
||
} catch (err) {
|
||
return response.status(400).send('Is not a valid image');
|
||
}
|
||
});
|
||
|
||
app.post('/getgroups', jsonParser, (_, response) => {
|
||
const groups = [];
|
||
|
||
if (!fs.existsSync(directories.groups)) {
|
||
fs.mkdirSync(directories.groups);
|
||
}
|
||
|
||
const files = fs.readdirSync(directories.groups);
|
||
files.forEach(function (file) {
|
||
const fileContents = fs.readFileSync(path.join(directories.groups, file), 'utf8');
|
||
const group = json5.parse(fileContents);
|
||
groups.push(group);
|
||
});
|
||
|
||
return response.send(groups);
|
||
});
|
||
|
||
app.post('/creategroup', jsonParser, (request, response) => {
|
||
if (!request.body) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const id = Date.now();
|
||
const groupMetadata = {
|
||
id: id,
|
||
name: request.body.name ?? 'New Group',
|
||
members: request.body.members ?? [],
|
||
avatar_url: request.body.avatar_url,
|
||
allow_self_responses: !!request.body.allow_self_responses,
|
||
activation_strategy: request.body.activation_strategy ?? 0,
|
||
chat_metadata: request.body.chat_metadata ?? {},
|
||
fav: request.body.fav,
|
||
chat_id: request.body.chat_id ?? id,
|
||
chats: request.body.chats ?? [id],
|
||
};
|
||
const pathToFile = path.join(directories.groups, `${id}.json`);
|
||
const fileData = JSON.stringify(groupMetadata);
|
||
|
||
if (!fs.existsSync(directories.groups)) {
|
||
fs.mkdirSync(directories.groups);
|
||
}
|
||
|
||
fs.writeFileSync(pathToFile, fileData);
|
||
return response.send(groupMetadata);
|
||
});
|
||
|
||
app.post('/editgroup', jsonParser, (request, response) => {
|
||
if (!request.body || !request.body.id) {
|
||
return response.sendStatus(400);
|
||
}
|
||
const id = request.body.id;
|
||
const pathToFile = path.join(directories.groups, `${id}.json`);
|
||
const fileData = JSON.stringify(request.body);
|
||
|
||
fs.writeFileSync(pathToFile, fileData);
|
||
return response.send({ ok: true });
|
||
});
|
||
|
||
app.post('/getgroupchat', jsonParser, (request, response) => {
|
||
if (!request.body || !request.body.id) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const id = request.body.id;
|
||
const pathToFile = path.join(directories.groupChats, `${id}.jsonl`);
|
||
|
||
if (fs.existsSync(pathToFile)) {
|
||
const data = fs.readFileSync(pathToFile, 'utf8');
|
||
const lines = data.split('\n');
|
||
|
||
// Iterate through the array of strings and parse each line as JSON
|
||
const jsonData = lines.map(json5.parse);
|
||
return response.send(jsonData);
|
||
} else {
|
||
return response.send([]);
|
||
}
|
||
});
|
||
|
||
app.post('/deletegroupchat', jsonParser, (request, response) => {
|
||
if (!request.body || !request.body.id) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const id = request.body.id;
|
||
const pathToFile = path.join(directories.groupChats, `${id}.jsonl`);
|
||
|
||
if (fs.existsSync(pathToFile)) {
|
||
fs.rmSync(pathToFile);
|
||
return response.send({ ok: true });
|
||
}
|
||
|
||
return response.send({ error: true });
|
||
});
|
||
|
||
app.post('/savegroupchat', jsonParser, (request, response) => {
|
||
if (!request.body || !request.body.id) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const id = request.body.id;
|
||
const pathToFile = path.join(directories.groupChats, `${id}.jsonl`);
|
||
|
||
if (!fs.existsSync(directories.groupChats)) {
|
||
fs.mkdirSync(directories.groupChats);
|
||
}
|
||
|
||
let chat_data = request.body.chat;
|
||
let jsonlData = chat_data.map(JSON.stringify).join('\n');
|
||
fs.writeFileSync(pathToFile, jsonlData, 'utf8');
|
||
return response.send({ ok: true });
|
||
});
|
||
|
||
app.post('/deletegroup', jsonParser, async (request, response) => {
|
||
if (!request.body || !request.body.id) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const id = request.body.id;
|
||
const pathToGroup = path.join(directories.groups, sanitize(`${id}.json`));
|
||
const pathToChat = path.join(directories.groupChats, sanitize(`${id}.jsonl`));
|
||
|
||
if (fs.existsSync(pathToGroup)) {
|
||
fs.rmSync(pathToGroup);
|
||
}
|
||
|
||
if (fs.existsSync(pathToChat)) {
|
||
fs.rmSync(pathToChat);
|
||
}
|
||
|
||
return response.send({ ok: true });
|
||
});
|
||
|
||
const POE_DEFAULT_BOT = 'a2';
|
||
|
||
async function getPoeClient(token, useCache = false) {
|
||
let client = new poe.Client(false, useCache);
|
||
await client.init(token);
|
||
return client;
|
||
}
|
||
|
||
app.post('/status_poe', jsonParser, async (request, response) => {
|
||
if (!request.body.token) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
try {
|
||
const client = await getPoeClient(request.body.token);
|
||
const botNames = client.get_bot_names();
|
||
client.disconnect_ws();
|
||
|
||
return response.send({ 'bot_names': botNames });
|
||
}
|
||
catch (err) {
|
||
console.error(err);
|
||
return response.sendStatus(401);
|
||
}
|
||
});
|
||
|
||
app.post('/purge_poe', jsonParser, async (request, response) => {
|
||
if (!request.body.token) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const token = request.body.token;
|
||
const bot = request.body.bot ?? POE_DEFAULT_BOT;
|
||
const count = request.body.count ?? -1;
|
||
|
||
try {
|
||
const client = await getPoeClient(token, true);
|
||
await client.purge_conversation(bot, count);
|
||
client.disconnect_ws();
|
||
|
||
return response.send({ "ok": true });
|
||
}
|
||
catch (err) {
|
||
console.error(err);
|
||
return response.sendStatus(500);
|
||
}
|
||
});
|
||
|
||
app.post('/generate_poe', jsonParser, async (request, response) => {
|
||
if (!request.body.token || !request.body.prompt) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const token = request.body.token;
|
||
const prompt = request.body.prompt;
|
||
const bot = request.body.bot ?? POE_DEFAULT_BOT;
|
||
const streaming = request.body.streaming ?? false;
|
||
|
||
let client;
|
||
|
||
try {
|
||
client = await getPoeClient(token, true);
|
||
}
|
||
catch (error) {
|
||
console.error(error);
|
||
return response.sendStatus(500);
|
||
}
|
||
|
||
if (streaming) {
|
||
let isStreamingStopped = false;
|
||
request.socket.on('close', function () {
|
||
isStreamingStopped = true;
|
||
client.abortController.abort();
|
||
});
|
||
|
||
try {
|
||
response.writeHead(200, {
|
||
'Content-Type': 'text/plain;charset=utf-8',
|
||
'Transfer-Encoding': 'chunked',
|
||
'Cache-Control': 'no-transform',
|
||
});
|
||
|
||
let reply = '';
|
||
for await (const mes of client.send_message(bot, prompt)) {
|
||
if (isStreamingStopped) {
|
||
console.error('Streaming stopped by user. Closing websocket...');
|
||
break;
|
||
}
|
||
|
||
let newText = mes.text.substring(reply.length);
|
||
reply = mes.text;
|
||
response.write(newText);
|
||
}
|
||
console.log(reply);
|
||
}
|
||
catch (err) {
|
||
console.error(err);
|
||
}
|
||
finally {
|
||
client.disconnect_ws();
|
||
return response.end();
|
||
}
|
||
}
|
||
else {
|
||
try {
|
||
let reply;
|
||
for await (const mes of client.send_message(bot, prompt)) {
|
||
reply = mes.text;
|
||
}
|
||
console.log(reply);
|
||
client.disconnect_ws();
|
||
return response.send({ 'reply': reply });
|
||
}
|
||
catch {
|
||
client.disconnect_ws();
|
||
return response.sendStatus(500);
|
||
}
|
||
}
|
||
});
|
||
|
||
app.get('/discover_extensions', jsonParser, function (_, response) {
|
||
const extensions = fs
|
||
.readdirSync(directories.extensions)
|
||
.filter(f => fs.statSync(path.join(directories.extensions, f)).isDirectory());
|
||
|
||
return response.send(extensions);
|
||
});
|
||
|
||
app.get('/get_sprites', jsonParser, function (request, response) {
|
||
const name = request.query.name;
|
||
const spritesPath = path.join(directories.characters, name);
|
||
let sprites = [];
|
||
|
||
try {
|
||
if (fs.existsSync(spritesPath) && fs.statSync(spritesPath).isDirectory()) {
|
||
sprites = fs.readdirSync(spritesPath)
|
||
.filter(file => {
|
||
const mimeType = mime.lookup(file);
|
||
return mimeType && mimeType.startsWith('image/');
|
||
})
|
||
.map((file) => {
|
||
const pathToSprite = path.join(spritesPath, file);
|
||
return {
|
||
label: path.parse(pathToSprite).name.toLowerCase(),
|
||
path: `/characters/${name}/${file}`,
|
||
};
|
||
});
|
||
}
|
||
}
|
||
catch (err) {
|
||
console.log(err);
|
||
}
|
||
finally {
|
||
return response.send(sprites);
|
||
}
|
||
});
|
||
|
||
function getThumbnailFolder(type) {
|
||
let thumbnailFolder;
|
||
|
||
switch (type) {
|
||
case 'bg':
|
||
thumbnailFolder = directories.thumbnailsBg;
|
||
break;
|
||
case 'avatar':
|
||
thumbnailFolder = directories.thumbnailsAvatar;
|
||
break;
|
||
}
|
||
|
||
return thumbnailFolder;
|
||
}
|
||
|
||
function getOriginalFolder(type) {
|
||
let originalFolder;
|
||
|
||
switch (type) {
|
||
case 'bg':
|
||
originalFolder = directories.backgrounds;
|
||
break;
|
||
case 'avatar':
|
||
originalFolder = directories.characters;
|
||
break;
|
||
}
|
||
|
||
return originalFolder;
|
||
}
|
||
|
||
function invalidateThumbnail(type, file) {
|
||
const folder = getThumbnailFolder(type);
|
||
const pathToThumbnail = path.join(folder, file);
|
||
|
||
if (fs.existsSync(pathToThumbnail)) {
|
||
fs.rmSync(pathToThumbnail);
|
||
}
|
||
}
|
||
|
||
async function ensureThumbnailCache() {
|
||
const cacheFiles = fs.readdirSync(directories.thumbnailsBg);
|
||
|
||
// files exist, all ok
|
||
if (cacheFiles.length) {
|
||
return;
|
||
}
|
||
|
||
console.log('Generating thumbnails cache. Please wait...');
|
||
|
||
const bgFiles = fs.readdirSync(directories.backgrounds);
|
||
const tasks = [];
|
||
|
||
for (const file of bgFiles) {
|
||
tasks.push(generateThumbnail('bg', file));
|
||
}
|
||
|
||
await Promise.all(tasks);
|
||
console.log(`Done! Generated: ${bgFiles.length} preview images`);
|
||
}
|
||
|
||
async function generateThumbnail(type, file) {
|
||
const pathToCachedFile = path.join(getThumbnailFolder(type), file);
|
||
const pathToOriginalFile = path.join(getOriginalFolder(type), file);
|
||
|
||
const cachedFileExists = fs.existsSync(pathToCachedFile);
|
||
const originalFileExists = fs.existsSync(pathToOriginalFile);
|
||
|
||
// to handle cases when original image was updated after thumb creation
|
||
let shouldRegenerate = false;
|
||
|
||
if (cachedFileExists && originalFileExists) {
|
||
const originalStat = fs.statSync(pathToOriginalFile);
|
||
const cachedStat = fs.statSync(pathToCachedFile);
|
||
|
||
if (originalStat.mtimeMs > cachedStat.ctimeMs) {
|
||
//console.log('Original file changed. Regenerating thumbnail...');
|
||
shouldRegenerate = true;
|
||
}
|
||
}
|
||
|
||
if (cachedFileExists && !shouldRegenerate) {
|
||
return pathToCachedFile;
|
||
}
|
||
|
||
if (!originalFileExists) {
|
||
return null;
|
||
}
|
||
|
||
const imageSizes = { 'bg': [160, 90], 'avatar': [96, 144] };
|
||
const mySize = imageSizes[type];
|
||
|
||
try {
|
||
const image = await jimp.read(pathToOriginalFile);
|
||
const buffer = await image.cover(mySize[0], mySize[1]).quality(95).getBufferAsync(mime.lookup('jpg'));
|
||
fs.writeFileSync(pathToCachedFile, buffer);
|
||
}
|
||
catch (err) {
|
||
return null;
|
||
}
|
||
|
||
return pathToCachedFile;
|
||
}
|
||
|
||
app.get('/thumbnail', jsonParser, async function (request, response) {
|
||
const type = request.query.type;
|
||
const file = sanitize(request.query.file);
|
||
|
||
if (!type || !file) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
if (!(type == 'bg' || type == 'avatar')) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
if (sanitize(file) !== file) {
|
||
console.error('Malicious filename prevented');
|
||
return response.sendStatus(403);
|
||
}
|
||
|
||
if (config.disableThumbnails == true) {
|
||
const pathToOriginalFile = path.join(getOriginalFolder(type), file);
|
||
return response.sendFile(pathToOriginalFile, { root: __dirname });
|
||
}
|
||
|
||
const pathToCachedFile = await generateThumbnail(type, file);
|
||
|
||
if (!pathToCachedFile) {
|
||
return response.sendStatus(404);
|
||
}
|
||
|
||
return response.sendFile(pathToCachedFile, { root: __dirname });
|
||
});
|
||
|
||
/* OpenAI */
|
||
app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_openai = response) {
|
||
if (!request.body) return response_getstatus_openai.sendStatus(400);
|
||
api_key_openai = request.body.key;
|
||
const api_url = new URL(request.body.reverse_proxy || api_openai).toString();
|
||
const args = {
|
||
headers: { "Authorization": "Bearer " + api_key_openai }
|
||
};
|
||
client.get(api_url + "/models", args, function (data, response) {
|
||
if (response.statusCode == 200) {
|
||
response_getstatus_openai.send(data);
|
||
const modelIds = data?.data?.map(x => x.id)?.sort();
|
||
console.log('Available OpenAI models:', modelIds);
|
||
}
|
||
if (response.statusCode == 401) {
|
||
console.log('Access Token is incorrect.');
|
||
response_getstatus_openai.send({ error: true });
|
||
}
|
||
if (response.statusCode == 404) {
|
||
console.log('Endpoint not found.');
|
||
response_getstatus_openai.send({ error: true });
|
||
}
|
||
if (response.statusCode == 500 || response.statusCode == 501 || response.statusCode == 501 || response.statusCode == 503 || response.statusCode == 507) {
|
||
console.log(data);
|
||
response_getstatus_openai.send({ error: true });
|
||
}
|
||
}).on('error', function (err) {
|
||
response_getstatus_openai.send({ error: true });
|
||
});
|
||
});
|
||
|
||
app.post("/openai_bias", jsonParser, async function (request, response) {
|
||
if (!request.body || !Array.isArray(request.body))
|
||
return response.sendStatus(400);
|
||
|
||
let result = {};
|
||
|
||
const tokenizer = tiktoken.encoding_for_model(request.query.model === 'gpt-4-0314' ? 'gpt-4' : request.query.model);
|
||
|
||
for (const entry of request.body) {
|
||
if (!entry || !entry.text) {
|
||
continue;
|
||
}
|
||
|
||
const tokens = tokenizer.encode(entry.text);
|
||
|
||
for (const token of tokens) {
|
||
result[token] = entry.value;
|
||
}
|
||
}
|
||
|
||
tokenizer.free();
|
||
return response.send(result);
|
||
});
|
||
|
||
// Shamelessly stolen from Agnai
|
||
app.post("/openai_usage", jsonParser, async function (request, response) {
|
||
if (!request.body) return response.sendStatus(400);
|
||
const key = request.body.key;
|
||
const api_url = new URL(request.body.reverse_proxy || api_openai).toString();
|
||
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
Authorization: `Bearer ${key}`,
|
||
};
|
||
|
||
const date = new Date();
|
||
date.setDate(1);
|
||
const start_date = date.toISOString().slice(0, 10);
|
||
|
||
date.setMonth(date.getMonth() + 1);
|
||
const end_date = date.toISOString().slice(0, 10);
|
||
|
||
try {
|
||
const res = await getAsync(
|
||
`${api_url}/dashboard/billing/usage?start_date=${start_date}&end_date=${end_date}`,
|
||
{ headers },
|
||
);
|
||
return response.send(res);
|
||
}
|
||
catch {
|
||
return response.sendStatus(400);
|
||
}
|
||
});
|
||
|
||
app.post("/deletepreset_openai", jsonParser, function (request, response) {
|
||
if (!request.body || !request.body.name) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const name = request.body.name;
|
||
const pathToFile = path.join(directories.openAI_Settings, `${name}.settings`);
|
||
|
||
if (fs.existsSync(pathToFile)) {
|
||
fs.rmSync(pathToFile);
|
||
return response.send({ ok: true });
|
||
}
|
||
|
||
return response.send({ error: true });
|
||
});
|
||
|
||
app.post("/generate_openai", jsonParser, function (request, response_generate_openai) {
|
||
if (!request.body) return response_generate_openai.sendStatus(400);
|
||
const api_url = new URL(request.body.reverse_proxy || api_openai).toString();
|
||
|
||
const controller = new AbortController();
|
||
request.socket.on('close', function () {
|
||
controller.abort();
|
||
});
|
||
|
||
console.log(request.body);
|
||
const config = {
|
||
method: 'post',
|
||
url: api_url + '/chat/completions',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer ' + api_key_openai
|
||
},
|
||
data: {
|
||
"messages": request.body.messages,
|
||
"model": request.body.model,
|
||
"temperature": request.body.temperature,
|
||
"max_tokens": request.body.max_tokens,
|
||
"stream": request.body.stream,
|
||
"presence_penalty": request.body.presence_penalty,
|
||
"frequency_penalty": request.body.frequency_penalty,
|
||
"stop": request.body.stop,
|
||
"logit_bias": request.body.logit_bias
|
||
},
|
||
signal: controller.signal,
|
||
};
|
||
|
||
if (request.body.stream)
|
||
config.responseType = 'stream';
|
||
|
||
axios(config)
|
||
.then(function (response) {
|
||
if (response.status <= 299) {
|
||
if (request.body.stream) {
|
||
console.log("Streaming request in progress")
|
||
response.data.pipe(response_generate_openai);
|
||
response.data.on('end', function () {
|
||
console.log("Streaming request finished");
|
||
response_generate_openai.end();
|
||
});
|
||
} else {
|
||
response_generate_openai.send(response.data);
|
||
console.log(response.data);
|
||
console.log(response.data?.choices[0]?.message);
|
||
}
|
||
} else if (response.status == 400) {
|
||
console.log('Validation error');
|
||
response_generate_openai.send({ error: true });
|
||
} else if (response.status == 401) {
|
||
console.log('Access Token is incorrect');
|
||
response_generate_openai.send({ error: true });
|
||
} else if (response.status == 402) {
|
||
console.log('An active subscription is required to access this endpoint');
|
||
response_generate_openai.send({ error: true });
|
||
} else if (response.status == 429) {
|
||
console.log('Out of quota');
|
||
response_generate_openai.send({ error: true, quota_error: true, });
|
||
} else if (response.status == 500 || response.status == 409 || response.status == 504) {
|
||
if (request.body.stream) {
|
||
response.data.on('data', chunk => {
|
||
console.log(chunk.toString());
|
||
});
|
||
} else {
|
||
console.log(response.data);
|
||
}
|
||
response_generate_openai.send({ error: true });
|
||
}
|
||
})
|
||
.catch(function (error) {
|
||
if (error.response) {
|
||
if (request.body.stream) {
|
||
error.response.data.on('data', chunk => {
|
||
console.log(chunk.toString());
|
||
});
|
||
} else {
|
||
console.log(error.response.data);
|
||
}
|
||
}
|
||
try {
|
||
const quota_error = error.response.status == 429;
|
||
response_generate_openai.send({ error: true, quota_error });
|
||
} catch (error) {
|
||
console.error(error);
|
||
return response_generate_openai.send({ error: true });
|
||
}
|
||
});
|
||
});
|
||
|
||
app.post("/tokenize_openai", jsonParser, function (request, response_tokenize_openai = response) {
|
||
if (!request.body) return response_tokenize_openai.sendStatus(400);
|
||
|
||
const tokensPerName = request.query.model.includes('gpt-4') ? 1 : -1;
|
||
const tokensPerMessage = request.query.model.includes('gpt-4') ? 3 : 4;
|
||
const tokensPadding = 3;
|
||
|
||
const tokenizer = tiktoken.encoding_for_model(request.query.model === 'gpt-4-0314' ? 'gpt-4' : request.query.model);
|
||
|
||
let num_tokens = 0;
|
||
for (const msg of request.body) {
|
||
num_tokens += tokensPerMessage;
|
||
for (const [key, value] of Object.entries(msg)) {
|
||
num_tokens += tokenizer.encode(value).length;
|
||
if (key == "name") {
|
||
num_tokens += tokensPerName;
|
||
}
|
||
}
|
||
}
|
||
num_tokens += tokensPadding;
|
||
|
||
tokenizer.free();
|
||
|
||
response_tokenize_openai.send({ "token_count": num_tokens });
|
||
});
|
||
|
||
app.post("/savepreset_openai", jsonParser, function (request, response) {
|
||
const name = sanitize(request.query.name);
|
||
if (!request.body || !name) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const filename = `${name}.settings`;
|
||
const fullpath = path.join(directories.openAI_Settings, filename);
|
||
fs.writeFileSync(fullpath, JSON.stringify(request.body), 'utf-8');
|
||
return response.send({ name });
|
||
});
|
||
|
||
app.post("/tokenize_llama", jsonParser, async function (request, response) {
|
||
if (!request.body) {
|
||
return response.sendStatus(400);
|
||
}
|
||
|
||
const count = await countTokensLlama(request.body.text);
|
||
return response.send({ count });
|
||
});
|
||
|
||
// ** REST CLIENT ASYNC WRAPPERS **
|
||
function deleteAsync(url, args) {
|
||
return new Promise((resolve, reject) => {
|
||
client.delete(url, args, (data, response) => {
|
||
if (response.statusCode >= 400) {
|
||
reject(data);
|
||
}
|
||
resolve(data);
|
||
}).on('error', e => reject(e));
|
||
})
|
||
}
|
||
|
||
function putAsync(url, args) {
|
||
return new Promise((resolve, reject) => {
|
||
client.put(url, args, (data, response) => {
|
||
if (response.statusCode >= 400) {
|
||
reject(data);
|
||
}
|
||
resolve(data);
|
||
}).on('error', e => reject(e));
|
||
})
|
||
}
|
||
|
||
function postAsync(url, args) {
|
||
return new Promise((resolve, reject) => {
|
||
client.post(url, args, (data, response) => {
|
||
if (response.statusCode >= 400) {
|
||
reject([data, response]);
|
||
}
|
||
resolve(data);
|
||
}).on('error', e => reject(e));
|
||
})
|
||
}
|
||
|
||
function getAsync(url, args) {
|
||
return new Promise((resolve, reject) => {
|
||
client.get(url, args, (data, response) => {
|
||
if (response.statusCode >= 400) {
|
||
reject(data);
|
||
}
|
||
resolve(data);
|
||
}).on('error', e => reject(e));
|
||
})
|
||
}
|
||
// ** END **
|
||
|
||
const tavernUrl = new URL(
|
||
(cliArguments.ssl ? 'https://' : 'http://') +
|
||
(listen ? '0.0.0.0' : '127.0.0.1') +
|
||
(':' + server_port)
|
||
);
|
||
|
||
const autorunUrl = new URL(
|
||
(cliArguments.ssl ? 'https://' : 'http://') +
|
||
('127.0.0.1') +
|
||
(':' + server_port)
|
||
);
|
||
|
||
const setupTasks = async function () {
|
||
ensurePublicDirectoriesExist();
|
||
await ensureThumbnailCache();
|
||
|
||
// Colab users could run the embedded tool
|
||
if (!is_colab) await convertWebp();
|
||
|
||
await spp.load(`./src/sentencepiece/tokenizer.model`);
|
||
|
||
console.log('Launching...');
|
||
|
||
if (autorun) open(autorunUrl.toString());
|
||
console.log('SillyTavern is listening on: ' + tavernUrl);
|
||
if (listen &&
|
||
!config.whitelistMode &&
|
||
!config.basicAuthMode)
|
||
console.log('Your SillyTavern is currently open to the public. To increase security, consider enabling whitelisting or basic authentication.')
|
||
}
|
||
|
||
if (true === cliArguments.ssl)
|
||
https.createServer(
|
||
{
|
||
cert: fs.readFileSync(cliArguments.certPath),
|
||
key: fs.readFileSync(cliArguments.keyPath)
|
||
}, app)
|
||
.listen(
|
||
tavernUrl.port,
|
||
tavernUrl.hostname,
|
||
setupTasks
|
||
);
|
||
else
|
||
http.createServer(app).listen(
|
||
tavernUrl.port,
|
||
tavernUrl.hostname,
|
||
setupTasks
|
||
);
|
||
|
||
async function convertWebp() {
|
||
const files = fs.readdirSync(directories.characters).filter(e => e.endsWith(".webp"));
|
||
|
||
if (!files.length) {
|
||
return;
|
||
}
|
||
|
||
console.log(`${files.length} WEBP files will be automatically converted.`);
|
||
|
||
for (const file of files) {
|
||
try {
|
||
const source = path.join(directories.characters, file);
|
||
const dest = path.join(directories.characters, path.basename(file, ".webp") + ".png");
|
||
|
||
if (fs.existsSync(dest)) {
|
||
console.log(`${dest} already exists. Delete ${source} manually`);
|
||
continue;
|
||
}
|
||
|
||
console.log(`Read... ${source}`);
|
||
const data = await charaRead(source);
|
||
|
||
console.log(`Convert... ${source} -> ${dest}`);
|
||
await webp.dwebp(source, dest, "-o");
|
||
|
||
console.log(`Write... ${dest}`);
|
||
const success = await charaWrite(dest, data, path.parse(dest).name);
|
||
|
||
if (!success) {
|
||
console.log(`Failure on ${source} -> ${dest}`);
|
||
continue;
|
||
}
|
||
|
||
console.log(`Remove... ${source}`);
|
||
fs.rmSync(source);
|
||
} catch (err) {
|
||
console.log(err);
|
||
}
|
||
}
|
||
}
|
||
|
||
function ensurePublicDirectoriesExist() {
|
||
for (const dir of Object.values(directories)) {
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
}
|
||
}
|