Merge pull request #1473 from valadaptive/chats-endpoint

Move chat endpoints into their own module
This commit is contained in:
Cohee 2023-12-07 12:36:43 +02:00 committed by GitHub
commit c0fb5a4787
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 481 additions and 444 deletions

View File

@ -1249,7 +1249,7 @@ async function getCharacters() {
}
async function delChat(chatfile) {
const response = await fetch('/delchat', {
const response = await fetch('/api/chats/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
@ -3883,7 +3883,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
toastr.error(data.response, 'API Error');
}
}
console.debug('/savechat called by /Generate');
console.debug('/api/chats/save called by /Generate');
await saveChatConditional();
is_send_press = false;
@ -4903,7 +4903,7 @@ async function renamePastChats(newAvatar, newValue) {
for (const { file_name } of pastChats) {
try {
const fileNameWithoutExtension = file_name.replace('.jsonl', '');
const getChatResponse = await fetch('/getchat', {
const getChatResponse = await fetch('/api/chats/get', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
@ -4927,7 +4927,7 @@ async function renamePastChats(newAvatar, newValue) {
}
}
const saveChatResponse = await fetch('/savechat', {
const saveChatResponse = await fetch('/api/chats/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
@ -5018,7 +5018,7 @@ async function saveChat(chat_name, withMetadata, mesId) {
];
return jQuery.ajax({
type: 'POST',
url: '/savechat',
url: '/api/chats/save',
data: JSON.stringify({
ch_name: characters[this_chid].name,
file_name: file_name,
@ -5110,11 +5110,11 @@ function getThumbnailUrl(type, file) {
}
async function getChat() {
//console.log('/getchat -- entered for -- ' + characters[this_chid].name);
//console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);
try {
const response = await $.ajax({
type: 'POST',
url: '/getchat',
url: '/api/chats/get',
data: JSON.stringify({
ch_name: characters[this_chid].name,
file_name: characters[this_chid].chat,
@ -5875,7 +5875,7 @@ export async function getChatsFromFiles(data, isGroupChat) {
let chat_promise = chat_list.map(({ file_name }) => {
return new Promise(async (res, rej) => {
try {
const endpoint = isGroupChat ? '/getgroupchat' : '/getchat';
const endpoint = isGroupChat ? '/api/chats/group/get' : '/api/chats/get';
const requestBody = isGroupChat
? JSON.stringify({ id: file_name })
: JSON.stringify({
@ -6562,7 +6562,7 @@ export async function saveChatConditional() {
async function importCharacterChat(formData) {
await jQuery.ajax({
type: 'POST',
url: '/importchat',
url: '/api/chats/import',
data: formData,
beforeSend: function () {
},
@ -8161,7 +8161,8 @@ jQuery(async function () {
};
try {
const response = await fetch('/renamechat', {
showLoader();
const response = await fetch('/api/chats/rename', {
method: 'POST',
body: JSON.stringify(body),
headers: getRequestHeaders(),
@ -8193,8 +8194,11 @@ jQuery(async function () {
$('#option_select_chat').trigger('click');
$('#options').hide();
} catch {
hideLoader();
await delay(500);
await callPopup('An error has occurred. Chat was not renamed.', 'text');
} finally {
hideLoader();
}
});
@ -8215,7 +8219,7 @@ jQuery(async function () {
};
console.log(body);
try {
const response = await fetch('/exportchat', {
const response = await fetch('/api/chats/export', {
method: 'POST',
body: JSON.stringify(body),
headers: getRequestHeaders(),

View File

@ -320,7 +320,7 @@ async function convertSoloToGroupChat() {
}
// Save group chat
const createChatResponse = await fetch('/savegroupchat', {
const createChatResponse = await fetch('/api/chats/group/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ id: chatName, chat: groupChat }),

View File

@ -152,7 +152,7 @@ async function regenerateGroup() {
}
async function loadGroupChat(chatId) {
const response = await fetch('/getgroupchat', {
const response = await fetch('/api/chats/group/get', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ id: chatId }),
@ -401,7 +401,7 @@ async function saveGroupChat(groupId, shouldSaveGroup) {
const group = groups.find(x => x.id == groupId);
const chat_id = group.chat_id;
group['date_last_chat'] = Date.now();
const response = await fetch('/savegroupchat', {
const response = await fetch('/api/chats/group/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ id: chat_id, chat: [...chat] }),
@ -455,7 +455,7 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) {
}
if (hadChanges) {
const saveChatResponse = await fetch('/savegroupchat', {
const saveChatResponse = await fetch('/api/chats/group/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ id: chatId, chat: [...messages] }),
@ -1659,7 +1659,7 @@ export async function deleteGroupChat(groupId, chatId) {
delete group.past_metadata[chatId];
updateChatMetadata(group.chat_metadata, true);
const response = await fetch('/deletegroupchat', {
const response = await fetch('/api/chats/group/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ id: chatId }),
@ -1679,7 +1679,7 @@ export async function deleteGroupChat(groupId, chatId) {
export async function importGroupChat(formData) {
await jQuery.ajax({
type: 'POST',
url: '/importgroupchat',
url: '/api/chats/group/import',
data: formData,
beforeSend: function () {
},
@ -1720,7 +1720,7 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
await editGroup(groupId, true, false);
await fetch('/savegroupchat', {
await fetch('/api/chats/group/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ id: name, chat: [...trimmed_chat] }),

442
server.js
View File

@ -6,7 +6,6 @@ const fs = require('fs');
const http = require('http');
const https = require('https');
const path = require('path');
const readline = require('readline');
const util = require('util');
const { Readable } = require('stream');
@ -48,7 +47,7 @@ const { jsonParser, urlencodedParser } = require('./src/express-common.js');
const contentManager = require('./src/endpoints/content-manager');
const statsHelpers = require('./statsHelpers.js');
const { readSecret, migrateSecrets, SECRET_KEYS } = require('./src/endpoints/secrets');
const { delay, getVersion, getConfigValue, color, uuidv4, humanizedISO8601DateTime, tryParse, clientRelativePath, removeFileExtension } = require('./src/util');
const { delay, getVersion, getConfigValue, color, uuidv4, humanizedISO8601DateTime, tryParse, clientRelativePath, removeFileExtension, generateTimestamp, removeOldBackups } = require('./src/util');
const { invalidateThumbnail, ensureThumbnailCache } = require('./src/endpoints/thumbnails');
const { getTokenizerModel, getTiktokenTokenizer, loadTokenizers, TEXT_COMPLETION_MODELS, getSentencepiceTokenizer, sentencepieceTokenizers } = require('./src/endpoints/tokenizers');
const { convertClaudePrompt } = require('./src/chat-completion');
@ -723,55 +722,6 @@ app.post('/api/textgenerationwebui/generate', jsonParser, async function (reques
}
});
app.post('/savechat', jsonParser, function (request, response) {
try {
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');
writeFileAtomicSync(`${DIRECTORIES.chats + sanitize(dir_name)}/${sanitize(String(request.body.file_name))}.jsonl`, jsonlData, 'utf8');
backupChat(dir_name, jsonlData);
return response.send({ result: 'ok' });
} catch (error) {
response.send(error);
return console.log(error);
}
});
app.post('/getchat', jsonParser, function (request, response) {
try {
const dirName = String(request.body.avatar_url).replace('.png', '');
const chatDirExists = fs.existsSync(DIRECTORIES.chats + dirName);
//if no chat dir for the character is found, make one with the character name
if (!chatDirExists) {
fs.mkdirSync(DIRECTORIES.chats + dirName);
return response.send({});
}
if (!request.body.file_name) {
return response.send({});
}
const fileName = `${DIRECTORIES.chats + dirName}/${sanitize(String(request.body.file_name))}.jsonl`;
const chatFileExists = fs.existsSync(fileName);
if (!chatFileExists) {
return response.send({});
}
const data = fs.readFileSync(fileName, 'utf8');
const lines = data.split('\n');
// Iterate through the array of strings and parse each line as JSON
const jsonData = lines.map((l) => { try { return JSON.parse(l); } catch (_) { return; } }).filter(x => x);
return response.send(jsonData);
} catch (error) {
console.error(error);
return response.send({});
}
});
// Only called for kobold
app.post('/getstatus', jsonParser, async function (request, response) {
if (!request.body) return response.sendStatus(400);
@ -829,30 +779,6 @@ app.post('/getstatus', jsonParser, async function (request, response) {
}
});
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 });
});
/**
* Handle a POST request to get the stats object
*
@ -967,37 +893,6 @@ app.post('/delbackground', jsonParser, function (request, response) {
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 dirName = String(request.body.avatar_url).replace('.png', '');
const fileName = `${DIRECTORIES.chats + dirName}/${sanitize(String(request.body.chatfile))}`;
const chatFileExists = fs.existsSync(fileName);
if (!chatFileExists) {
console.log(`Chat file not found '${fileName}'`);
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('/renamebackground', jsonParser, function (request, response) {
if (!request.body) return response.sendStatus(400);
@ -1237,215 +1132,6 @@ function getImages(path) {
.sort(Intl.Collator().compare);
}
app.post('/exportchat', jsonParser, async function (request, response) {
if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {
return response.sendStatus(400);
}
const pathToFolder = request.body.is_group
? DIRECTORIES.groupChats
: path.join(DIRECTORIES.chats, String(request.body.avatar_url).replace('.png', ''));
let filename = path.join(pathToFolder, request.body.file);
let exportfilename = request.body.exportfilename;
if (!fs.existsSync(filename)) {
const errorMessage = {
message: `Could not find JSONL file to export. Source chat file: ${filename}.`,
};
console.log(errorMessage.message);
return response.status(404).json(errorMessage);
}
try {
// Short path for JSONL files
if (request.body.format == 'jsonl') {
try {
const rawFile = fs.readFileSync(filename, 'utf8');
const successMessage = {
message: `Chat saved to ${exportfilename}`,
result: rawFile,
};
console.log(`Chat exported as ${exportfilename}`);
return response.status(200).json(successMessage);
}
catch (err) {
console.error(err);
const errorMessage = {
message: `Could not read JSONL file to export. Source chat file: ${filename}.`,
};
console.log(errorMessage.message);
return response.status(500).json(errorMessage);
}
}
const readStream = fs.createReadStream(filename);
const rl = readline.createInterface({
input: readStream,
});
let buffer = '';
rl.on('line', (line) => {
const data = JSON.parse(line);
if (data.mes) {
const name = data.name;
const message = (data?.extra?.display_text || data?.mes || '').replace(/\r?\n/g, '\n');
buffer += (`${name}: ${message}\n\n`);
}
});
rl.on('close', () => {
const successMessage = {
message: `Chat saved to ${exportfilename}`,
result: buffer,
};
console.log(`Chat exported as ${exportfilename}`);
return response.status(200).json(successMessage);
});
}
catch (err) {
console.log('chat export failed.');
console.log(err);
return response.sendStatus(400);
}
});
app.post('/importgroupchat', urlencodedParser, function (request, response) {
try {
const filedata = request.file;
if (!filedata) {
return response.sendStatus(400);
}
const chatname = humanizedISO8601DateTime();
const pathToUpload = path.join(UPLOADS_PATH, filedata.filename);
const pathToNewFile = path.join(DIRECTORIES.groupChats, `${chatname}.jsonl`);
fs.copyFileSync(pathToUpload, pathToNewFile);
fs.unlinkSync(pathToUpload);
return response.send({ res: chatname });
} catch (error) {
console.error(error);
return response.send({ error: true });
}
});
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;
let user_name = request.body.user_name || 'You';
if (!filedata) {
return response.sendStatus(400);
}
try {
const data = fs.readFileSync(path.join(UPLOADS_PATH, filedata.filename), 'utf8');
if (format === 'json') {
const jsonData = JSON.parse(data);
if (jsonData.histories !== undefined) {
//console.log('/importchat confirms JSON histories are defined');
const chat = {
from(history) {
return [
{
user_name: user_name,
character_name: ch_name,
create_date: humanizedISO8601DateTime(),
},
...history.msgs.map(
(message) => ({
name: message.src.is_human ? user_name : ch_name,
is_user: message.src.is_human,
send_date: humanizedISO8601DateTime(),
mes: message.text,
}),
)];
},
};
const newChats = [];
(jsonData.histories.histories ?? []).forEach((history) => {
newChats.push(chat.from(history));
});
const errors = [];
for (const chat of newChats) {
const filePath = `${DIRECTORIES.chats + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`;
const fileContent = chat.map(tryParse).filter(x => x).join('\n');
try {
writeFileAtomicSync(filePath, fileContent, 'utf8');
} catch (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 if (Array.isArray(jsonData.data_visible)) {
// oobabooga's format
/** @type {object[]} */
const chat = [{
user_name: user_name,
character_name: ch_name,
create_date: humanizedISO8601DateTime(),
}];
for (const arr of jsonData.data_visible) {
if (arr[0]) {
const userMessage = {
name: user_name,
is_user: true,
send_date: humanizedISO8601DateTime(),
mes: arr[0],
};
chat.push(userMessage);
}
if (arr[1]) {
const charMessage = {
name: ch_name,
is_user: false,
send_date: humanizedISO8601DateTime(),
mes: arr[1],
};
chat.push(charMessage);
}
}
const chatContent = chat.map(obj => JSON.stringify(obj)).join('\n');
writeFileAtomicSync(`${DIRECTORIES.chats + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`, chatContent, 'utf8');
response.send({ res: true });
} else {
console.log('Incorrect chat format .json');
return response.send({ error: true });
}
}
if (format === 'jsonl') {
const line = data.split('\n')[0];
let jsonData = JSON.parse(line);
if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
fs.copyFileSync(path.join(UPLOADS_PATH, filedata.filename), (`${DIRECTORIES.chats + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`));
response.send({ res: true });
} else {
console.log('Incorrect chat format .jsonl');
return response.send({ error: true });
}
}
} catch (error) {
console.error(error);
return response.send({ error: true });
}
});
app.post('/importworldinfo', urlencodedParser, (request, response) => {
if (!request.file) return response.sendStatus(400);
@ -1701,61 +1387,6 @@ app.post('/editgroup', jsonParser, (request, response) => {
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(line => tryParse(line)).filter(x => x);
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');
writeFileAtomicSync(pathToFile, jsonlData, 'utf8');
backupChat(String(id), jsonlData);
return response.send({ ok: true });
});
app.post('/deletegroup', jsonParser, async (request, response) => {
if (!request.body || !request.body.id) {
return response.sendStatus(400);
@ -2605,6 +2236,18 @@ redirect('/importcharacter', '/api/characters/import');
redirect('/dupecharacter', '/api/characters/duplicate');
redirect('/exportcharacter', '/api/characters/export');
// Redirect deprecated chat API endpoints
redirect('/savechat', '/api/chats/save');
redirect('/getchat', '/api/chats/get');
redirect('/renamechat', '/api/chats/rename');
redirect('/delchat', '/api/chats/delete');
redirect('/exportchat', '/api/chats/export');
redirect('/importgroupchat', '/api/chats/group/import');
redirect('/importchat', '/api/chats/import');
redirect('/getgroupchat', '/api/chats/group/get');
redirect('/deletegroupchat', '/api/chats/group/delete');
redirect('/savegroupchat', '/api/chats/group/save');
// ** REST CLIENT ASYNC WRAPPERS **
/**
@ -2656,6 +2299,9 @@ app.use('/api/files', require('./src/endpoints/files').router);
// Character management
app.use('/api/characters', require('./src/endpoints/characters').router);
// Chat management
app.use('/api/chats', require('./src/endpoints/chats').router);
// Character sprite management
app.use('/api/sprites', require('./src/endpoints/sprites').router);
@ -2760,47 +2406,6 @@ if (cliArguments.ssl) {
);
}
function generateTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
}
/**
*
* @param {string} name
* @param {string} chat
*/
function backupChat(name, chat) {
try {
const isBackupDisabled = getConfigValue('disableChatBackup', false);
if (isBackupDisabled) {
return;
}
if (!fs.existsSync(DIRECTORIES.backups)) {
fs.mkdirSync(DIRECTORIES.backups);
}
// replace non-alphanumeric characters with underscores
name = sanitize(name).replace(/[^a-z0-9]/gi, '_').toLowerCase();
const backupFile = path.join(DIRECTORIES.backups, `chat_${name}_${generateTimestamp()}.jsonl`);
writeFileAtomicSync(backupFile, chat, 'utf-8');
removeOldBackups(`chat_${name}_`);
} catch (err) {
console.log(`Could not backup chat for ${name}`, err);
}
}
function backupSettings() {
try {
if (!fs.existsSync(DIRECTORIES.backups)) {
@ -2816,21 +2421,6 @@ function backupSettings() {
}
}
/**
* @param {string} prefix
*/
function removeOldBackups(prefix) {
const MAX_BACKUPS = 25;
let files = fs.readdirSync(DIRECTORIES.backups).filter(f => f.startsWith(prefix));
if (files.length > MAX_BACKUPS) {
files = files.map(f => path.join(DIRECTORIES.backups, f));
files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
fs.rmSync(files[0]);
}
}
function ensurePublicDirectoriesExist() {
for (const dir of Object.values(DIRECTORIES)) {
if (!fs.existsSync(dir)) {

411
src/endpoints/chats.js Normal file
View File

@ -0,0 +1,411 @@
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const express = require('express');
const sanitize = require('sanitize-filename');
const writeFileAtomicSync = require('write-file-atomic').sync;
const { jsonParser, urlencodedParser } = require('../express-common');
const { DIRECTORIES, UPLOADS_PATH } = require('../constants');
const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util');
/**
*
* @param {string} name
* @param {string} chat
*/
function backupChat(name, chat) {
try {
const isBackupDisabled = getConfigValue('disableChatBackup', false);
if (isBackupDisabled) {
return;
}
if (!fs.existsSync(DIRECTORIES.backups)) {
fs.mkdirSync(DIRECTORIES.backups);
}
// replace non-alphanumeric characters with underscores
name = sanitize(name).replace(/[^a-z0-9]/gi, '_').toLowerCase();
const backupFile = path.join(DIRECTORIES.backups, `chat_${name}_${generateTimestamp()}.jsonl`);
writeFileAtomicSync(backupFile, chat, 'utf-8');
removeOldBackups(`chat_${name}_`);
} catch (err) {
console.log(`Could not backup chat for ${name}`, err);
}
}
const router = express.Router();
router.post('/save', jsonParser, function (request, response) {
try {
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');
writeFileAtomicSync(`${DIRECTORIES.chats + sanitize(dir_name)}/${sanitize(String(request.body.file_name))}.jsonl`, jsonlData, 'utf8');
backupChat(dir_name, jsonlData);
return response.send({ result: 'ok' });
} catch (error) {
response.send(error);
return console.log(error);
}
});
router.post('/get', jsonParser, function (request, response) {
try {
const dirName = String(request.body.avatar_url).replace('.png', '');
const chatDirExists = fs.existsSync(DIRECTORIES.chats + dirName);
//if no chat dir for the character is found, make one with the character name
if (!chatDirExists) {
fs.mkdirSync(DIRECTORIES.chats + dirName);
return response.send({});
}
if (!request.body.file_name) {
return response.send({});
}
const fileName = `${DIRECTORIES.chats + dirName}/${sanitize(String(request.body.file_name))}.jsonl`;
const chatFileExists = fs.existsSync(fileName);
if (!chatFileExists) {
return response.send({});
}
const data = fs.readFileSync(fileName, 'utf8');
const lines = data.split('\n');
// Iterate through the array of strings and parse each line as JSON
const jsonData = lines.map((l) => { try { return JSON.parse(l); } catch (_) { return; } }).filter(x => x);
return response.send(jsonData);
} catch (error) {
console.error(error);
return response.send({});
}
});
router.post('/rename', 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 });
});
router.post('/delete', jsonParser, function (request, response) {
console.log('/api/chats/delete 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 dirName = String(request.body.avatar_url).replace('.png', '');
const fileName = `${DIRECTORIES.chats + dirName}/${sanitize(String(request.body.chatfile))}`;
const chatFileExists = fs.existsSync(fileName);
if (!chatFileExists) {
console.log(`Chat file not found '${fileName}'`);
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');
});
router.post('/export', jsonParser, async function (request, response) {
if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {
return response.sendStatus(400);
}
const pathToFolder = request.body.is_group
? DIRECTORIES.groupChats
: path.join(DIRECTORIES.chats, String(request.body.avatar_url).replace('.png', ''));
let filename = path.join(pathToFolder, request.body.file);
let exportfilename = request.body.exportfilename;
if (!fs.existsSync(filename)) {
const errorMessage = {
message: `Could not find JSONL file to export. Source chat file: ${filename}.`,
};
console.log(errorMessage.message);
return response.status(404).json(errorMessage);
}
try {
// Short path for JSONL files
if (request.body.format == 'jsonl') {
try {
const rawFile = fs.readFileSync(filename, 'utf8');
const successMessage = {
message: `Chat saved to ${exportfilename}`,
result: rawFile,
};
console.log(`Chat exported as ${exportfilename}`);
return response.status(200).json(successMessage);
}
catch (err) {
console.error(err);
const errorMessage = {
message: `Could not read JSONL file to export. Source chat file: ${filename}.`,
};
console.log(errorMessage.message);
return response.status(500).json(errorMessage);
}
}
const readStream = fs.createReadStream(filename);
const rl = readline.createInterface({
input: readStream,
});
let buffer = '';
rl.on('line', (line) => {
const data = JSON.parse(line);
if (data.mes) {
const name = data.name;
const message = (data?.extra?.display_text || data?.mes || '').replace(/\r?\n/g, '\n');
buffer += (`${name}: ${message}\n\n`);
}
});
rl.on('close', () => {
const successMessage = {
message: `Chat saved to ${exportfilename}`,
result: buffer,
};
console.log(`Chat exported as ${exportfilename}`);
return response.status(200).json(successMessage);
});
}
catch (err) {
console.log('chat export failed.');
console.log(err);
return response.sendStatus(400);
}
});
router.post('/group/import', urlencodedParser, function (request, response) {
try {
const filedata = request.file;
if (!filedata) {
return response.sendStatus(400);
}
const chatname = humanizedISO8601DateTime();
const pathToUpload = path.join(UPLOADS_PATH, filedata.filename);
const pathToNewFile = path.join(DIRECTORIES.groupChats, `${chatname}.jsonl`);
fs.copyFileSync(pathToUpload, pathToNewFile);
fs.unlinkSync(pathToUpload);
return response.send({ res: chatname });
} catch (error) {
console.error(error);
return response.send({ error: true });
}
});
router.post('/import', 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;
let user_name = request.body.user_name || 'You';
if (!filedata) {
return response.sendStatus(400);
}
try {
const data = fs.readFileSync(path.join(UPLOADS_PATH, filedata.filename), 'utf8');
if (format === 'json') {
const jsonData = JSON.parse(data);
if (jsonData.histories !== undefined) {
//console.log('/api/chats/import confirms JSON histories are defined');
const chat = {
from(history) {
return [
{
user_name: user_name,
character_name: ch_name,
create_date: humanizedISO8601DateTime(),
},
...history.msgs.map(
(message) => ({
name: message.src.is_human ? user_name : ch_name,
is_user: message.src.is_human,
send_date: humanizedISO8601DateTime(),
mes: message.text,
}),
)];
},
};
const newChats = [];
(jsonData.histories.histories ?? []).forEach((history) => {
newChats.push(chat.from(history));
});
const errors = [];
for (const chat of newChats) {
const filePath = `${DIRECTORIES.chats + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`;
const fileContent = chat.map(tryParse).filter(x => x).join('\n');
try {
writeFileAtomicSync(filePath, fileContent, 'utf8');
} catch (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 if (Array.isArray(jsonData.data_visible)) {
// oobabooga's format
/** @type {object[]} */
const chat = [{
user_name: user_name,
character_name: ch_name,
create_date: humanizedISO8601DateTime(),
}];
for (const arr of jsonData.data_visible) {
if (arr[0]) {
const userMessage = {
name: user_name,
is_user: true,
send_date: humanizedISO8601DateTime(),
mes: arr[0],
};
chat.push(userMessage);
}
if (arr[1]) {
const charMessage = {
name: ch_name,
is_user: false,
send_date: humanizedISO8601DateTime(),
mes: arr[1],
};
chat.push(charMessage);
}
}
const chatContent = chat.map(obj => JSON.stringify(obj)).join('\n');
writeFileAtomicSync(`${DIRECTORIES.chats + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`, chatContent, 'utf8');
response.send({ res: true });
} else {
console.log('Incorrect chat format .json');
return response.send({ error: true });
}
}
if (format === 'jsonl') {
const line = data.split('\n')[0];
let jsonData = JSON.parse(line);
if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
fs.copyFileSync(path.join(UPLOADS_PATH, filedata.filename), (`${DIRECTORIES.chats + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`));
response.send({ res: true });
} else {
console.log('Incorrect chat format .jsonl');
return response.send({ error: true });
}
}
} catch (error) {
console.error(error);
return response.send({ error: true });
}
});
router.post('/group/get', 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(line => tryParse(line)).filter(x => x);
return response.send(jsonData);
} else {
return response.send([]);
}
});
router.post('/group/delete', 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 });
});
router.post('/group/save', 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');
writeFileAtomicSync(pathToFile, jsonlData, 'utf8');
backupChat(String(id), jsonlData);
return response.send({ ok: true });
});
module.exports = { router };

View File

@ -7,6 +7,8 @@ const mime = require('mime-types');
const yaml = require('yaml');
const { default: simpleGit } = require('simple-git');
const { DIRECTORIES } = require('./constants');
/**
* Returns the config object from the config.yaml file.
* @returns {object} Config object
@ -307,6 +309,34 @@ function removeFileExtension(filename) {
return filename.replace(/\.[^.]+$/, '');
}
function generateTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
}
/**
* @param {string} prefix
*/
function removeOldBackups(prefix) {
const MAX_BACKUPS = 25;
let files = fs.readdirSync(DIRECTORIES.backups).filter(f => f.startsWith(prefix));
if (files.length > MAX_BACKUPS) {
files = files.map(f => path.join(DIRECTORIES.backups, f));
files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
fs.rmSync(files[0]);
}
}
module.exports = {
getConfig,
getConfigValue,
@ -323,4 +353,6 @@ module.exports = {
tryParse,
clientRelativePath,
removeFileExtension,
generateTimestamp,
removeOldBackups,
};