diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index cf1c45426..d29cbe222 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -424,7 +424,7 @@ async function loadWorldInfoData(name) { return worldInfoCache[name]; } - const response = await fetch('/getworldinfo', { + const response = await fetch('/api/worldinfo/get', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ name: name }), @@ -1402,7 +1402,7 @@ function createWorldInfoEntry(name, data, fromSlashCommand = false) { } async function _save(name, data) { - await fetch('/editworldinfo', { + await fetch('/api/worldinfo/edit', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ name: name, data: data }), @@ -1464,7 +1464,7 @@ async function deleteWorldInfo(worldInfoName) { return; } - const response = await fetch('/deleteworldinfo', { + const response = await fetch('/api/worldinfo/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ name: worldInfoName }), @@ -2269,7 +2269,7 @@ export async function importWorldInfo(file) { jQuery.ajax({ type: 'POST', - url: '/importworldinfo', + url: '/api/worldinfo/import', data: formData, beforeSend: () => { }, cache: false, diff --git a/server.js b/server.js index 794f2db27..6a82378ae 100644 --- a/server.js +++ b/server.js @@ -51,7 +51,6 @@ const { delay, getVersion, getConfigValue, color, uuidv4, tryParse, clientRelati 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'); -const { readWorldInfoFile } = require('./src/worldinfo'); // Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. // https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 @@ -1060,34 +1059,6 @@ app.post('/getsettings', jsonParser, (request, response) => { }); }); -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); @@ -1132,66 +1103,6 @@ function getImages(path) { .sort(Intl.Collator().compare); } -app.post('/importworldinfo', urlencodedParser, (request, response) => { - if (!request.file) return response.sendStatus(400); - - const filename = `${path.parse(sanitize(request.file.originalname)).name}.json`; - - let fileContents = null; - - if (request.body.convertedData) { - fileContents = request.body.convertedData; - } else { - const pathToUpload = path.join(UPLOADS_PATH, request.file.filename); - fileContents = fs.readFileSync(pathToUpload, 'utf8'); - fs.unlinkSync(pathToUpload); - } - - try { - const worldContent = JSON.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'); - } - - writeFileAtomicSync(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 = `${sanitize(request.body.name)}.json`; - const pathToFile = path.join(DIRECTORIES.worlds, filename); - - writeFileAtomicSync(pathToFile, JSON.stringify(request.body.data, null, 4)); - - return response.send({ ok: true }); -}); - app.post('/uploaduseravatar', urlencodedParser, async (request, response) => { if (!request.file) return response.sendStatus(400); @@ -2134,6 +2045,12 @@ redirect('/creategroup', '/api/groups/create'); redirect('/editgroup', '/api/groups/edit'); redirect('/deletegroup', '/api/groups/delete'); +// Redirect deprecated worldinfo API endpoints +redirect('/getworldinfo', '/api/worldinfo/get'); +redirect('/deleteworldinfo', '/api/worldinfo/delete'); +redirect('/importworldinfo', '/api/worldinfo/import'); +redirect('/editworldinfo', '/api/worldinfo/edit'); + // ** REST CLIENT ASYNC WRAPPERS ** /** @@ -2191,6 +2108,9 @@ app.use('/api/chats', require('./src/endpoints/chats').router); // Group management app.use('/api/groups', require('./src/endpoints/groups').router); +// World info management +app.use('/api/worldinfo', require('./src/endpoints/worldinfo').router); + // Character sprite management app.use('/api/sprites', require('./src/endpoints/sprites').router); diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 5f858cb11..f21c29a5f 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -16,7 +16,7 @@ const { jsonParser, urlencodedParser } = require('../express-common'); const { deepMerge, humanizedISO8601DateTime, tryParse } = require('../util'); const { TavernCardValidator } = require('../validator/TavernCardValidator'); const characterCardParser = require('../character-card-parser.js'); -const { readWorldInfoFile, convertWorldInfoToCharacterBook } = require('../worldinfo'); +const { readWorldInfoFile } = require('./worldinfo'); const { invalidateThumbnail } = require('./thumbnails'); const { importRisuSprites } = require('./sprites'); @@ -330,6 +330,46 @@ function charaFormatData(data) { return char; } +/** + * @param {string} name Name of World Info file + * @param {object} entries Entries object + */ +function convertWorldInfoToCharacterBook(name, entries) { + /** @type {{ entries: object[]; name: string }} */ + const result = { entries: [], name }; + + for (const index in entries) { + const entry = entries[index]; + + const originalEntry = { + id: entry.uid, + keys: entry.key, + secondary_keys: entry.keysecondary, + comment: entry.comment, + content: entry.content, + constant: entry.constant, + selective: entry.selective, + insertion_order: entry.order, + enabled: !entry.disable, + position: entry.position == 0 ? 'before_char' : 'after_char', + extensions: { + position: entry.position, + exclude_recursion: entry.excludeRecursion, + display_index: entry.displayIndex, + probability: entry.probability ?? null, + useProbability: entry.useProbability ?? false, + depth: entry.depth ?? 4, + selectiveLogic: entry.selectiveLogic ?? 0, + group: entry.group ?? '', + }, + }; + + result.entries.push(originalEntry); + } + + return result; +} + const router = express.Router(); router.post('/create', urlencodedParser, async function (request, response) { diff --git a/src/endpoints/worldinfo.js b/src/endpoints/worldinfo.js new file mode 100644 index 000000000..16a1db8eb --- /dev/null +++ b/src/endpoints/worldinfo.js @@ -0,0 +1,120 @@ +const fs = require('fs'); +const path = require('path'); +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'); + +function readWorldInfoFile(worldInfoName) { + const dummyObject = { entries: {} }; + + if (!worldInfoName) { + return dummyObject; + } + + const filename = `${worldInfoName}.json`; + const pathToWorldInfo = path.join(DIRECTORIES.worlds, filename); + + if (!fs.existsSync(pathToWorldInfo)) { + console.log(`World info file ${filename} doesn't exist.`); + return dummyObject; + } + + const worldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8'); + const worldInfo = JSON.parse(worldInfoText); + return worldInfo; +} + +const router = express.Router(); + +router.post('/get', jsonParser, (request, response) => { + if (!request.body?.name) { + return response.sendStatus(400); + } + + const file = readWorldInfoFile(request.body.name); + + return response.send(file); +}); + +router.post('/delete', 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); +}); + +router.post('/import', urlencodedParser, (request, response) => { + if (!request.file) return response.sendStatus(400); + + const filename = `${path.parse(sanitize(request.file.originalname)).name}.json`; + + let fileContents = null; + + if (request.body.convertedData) { + fileContents = request.body.convertedData; + } else { + const pathToUpload = path.join(UPLOADS_PATH, request.file.filename); + fileContents = fs.readFileSync(pathToUpload, 'utf8'); + fs.unlinkSync(pathToUpload); + } + + try { + const worldContent = JSON.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'); + } + + writeFileAtomicSync(pathToNewFile, fileContents); + return response.send({ name: worldName }); +}); + +router.post('/edit', 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 = `${sanitize(request.body.name)}.json`; + const pathToFile = path.join(DIRECTORIES.worlds, filename); + + writeFileAtomicSync(pathToFile, JSON.stringify(request.body.data, null, 4)); + + return response.send({ ok: true }); +}); + +module.exports = { router, readWorldInfoFile }; diff --git a/src/worldinfo.js b/src/worldinfo.js deleted file mode 100644 index 31fdd21e4..000000000 --- a/src/worldinfo.js +++ /dev/null @@ -1,68 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const { DIRECTORIES } = require('./constants'); - -function readWorldInfoFile(worldInfoName) { - const dummyObject = { entries: {} }; - - if (!worldInfoName) { - return dummyObject; - } - - const filename = `${worldInfoName}.json`; - const pathToWorldInfo = path.join(DIRECTORIES.worlds, filename); - - if (!fs.existsSync(pathToWorldInfo)) { - console.log(`World info file ${filename} doesn't exist.`); - return dummyObject; - } - - const worldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8'); - const worldInfo = JSON.parse(worldInfoText); - return worldInfo; -} - -/** - * @param {string} name Name of World Info file - * @param {object} entries Entries object - */ -function convertWorldInfoToCharacterBook(name, entries) { - /** @type {{ entries: object[]; name: string }} */ - const result = { entries: [], name }; - - for (const index in entries) { - const entry = entries[index]; - - const originalEntry = { - id: entry.uid, - keys: entry.key, - secondary_keys: entry.keysecondary, - comment: entry.comment, - content: entry.content, - constant: entry.constant, - selective: entry.selective, - insertion_order: entry.order, - enabled: !entry.disable, - position: entry.position == 0 ? 'before_char' : 'after_char', - extensions: { - position: entry.position, - exclude_recursion: entry.excludeRecursion, - display_index: entry.displayIndex, - probability: entry.probability ?? null, - useProbability: entry.useProbability ?? false, - depth: entry.depth ?? 4, - selectiveLogic: entry.selectiveLogic ?? 0, - group: entry.group ?? '', - }, - }; - - result.entries.push(originalEntry); - } - - return result; -} - -module.exports = { - readWorldInfoFile, - convertWorldInfoToCharacterBook, -};