mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	Extract sprite and custom content endpoints to a separate files. Update constants references
This commit is contained in:
		| @@ -8713,7 +8713,7 @@ jQuery(async function () { | |||||||
|         const url = input.trim(); |         const url = input.trim(); | ||||||
|         console.debug('Custom content import started', url); |         console.debug('Custom content import started', url); | ||||||
|  |  | ||||||
|         const request = await fetch('/import_custom', { |         const request = await fetch('/api/content/import', { | ||||||
|             method: 'POST', |             method: 'POST', | ||||||
|             headers: getRequestHeaders(), |             headers: getRequestHeaders(), | ||||||
|             body: JSON.stringify({ url }), |             body: JSON.stringify({ url }), | ||||||
|   | |||||||
| @@ -1249,7 +1249,7 @@ async function onClickExpressionUpload(event) { | |||||||
|         formData.append('label', id); |         formData.append('label', id); | ||||||
|         formData.append('avatar', file); |         formData.append('avatar', file); | ||||||
|  |  | ||||||
|         await handleFileUpload('/upload_sprite', formData); |         await handleFileUpload('/api/sprites/upload', formData); | ||||||
|  |  | ||||||
|         // Reset the input |         // Reset the input | ||||||
|         e.target.form.reset(); |         e.target.form.reset(); | ||||||
| @@ -1355,7 +1355,7 @@ async function onClickExpressionUploadPackButton() { | |||||||
|         formData.append('name', name); |         formData.append('name', name); | ||||||
|         formData.append('avatar', file); |         formData.append('avatar', file); | ||||||
|  |  | ||||||
|         const { count } = await handleFileUpload('/upload_sprite_pack', formData); |         const { count } = await handleFileUpload('/api/sprites/upload-zip', formData); | ||||||
|         toastr.success(`Uploaded ${count} image(s) for ${name}`); |         toastr.success(`Uploaded ${count} image(s) for ${name}`); | ||||||
|  |  | ||||||
|         // Reset the input |         // Reset the input | ||||||
| @@ -1382,7 +1382,7 @@ async function onClickExpressionDelete(event) { | |||||||
|     const name = $('#image_list').data('name'); |     const name = $('#image_list').data('name'); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|         await fetch('/delete_sprite', { |         await fetch('/api/sprites/delete', { | ||||||
|             method: 'POST', |             method: 'POST', | ||||||
|             headers: getRequestHeaders(), |             headers: getRequestHeaders(), | ||||||
|             body: JSON.stringify({ name, label: id }), |             body: JSON.stringify({ name, label: id }), | ||||||
|   | |||||||
							
								
								
									
										519
									
								
								server.js
									
									
									
									
									
								
							
							
						
						
									
										519
									
								
								server.js
									
									
									
									
									
								
							| @@ -62,7 +62,7 @@ const characterCardParser = require('./src/character-card-parser.js'); | |||||||
| const contentManager = require('./src/content-manager'); | const contentManager = require('./src/content-manager'); | ||||||
| const statsHelpers = require('./statsHelpers.js'); | const statsHelpers = require('./statsHelpers.js'); | ||||||
| const { readSecret, migrateSecrets, SECRET_KEYS } = require('./src/secrets'); | const { readSecret, migrateSecrets, SECRET_KEYS } = require('./src/secrets'); | ||||||
| const { delay, getVersion, getImageBuffers } = require('./src/util'); | const { delay, getVersion } = require('./src/util'); | ||||||
| const { invalidateThumbnail, ensureThumbnailCache } = require('./src/thumbnails'); | const { invalidateThumbnail, ensureThumbnailCache } = require('./src/thumbnails'); | ||||||
|  |  | ||||||
| // Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. | // Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. | ||||||
| @@ -313,13 +313,12 @@ function humanizedISO8601DateTime(date) { | |||||||
|  |  | ||||||
| var charactersPath = 'public/characters/'; | var charactersPath = 'public/characters/'; | ||||||
| var chatsPath = 'public/chats/'; | var chatsPath = 'public/chats/'; | ||||||
| const UPLOADS_PATH = './uploads'; |  | ||||||
| const SETTINGS_FILE = './public/settings.json'; | const SETTINGS_FILE = './public/settings.json'; | ||||||
| const AVATAR_WIDTH = 400; | const AVATAR_WIDTH = 400; | ||||||
| const AVATAR_HEIGHT = 600; | const AVATAR_HEIGHT = 600; | ||||||
| const jsonParser = express.json({ limit: '100mb' }); | const jsonParser = express.json({ limit: '100mb' }); | ||||||
| const urlencodedParser = express.urlencoded({ extended: true, limit: '100mb' }); | const urlencodedParser = express.urlencoded({ extended: true, limit: '100mb' }); | ||||||
| const { directories } = require('./src/constants'); | const { DIRECTORIES, UPLOADS_PATH } = require('./src/constants'); | ||||||
|  |  | ||||||
| // CSRF Protection // | // CSRF Protection // | ||||||
| if (cliArguments.disableCsrf === false) { | if (cliArguments.disableCsrf === false) { | ||||||
| @@ -1036,7 +1035,7 @@ app.post("/createcharacter", urlencodedParser, async function (request, response | |||||||
|     const internalName = getPngName(request.body.ch_name); |     const internalName = getPngName(request.body.ch_name); | ||||||
|     const avatarName = `${internalName}.png`; |     const avatarName = `${internalName}.png`; | ||||||
|     const defaultAvatar = './public/img/ai4.png'; |     const defaultAvatar = './public/img/ai4.png'; | ||||||
|     const chatsPath = directories.chats + internalName; //path.join(chatsPath, internalName); |     const chatsPath = DIRECTORIES.chats + internalName; //path.join(chatsPath, internalName); | ||||||
|  |  | ||||||
|     if (!fs.existsSync(chatsPath)) fs.mkdirSync(chatsPath); |     if (!fs.existsSync(chatsPath)) fs.mkdirSync(chatsPath); | ||||||
|  |  | ||||||
| @@ -1056,8 +1055,8 @@ app.post('/renamechat', jsonParser, async function (request, response) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const pathToFolder = request.body.is_group |     const pathToFolder = request.body.is_group | ||||||
|         ? directories.groupChats |         ? DIRECTORIES.groupChats | ||||||
|         : path.join(directories.chats, String(request.body.avatar_url).replace('.png', '')); |         : path.join(DIRECTORIES.chats, String(request.body.avatar_url).replace('.png', '')); | ||||||
|     const pathToOriginalFile = path.join(pathToFolder, request.body.original_file); |     const pathToOriginalFile = path.join(pathToFolder, request.body.original_file); | ||||||
|     const pathToRenamedFile = path.join(pathToFolder, request.body.renamed_file); |     const pathToRenamedFile = path.join(pathToFolder, request.body.renamed_file); | ||||||
|     console.log('Old chat name', pathToOriginalFile); |     console.log('Old chat name', pathToOriginalFile); | ||||||
| @@ -1471,7 +1470,7 @@ app.post('/deleteuseravatar', jsonParser, function (request, response) { | |||||||
|         return response.sendStatus(403); |         return response.sendStatus(403); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const fileName = path.join(directories.avatars, sanitize(request.body.avatar)); |     const fileName = path.join(DIRECTORIES.avatars, sanitize(request.body.avatar)); | ||||||
|  |  | ||||||
|     if (fs.existsSync(fileName)) { |     if (fs.existsSync(fileName)) { | ||||||
|         fs.rmSync(fileName); |         fs.rmSync(fileName); | ||||||
| @@ -1666,41 +1665,41 @@ app.post('/getsettings', jsonParser, (request, response) => { | |||||||
|  |  | ||||||
|     // NovelAI Settings |     // NovelAI Settings | ||||||
|     const { fileContents: novelai_settings, fileNames: novelai_setting_names } |     const { fileContents: novelai_settings, fileNames: novelai_setting_names } | ||||||
|         = readPresetsFromDirectory(directories.novelAI_Settings, { |         = readPresetsFromDirectory(DIRECTORIES.novelAI_Settings, { | ||||||
|             sortFunction: sortByName(directories.novelAI_Settings), |             sortFunction: sortByName(DIRECTORIES.novelAI_Settings), | ||||||
|             removeFileExtension: true |             removeFileExtension: true | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|     // OpenAI Settings |     // OpenAI Settings | ||||||
|     const { fileContents: openai_settings, fileNames: openai_setting_names } |     const { fileContents: openai_settings, fileNames: openai_setting_names } | ||||||
|         = readPresetsFromDirectory(directories.openAI_Settings, { |         = readPresetsFromDirectory(DIRECTORIES.openAI_Settings, { | ||||||
|             sortFunction: sortByModifiedDate(directories.openAI_Settings), removeFileExtension: true |             sortFunction: sortByModifiedDate(DIRECTORIES.openAI_Settings), removeFileExtension: true | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|     // TextGenerationWebUI Settings |     // TextGenerationWebUI Settings | ||||||
|     const { fileContents: textgenerationwebui_presets, fileNames: textgenerationwebui_preset_names } |     const { fileContents: textgenerationwebui_presets, fileNames: textgenerationwebui_preset_names } | ||||||
|         = readPresetsFromDirectory(directories.textGen_Settings, { |         = readPresetsFromDirectory(DIRECTORIES.textGen_Settings, { | ||||||
|             sortFunction: sortByName(directories.textGen_Settings), removeFileExtension: true |             sortFunction: sortByName(DIRECTORIES.textGen_Settings), removeFileExtension: true | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|     //Kobold |     //Kobold | ||||||
|     const { fileContents: koboldai_settings, fileNames: koboldai_setting_names } |     const { fileContents: koboldai_settings, fileNames: koboldai_setting_names } | ||||||
|         = readPresetsFromDirectory(directories.koboldAI_Settings, { |         = readPresetsFromDirectory(DIRECTORIES.koboldAI_Settings, { | ||||||
|             sortFunction: sortByName(directories.koboldAI_Settings), removeFileExtension: true |             sortFunction: sortByName(DIRECTORIES.koboldAI_Settings), removeFileExtension: true | ||||||
|         }) |         }) | ||||||
|  |  | ||||||
|     const worldFiles = fs |     const worldFiles = fs | ||||||
|         .readdirSync(directories.worlds) |         .readdirSync(DIRECTORIES.worlds) | ||||||
|         .filter(file => path.extname(file).toLowerCase() === '.json') |         .filter(file => path.extname(file).toLowerCase() === '.json') | ||||||
|         .sort((a, b) => a.localeCompare(b)); |         .sort((a, b) => a.localeCompare(b)); | ||||||
|     const world_names = worldFiles.map(item => path.parse(item).name); |     const world_names = worldFiles.map(item => path.parse(item).name); | ||||||
|  |  | ||||||
|     const themes = readAndParseFromDirectory(directories.themes); |     const themes = readAndParseFromDirectory(DIRECTORIES.themes); | ||||||
|     const movingUIPresets = readAndParseFromDirectory(directories.movingUI); |     const movingUIPresets = readAndParseFromDirectory(DIRECTORIES.movingUI); | ||||||
|     const quickReplyPresets = readAndParseFromDirectory(directories.quickreplies); |     const quickReplyPresets = readAndParseFromDirectory(DIRECTORIES.quickreplies); | ||||||
|  |  | ||||||
|     const instruct = readAndParseFromDirectory(directories.instruct); |     const instruct = readAndParseFromDirectory(DIRECTORIES.instruct); | ||||||
|     const context = readAndParseFromDirectory(directories.context); |     const context = readAndParseFromDirectory(DIRECTORIES.context); | ||||||
|  |  | ||||||
|     response.send({ |     response.send({ | ||||||
|         settings, |         settings, | ||||||
| @@ -1739,7 +1738,7 @@ app.post('/deleteworldinfo', jsonParser, (request, response) => { | |||||||
|  |  | ||||||
|     const worldInfoName = request.body.name; |     const worldInfoName = request.body.name; | ||||||
|     const filename = sanitize(`${worldInfoName}.json`); |     const filename = sanitize(`${worldInfoName}.json`); | ||||||
|     const pathToWorldInfo = path.join(directories.worlds, filename); |     const pathToWorldInfo = path.join(DIRECTORIES.worlds, filename); | ||||||
|  |  | ||||||
|     if (!fs.existsSync(pathToWorldInfo)) { |     if (!fs.existsSync(pathToWorldInfo)) { | ||||||
|         throw new Error(`World info file ${filename} doesn't exist.`); |         throw new Error(`World info file ${filename} doesn't exist.`); | ||||||
| @@ -1755,7 +1754,7 @@ app.post('/savetheme', jsonParser, (request, response) => { | |||||||
|         return response.sendStatus(400); |         return response.sendStatus(400); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const filename = path.join(directories.themes, sanitize(request.body.name) + '.json'); |     const filename = path.join(DIRECTORIES.themes, sanitize(request.body.name) + '.json'); | ||||||
|     writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); |     writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); | ||||||
|  |  | ||||||
|     return response.sendStatus(200); |     return response.sendStatus(200); | ||||||
| @@ -1766,7 +1765,7 @@ app.post('/savemovingui', jsonParser, (request, response) => { | |||||||
|         return response.sendStatus(400); |         return response.sendStatus(400); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const filename = path.join(directories.movingUI, sanitize(request.body.name) + '.json'); |     const filename = path.join(DIRECTORIES.movingUI, sanitize(request.body.name) + '.json'); | ||||||
|     writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); |     writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); | ||||||
|  |  | ||||||
|     return response.sendStatus(200); |     return response.sendStatus(200); | ||||||
| @@ -1777,7 +1776,7 @@ app.post('/savequickreply', jsonParser, (request, response) => { | |||||||
|         return response.sendStatus(400); |         return response.sendStatus(400); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const filename = path.join(directories.quickreplies, sanitize(request.body.name) + '.json'); |     const filename = path.join(DIRECTORIES.quickreplies, sanitize(request.body.name) + '.json'); | ||||||
|     writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); |     writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); | ||||||
|  |  | ||||||
|     return response.sendStatus(200); |     return response.sendStatus(200); | ||||||
| @@ -1826,7 +1825,7 @@ function readWorldInfoFile(worldInfoName) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const filename = `${worldInfoName}.json`; |     const filename = `${worldInfoName}.json`; | ||||||
|     const pathToWorldInfo = path.join(directories.worlds, filename); |     const pathToWorldInfo = path.join(DIRECTORIES.worlds, filename); | ||||||
|  |  | ||||||
|     if (!fs.existsSync(pathToWorldInfo)) { |     if (!fs.existsSync(pathToWorldInfo)) { | ||||||
|         throw new Error(`World info file ${filename} doesn't exist.`); |         throw new Error(`World info file ${filename} doesn't exist.`); | ||||||
| @@ -1941,6 +1940,7 @@ app.post("/importcharacter", urlencodedParser, async function (request, response | |||||||
|     let uploadPath = path.join(UPLOADS_PATH, filedata.filename); |     let uploadPath = path.join(UPLOADS_PATH, filedata.filename); | ||||||
|     var format = request.body.file_type; |     var format = request.body.file_type; | ||||||
|     const defaultAvatarPath = './public/img/ai4.png'; |     const defaultAvatarPath = './public/img/ai4.png'; | ||||||
|  |     const { importRisuSprites } = require('./src/sprites'); | ||||||
|     //console.log(format); |     //console.log(format); | ||||||
|     if (filedata) { |     if (filedata) { | ||||||
|         if (format == 'json') { |         if (format == 'json') { | ||||||
| @@ -2082,7 +2082,7 @@ app.post("/dupecharacter", jsonParser, async function (request, response) { | |||||||
|             console.log(request.body); |             console.log(request.body); | ||||||
|             return response.sendStatus(400); |             return response.sendStatus(400); | ||||||
|         } |         } | ||||||
|         let filename = path.join(directories.characters, sanitize(request.body.avatar_url)); |         let filename = path.join(DIRECTORIES.characters, sanitize(request.body.avatar_url)); | ||||||
|         if (!fs.existsSync(filename)) { |         if (!fs.existsSync(filename)) { | ||||||
|             console.log('file for dupe not found'); |             console.log('file for dupe not found'); | ||||||
|             console.log(filename); |             console.log(filename); | ||||||
| @@ -2104,11 +2104,11 @@ app.post("/dupecharacter", jsonParser, async function (request, response) { | |||||||
|             baseName = nameParts.join("_"); // original filename is completely the baseName |             baseName = nameParts.join("_"); // original filename is completely the baseName | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         newFilename = path.join(directories.characters, `${baseName}_${suffix}${path.extname(filename)}`); |         newFilename = path.join(DIRECTORIES.characters, `${baseName}_${suffix}${path.extname(filename)}`); | ||||||
|  |  | ||||||
|         while (fs.existsSync(newFilename)) { |         while (fs.existsSync(newFilename)) { | ||||||
|             let suffixStr = "_" + suffix; |             let suffixStr = "_" + suffix; | ||||||
|             newFilename = path.join(directories.characters, `${baseName}${suffixStr}${path.extname(filename)}`); |             newFilename = path.join(DIRECTORIES.characters, `${baseName}${suffixStr}${path.extname(filename)}`); | ||||||
|             suffix++; |             suffix++; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -2127,8 +2127,8 @@ app.post("/exportchat", jsonParser, async function (request, response) { | |||||||
|         return response.sendStatus(400); |         return response.sendStatus(400); | ||||||
|     } |     } | ||||||
|     const pathToFolder = request.body.is_group |     const pathToFolder = request.body.is_group | ||||||
|         ? directories.groupChats |         ? DIRECTORIES.groupChats | ||||||
|         : path.join(directories.chats, String(request.body.avatar_url).replace('.png', '')); |         : path.join(DIRECTORIES.chats, String(request.body.avatar_url).replace('.png', '')); | ||||||
|     let filename = path.join(pathToFolder, request.body.file); |     let filename = path.join(pathToFolder, request.body.file); | ||||||
|     let exportfilename = request.body.exportfilename |     let exportfilename = request.body.exportfilename | ||||||
|     if (!fs.existsSync(filename)) { |     if (!fs.existsSync(filename)) { | ||||||
| @@ -2195,7 +2195,7 @@ app.post("/exportcharacter", jsonParser, async function (request, response) { | |||||||
|         return response.sendStatus(400); |         return response.sendStatus(400); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let filename = path.join(directories.characters, sanitize(request.body.avatar_url)); |     let filename = path.join(DIRECTORIES.characters, sanitize(request.body.avatar_url)); | ||||||
|  |  | ||||||
|     if (!fs.existsSync(filename)) { |     if (!fs.existsSync(filename)) { | ||||||
|         return response.sendStatus(404); |         return response.sendStatus(404); | ||||||
| @@ -2230,7 +2230,7 @@ app.post("/importgroupchat", urlencodedParser, function (request, response) { | |||||||
|  |  | ||||||
|         const chatname = humanizedISO8601DateTime(); |         const chatname = humanizedISO8601DateTime(); | ||||||
|         const pathToUpload = path.join(UPLOADS_PATH, filedata.filename); |         const pathToUpload = path.join(UPLOADS_PATH, filedata.filename); | ||||||
|         const pathToNewFile = path.join(directories.groupChats, `${chatname}.jsonl`); |         const pathToNewFile = path.join(DIRECTORIES.groupChats, `${chatname}.jsonl`); | ||||||
|         fs.copyFileSync(pathToUpload, pathToNewFile); |         fs.copyFileSync(pathToUpload, pathToNewFile); | ||||||
|         fs.unlinkSync(pathToUpload); |         fs.unlinkSync(pathToUpload); | ||||||
|         return response.send({ res: chatname }); |         return response.send({ res: chatname }); | ||||||
| @@ -2385,7 +2385,7 @@ app.post('/importworldinfo', urlencodedParser, (request, response) => { | |||||||
|         return response.status(400).send('Is not a valid world info file'); |         return response.status(400).send('Is not a valid world info file'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const pathToNewFile = path.join(directories.worlds, filename); |     const pathToNewFile = path.join(DIRECTORIES.worlds, filename); | ||||||
|     const worldName = path.parse(pathToNewFile).name; |     const worldName = path.parse(pathToNewFile).name; | ||||||
|  |  | ||||||
|     if (!worldName) { |     if (!worldName) { | ||||||
| @@ -2414,7 +2414,7 @@ app.post('/editworldinfo', jsonParser, (request, response) => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const filename = `${sanitize(request.body.name)}.json`; |     const filename = `${sanitize(request.body.name)}.json`; | ||||||
|     const pathToFile = path.join(directories.worlds, filename); |     const pathToFile = path.join(DIRECTORIES.worlds, filename); | ||||||
|  |  | ||||||
|     writeFileAtomicSync(pathToFile, JSON.stringify(request.body.data, null, 4)); |     writeFileAtomicSync(pathToFile, JSON.stringify(request.body.data, null, 4)); | ||||||
|  |  | ||||||
| @@ -2436,7 +2436,7 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => { | |||||||
|         const image = await rawImg.cover(AVATAR_WIDTH, AVATAR_HEIGHT).getBufferAsync(jimp.MIME_PNG); |         const image = await rawImg.cover(AVATAR_WIDTH, AVATAR_HEIGHT).getBufferAsync(jimp.MIME_PNG); | ||||||
|  |  | ||||||
|         const filename = request.body.overwrite_name || `${Date.now()}.png`; |         const filename = request.body.overwrite_name || `${Date.now()}.png`; | ||||||
|         const pathToNewFile = path.join(directories.avatars, filename); |         const pathToNewFile = path.join(DIRECTORIES.avatars, filename); | ||||||
|         writeFileAtomicSync(pathToNewFile, image); |         writeFileAtomicSync(pathToNewFile, image); | ||||||
|         fs.rmSync(pathToUpload); |         fs.rmSync(pathToUpload); | ||||||
|         return response.send({ path: filename }); |         return response.send({ path: filename }); | ||||||
| @@ -2493,9 +2493,9 @@ app.post('/uploadimage', jsonParser, async (request, response) => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // if character is defined, save to a sub folder for that character |     // if character is defined, save to a sub folder for that character | ||||||
|     let pathToNewFile = path.join(directories.userImages, filename); |     let pathToNewFile = path.join(DIRECTORIES.userImages, filename); | ||||||
|     if (request.body.ch_name) { |     if (request.body.ch_name) { | ||||||
|         pathToNewFile = path.join(directories.userImages, request.body.ch_name, filename); |         pathToNewFile = path.join(DIRECTORIES.userImages, request.body.ch_name, filename); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
| @@ -2531,16 +2531,16 @@ app.post('/listimgfiles/:folder', (req, res) => { | |||||||
| app.post('/getgroups', jsonParser, (_, response) => { | app.post('/getgroups', jsonParser, (_, response) => { | ||||||
|     const groups = []; |     const groups = []; | ||||||
|  |  | ||||||
|     if (!fs.existsSync(directories.groups)) { |     if (!fs.existsSync(DIRECTORIES.groups)) { | ||||||
|         fs.mkdirSync(directories.groups); |         fs.mkdirSync(DIRECTORIES.groups); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const files = fs.readdirSync(directories.groups).filter(x => path.extname(x) === '.json'); |     const files = fs.readdirSync(DIRECTORIES.groups).filter(x => path.extname(x) === '.json'); | ||||||
|     const chats = fs.readdirSync(directories.groupChats).filter(x => path.extname(x) === '.jsonl'); |     const chats = fs.readdirSync(DIRECTORIES.groupChats).filter(x => path.extname(x) === '.jsonl'); | ||||||
|  |  | ||||||
|     files.forEach(function (file) { |     files.forEach(function (file) { | ||||||
|         try { |         try { | ||||||
|             const filePath = path.join(directories.groups, file); |             const filePath = path.join(DIRECTORIES.groups, file); | ||||||
|             const fileContents = fs.readFileSync(filePath, 'utf8'); |             const fileContents = fs.readFileSync(filePath, 'utf8'); | ||||||
|             const group = json5.parse(fileContents); |             const group = json5.parse(fileContents); | ||||||
|             const groupStat = fs.statSync(filePath); |             const groupStat = fs.statSync(filePath); | ||||||
| @@ -2553,7 +2553,7 @@ app.post('/getgroups', jsonParser, (_, response) => { | |||||||
|             if (Array.isArray(group.chats) && Array.isArray(chats)) { |             if (Array.isArray(group.chats) && Array.isArray(chats)) { | ||||||
|                 for (const chat of chats) { |                 for (const chat of chats) { | ||||||
|                     if (group.chats.includes(path.parse(chat).name)) { |                     if (group.chats.includes(path.parse(chat).name)) { | ||||||
|                         const chatStat = fs.statSync(path.join(directories.groupChats, chat)); |                         const chatStat = fs.statSync(path.join(DIRECTORIES.groupChats, chat)); | ||||||
|                         chat_size += chatStat.size; |                         chat_size += chatStat.size; | ||||||
|                         date_last_chat = Math.max(date_last_chat, chatStat.mtimeMs); |                         date_last_chat = Math.max(date_last_chat, chatStat.mtimeMs); | ||||||
|                     } |                     } | ||||||
| @@ -2591,11 +2591,11 @@ app.post('/creategroup', jsonParser, (request, response) => { | |||||||
|         chat_id: request.body.chat_id ?? id, |         chat_id: request.body.chat_id ?? id, | ||||||
|         chats: request.body.chats ?? [id], |         chats: request.body.chats ?? [id], | ||||||
|     }; |     }; | ||||||
|     const pathToFile = path.join(directories.groups, `${id}.json`); |     const pathToFile = path.join(DIRECTORIES.groups, `${id}.json`); | ||||||
|     const fileData = JSON.stringify(groupMetadata); |     const fileData = JSON.stringify(groupMetadata); | ||||||
|  |  | ||||||
|     if (!fs.existsSync(directories.groups)) { |     if (!fs.existsSync(DIRECTORIES.groups)) { | ||||||
|         fs.mkdirSync(directories.groups); |         fs.mkdirSync(DIRECTORIES.groups); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     writeFileAtomicSync(pathToFile, fileData); |     writeFileAtomicSync(pathToFile, fileData); | ||||||
| @@ -2607,7 +2607,7 @@ app.post('/editgroup', jsonParser, (request, response) => { | |||||||
|         return response.sendStatus(400); |         return response.sendStatus(400); | ||||||
|     } |     } | ||||||
|     const id = request.body.id; |     const id = request.body.id; | ||||||
|     const pathToFile = path.join(directories.groups, `${id}.json`); |     const pathToFile = path.join(DIRECTORIES.groups, `${id}.json`); | ||||||
|     const fileData = JSON.stringify(request.body); |     const fileData = JSON.stringify(request.body); | ||||||
|  |  | ||||||
|     writeFileAtomicSync(pathToFile, fileData); |     writeFileAtomicSync(pathToFile, fileData); | ||||||
| @@ -2620,7 +2620,7 @@ app.post('/getgroupchat', jsonParser, (request, response) => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const id = request.body.id; |     const id = request.body.id; | ||||||
|     const pathToFile = path.join(directories.groupChats, `${id}.jsonl`); |     const pathToFile = path.join(DIRECTORIES.groupChats, `${id}.jsonl`); | ||||||
|  |  | ||||||
|     if (fs.existsSync(pathToFile)) { |     if (fs.existsSync(pathToFile)) { | ||||||
|         const data = fs.readFileSync(pathToFile, 'utf8'); |         const data = fs.readFileSync(pathToFile, 'utf8'); | ||||||
| @@ -2640,7 +2640,7 @@ app.post('/deletegroupchat', jsonParser, (request, response) => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const id = request.body.id; |     const id = request.body.id; | ||||||
|     const pathToFile = path.join(directories.groupChats, `${id}.jsonl`); |     const pathToFile = path.join(DIRECTORIES.groupChats, `${id}.jsonl`); | ||||||
|  |  | ||||||
|     if (fs.existsSync(pathToFile)) { |     if (fs.existsSync(pathToFile)) { | ||||||
|         fs.rmSync(pathToFile); |         fs.rmSync(pathToFile); | ||||||
| @@ -2656,10 +2656,10 @@ app.post('/savegroupchat', jsonParser, (request, response) => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const id = request.body.id; |     const id = request.body.id; | ||||||
|     const pathToFile = path.join(directories.groupChats, `${id}.jsonl`); |     const pathToFile = path.join(DIRECTORIES.groupChats, `${id}.jsonl`); | ||||||
|  |  | ||||||
|     if (!fs.existsSync(directories.groupChats)) { |     if (!fs.existsSync(DIRECTORIES.groupChats)) { | ||||||
|         fs.mkdirSync(directories.groupChats); |         fs.mkdirSync(DIRECTORIES.groupChats); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let chat_data = request.body.chat; |     let chat_data = request.body.chat; | ||||||
| @@ -2674,7 +2674,7 @@ app.post('/deletegroup', jsonParser, async (request, response) => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const id = request.body.id; |     const id = request.body.id; | ||||||
|     const pathToGroup = path.join(directories.groups, sanitize(`${id}.json`)); |     const pathToGroup = path.join(DIRECTORIES.groups, sanitize(`${id}.json`)); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|         // Delete group chats |         // Delete group chats | ||||||
| @@ -2683,7 +2683,7 @@ app.post('/deletegroup', jsonParser, async (request, response) => { | |||||||
|         if (group && Array.isArray(group.chats)) { |         if (group && Array.isArray(group.chats)) { | ||||||
|             for (const chat of group.chats) { |             for (const chat of group.chats) { | ||||||
|                 console.log('Deleting group chat', chat); |                 console.log('Deleting group chat', chat); | ||||||
|                 const pathToFile = path.join(directories.groupChats, `${id}.jsonl`); |                 const pathToFile = path.join(DIRECTORIES.groupChats, `${id}.jsonl`); | ||||||
|  |  | ||||||
|                 if (fs.existsSync(pathToFile)) { |                 if (fs.existsSync(pathToFile)) { | ||||||
|                     fs.rmSync(pathToFile); |                     fs.rmSync(pathToFile); | ||||||
| @@ -2717,7 +2717,7 @@ function getSpritesPath(name, isSubfolder) { | |||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return path.join(directories.characters, characterName, subfolderName); |         return path.join(DIRECTORIES.characters, characterName, subfolderName); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     name = sanitize(name); |     name = sanitize(name); | ||||||
| @@ -2726,7 +2726,7 @@ function getSpritesPath(name, isSubfolder) { | |||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return path.join(directories.characters, name); |     return path.join(DIRECTORIES.characters, name); | ||||||
| } | } | ||||||
|  |  | ||||||
| app.get('/get_sprites', jsonParser, function (request, response) { | app.get('/get_sprites', jsonParser, function (request, response) { | ||||||
| @@ -2886,7 +2886,7 @@ app.post("/deletepreset_openai", jsonParser, function (request, response) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const name = request.body.name; |     const name = request.body.name; | ||||||
|     const pathToFile = path.join(directories.openAI_Settings, `${name}.settings`); |     const pathToFile = path.join(DIRECTORIES.openAI_Settings, `${name}.settings`); | ||||||
|  |  | ||||||
|     if (fs.existsSync(pathToFile)) { |     if (fs.existsSync(pathToFile)) { | ||||||
|         fs.rmSync(pathToFile); |         fs.rmSync(pathToFile); | ||||||
| @@ -3496,7 +3496,7 @@ app.post("/savepreset_openai", jsonParser, function (request, response) { | |||||||
|     if (!name) return response.sendStatus(400); |     if (!name) return response.sendStatus(400); | ||||||
|  |  | ||||||
|     const filename = `${name}.settings`; |     const filename = `${name}.settings`; | ||||||
|     const fullpath = path.join(directories.openAI_Settings, filename); |     const fullpath = path.join(DIRECTORIES.openAI_Settings, filename); | ||||||
|     writeFileAtomicSync(fullpath, JSON.stringify(request.body, null, 4), 'utf-8'); |     writeFileAtomicSync(fullpath, JSON.stringify(request.body, null, 4), 'utf-8'); | ||||||
|     return response.send({ name }); |     return response.send({ name }); | ||||||
| }); | }); | ||||||
| @@ -3505,15 +3505,15 @@ function getPresetSettingsByAPI(apiId) { | |||||||
|     switch (apiId) { |     switch (apiId) { | ||||||
|         case 'kobold': |         case 'kobold': | ||||||
|         case 'koboldhorde': |         case 'koboldhorde': | ||||||
|             return { folder: directories.koboldAI_Settings, extension: '.settings' }; |             return { folder: DIRECTORIES.koboldAI_Settings, extension: '.settings' }; | ||||||
|         case 'novel': |         case 'novel': | ||||||
|             return { folder: directories.novelAI_Settings, extension: '.settings' }; |             return { folder: DIRECTORIES.novelAI_Settings, extension: '.settings' }; | ||||||
|         case 'textgenerationwebui': |         case 'textgenerationwebui': | ||||||
|             return { folder: directories.textGen_Settings, extension: '.settings' }; |             return { folder: DIRECTORIES.textGen_Settings, extension: '.settings' }; | ||||||
|         case 'instruct': |         case 'instruct': | ||||||
|             return { folder: directories.instruct, extension: '.json' }; |             return { folder: DIRECTORIES.instruct, extension: '.json' }; | ||||||
|         case 'context': |         case 'context': | ||||||
|             return { folder: directories.context, extension: '.json' }; |             return { folder: DIRECTORIES.context, extension: '.json' }; | ||||||
|         default: |         default: | ||||||
|             return { folder: null, extension: null }; |             return { folder: null, extension: null }; | ||||||
|     } |     } | ||||||
| @@ -3663,6 +3663,45 @@ async function postAsync(url, args) { return fetchJSON(url, { method: 'POST', ti | |||||||
|  |  | ||||||
| // ** END ** | // ** END ** | ||||||
|  |  | ||||||
|  | // Secrets managemenet | ||||||
|  | require('./src/secrets').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Thumbnail generation | ||||||
|  | require('./src/thumbnails').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // NovelAI generation | ||||||
|  | require('./src/novelai').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Third-party extensions | ||||||
|  | require('./src/extensions').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Asset management | ||||||
|  | require('./src/assets').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Character sprite management | ||||||
|  | require('./src/sprites').registerEndpoints(app, jsonParser, urlencodedParser); | ||||||
|  |  | ||||||
|  | // Custom content management | ||||||
|  | require('./src/content-manager').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Stable Diffusion generation | ||||||
|  | require('./src/stable-diffusion').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // LLM and SD Horde generation | ||||||
|  | require('./src/horde').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Vector storage DB | ||||||
|  | require('./src/vectors').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Chat translation | ||||||
|  | require('./src/translate').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Emotion classification | ||||||
|  | require('./src/classify').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
|  | // Image captioning | ||||||
|  | require('./src/caption').registerEndpoints(app, jsonParser); | ||||||
|  |  | ||||||
| const tavernUrl = new URL( | const tavernUrl = new URL( | ||||||
|     (cliArguments.ssl ? 'https://' : 'http://') + |     (cliArguments.ssl ? 'https://' : 'http://') + | ||||||
|     (listen ? '0.0.0.0' : '127.0.0.1') + |     (listen ? '0.0.0.0' : '127.0.0.1') + | ||||||
| @@ -3694,7 +3733,7 @@ const setupTasks = async function () { | |||||||
|         loadClaudeTokenizer('src/claude.json'), |         loadClaudeTokenizer('src/claude.json'), | ||||||
|     ]); |     ]); | ||||||
|  |  | ||||||
|     await statsHelpers.loadStatsFile(directories.chats, directories.characters); |     await statsHelpers.loadStatsFile(DIRECTORIES.chats, DIRECTORIES.characters); | ||||||
|  |  | ||||||
|     // Set up event listeners for a graceful shutdown |     // Set up event listeners for a graceful shutdown | ||||||
|     process.on('SIGINT', statsHelpers.writeStatsToFileAndExit); |     process.on('SIGINT', statsHelpers.writeStatsToFileAndExit); | ||||||
| @@ -3762,16 +3801,16 @@ function backupSettings() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|         if (!fs.existsSync(directories.backups)) { |         if (!fs.existsSync(DIRECTORIES.backups)) { | ||||||
|             fs.mkdirSync(directories.backups); |             fs.mkdirSync(DIRECTORIES.backups); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const backupFile = path.join(directories.backups, `settings_${generateTimestamp()}.json`); |         const backupFile = path.join(DIRECTORIES.backups, `settings_${generateTimestamp()}.json`); | ||||||
|         fs.copyFileSync(SETTINGS_FILE, backupFile); |         fs.copyFileSync(SETTINGS_FILE, backupFile); | ||||||
|  |  | ||||||
|         let files = fs.readdirSync(directories.backups).filter(f => f.startsWith('settings_')); |         let files = fs.readdirSync(DIRECTORIES.backups).filter(f => f.startsWith('settings_')); | ||||||
|         if (files.length > MAX_BACKUPS) { |         if (files.length > MAX_BACKUPS) { | ||||||
|             files = files.map(f => path.join(directories.backups, f)); |             files = files.map(f => path.join(DIRECTORIES.backups, f)); | ||||||
|             files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs); |             files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs); | ||||||
|  |  | ||||||
|             fs.rmSync(files[0]); |             fs.rmSync(files[0]); | ||||||
| @@ -3782,347 +3821,9 @@ function backupSettings() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function ensurePublicDirectoriesExist() { | function ensurePublicDirectoriesExist() { | ||||||
|     for (const dir of Object.values(directories)) { |     for (const dir of Object.values(DIRECTORIES)) { | ||||||
|         if (!fs.existsSync(dir)) { |         if (!fs.existsSync(dir)) { | ||||||
|             fs.mkdirSync(dir, { recursive: true }); |             fs.mkdirSync(dir, { recursive: true }); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| app.post('/delete_sprite', jsonParser, async (request, response) => { |  | ||||||
|     const label = request.body.label; |  | ||||||
|     const name = request.body.name; |  | ||||||
|  |  | ||||||
|     if (!label || !name) { |  | ||||||
|         return response.sendStatus(400); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|         const spritesPath = path.join(directories.characters, name); |  | ||||||
|  |  | ||||||
|         // No sprites folder exists, or not a directory |  | ||||||
|         if (!fs.existsSync(spritesPath) || !fs.statSync(spritesPath).isDirectory()) { |  | ||||||
|             return response.sendStatus(404); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const files = fs.readdirSync(spritesPath); |  | ||||||
|  |  | ||||||
|         // Remove existing sprite with the same label |  | ||||||
|         for (const file of files) { |  | ||||||
|             if (path.parse(file).name === label) { |  | ||||||
|                 fs.rmSync(path.join(spritesPath, file)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return response.sendStatus(200); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error(error); |  | ||||||
|         return response.sendStatus(500); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| app.post('/upload_sprite_pack', urlencodedParser, async (request, response) => { |  | ||||||
|     const file = request.file; |  | ||||||
|     const name = request.body.name; |  | ||||||
|  |  | ||||||
|     if (!file || !name) { |  | ||||||
|         return response.sendStatus(400); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|         const spritesPath = path.join(directories.characters, name); |  | ||||||
|  |  | ||||||
|         // Create sprites folder if it doesn't exist |  | ||||||
|         if (!fs.existsSync(spritesPath)) { |  | ||||||
|             fs.mkdirSync(spritesPath); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Path to sprites is not a directory. This should never happen. |  | ||||||
|         if (!fs.statSync(spritesPath).isDirectory()) { |  | ||||||
|             return response.sendStatus(404); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const spritePackPath = path.join(UPLOADS_PATH, file.filename); |  | ||||||
|         const sprites = await getImageBuffers(spritePackPath); |  | ||||||
|         const files = fs.readdirSync(spritesPath); |  | ||||||
|  |  | ||||||
|         for (const [filename, buffer] of sprites) { |  | ||||||
|             // Remove existing sprite with the same label |  | ||||||
|             const existingFile = files.find(file => path.parse(file).name === path.parse(filename).name); |  | ||||||
|  |  | ||||||
|             if (existingFile) { |  | ||||||
|                 fs.rmSync(path.join(spritesPath, existingFile)); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Write sprite buffer to disk |  | ||||||
|             const pathToSprite = path.join(spritesPath, filename); |  | ||||||
|             writeFileAtomicSync(pathToSprite, buffer); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Remove uploaded ZIP file |  | ||||||
|         fs.rmSync(spritePackPath); |  | ||||||
|         return response.send({ count: sprites.length }); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error(error); |  | ||||||
|         return response.sendStatus(500); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| app.post('/upload_sprite', urlencodedParser, async (request, response) => { |  | ||||||
|     const file = request.file; |  | ||||||
|     const label = request.body.label; |  | ||||||
|     const name = request.body.name; |  | ||||||
|  |  | ||||||
|     if (!file || !label || !name) { |  | ||||||
|         return response.sendStatus(400); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|         const spritesPath = path.join(directories.characters, name); |  | ||||||
|  |  | ||||||
|         // Create sprites folder if it doesn't exist |  | ||||||
|         if (!fs.existsSync(spritesPath)) { |  | ||||||
|             fs.mkdirSync(spritesPath); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Path to sprites is not a directory. This should never happen. |  | ||||||
|         if (!fs.statSync(spritesPath).isDirectory()) { |  | ||||||
|             return response.sendStatus(404); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const files = fs.readdirSync(spritesPath); |  | ||||||
|  |  | ||||||
|         // Remove existing sprite with the same label |  | ||||||
|         for (const file of files) { |  | ||||||
|             if (path.parse(file).name === label) { |  | ||||||
|                 fs.rmSync(path.join(spritesPath, file)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const filename = label + path.parse(file.originalname).ext; |  | ||||||
|         const spritePath = path.join(UPLOADS_PATH, file.filename); |  | ||||||
|         const pathToFile = path.join(spritesPath, filename); |  | ||||||
|         // Copy uploaded file to sprites folder |  | ||||||
|         fs.cpSync(spritePath, pathToFile); |  | ||||||
|         // Remove uploaded file |  | ||||||
|         fs.rmSync(spritePath); |  | ||||||
|         return response.sendStatus(200); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error(error); |  | ||||||
|         return response.sendStatus(500); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| app.post('/import_custom', jsonParser, async (request, response) => { |  | ||||||
|     if (!request.body.url) { |  | ||||||
|         return response.sendStatus(400); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|         const url = request.body.url; |  | ||||||
|         let result; |  | ||||||
|  |  | ||||||
|         const chubParsed = parseChubUrl(url); |  | ||||||
|  |  | ||||||
|         if (chubParsed?.type === 'character') { |  | ||||||
|             console.log('Downloading chub character:', chubParsed.id); |  | ||||||
|             result = await downloadChubCharacter(chubParsed.id); |  | ||||||
|         } |  | ||||||
|         else if (chubParsed?.type === 'lorebook') { |  | ||||||
|             console.log('Downloading chub lorebook:', chubParsed.id); |  | ||||||
|             result = await downloadChubLorebook(chubParsed.id); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             return response.sendStatus(404); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (result.fileType) response.set('Content-Type', result.fileType) |  | ||||||
|         response.set('Content-Disposition', `attachment; filename="${result.fileName}"`); |  | ||||||
|         response.set('X-Custom-Content-Type', chubParsed?.type); |  | ||||||
|         return response.send(result.buffer); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.log('Importing custom content failed', error); |  | ||||||
|         return response.sendStatus(500); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| async function downloadChubLorebook(id) { |  | ||||||
|     const result = await fetch('https://api.chub.ai/api/lorebooks/download', { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { 'Content-Type': 'application/json' }, |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             "fullPath": id, |  | ||||||
|             "format": "SILLYTAVERN", |  | ||||||
|         }), |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (!result.ok) { |  | ||||||
|         console.log(await result.text()); |  | ||||||
|         throw new Error('Failed to download lorebook'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const name = id.split('/').pop(); |  | ||||||
|     const buffer = await result.buffer(); |  | ||||||
|     const fileName = `${sanitize(name)}.json`; |  | ||||||
|     const fileType = result.headers.get('content-type'); |  | ||||||
|  |  | ||||||
|     return { buffer, fileName, fileType }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function downloadChubCharacter(id) { |  | ||||||
|     const result = await fetch('https://api.chub.ai/api/characters/download', { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { 'Content-Type': 'application/json' }, |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             "format": "tavern", |  | ||||||
|             "fullPath": id, |  | ||||||
|         }) |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (!result.ok) { |  | ||||||
|         throw new Error('Failed to download character'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const buffer = await result.buffer(); |  | ||||||
|     const fileName = result.headers.get('content-disposition')?.split('filename=')[1] || `${sanitize(id)}.png`; |  | ||||||
|     const fileType = result.headers.get('content-type'); |  | ||||||
|  |  | ||||||
|     return { buffer, fileName, fileType }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * |  | ||||||
|  * @param {String} str |  | ||||||
|  * @returns { { id: string, type: "character" | "lorebook" } | null } |  | ||||||
|  */ |  | ||||||
| function parseChubUrl(str) { |  | ||||||
|     const splitStr = str.split('/'); |  | ||||||
|     const length = splitStr.length; |  | ||||||
|  |  | ||||||
|     if (length < 2) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let domainIndex = -1; |  | ||||||
|  |  | ||||||
|     splitStr.forEach((part, index) => { |  | ||||||
|         if (part === 'www.chub.ai' || part === 'chub.ai') { |  | ||||||
|             domainIndex = index; |  | ||||||
|         } |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|     const lastTwo = domainIndex !== -1 ? splitStr.slice(domainIndex + 1) : splitStr; |  | ||||||
|  |  | ||||||
|     const firstPart = lastTwo[0].toLowerCase(); |  | ||||||
|  |  | ||||||
|     if (firstPart === 'characters' || firstPart === 'lorebooks') { |  | ||||||
|         const type = firstPart === 'characters' ? 'character' : 'lorebook'; |  | ||||||
|         const id = type === 'character' ? lastTwo.slice(1).join('/') : lastTwo.join('/'); |  | ||||||
|         return { |  | ||||||
|             id: id, |  | ||||||
|             type: type |  | ||||||
|         }; |  | ||||||
|     } else if (length === 2) { |  | ||||||
|         return { |  | ||||||
|             id: lastTwo.join('/'), |  | ||||||
|             type: 'character' |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return null; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function importRisuSprites(data) { |  | ||||||
|     try { |  | ||||||
|         const name = data?.data?.name; |  | ||||||
|         const risuData = data?.data?.extensions?.risuai; |  | ||||||
|  |  | ||||||
|         // Not a Risu AI character |  | ||||||
|         if (!risuData || !name) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         let images = []; |  | ||||||
|  |  | ||||||
|         if (Array.isArray(risuData.additionalAssets)) { |  | ||||||
|             images = images.concat(risuData.additionalAssets); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (Array.isArray(risuData.emotions)) { |  | ||||||
|             images = images.concat(risuData.emotions); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // No sprites to import |  | ||||||
|         if (images.length === 0) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Create sprites folder if it doesn't exist |  | ||||||
|         const spritesPath = path.join(directories.characters, name); |  | ||||||
|         if (!fs.existsSync(spritesPath)) { |  | ||||||
|             fs.mkdirSync(spritesPath); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Path to sprites is not a directory. This should never happen. |  | ||||||
|         if (!fs.statSync(spritesPath).isDirectory()) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         console.log(`RisuAI: Found ${images.length} sprites for ${name}. Writing to disk.`); |  | ||||||
|         const files = fs.readdirSync(spritesPath); |  | ||||||
|  |  | ||||||
|         outer: for (const [label, fileBase64] of images) { |  | ||||||
|             // Remove existing sprite with the same label |  | ||||||
|             for (const file of files) { |  | ||||||
|                 if (path.parse(file).name === label) { |  | ||||||
|                     console.log(`RisuAI: The sprite ${label} for ${name} already exists. Skipping.`); |  | ||||||
|                     continue outer; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const filename = label + '.png'; |  | ||||||
|             const pathToFile = path.join(spritesPath, filename); |  | ||||||
|             writeFileAtomicSync(pathToFile, fileBase64, { encoding: 'base64' }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Remove additionalAssets and emotions from data (they are now in the sprites folder) |  | ||||||
|         delete data.data.extensions.risuai.additionalAssets; |  | ||||||
|         delete data.data.extensions.risuai.emotions; |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error(error); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| // Secrets managemenet |  | ||||||
| require('./src/secrets').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // Thumbnail generation |  | ||||||
| require('./src/thumbnails').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // NovelAI generation |  | ||||||
| require('./src/novelai').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // Third-party extensions |  | ||||||
| require('./src/extensions').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // Asset management |  | ||||||
| require('./src/assets').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // Stable Diffusion generation |  | ||||||
| require('./src/stable-diffusion').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // LLM and SD Horde generation |  | ||||||
| require('./src/horde').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // Vector storage DB |  | ||||||
| require('./src/vectors').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // Chat translation |  | ||||||
| require('./src/translate').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // Emotion classification |  | ||||||
| require('./src/classify').registerEndpoints(app, jsonParser); |  | ||||||
|  |  | ||||||
| // Image captioning |  | ||||||
| require('./src/caption').registerEndpoints(app, jsonParser); |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ const fs = require('fs'); | |||||||
| const sanitize = require('sanitize-filename'); | const sanitize = require('sanitize-filename'); | ||||||
| const fetch = require('node-fetch').default; | const fetch = require('node-fetch').default; | ||||||
| const { finished } = require('stream/promises'); | const { finished } = require('stream/promises'); | ||||||
| const { directories, UNSAFE_EXTENSIONS } = require('./constants'); | const { DIRECTORIES, UNSAFE_EXTENSIONS } = require('./constants'); | ||||||
|  |  | ||||||
| const VALID_CATEGORIES = ["bgm", "ambient"]; | const VALID_CATEGORIES = ["bgm", "ambient"]; | ||||||
|  |  | ||||||
| @@ -52,7 +52,7 @@ function registerEndpoints(app, jsonParser) { | |||||||
|      * @returns {void} |      * @returns {void} | ||||||
|      */ |      */ | ||||||
|     app.post('/api/assets/get', jsonParser, async (_, response) => { |     app.post('/api/assets/get', jsonParser, async (_, response) => { | ||||||
|         const folderPath = path.join(directories.assets); |         const folderPath = path.join(DIRECTORIES.assets); | ||||||
|         let output = {} |         let output = {} | ||||||
|         //console.info("Checking files into",folderPath); |         //console.info("Checking files into",folderPath); | ||||||
|  |  | ||||||
| @@ -114,8 +114,8 @@ function registerEndpoints(app, jsonParser) { | |||||||
|         if (safe_input == '') |         if (safe_input == '') | ||||||
|             return response.sendStatus(400); |             return response.sendStatus(400); | ||||||
|  |  | ||||||
|         const temp_path = path.join(directories.assets, "temp", safe_input) |         const temp_path = path.join(DIRECTORIES.assets, "temp", safe_input) | ||||||
|         const file_path = path.join(directories.assets, category, safe_input) |         const file_path = path.join(DIRECTORIES.assets, category, safe_input) | ||||||
|         console.debug("Request received to download", url, "to", file_path); |         console.debug("Request received to download", url, "to", file_path); | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
| @@ -173,7 +173,7 @@ function registerEndpoints(app, jsonParser) { | |||||||
|         if (safe_input == '') |         if (safe_input == '') | ||||||
|             return response.sendStatus(400); |             return response.sendStatus(400); | ||||||
|  |  | ||||||
|         const file_path = path.join(directories.assets, category, safe_input) |         const file_path = path.join(DIRECTORIES.assets, category, safe_input) | ||||||
|         console.debug("Request received to delete", category, file_path); |         console.debug("Request received to delete", category, file_path); | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
| @@ -222,7 +222,7 @@ function registerEndpoints(app, jsonParser) { | |||||||
|             return response.sendStatus(400); |             return response.sendStatus(400); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const folderPath = path.join(directories.characters, name, category); |         const folderPath = path.join(DIRECTORIES.characters, name, category); | ||||||
|  |  | ||||||
|         let output = []; |         let output = []; | ||||||
|         try { |         try { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| const directories = { | const DIRECTORIES = { | ||||||
|     worlds: 'public/worlds/', |     worlds: 'public/worlds/', | ||||||
|     avatars: 'public/User Avatars', |     avatars: 'public/User Avatars', | ||||||
|     images: 'public/img/', |     images: 'public/img/', | ||||||
| @@ -102,7 +102,10 @@ const UNSAFE_EXTENSIONS = [ | |||||||
|     ".ws", |     ".ws", | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | const UPLOADS_PATH = './uploads'; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     directories, |     DIRECTORIES, | ||||||
|     UNSAFE_EXTENSIONS, |     UNSAFE_EXTENSIONS, | ||||||
|  |     UPLOADS_PATH, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| const fs = require('fs'); | const fs = require('fs'); | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
|  | const fetch = require('node-fetch').default; | ||||||
|  | const sanitize = require('sanitize-filename'); | ||||||
| const config = require(path.join(process.cwd(), './config.conf')); | const config = require(path.join(process.cwd(), './config.conf')); | ||||||
| const contentDirectory = path.join(process.cwd(), 'default/content'); | const contentDirectory = path.join(process.cwd(), 'default/content'); | ||||||
| const contentLogPath = path.join(contentDirectory, 'content.log'); | const contentLogPath = path.join(contentDirectory, 'content.log'); | ||||||
| @@ -83,6 +85,136 @@ function getContentLog() { | |||||||
|     return contentLogText.split('\n'); |     return contentLogText.split('\n'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function downloadChubLorebook(id) { | ||||||
|  |     const result = await fetch('https://api.chub.ai/api/lorebooks/download', { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { 'Content-Type': 'application/json' }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |             "fullPath": id, | ||||||
|  |             "format": "SILLYTAVERN", | ||||||
|  |         }), | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (!result.ok) { | ||||||
|  |         const text = await result.text(); | ||||||
|  |         console.log('Chub returned error', result.statusText, text); | ||||||
|  |         throw new Error('Failed to download lorebook'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const name = id.split('/').pop(); | ||||||
|  |     const buffer = await result.buffer(); | ||||||
|  |     const fileName = `${sanitize(name)}.json`; | ||||||
|  |     const fileType = result.headers.get('content-type'); | ||||||
|  |  | ||||||
|  |     return { buffer, fileName, fileType }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function downloadChubCharacter(id) { | ||||||
|  |     const result = await fetch('https://api.chub.ai/api/characters/download', { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { 'Content-Type': 'application/json' }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |             "format": "tavern", | ||||||
|  |             "fullPath": id, | ||||||
|  |         }) | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (!result.ok) { | ||||||
|  |         const text = await result.text(); | ||||||
|  |         console.log('Chub returned error', result.statusText, text); | ||||||
|  |         throw new Error('Failed to download character'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const buffer = await result.buffer(); | ||||||
|  |     const fileName = result.headers.get('content-disposition')?.split('filename=')[1] || `${sanitize(id)}.png`; | ||||||
|  |     const fileType = result.headers.get('content-type'); | ||||||
|  |  | ||||||
|  |     return { buffer, fileName, fileType }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * | ||||||
|  |  * @param {String} str | ||||||
|  |  * @returns { { id: string, type: "character" | "lorebook" } | null } | ||||||
|  |  */ | ||||||
|  | function parseChubUrl(str) { | ||||||
|  |     const splitStr = str.split('/'); | ||||||
|  |     const length = splitStr.length; | ||||||
|  |  | ||||||
|  |     if (length < 2) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let domainIndex = -1; | ||||||
|  |  | ||||||
|  |     splitStr.forEach((part, index) => { | ||||||
|  |         if (part === 'www.chub.ai' || part === 'chub.ai') { | ||||||
|  |             domainIndex = index; | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     const lastTwo = domainIndex !== -1 ? splitStr.slice(domainIndex + 1) : splitStr; | ||||||
|  |  | ||||||
|  |     const firstPart = lastTwo[0].toLowerCase(); | ||||||
|  |  | ||||||
|  |     if (firstPart === 'characters' || firstPart === 'lorebooks') { | ||||||
|  |         const type = firstPart === 'characters' ? 'character' : 'lorebook'; | ||||||
|  |         const id = type === 'character' ? lastTwo.slice(1).join('/') : lastTwo.join('/'); | ||||||
|  |         return { | ||||||
|  |             id: id, | ||||||
|  |             type: type | ||||||
|  |         }; | ||||||
|  |     } else if (length === 2) { | ||||||
|  |         return { | ||||||
|  |             id: lastTwo.join('/'), | ||||||
|  |             type: 'character' | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Registers endpoints for custom content management | ||||||
|  |  * @param {import('express').Express} app Express app | ||||||
|  |  * @param {any} jsonParser JSON parser middleware | ||||||
|  |  */ | ||||||
|  | function registerEndpoints(app, jsonParser) { | ||||||
|  |     app.post('/api/content/import', jsonParser, async (request, response) => { | ||||||
|  |         if (!request.body.url) { | ||||||
|  |             return response.sendStatus(400); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             const url = request.body.url; | ||||||
|  |             let result; | ||||||
|  |  | ||||||
|  |             const chubParsed = parseChubUrl(url); | ||||||
|  |  | ||||||
|  |             if (chubParsed?.type === 'character') { | ||||||
|  |                 console.log('Downloading chub character:', chubParsed.id); | ||||||
|  |                 result = await downloadChubCharacter(chubParsed.id); | ||||||
|  |             } | ||||||
|  |             else if (chubParsed?.type === 'lorebook') { | ||||||
|  |                 console.log('Downloading chub lorebook:', chubParsed.id); | ||||||
|  |                 result = await downloadChubLorebook(chubParsed.id); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 return response.sendStatus(404); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (result.fileType) response.set('Content-Type', result.fileType) | ||||||
|  |             response.set('Content-Disposition', `attachment; filename="${result.fileName}"`); | ||||||
|  |             response.set('X-Custom-Content-Type', chubParsed?.type); | ||||||
|  |             return response.send(result.buffer); | ||||||
|  |         } catch (error) { | ||||||
|  |             console.log('Importing custom content failed', error); | ||||||
|  |             return response.sendStatus(500); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     checkForNewContent, |     checkForNewContent, | ||||||
|  |     registerEndpoints, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ const path = require('path'); | |||||||
| const fs = require('fs'); | const fs = require('fs'); | ||||||
| const simpleGit = require('simple-git'); | const simpleGit = require('simple-git'); | ||||||
| const sanitize = require('sanitize-filename'); | const sanitize = require('sanitize-filename'); | ||||||
| const { directories } = require('./constants'); | const { DIRECTORIES } = require('./constants'); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This function extracts the extension information from the manifest file. |  * This function extracts the extension information from the manifest file. | ||||||
| @@ -70,12 +70,12 @@ function registerEndpoints(app, jsonParser) { | |||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             // make sure the third-party directory exists |             // make sure the third-party directory exists | ||||||
|             if (!fs.existsSync(path.join(directories.extensions, 'third-party'))) { |             if (!fs.existsSync(path.join(DIRECTORIES.extensions, 'third-party'))) { | ||||||
|                 fs.mkdirSync(path.join(directories.extensions, 'third-party')); |                 fs.mkdirSync(path.join(DIRECTORIES.extensions, 'third-party')); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             const url = request.body.url; |             const url = request.body.url; | ||||||
|             const extensionPath = path.join(directories.extensions, 'third-party', path.basename(url, '.git')); |             const extensionPath = path.join(DIRECTORIES.extensions, 'third-party', path.basename(url, '.git')); | ||||||
|  |  | ||||||
|             if (fs.existsSync(extensionPath)) { |             if (fs.existsSync(extensionPath)) { | ||||||
|                 return response.status(409).send(`Directory already exists at ${extensionPath}`); |                 return response.status(409).send(`Directory already exists at ${extensionPath}`); | ||||||
| @@ -116,7 +116,7 @@ function registerEndpoints(app, jsonParser) { | |||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             const extensionName = request.body.extensionName; |             const extensionName = request.body.extensionName; | ||||||
|             const extensionPath = path.join(directories.extensions, 'third-party', extensionName); |             const extensionPath = path.join(DIRECTORIES.extensions, 'third-party', extensionName); | ||||||
|  |  | ||||||
|             if (!fs.existsSync(extensionPath)) { |             if (!fs.existsSync(extensionPath)) { | ||||||
|                 return response.status(404).send(`Directory does not exist at ${extensionPath}`); |                 return response.status(404).send(`Directory does not exist at ${extensionPath}`); | ||||||
| @@ -162,7 +162,7 @@ function registerEndpoints(app, jsonParser) { | |||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             const extensionName = request.body.extensionName; |             const extensionName = request.body.extensionName; | ||||||
|             const extensionPath = path.join(directories.extensions, 'third-party', extensionName); |             const extensionPath = path.join(DIRECTORIES.extensions, 'third-party', extensionName); | ||||||
|  |  | ||||||
|             if (!fs.existsSync(extensionPath)) { |             if (!fs.existsSync(extensionPath)) { | ||||||
|                 return response.status(404).send(`Directory does not exist at ${extensionPath}`); |                 return response.status(404).send(`Directory does not exist at ${extensionPath}`); | ||||||
| @@ -201,7 +201,7 @@ function registerEndpoints(app, jsonParser) { | |||||||
|         const extensionName = sanitize(request.body.extensionName); |         const extensionName = sanitize(request.body.extensionName); | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             const extensionPath = path.join(directories.extensions, 'third-party', extensionName); |             const extensionPath = path.join(DIRECTORIES.extensions, 'third-party', extensionName); | ||||||
|  |  | ||||||
|             if (!fs.existsSync(extensionPath)) { |             if (!fs.existsSync(extensionPath)) { | ||||||
|                 return response.status(404).send(`Directory does not exist at ${extensionPath}`); |                 return response.status(404).send(`Directory does not exist at ${extensionPath}`); | ||||||
| @@ -226,19 +226,19 @@ function registerEndpoints(app, jsonParser) { | |||||||
|  |  | ||||||
|         // get all folders in the extensions folder, except third-party |         // get all folders in the extensions folder, except third-party | ||||||
|         const extensions = fs |         const extensions = fs | ||||||
|             .readdirSync(directories.extensions) |             .readdirSync(DIRECTORIES.extensions) | ||||||
|             .filter(f => fs.statSync(path.join(directories.extensions, f)).isDirectory()) |             .filter(f => fs.statSync(path.join(DIRECTORIES.extensions, f)).isDirectory()) | ||||||
|             .filter(f => f !== 'third-party'); |             .filter(f => f !== 'third-party'); | ||||||
|  |  | ||||||
|         // get all folders in the third-party folder, if it exists |         // get all folders in the third-party folder, if it exists | ||||||
|  |  | ||||||
|         if (!fs.existsSync(path.join(directories.extensions, 'third-party'))) { |         if (!fs.existsSync(path.join(DIRECTORIES.extensions, 'third-party'))) { | ||||||
|             return response.send(extensions); |             return response.send(extensions); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const thirdPartyExtensions = fs |         const thirdPartyExtensions = fs | ||||||
|             .readdirSync(path.join(directories.extensions, 'third-party')) |             .readdirSync(path.join(DIRECTORIES.extensions, 'third-party')) | ||||||
|             .filter(f => fs.statSync(path.join(directories.extensions, 'third-party', f)).isDirectory()); |             .filter(f => fs.statSync(path.join(DIRECTORIES.extensions, 'third-party', f)).isDirectory()); | ||||||
|  |  | ||||||
|         // add the third-party extensions to the extensions array |         // add the third-party extensions to the extensions array | ||||||
|         extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`)); |         extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`)); | ||||||
|   | |||||||
							
								
								
									
										209
									
								
								src/sprites.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								src/sprites.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | |||||||
|  |  | ||||||
|  | const fs = require('fs'); | ||||||
|  | const path = require('path'); | ||||||
|  | const writeFileAtomicSync = require('write-file-atomic').sync; | ||||||
|  | const { DIRECTORIES, UPLOADS_PATH } = require('./constants'); | ||||||
|  | const { getImageBuffers } = require('./util'); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Imports base64 encoded sprites from RisuAI character data. | ||||||
|  |  * @param {object} data RisuAI character data | ||||||
|  |  * @returns {void} | ||||||
|  |  */ | ||||||
|  | function importRisuSprites(data) { | ||||||
|  |     try { | ||||||
|  |         const name = data?.data?.name; | ||||||
|  |         const risuData = data?.data?.extensions?.risuai; | ||||||
|  |  | ||||||
|  |         // Not a Risu AI character | ||||||
|  |         if (!risuData || !name) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let images = []; | ||||||
|  |  | ||||||
|  |         if (Array.isArray(risuData.additionalAssets)) { | ||||||
|  |             images = images.concat(risuData.additionalAssets); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (Array.isArray(risuData.emotions)) { | ||||||
|  |             images = images.concat(risuData.emotions); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // No sprites to import | ||||||
|  |         if (images.length === 0) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Create sprites folder if it doesn't exist | ||||||
|  |         const spritesPath = path.join(DIRECTORIES.characters, name); | ||||||
|  |         if (!fs.existsSync(spritesPath)) { | ||||||
|  |             fs.mkdirSync(spritesPath); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Path to sprites is not a directory. This should never happen. | ||||||
|  |         if (!fs.statSync(spritesPath).isDirectory()) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         console.log(`RisuAI: Found ${images.length} sprites for ${name}. Writing to disk.`); | ||||||
|  |         const files = fs.readdirSync(spritesPath); | ||||||
|  |  | ||||||
|  |         outer: for (const [label, fileBase64] of images) { | ||||||
|  |             // Remove existing sprite with the same label | ||||||
|  |             for (const file of files) { | ||||||
|  |                 if (path.parse(file).name === label) { | ||||||
|  |                     console.log(`RisuAI: The sprite ${label} for ${name} already exists. Skipping.`); | ||||||
|  |                     continue outer; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const filename = label + '.png'; | ||||||
|  |             const pathToFile = path.join(spritesPath, filename); | ||||||
|  |             writeFileAtomicSync(pathToFile, fileBase64, { encoding: 'base64' }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Remove additionalAssets and emotions from data (they are now in the sprites folder) | ||||||
|  |         delete data.data.extensions.risuai.additionalAssets; | ||||||
|  |         delete data.data.extensions.risuai.emotions; | ||||||
|  |     } catch (error) { | ||||||
|  |         console.error(error); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Registers the endpoints for the sprite management. | ||||||
|  |  * @param {import('express').Express} app Express app | ||||||
|  |  * @param {any} jsonParser JSON parser middleware | ||||||
|  |  * @param {any} urlencodedParser URL encoded parser middleware | ||||||
|  |  */ | ||||||
|  | function registerEndpoints(app, jsonParser, urlencodedParser) { | ||||||
|  |     app.post('/api/sprites/delete', jsonParser, async (request, response) => { | ||||||
|  |         const label = request.body.label; | ||||||
|  |         const name = request.body.name; | ||||||
|  |  | ||||||
|  |         if (!label || !name) { | ||||||
|  |             return response.sendStatus(400); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             const spritesPath = path.join(DIRECTORIES.characters, name); | ||||||
|  |  | ||||||
|  |             // No sprites folder exists, or not a directory | ||||||
|  |             if (!fs.existsSync(spritesPath) || !fs.statSync(spritesPath).isDirectory()) { | ||||||
|  |                 return response.sendStatus(404); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const files = fs.readdirSync(spritesPath); | ||||||
|  |  | ||||||
|  |             // Remove existing sprite with the same label | ||||||
|  |             for (const file of files) { | ||||||
|  |                 if (path.parse(file).name === label) { | ||||||
|  |                     fs.rmSync(path.join(spritesPath, file)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return response.sendStatus(200); | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error(error); | ||||||
|  |             return response.sendStatus(500); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     app.post('/api/sprites/upload-zip', urlencodedParser, async (request, response) => { | ||||||
|  |         const file = request.file; | ||||||
|  |         const name = request.body.name; | ||||||
|  |  | ||||||
|  |         if (!file || !name) { | ||||||
|  |             return response.sendStatus(400); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             const spritesPath = path.join(DIRECTORIES.characters, name); | ||||||
|  |  | ||||||
|  |             // Create sprites folder if it doesn't exist | ||||||
|  |             if (!fs.existsSync(spritesPath)) { | ||||||
|  |                 fs.mkdirSync(spritesPath); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Path to sprites is not a directory. This should never happen. | ||||||
|  |             if (!fs.statSync(spritesPath).isDirectory()) { | ||||||
|  |                 return response.sendStatus(404); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const spritePackPath = path.join(UPLOADS_PATH, file.filename); | ||||||
|  |             const sprites = await getImageBuffers(spritePackPath); | ||||||
|  |             const files = fs.readdirSync(spritesPath); | ||||||
|  |  | ||||||
|  |             for (const [filename, buffer] of sprites) { | ||||||
|  |                 // Remove existing sprite with the same label | ||||||
|  |                 const existingFile = files.find(file => path.parse(file).name === path.parse(filename).name); | ||||||
|  |  | ||||||
|  |                 if (existingFile) { | ||||||
|  |                     fs.rmSync(path.join(spritesPath, existingFile)); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // Write sprite buffer to disk | ||||||
|  |                 const pathToSprite = path.join(spritesPath, filename); | ||||||
|  |                 writeFileAtomicSync(pathToSprite, buffer); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Remove uploaded ZIP file | ||||||
|  |             fs.rmSync(spritePackPath); | ||||||
|  |             return response.send({ count: sprites.length }); | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error(error); | ||||||
|  |             return response.sendStatus(500); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     app.post('/api/sprites/upload', urlencodedParser, async (request, response) => { | ||||||
|  |         const file = request.file; | ||||||
|  |         const label = request.body.label; | ||||||
|  |         const name = request.body.name; | ||||||
|  |  | ||||||
|  |         if (!file || !label || !name) { | ||||||
|  |             return response.sendStatus(400); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             const spritesPath = path.join(DIRECTORIES.characters, name); | ||||||
|  |  | ||||||
|  |             // Create sprites folder if it doesn't exist | ||||||
|  |             if (!fs.existsSync(spritesPath)) { | ||||||
|  |                 fs.mkdirSync(spritesPath); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Path to sprites is not a directory. This should never happen. | ||||||
|  |             if (!fs.statSync(spritesPath).isDirectory()) { | ||||||
|  |                 return response.sendStatus(404); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const files = fs.readdirSync(spritesPath); | ||||||
|  |  | ||||||
|  |             // Remove existing sprite with the same label | ||||||
|  |             for (const file of files) { | ||||||
|  |                 if (path.parse(file).name === label) { | ||||||
|  |                     fs.rmSync(path.join(spritesPath, file)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const filename = label + path.parse(file.originalname).ext; | ||||||
|  |             const spritePath = path.join(UPLOADS_PATH, file.filename); | ||||||
|  |             const pathToFile = path.join(spritesPath, filename); | ||||||
|  |             // Copy uploaded file to sprites folder | ||||||
|  |             fs.cpSync(spritePath, pathToFile); | ||||||
|  |             // Remove uploaded file | ||||||
|  |             fs.rmSync(spritePath); | ||||||
|  |             return response.sendStatus(200); | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error(error); | ||||||
|  |             return response.sendStatus(500); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     registerEndpoints, | ||||||
|  |     importRisuSprites, | ||||||
|  | } | ||||||
| @@ -3,7 +3,7 @@ const path = require('path'); | |||||||
| const sanitize = require('sanitize-filename'); | const sanitize = require('sanitize-filename'); | ||||||
| const jimp = require('jimp'); | const jimp = require('jimp'); | ||||||
| const writeFileAtomicSync = require('write-file-atomic').sync; | const writeFileAtomicSync = require('write-file-atomic').sync; | ||||||
| const { directories } = require('./constants'); | const { DIRECTORIES } = require('./constants'); | ||||||
| const { getConfigValue } = require('./util'); | const { getConfigValue } = require('./util'); | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -16,10 +16,10 @@ function getThumbnailFolder(type) { | |||||||
|  |  | ||||||
|     switch (type) { |     switch (type) { | ||||||
|         case 'bg': |         case 'bg': | ||||||
|             thumbnailFolder = directories.thumbnailsBg; |             thumbnailFolder = DIRECTORIES.thumbnailsBg; | ||||||
|             break; |             break; | ||||||
|         case 'avatar': |         case 'avatar': | ||||||
|             thumbnailFolder = directories.thumbnailsAvatar; |             thumbnailFolder = DIRECTORIES.thumbnailsAvatar; | ||||||
|             break; |             break; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -36,10 +36,10 @@ function getOriginalFolder(type) { | |||||||
|  |  | ||||||
|     switch (type) { |     switch (type) { | ||||||
|         case 'bg': |         case 'bg': | ||||||
|             originalFolder = directories.backgrounds; |             originalFolder = DIRECTORIES.backgrounds; | ||||||
|             break; |             break; | ||||||
|         case 'avatar': |         case 'avatar': | ||||||
|             originalFolder = directories.characters; |             originalFolder = DIRECTORIES.characters; | ||||||
|             break; |             break; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -129,7 +129,7 @@ async function generateThumbnail(type, file) { | |||||||
|  * @returns {Promise<void>} Promise that resolves when the cache is validated |  * @returns {Promise<void>} Promise that resolves when the cache is validated | ||||||
|  */ |  */ | ||||||
| async function ensureThumbnailCache() { | async function ensureThumbnailCache() { | ||||||
|     const cacheFiles = fs.readdirSync(directories.thumbnailsBg); |     const cacheFiles = fs.readdirSync(DIRECTORIES.thumbnailsBg); | ||||||
|  |  | ||||||
|     // files exist, all ok |     // files exist, all ok | ||||||
|     if (cacheFiles.length) { |     if (cacheFiles.length) { | ||||||
| @@ -138,7 +138,7 @@ async function ensureThumbnailCache() { | |||||||
|  |  | ||||||
|     console.log('Generating thumbnails cache. Please wait...'); |     console.log('Generating thumbnails cache. Please wait...'); | ||||||
|  |  | ||||||
|     const bgFiles = fs.readdirSync(directories.backgrounds); |     const bgFiles = fs.readdirSync(DIRECTORIES.backgrounds); | ||||||
|     const tasks = []; |     const tasks = []; | ||||||
|  |  | ||||||
|     for (const file of bgFiles) { |     for (const file of bgFiles) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user