Update all endpoints to use user directories

This commit is contained in:
Cohee
2024-04-07 01:47:07 +03:00
parent cd5aec7368
commit b07a6a9a78
39 changed files with 941 additions and 751 deletions

View File

@ -9,7 +9,7 @@ const _ = require('lodash');
const jimp = require('jimp');
const { DIRECTORIES, UPLOADS_PATH, AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
const { UPLOADS_PATH, AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
const { jsonParser, urlencodedParser } = require('../express-common');
const { deepMerge, humanizedISO8601DateTime, tryParse } = require('../util');
const { TavernCardValidator } = require('../validator/TavernCardValidator');
@ -19,82 +19,99 @@ const { invalidateThumbnail } = require('./thumbnails');
const { importRisuSprites } = require('./sprites');
const defaultAvatarPath = './public/img/ai4.png';
let characters = {};
// KV-store for parsed character data
const characterDataCache = new Map();
/**
* Reads the character card from the specified image file.
* @param {string} img_url - Path to the image file
* @param {string} input_format - 'png'
* @param {string} inputFile - Path to the image file
* @param {string} inputFormat - 'png'
* @returns {Promise<string | undefined>} - Character card data
*/
async function charaRead(img_url, input_format = 'png') {
const stat = fs.statSync(img_url);
const cacheKey = `${img_url}-${stat.mtimeMs}`;
async function readCharacterData(inputFile, inputFormat = 'png') {
const stat = fs.statSync(inputFile);
const cacheKey = `${inputFile}-${stat.mtimeMs}`;
if (characterDataCache.has(cacheKey)) {
return characterDataCache.get(cacheKey);
}
const result = characterCardParser.parse(img_url, input_format);
const result = characterCardParser.parse(inputFile, inputFormat);
characterDataCache.set(cacheKey, result);
return result;
}
/**
* @param {express.Response | undefined} response
* @param {{file_name: string} | string} mes
* Writes the character card to the specified image file.
* @param {string} inputFile - Path to the image file
* @param {string} data - Character card data
* @param {string} outputFile - Target image file name
* @param {import('express').Request} request - Express request obejct
* @param {Crop|undefined} crop - Crop parameters
* @returns {Promise<boolean>} - True if the operation was successful
*/
async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok', crop = undefined) {
async function writeCharacterData(inputFile, data, outputFile, request, crop = undefined) {
try {
// Reset the cache
for (const key of characterDataCache.keys()) {
if (key.startsWith(img_url)) {
if (key.startsWith(inputFile)) {
characterDataCache.delete(key);
break;
}
}
// Read the image, resize, and save it as a PNG into the buffer
const inputImage = await tryReadImage(img_url, crop);
const inputImage = await tryReadImage(inputFile, crop);
// Get the chunks
const outputImage = characterCardParser.write(inputImage, data);
const outputImagePath = path.join(request.user.directories.characters, `${outputFile}.png`);
writeFileAtomicSync(DIRECTORIES.characters + target_img + '.png', outputImage);
if (response !== undefined) response.send(mes);
writeFileAtomicSync(outputImagePath, outputImage);
return true;
} catch (err) {
console.log(err);
if (response !== undefined) response.status(500).send(err);
return false;
}
}
async function tryReadImage(img_url, crop) {
/**
* @typedef {Object} Crop
* @property {number} x X-coordinate
* @property {number} y Y-coordinate
* @property {number} width Width
* @property {number} height Height
* @property {boolean} want_resize Resize the image to the standard avatar size
*/
/**
* Reads an image file and applies crop if defined.
* @param {string} imgPath Path to the image file
* @param {Crop|undefined} crop Crop parameters
* @returns {Promise<Buffer>} Image buffer
*/
async function tryReadImage(imgPath, crop) {
try {
let rawImg = await jimp.read(img_url);
let final_width = rawImg.bitmap.width, final_height = rawImg.bitmap.height;
let rawImg = await jimp.read(imgPath);
let finalWidth = rawImg.bitmap.width, finalHeight = rawImg.bitmap.height;
// Apply crop if defined
if (typeof crop == 'object' && [crop.x, crop.y, crop.width, crop.height].every(x => typeof x === 'number')) {
rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height);
// Apply standard resize if requested
if (crop.want_resize) {
final_width = AVATAR_WIDTH;
final_height = AVATAR_HEIGHT;
finalWidth = AVATAR_WIDTH;
finalHeight = AVATAR_HEIGHT;
} else {
final_width = crop.width;
final_height = crop.height;
finalWidth = crop.width;
finalHeight = crop.height;
}
}
const image = await rawImg.cover(final_width, final_height).getBufferAsync(jimp.MIME_PNG);
const image = await rawImg.cover(finalWidth, finalHeight).getBufferAsync(jimp.MIME_PNG);
return image;
}
// If it's an unsupported type of image (APNG) - just read the file as buffer
catch {
return fs.readFileSync(img_url);
return fs.readFileSync(imgPath);
}
}
@ -131,54 +148,57 @@ const calculateDataSize = (data) => {
* processCharacter - Process a given character, read its data and calculate its statistics.
*
* @param {string} item The name of the character.
* @param {number} i The index of the character in the characters list.
* @return {Promise} A Promise that resolves when the character processing is done.
* @param {import('../users').UserDirectoryList} directories User directories
* @return {Promise<object>} A Promise that resolves when the character processing is done.
*/
const processCharacter = async (item, i) => {
const processCharacter = async (item, directories) => {
try {
const img_data = await charaRead(DIRECTORIES.characters + item);
if (img_data === undefined) throw new Error('Failed to read character file');
const imgFile = path.join(directories.characters, item);
const imgData = await readCharacterData(imgFile);
if (imgData === undefined) throw new Error('Failed to read character file');
let jsonObject = getCharaCardV2(JSON.parse(img_data), false);
let jsonObject = getCharaCardV2(JSON.parse(imgData), directories, false);
jsonObject.avatar = item;
characters[i] = jsonObject;
characters[i]['json_data'] = img_data;
const charStat = fs.statSync(path.join(DIRECTORIES.characters, item));
characters[i]['date_added'] = charStat.ctimeMs;
characters[i]['create_date'] = jsonObject['create_date'] || humanizedISO8601DateTime(charStat.ctimeMs);
const char_dir = path.join(DIRECTORIES.chats, item.replace('.png', ''));
const character = jsonObject;
character['json_data'] = imgData;
const charStat = fs.statSync(path.join(directories.characters, item));
character['date_added'] = charStat.ctimeMs;
character['create_date'] = jsonObject['create_date'] || humanizedISO8601DateTime(charStat.ctimeMs);
const chatsDirectory = path.join(directories.chats, item.replace('.png', ''));
const { chatSize, dateLastChat } = calculateChatSize(char_dir);
characters[i]['chat_size'] = chatSize;
characters[i]['date_last_chat'] = dateLastChat;
characters[i]['data_size'] = calculateDataSize(jsonObject?.data);
const { chatSize, dateLastChat } = calculateChatSize(chatsDirectory);
character['chat_size'] = chatSize;
character['date_last_chat'] = dateLastChat;
character['data_size'] = calculateDataSize(jsonObject?.data);
return character;
}
catch (err) {
characters[i] = {
console.log(`Could not process character: ${item}`);
if (err instanceof SyntaxError) {
console.log(`${item} does not contain a valid JSON object.`);
} else {
console.log('An unexpected error occurred: ', err);
}
return {
date_added: 0,
date_last_chat: 0,
chat_size: 0,
};
console.log(`Could not process character: ${item}`);
if (err instanceof SyntaxError) {
console.log('String [' + i + '] is not valid JSON!');
} else {
console.log('An unexpected error occurred: ', err);
}
}
};
/**
* Convert a character object to Spec V2 format.
* @param {object} jsonObject Character object
* @param {import('../users').UserDirectoryList} directories User directories
* @param {boolean} hoistDate Will set the chat and create_date fields to the current date if they are missing
* @returns {object} Character object in Spec V2 format
*/
function getCharaCardV2(jsonObject, hoistDate = true) {
function getCharaCardV2(jsonObject, directories, hoistDate = true) {
if (jsonObject.spec === undefined) {
jsonObject = convertToV2(jsonObject);
jsonObject = convertToV2(jsonObject, directories);
if (hoistDate && !jsonObject.create_date) {
jsonObject.create_date = humanizedISO8601DateTime();
@ -192,9 +212,10 @@ function getCharaCardV2(jsonObject, hoistDate = true) {
/**
* Convert a character object to Spec V2 format.
* @param {object} char Character object
* @param {import('../users').UserDirectoryList} directories User directories
* @returns {object} Character object in Spec V2 format
*/
function convertToV2(char) {
function convertToV2(char, directories) {
// Simulate incoming data from frontend form
const result = charaFormatData({
json_data: JSON.stringify(char),
@ -212,7 +233,7 @@ function convertToV2(char) {
depth_prompt_prompt: char.depth_prompt_prompt,
depth_prompt_depth: char.depth_prompt_depth,
depth_prompt_role: char.depth_prompt_role,
});
}, directories);
result.chat = char.chat ?? humanizedISO8601DateTime();
result.create_date = char.create_date;
@ -278,8 +299,13 @@ function readFromV2(char) {
return char;
}
//***************** Main functions
function charaFormatData(data) {
/**
* Format character data to Spec V2 format.
* @param {object} data Character data
* @param {import('../users').UserDirectoryList} directories User directories
* @returns
*/
function charaFormatData(data, directories) {
// This is supposed to save all the foreign keys that ST doesn't care about
const char = tryParse(data.json_data) || {};
@ -344,7 +370,7 @@ function charaFormatData(data) {
if (data.world) {
try {
const file = readWorldInfoFile(data.world, false);
const file = readWorldInfoFile(directories, data.world, false);
// File was imported - save it to the character book
if (file && file.originalData) {
@ -423,15 +449,16 @@ function convertWorldInfoToCharacterBook(name, entries) {
/**
* Import a character from a YAML file.
* @param {string} uploadPath Path to the uploaded file
* @param {import('express').Response} response Express response object
* @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects
* @returns {Promise<string>} Internal name of the character
*/
function importFromYaml(uploadPath, response) {
async function importFromYaml(uploadPath, context) {
const fileText = fs.readFileSync(uploadPath, 'utf8');
fs.rmSync(uploadPath);
const yamlData = yaml.parse(fileText);
console.log('importing from yaml');
console.log('Importing from YAML');
yamlData.name = sanitize(yamlData.name);
const fileName = getPngName(yamlData.name);
const fileName = getPngName(yamlData.name, context.request.user.directories);
let char = convertToV2({
'name': yamlData.name,
'description': yamlData.context ?? '',
@ -446,32 +473,177 @@ function importFromYaml(uploadPath, response) {
'talkativeness': 0.5,
'creator': '',
'tags': '',
});
charaWrite(defaultAvatarPath, JSON.stringify(char), fileName, response, { file_name: fileName });
}, context.request.user.directories);
const result = await writeCharacterData(defaultAvatarPath, JSON.stringify(char), fileName, context.request);
return result ? fileName : '';
}
/**
* Import a character from a JSON file.
* @param {string} uploadPath Path to the uploaded file
* @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects
* @returns {Promise<string>} Internal name of the character
*/
async function importFromJson(uploadPath, { request }) {
const data = fs.readFileSync(uploadPath, 'utf8');
fs.unlinkSync(uploadPath);
let jsonData = JSON.parse(data);
if (jsonData.spec !== undefined) {
console.log('Importing from v2 json');
importRisuSprites(request.user.directories, jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
const pngName = getPngName(jsonData.data?.name || jsonData.name, request.user.directories);
const char = JSON.stringify(jsonData);
const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);
return result ? pngName : '';
} else if (jsonData.name !== undefined) {
console.log('Importing from v1 json');
jsonData.name = sanitize(jsonData.name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
const pngName = getPngName(jsonData.name, request.user.directories);
let char = {
'name': jsonData.name,
'description': jsonData.description ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'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,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char, request.user.directories);
let charJSON = JSON.stringify(char);
const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);
return result ? pngName : '';
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
console.log('Importing from gradio json');
jsonData.char_name = sanitize(jsonData.char_name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
const pngName = getPngName(jsonData.char_name, request.user.directories);
let char = {
'name': jsonData.char_name,
'description': jsonData.char_persona ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'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,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char, request.user.directories);
const charJSON = JSON.stringify(char);
const result = await writeCharacterData(defaultAvatarPath, charJSON, pngName, request);
return result ? pngName : '';
}
return '';
}
/**
* Import a character from a PNG file.
* @param {string} uploadPath Path to the uploaded file
* @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects
* @param {string|undefined} preservedFileName Preserved file name
* @returns {Promise<string>} Internal name of the character
*/
async function importFromPng(uploadPath, { request }, preservedFileName) {
const imgData = await readCharacterData(uploadPath);
if (imgData === undefined) throw new Error('Failed to read character data');
let jsonData = JSON.parse(imgData);
jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
if (jsonData.spec !== undefined) {
console.log('Found a v2 character file.');
importRisuSprites(request.user.directories, jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
const char = JSON.stringify(jsonData);
const result = await writeCharacterData(uploadPath, char, pngName, request);
fs.unlinkSync(uploadPath);
return result ? pngName : '';
} else if (jsonData.name !== undefined) {
console.log('Found a v1 character file.');
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
let char = {
'name': jsonData.name,
'description': jsonData.description ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'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,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char, request.user.directories);
const charJSON = JSON.stringify(char);
const result = await writeCharacterData(uploadPath, charJSON, pngName, request);
fs.unlinkSync(uploadPath);
return result ? pngName : '';
}
return '';
}
const router = express.Router();
router.post('/create', urlencodedParser, async function (request, response) {
if (!request.body) return response.sendStatus(400);
try {
if (!request.body) return response.sendStatus(400);
request.body.ch_name = sanitize(request.body.ch_name);
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);
const char = JSON.stringify(charaFormatData(request.body, request.user.directories));
const internalName = getPngName(request.body.ch_name, request.user.directories);
const avatarName = `${internalName}.png`;
const defaultAvatar = './public/img/ai4.png';
const chatsPath = path.join(request.user.directories.chats, internalName);
if (!fs.existsSync(chatsPath)) fs.mkdirSync(chatsPath);
if (!fs.existsSync(chatsPath)) fs.mkdirSync(chatsPath);
if (!request.file) {
charaWrite(defaultAvatar, char, internalName, response, avatarName);
} else {
const crop = tryParse(request.query.crop);
const uploadPath = path.join(UPLOADS_PATH, request.file.filename);
await charaWrite(uploadPath, char, internalName, response, avatarName, crop);
fs.unlinkSync(uploadPath);
if (!request.file) {
await writeCharacterData(defaultAvatar, char, internalName, request);
return response.send(avatarName);
} else {
const crop = tryParse(request.query.crop);
const uploadPath = path.join(UPLOADS_PATH, request.file.filename);
await writeCharacterData(uploadPath, char, internalName, request, crop);
fs.unlinkSync(uploadPath);
return response.send(avatarName);
}
} catch (err) {
console.error(err);
response.sendStatus(500);
}
});
@ -483,26 +655,26 @@ router.post('/rename', jsonParser, async function (request, response) {
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 newInternalName = getPngName(newName, request.user.directories);
const newAvatarName = `${newInternalName}.png`;
const oldAvatarPath = path.join(DIRECTORIES.characters, oldAvatarName);
const oldAvatarPath = path.join(request.user.directories.characters, oldAvatarName);
const oldChatsPath = path.join(DIRECTORIES.chats, oldInternalName);
const newChatsPath = path.join(DIRECTORIES.chats, newInternalName);
const oldChatsPath = path.join(request.user.directories.chats, oldInternalName);
const newChatsPath = path.join(request.user.directories.chats, newInternalName);
try {
// Read old file, replace name int it
const rawOldData = await charaRead(oldAvatarPath);
const rawOldData = await readCharacterData(oldAvatarPath);
if (rawOldData === undefined) throw new Error('Failed to read character file');
const oldData = getCharaCardV2(JSON.parse(rawOldData));
const oldData = getCharaCardV2(JSON.parse(rawOldData), request.user.directories);
_.set(oldData, 'data.name', newName);
_.set(oldData, 'name', newName);
const newData = JSON.stringify(oldData);
// Write data to new location
await charaWrite(oldAvatarPath, newData, newInternalName);
await writeCharacterData(oldAvatarPath, newData, newInternalName, request);
// Rename chats folder
if (fs.existsSync(oldChatsPath) && !fs.existsSync(newChatsPath)) {
@ -513,7 +685,7 @@ router.post('/rename', jsonParser, async function (request, response) {
fs.rmSync(oldAvatarPath);
// Return new avatar name to ST
return response.send({ 'avatar': newAvatarName });
return response.send({ avatar: newAvatarName });
}
catch (err) {
console.error(err);
@ -534,23 +706,25 @@ router.post('/edit', urlencodedParser, async function (request, response) {
return;
}
let char = charaFormatData(request.body);
let char = charaFormatData(request.body, request.user.directories);
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', '');
let targetFile = (request.body.avatar_url).replace('.png', '');
try {
if (!request.file) {
const avatarPath = path.join(DIRECTORIES.characters, request.body.avatar_url);
await charaWrite(avatarPath, char, target_img, response, 'Character saved');
const avatarPath = path.join(request.user.directories.characters, request.body.avatar_url);
await writeCharacterData(avatarPath, char, targetFile, request);
} else {
const crop = tryParse(request.query.crop);
const newAvatarPath = path.join(UPLOADS_PATH, request.file.filename);
invalidateThumbnail('avatar', request.body.avatar_url);
await charaWrite(newAvatarPath, char, target_img, response, 'Character saved', crop);
invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
fs.unlinkSync(newAvatarPath);
}
return response.sendStatus(200);
}
catch {
console.error('An error occured, character edit invalidated.');
@ -572,22 +746,20 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
console.log(request.body);
if (!request.body) {
console.error('Error: no response body detected');
response.status(400).send('Error: no response body detected');
return;
return response.status(400).send('Error: no response body detected');
}
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;
return response.status(400).send('Error: invalid name.');
}
try {
const avatarPath = path.join(DIRECTORIES.characters, request.body.avatar_url);
let charJSON = await charaRead(avatarPath);
const avatarPath = path.join(request.user.directories.characters, request.body.avatar_url);
const charJSON = await readCharacterData(avatarPath);
if (typeof charJSON !== 'string') throw new Error('Failed to read character file');
let char = JSON.parse(charJSON);
const char = JSON.parse(charJSON);
//check if the field exists
if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {
console.error('Error: invalid field.');
@ -597,7 +769,9 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
char[request.body.field] = request.body.value;
char.data[request.body.field] = request.body.value;
let newCharJSON = JSON.stringify(char);
await charaWrite(avatarPath, newCharJSON, (request.body.avatar_url).replace('.png', ''), response, 'Character saved');
const targetFile = (request.body.avatar_url).replace('.png', '');
await writeCharacterData(avatarPath, newCharJSON, targetFile, request);
return response.sendStatus(200);
} catch (err) {
console.error('An error occured, character edit invalidated.', err);
}
@ -617,30 +791,25 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
router.post('/merge-attributes', jsonParser, async function (request, response) {
try {
const update = request.body;
const avatarPath = path.join(DIRECTORIES.characters, update.avatar);
const avatarPath = path.join(request.user.directories.characters, update.avatar);
const pngStringData = await charaRead(avatarPath);
const pngStringData = await readCharacterData(avatarPath);
if (!pngStringData) {
console.error('Error: invalid character file.');
response.status(400).send('Error: invalid character file.');
return;
return response.status(400).send('Error: invalid character file.');
}
let character = JSON.parse(pngStringData);
character = deepMerge(character, update);
const validator = new TavernCardValidator(character);
const targetImg = (update.avatar).replace('.png', '');
//Accept either V1 or V2.
if (validator.validate()) {
await charaWrite(
avatarPath,
JSON.stringify(character),
(update.avatar).replace('.png', ''),
response,
'Character saved',
);
await writeCharacterData(avatarPath, JSON.stringify(character), targetImg, request);
response.sendStatus(200);
} else {
console.log(validator.lastValidationError);
response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError });
@ -660,13 +829,13 @@ router.post('/delete', jsonParser, async function (request, response) {
return response.sendStatus(403);
}
const avatarPath = DIRECTORIES.characters + request.body.avatar_url;
const avatarPath = path.join(request.user.directories.characters, request.body.avatar_url);
if (!fs.existsSync(avatarPath)) {
return response.sendStatus(400);
}
fs.rmSync(avatarPath);
invalidateThumbnail('avatar', request.body.avatar_url);
invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
let dir_name = (request.body.avatar_url.replace('.png', ''));
if (!dir_name.length) {
@ -676,7 +845,7 @@ router.post('/delete', jsonParser, async function (request, response) {
if (request.body.delete_chats == true) {
try {
await fs.promises.rm(path.join(DIRECTORIES.chats, sanitize(dir_name)), { recursive: true, force: true });
await fs.promises.rm(path.join(request.user.directories.chats, sanitize(dir_name)), { recursive: true, force: true });
} catch (err) {
console.error(err);
return response.sendStatus(500);
@ -696,46 +865,40 @@ router.post('/delete', jsonParser, async function (request, response) {
* The stats are calculated by the `calculateStats` function.
* The characters are processed by the `processCharacter` function.
*
* @param {object} request The HTTP request object.
* @param {object} response The HTTP response object.
* @return {undefined} Does not return a value.
* @param {import("express").Request} request The HTTP request object.
* @param {import("express").Response} response The HTTP response object.
* @return {void}
*/
router.post('/all', jsonParser, function (request, response) {
fs.readdir(DIRECTORIES.characters, async (err, files) => {
if (err) {
console.error(err);
return;
}
router.post('/all', jsonParser, async function (request, response) {
try {
const files = fs.readdirSync(request.user.directories.characters);
const pngFiles = files.filter(file => file.endsWith('.png'));
characters = {};
let processingPromises = pngFiles.map((file, index) => processCharacter(file, index));
await Promise.all(processingPromises); performance.mark('B');
// Filter out invalid/broken characters
characters = Object.values(characters).filter(x => x?.name).reduce((acc, val, index) => {
acc[index] = val;
return acc;
}, {});
response.send(JSON.stringify(characters));
});
const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories));
const data = await Promise.all(processingPromises);
return response.send(data);
} catch (err) {
console.error(err);
response.sendStatus(500);
}
});
router.post('/get', jsonParser, async function (request, response) {
if (!request.body) return response.sendStatus(400);
const item = request.body.avatar_url;
const filePath = path.join(DIRECTORIES.characters, item);
try {
if (!request.body) return response.sendStatus(400);
const item = request.body.avatar_url;
const filePath = path.join(request.user.directories.characters, item);
if (!fs.existsSync(filePath)) {
return response.sendStatus(404);
if (!fs.existsSync(filePath)) {
return response.sendStatus(404);
}
const data = await processCharacter(item, request.user.directories);
return response.send(data);
} catch (err) {
console.error(err);
response.sendStatus(500);
}
characters = {};
await processCharacter(item, 0);
return response.send(characters[0]);
});
router.post('/chats', jsonParser, async function (request, response) {
@ -744,7 +907,7 @@ router.post('/chats', jsonParser, async function (request, response) {
const characterDirectory = (request.body.avatar_url).replace('.png', '');
try {
const chatsDirectory = path.join(DIRECTORIES.chats, characterDirectory);
const chatsDirectory = path.join(request.user.directories.chats, characterDirectory);
const files = fs.readdirSync(chatsDirectory);
const jsonFiles = files.filter(file => path.extname(file) === '.jsonl');
@ -755,7 +918,7 @@ router.post('/chats', jsonParser, async function (request, response) {
const jsonFilesPromise = jsonFiles.map((file) => {
return new Promise(async (res) => {
const pathToFile = path.join(DIRECTORIES.chats, characterDirectory, file);
const pathToFile = path.join(request.user.directories.chats, characterDirectory, file);
const fileStream = fs.createReadStream(pathToFile);
const stats = fs.statSync(pathToFile);
const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
@ -805,11 +968,17 @@ router.post('/chats', jsonParser, async function (request, response) {
}
});
function getPngName(file) {
/**
* Gets the name for the uploaded PNG file.
* @param {string} file File name
* @param {import('../users').UserDirectoryList} directories User directories
* @returns {string} - The name for the uploaded PNG file
*/
function getPngName(file, directories) {
let i = 1;
let base_name = file;
while (fs.existsSync(DIRECTORIES.characters + file + '.png')) {
file = base_name + i;
const baseName = file;
while (fs.existsSync(path.join(directories.characters, `${file}.png`))) {
file = baseName + i;
i++;
}
return file;
@ -829,147 +998,35 @@ function getPreservedName(request) {
router.post('/import', urlencodedParser, async function (request, response) {
if (!request.body || !request.file) return response.sendStatus(400);
let png_name = '';
let filedata = request.file;
let uploadPath = path.join(UPLOADS_PATH, filedata.filename);
let format = request.body.file_type;
const uploadPath = path.join(UPLOADS_PATH, request.file.filename);
const format = request.body.file_type;
const preservedFileName = getPreservedName(request);
if (format == 'yaml' || format == 'yml') {
try {
importFromYaml(uploadPath, response);
} catch (err) {
console.log(err);
response.send({ error: true });
const formatImportFunctions = {
'yaml': importFromYaml,
'yml': importFromYaml,
'json': importFromJson,
'png': importFromPng,
};
try {
const importFunction = formatImportFunctions[format];
if (!importFunction) {
throw new Error(`Unsupported format: ${format}`);
}
} else if (format == 'json') {
fs.readFile(uploadPath, 'utf8', async (err, data) => {
fs.unlinkSync(uploadPath);
if (err) {
console.log(err);
response.send({ error: true });
}
const fileName = await importFunction(uploadPath, { request, response }, preservedFileName);
let jsonData = JSON.parse(data);
if (jsonData.spec !== undefined) {
console.log('importing from v2 json');
importRisuSprites(jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
png_name = getPngName(jsonData.data?.name || jsonData.name);
let char = JSON.stringify(jsonData);
charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
} else if (jsonData.name !== undefined) {
console.log('importing from v1 json');
jsonData.name = sanitize(jsonData.name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
png_name = getPngName(jsonData.name);
let char = {
'name': jsonData.name,
'description': jsonData.description ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'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,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
let charJSON = JSON.stringify(char);
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
console.log('importing from gradio json');
jsonData.char_name = sanitize(jsonData.char_name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
png_name = getPngName(jsonData.char_name);
let char = {
'name': jsonData.char_name,
'description': jsonData.char_persona ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'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,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
let charJSON = JSON.stringify(char);
charaWrite(defaultAvatarPath, charJSON, 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);
if (img_data === undefined) throw new Error('Failed to read character data');
let jsonData = JSON.parse(img_data);
jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
png_name = preservedFileName || getPngName(jsonData.name);
if (jsonData.spec !== undefined) {
console.log('Found a v2 character file.');
importRisuSprites(jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
const char = JSON.stringify(jsonData);
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath);
} else if (jsonData.name !== undefined) {
console.log('Found a v1 character file.');
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
let char = {
'name': jsonData.name,
'description': jsonData.description ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'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,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
const charJSON = JSON.stringify(char);
await charaWrite(uploadPath, charJSON, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath);
} else {
console.log('Unknown character card format');
response.send({ error: true });
}
} catch (err) {
console.log(err);
response.send({ error: true });
if (!fileName) {
console.error('Failed to import character');
return response.sendStatus(400);
}
response.send({ file_name: fileName });
} catch (err) {
console.log(err);
response.send({ error: true });
}
});
@ -980,7 +1037,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
console.log(request.body);
return response.sendStatus(400);
}
let filename = path.join(DIRECTORIES.characters, sanitize(request.body.avatar_url));
let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));
if (!fs.existsSync(filename)) {
console.log('file for dupe not found');
console.log(filename);
@ -1002,11 +1059,11 @@ router.post('/duplicate', jsonParser, async function (request, response) {
baseName = nameParts.join('_'); // original filename is completely the baseName
}
newFilename = path.join(DIRECTORIES.characters, `${baseName}_${suffix}${path.extname(filename)}`);
newFilename = path.join(request.user.directories.characters, `${baseName}_${suffix}${path.extname(filename)}`);
while (fs.existsSync(newFilename)) {
let suffixStr = '_' + suffix;
newFilename = path.join(DIRECTORIES.characters, `${baseName}${suffixStr}${path.extname(filename)}`);
newFilename = path.join(request.user.directories.characters, `${baseName}${suffixStr}${path.extname(filename)}`);
suffix++;
}
@ -1025,7 +1082,7 @@ router.post('/export', jsonParser, async function (request, response) {
return response.sendStatus(400);
}
let filename = path.join(DIRECTORIES.characters, sanitize(request.body.avatar_url));
let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url));
if (!fs.existsSync(filename)) {
return response.sendStatus(404);
@ -1036,9 +1093,9 @@ router.post('/export', jsonParser, async function (request, response) {
return response.sendFile(filename, { root: process.cwd() });
case 'json': {
try {
let json = await charaRead(filename);
let json = await readCharacterData(filename);
if (json === undefined) return response.sendStatus(400);
let jsonObject = getCharaCardV2(JSON.parse(json));
let jsonObject = getCharaCardV2(JSON.parse(json), request.user.directories);
return response.type('json').send(JSON.stringify(jsonObject, null, 4));
}
catch {