diff --git a/.gitignore b/.gitignore index 8ac21adf0..2464b8036 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ public/movingUI/ public/QuickReplies/ content.log cloudflared.exe +public/assets/ \ No newline at end of file diff --git a/public/assets/ambient/.placeholder b/public/assets/ambient/.placeholder new file mode 100644 index 000000000..a4faa1166 --- /dev/null +++ b/public/assets/ambient/.placeholder @@ -0,0 +1 @@ +Put ambient audio files here. \ No newline at end of file diff --git a/public/assets/bgm/.placeholder b/public/assets/bgm/.placeholder new file mode 100644 index 000000000..95839f44e --- /dev/null +++ b/public/assets/bgm/.placeholder @@ -0,0 +1 @@ +Put bgm audio files here diff --git a/public/assets/temp/.placeholder b/public/assets/temp/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js new file mode 100644 index 000000000..8bab38633 --- /dev/null +++ b/public/scripts/extensions/assets/index.js @@ -0,0 +1,246 @@ +/* +TODO: + - Check failed install file (0kb size ?) +*/ +//const DEBUG_TONY_SAMA_FORK_MODE = false + +import { getRequestHeaders, callPopup } from "../../../script.js"; +export { MODULE_NAME }; + +const MODULE_NAME = 'Assets'; +const DEBUG_PREFIX = " "; +let ASSETS_JSON_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json" + +const extensionName = "assets"; +const extensionFolderPath = `scripts/extensions/${extensionName}`; + +// DBG +//if (DEBUG_TONY_SAMA_FORK_MODE) +// ASSETS_JSON_URL = "https://raw.githubusercontent.com/Tony-sama/SillyTavern-Content/main/index.json" +let availableAssets = {}; +let currentAssets = {}; + +//#############################// +// Extension UI and Settings // +//#############################// + +const defaultSettings = { +} + +function downloadAssetsList(url) { + updateCurrentAssets().then(function () { + fetch(url) + .then(response => response.json()) + .then(json => { + + availableAssets = {}; + $("#assets_menu").empty(); + + console.debug(DEBUG_PREFIX, "Received assets dictionary", json); + + for (const i of json) { + //console.log(DEBUG_PREFIX,i) + if (availableAssets[i["type"]] === undefined) + availableAssets[i["type"]] = []; + + availableAssets[i["type"]].push(i); + } + + console.debug(DEBUG_PREFIX, "Updated available assets to", availableAssets); + + for (const assetType in availableAssets) { + let assetTypeMenu = $('
', { id: "assets_audio_ambient_div", class: "assets-list-div" }); + assetTypeMenu.append(`

${assetType}

`) + for (const i in availableAssets[assetType]) { + const asset = availableAssets[assetType][i]; + const elemId = `assets_install_${assetType}_${i}`; + let element = $('
+

Characters Voice Mapping

- - - - -
- - - - +
+ + + +
+
+ + + + + +
+
+
+ + Upload one archive per model. With .pth and .index (optional) inside.
+ Supported format: .zip .rar .7zip .7z +
+
+
+

Model Settings

+
+
+ + + + Tips: dio and pm faster, harvest slower but good.
+ Torchcrepe and rmvpe are good but uses GPU. +
+
+
+ + + + Controls accent strength, too high may produce artifact. + +
+
+ + + + Higher can reduce breathiness but may increase run time. + +
+
+ + + + Recommended +12 key for male to female conversion and -12 key for female to male conversion. + +
+
+ + + + Closer to 0 is closer to TTS and 1 is closer to trained voice. + Can help mask noise and sound more natural when set relatively low. + +
+
+ + + + Avoid non voice sounds. Lower is more being ignored. +
- Select Pitch Extraction
- - - - - - - - - - - - - - - -
-
@@ -284,8 +337,11 @@ $(document).ready(function () { $("#rvc_apply").on("click", onApplyClick); $("#rvc_delete").on("click", onDeleteClick); - $("#rvc_model_upload_file").show(); - $("#rvc_model_upload_button").on("click", onClickUpload); + $("#rvc_model_upload_files").hide(); + $("#rvc_model_upload_select_button").on("click", function() {$("#rvc_model_upload_files").click()}); + + $("#rvc_model_upload_files").on("change", onChangeUploadFiles); + //$("#rvc_model_upload_button").on("click", onClickUpload); $("#rvc_model_refresh_button").on("click", refreshVoiceList); } @@ -323,7 +379,7 @@ async function get_models_list(model_id) { /* Send an audio file to RVC to convert voice */ -async function rvcVoiceConversion(response, character) { +async function rvcVoiceConversion(response, character, text) { let apiResult // Check voice map @@ -341,8 +397,6 @@ async function rvcVoiceConversion(response, character) { const voice_settings = extension_settings.rvc.voiceMap[character]; - console.log("Sending tts audio data to RVC on extras server") - var requestData = new FormData(); requestData.append('AudioFile', audioData, 'record'); requestData.append("json", JSON.stringify({ @@ -352,9 +406,12 @@ async function rvcVoiceConversion(response, character) { "indexRate": voice_settings["indexRate"], "filterRadius": voice_settings["filterRadius"], "rmsMixRate": voice_settings["rmsMixRate"], - "protect": voice_settings["protect"] + "protect": voice_settings["protect"], + "text": text })); + console.log("Sending tts audio data to RVC on extras server",requestData) + const url = new URL(getApiUrl()); url.pathname = '/api/voice-conversion/rvc/process-audio'; @@ -405,11 +462,13 @@ async function moduleWorker() { function updateCharactersList() { let currentcharacters = new Set(); - for (const i of getContext().characters) { + const context = getContext(); + for (const i of context.characters) { currentcharacters.add(i.name); } - currentcharacters = Array.from(currentcharacters) + currentcharacters = Array.from(currentcharacters); + currentcharacters.unshift(context.name1); if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) { charactersList = currentcharacters diff --git a/public/scripts/extensions/tts/coqui.js b/public/scripts/extensions/tts/coqui.js index cfaf83fec..7f006c8b0 100644 --- a/public/scripts/extensions/tts/coqui.js +++ b/public/scripts/extensions/tts/coqui.js @@ -11,7 +11,6 @@ export { CoquiTtsProvider } const DEBUG_PREFIX = " "; const UPDATE_INTERVAL = 1000; -let inApiCall = false; let charactersList = []; // Updated with module worker let coquiApiModels = {}; // Initialized only once let coquiApiModelsFull = {}; // Initialized only once @@ -40,6 +39,12 @@ const languageLabels = { "ja": "Japanese" } + +const defaultSettings = { + voiceMap: "", + voiceMapDict: {} +} + function throwIfModuleMissing() { if (!modules.includes('coqui-tts')) { toastr.error(`Add coqui-tts to enable-modules and restart the Extras API.`, "Coqui TTS module not loaded.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); @@ -54,11 +59,13 @@ function resetModelSettings() { function updateCharactersList() { let currentcharacters = new Set(); - for (const i of getContext().characters) { + const context = getContext(); + for (const i of context.characters) { currentcharacters.add(i.name); } - currentcharacters = Array.from(currentcharacters) + currentcharacters = Array.from(currentcharacters); + currentcharacters.unshift(context.name1); if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) { charactersList = currentcharacters @@ -83,11 +90,13 @@ class CoquiTtsProvider { // Extension UI and Settings // //#############################// - settings + static instance; + settings = {}; - defaultSettings = { - voiceMap: "", - voiceMapDict: {} + // Singleton to allow acces to instance in event functions + constructor() { + if (CoquiTtsProvider.instance === undefined) + CoquiTtsProvider.instance = this; } get settingsHtml() { @@ -145,8 +154,12 @@ class CoquiTtsProvider { } loadSettings(settings) { + if (Object.keys(this.settings).length === 0) { + Object.assign(this.settings, defaultSettings) + } + // Only accept keys defined in defaultSettings - this.settings = this.defaultSettings + this.settings = defaultSettings; for (const key in settings) { if (key in this.settings) { @@ -156,7 +169,7 @@ class CoquiTtsProvider { } } - this.updateVoiceMap(); // Overide any manual modification + CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification $("#coqui_api_model_div").hide(); $("#coqui_local_model_div").hide(); @@ -167,24 +180,12 @@ class CoquiTtsProvider { $("#coqui_api_model_install_status").hide(); $("#coqui_api_model_install_button").hide(); - let that = this - $("#coqui_model_origin").on("change", function () { that.onModelOriginChange() }); - $("#coqui_api_language").on("change", function () { that.onModelLanguageChange() }); - $("#coqui_api_model_name").on("change", function () { that.onModelNameChange() }); + $("#coqui_model_origin").on("change", CoquiTtsProvider.onModelOriginChange); + $("#coqui_api_language").on("change", CoquiTtsProvider.onModelLanguageChange); + $("#coqui_api_model_name").on("change", CoquiTtsProvider.onModelNameChange); + $("#coqui_remove_char_mapping").on("click", CoquiTtsProvider.onRemoveClick); - $("#coqui_remove_char_mapping").on("click", function () { that.onRemoveClick() }); - - // Load characters list - $('#coqui_character_select') - .find('option') - .remove() - .end() - .append('') - .val('none') - - for (const charName of charactersList) { - $("#coqui_character_select").append(new Option(charName, charName)); - } + updateCharactersList(); // Load coqui-api settings from json file fetch("/scripts/extensions/tts/coqui_api_models_settings.json") @@ -192,18 +193,6 @@ class CoquiTtsProvider { .then(json => { coquiApiModels = json; console.debug(DEBUG_PREFIX,"initialized coqui-api model list to", coquiApiModels); - /* - $('#coqui_api_language') - .find('option') - .remove() - .end() - .append('') - .val('none'); - - for(let language in coquiApiModels) { - $("#coqui_api_language").append(new Option(languageLabels[language],language)); - console.log(DEBUG_PREFIX,"added language",language); - }*/ }); // Load coqui-api FULL settings from json file @@ -212,49 +201,33 @@ class CoquiTtsProvider { .then(json => { coquiApiModelsFull = json; console.debug(DEBUG_PREFIX,"initialized coqui-api full model list to", coquiApiModelsFull); - /* - $('#coqui_api_full_language') - .find('option') - .remove() - .end() - .append('') - .val('none'); - - for(let language in coquiApiModelsFull) { - $("#coqui_api_full_language").append(new Option(languageLabels[language],language)); - console.log(DEBUG_PREFIX,"added language",language); - }*/ }); } - updateVoiceMap() { - this.settings.voiceMap = ""; - for (let i in this.settings.voiceMapDict) { - const voice_settings = this.settings.voiceMapDict[i]; - this.settings.voiceMap += i + ":" + voice_settings["model_id"]; + static updateVoiceMap() { + CoquiTtsProvider.instance.settings.voiceMap = ""; + for (let i in CoquiTtsProvider.instance.settings.voiceMapDict) { + const voice_settings = CoquiTtsProvider.instance.settings.voiceMapDict[i]; + CoquiTtsProvider.instance.settings.voiceMap += i + ":" + voice_settings["model_id"]; if (voice_settings["model_language"] != null) - this.settings.voiceMap += "[" + voice_settings["model_language"] + "]"; + CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_language"] + "]"; if (voice_settings["model_speaker"] != null) - this.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]"; + CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]"; - this.settings.voiceMap += ","; + CoquiTtsProvider.instance.settings.voiceMap += ","; } - $("#tts_voice_map").val(this.settings.voiceMap); - extension_settings.tts.Coqui = this.settings; + $("#tts_voice_map").val(CoquiTtsProvider.instance.settings.voiceMap); + //extension_settings.tts.Coqui = extension_settings.tts.Coqui; } onSettingsChange() { - console.debug(DEBUG_PREFIX, "Settings changes", this.settings); - extension_settings.tts.Coqui = this.settings; + //console.debug(DEBUG_PREFIX, "Settings changes", CoquiTtsProvider.instance.settings); + CoquiTtsProvider.updateVoiceMap(); } async onApplyClick() { - if (inApiCall) { - return; // TOdo block dropdown - } - const character = $("#coqui_character_select").val(); const model_origin = $("#coqui_model_origin").val(); const model_language = $("#coqui_api_language").val(); @@ -262,16 +235,15 @@ class CoquiTtsProvider { let model_setting_language = $("#coqui_api_model_settings_language").val(); let model_setting_speaker = $("#coqui_api_model_settings_speaker").val(); - if (character === "none") { toastr.error(`Character not selected, please select one.`, DEBUG_PREFIX + " voice mapping character", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification return; } if (model_origin == "none") { toastr.error(`Origin not selected, please select one.`, DEBUG_PREFIX + " voice mapping origin", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification return; } @@ -280,25 +252,25 @@ class CoquiTtsProvider { if (model_name == "none") { toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification return; } - this.settings.voiceMapDict[character] = { model_type: "local", model_id: "local/" + model_id }; - console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", this.settings.voiceMapDict[character]); - this.updateVoiceMap(); // Overide any manual modification + CoquiTtsProvider.instance.settings.voiceMapDict[character] = { model_type: "local", model_id: "local/" + model_id }; + console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]); + CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification return; } if (model_language == "none") { toastr.error(`Language not selected, please select one.`, DEBUG_PREFIX + " voice mapping language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification return; } if (model_name == "none") { toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification return; } @@ -327,13 +299,13 @@ class CoquiTtsProvider { return; } - console.debug(DEBUG_PREFIX, "Current voice map: ", this.settings.voiceMap); + console.debug(DEBUG_PREFIX, "Current voice map: ", CoquiTtsProvider.instance.settings.voiceMap); - this.settings.voiceMapDict[character] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker }; + CoquiTtsProvider.instance.settings.voiceMapDict[character] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker }; - console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", this.settings.voiceMapDict[character]); + console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]); - this.updateVoiceMap(); + CoquiTtsProvider.updateVoiceMap(); let successMsg = character + ":" + model_id; if (model_setting_language != null) @@ -352,7 +324,7 @@ class CoquiTtsProvider { return output; } - async onRemoveClick() { + static async onRemoveClick() { const character = $("#coqui_character_select").val(); if (character === "none") { @@ -361,11 +333,11 @@ class CoquiTtsProvider { } // Todo erase from voicemap - delete (this.settings.voiceMapDict[character]); - this.updateVoiceMap(); // TODO + delete (CoquiTtsProvider.instance.settings.voiceMapDict[character]); + CoquiTtsProvider.updateVoiceMap(); // TODO } - async onModelOriginChange() { + static async onModelOriginChange() { throwIfModuleMissing() resetModelSettings(); const model_origin = $('#coqui_model_origin').val(); @@ -378,6 +350,9 @@ class CoquiTtsProvider { // show coqui model selected list (SAFE) if (model_origin == "coqui-api") { $("#coqui_local_model_div").hide(); + $("#coqui_api_model_div").hide(); + $("#coqui_api_model_name").hide(); + $("#coqui_api_model_settings").hide(); $('#coqui_api_language') .find('option') @@ -400,6 +375,9 @@ class CoquiTtsProvider { // show coqui model full list (UNSAFE) if (model_origin == "coqui-api-full") { $("#coqui_local_model_div").hide(); + $("#coqui_api_model_div").hide(); + $("#coqui_api_model_name").hide(); + $("#coqui_api_model_settings").hide(); $('#coqui_api_language') .find('option') @@ -427,7 +405,7 @@ class CoquiTtsProvider { } } - async onModelLanguageChange() { + static async onModelLanguageChange() { throwIfModuleMissing(); resetModelSettings(); $("#coqui_api_model_settings").hide(); @@ -460,7 +438,7 @@ class CoquiTtsProvider { } } - async onModelNameChange() { + static async onModelNameChange() { throwIfModuleMissing(); resetModelSettings(); $("#coqui_api_model_settings").hide(); @@ -551,8 +529,6 @@ class CoquiTtsProvider { $("#coqui_api_model_install_status").text("Model not found on extras server"); } - const onModelNameChange_pointer = this.onModelNameChange; - $("#coqui_api_model_install_button").off("click").on("click", async function () { try { $("#coqui_api_model_install_status").text("Downloading model..."); @@ -566,7 +542,7 @@ class CoquiTtsProvider { if (apiResult["status"] == "done") { $("#coqui_api_model_install_status").text("Model installed and ready to use!"); $("#coqui_api_model_install_button").hide(); - onModelNameChange_pointer(); + CoquiTtsProvider.onModelNameChange(); } if (apiResult["status"] == "downloading") { @@ -577,7 +553,7 @@ class CoquiTtsProvider { } catch (error) { console.error(error) toastr.error(error, DEBUG_PREFIX + " error with model download", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - onModelNameChange_pointer(); + CoquiTtsProvider.onModelNameChange(); } // will refresh model status }); diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 65e3e5a4a..d45542380 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -412,7 +412,7 @@ async function tts(text, voiceId, char) { // RVC injection if (extension_settings.rvc.enabled) - response = await rvcVoiceConversion(response, char) + response = await rvcVoiceConversion(response, char, text) addAudioJob(response) completeTtsJob() diff --git a/server.js b/server.js index 50e4dcfb3..a0f818baa 100644 --- a/server.js +++ b/server.js @@ -317,7 +317,8 @@ const directories = { instruct: 'public/instruct', context: 'public/context', backups: 'backups/', - quickreplies: 'public/QuickReplies' + quickreplies: 'public/QuickReplies', + assets: 'public/assets', }; // CSRF Protection // @@ -5011,3 +5012,242 @@ app.post('/delete_extension', jsonParser, async (request, response) => { return response.status(500).send(`Server Error: ${error.message}`); } }); + + +/** + * HTTP POST handler function to retrieve name of all files of a given folder path. + * + * @param {Object} request - HTTP Request object. Require folder path in query + * @param {Object} response - HTTP Response object will contain a list of file path. + * + * @returns {void} + */ +app.post('/get_assets', jsonParser, async (request, response) => { + const folderPath = path.join(directories.assets); + let output = {} + //console.info("Checking files into",folderPath); + + try { + if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) { + const folders = fs.readdirSync(folderPath) + .filter(filename => { + return fs.statSync(path.join(folderPath, filename)).isDirectory(); + }); + + for (const folder of folders) { + if (folder == "temp") + continue; + const files = fs.readdirSync(path.join(folderPath, folder)) + .filter(filename => { + return filename != ".placeholder"; + }); + output[folder] = []; + for (const file of files) { + output[folder].push(path.join("assets", folder, file)); + } + } + } + } + catch (err) { + console.log(err); + } + finally { + return response.send(output); + } +}); + + +function checkAssetFileName(inputFilename) { + // Sanitize filename + if (inputFilename.indexOf('\0') !== -1) { + console.debug("Bad request: poisong null bytes in filename."); + return ''; + } + + if (!/^[a-zA-Z0-9_\-\.]+$/.test(inputFilename)) { + console.debug("Bad request: illegal character in filename, only alphanumeric, '_', '-' are accepted."); + return ''; + } + + if (contentManager.unsafeExtensions.some(ext => inputFilename.toLowerCase().endsWith(ext))) { + console.debug("Bad request: forbidden file extension."); + return ''; + } + + if (inputFilename.startsWith('.')) { + console.debug("Bad request: filename cannot start with '.'"); + return ''; + } + + return path.normalize(inputFilename).replace(/^(\.\.(\/|\\|$))+/, '');; +} + +/** + * HTTP POST handler function to download the requested asset. + * + * @param {Object} request - HTTP Request object, expects a url, a category and a filename. + * @param {Object} response - HTTP Response only gives status. + * + * @returns {void} + */ +app.post('/asset_download', jsonParser, async (request, response) => { + const { Readable } = require('stream'); + const { finished } = require('stream/promises'); + const url = request.body.url; + const inputCategory = request.body.category; + const inputFilename = sanitize(request.body.filename); + const validCategories = ["bgm", "ambient"]; + + // Check category + let category = null; + for (i of validCategories) + if (i == inputCategory) + category = i; + + if (category === null) { + console.debug("Bad request: unsuported asset category."); + return response.sendStatus(400); + } + + // Sanitize filename + const safe_input = checkAssetFileName(inputFilename); + if (safe_input == '') + return response.sendFile(400); + + const temp_path = path.join(directories.assets, "temp", safe_input) + const file_path = path.join(directories.assets, category, safe_input) + console.debug("Request received to download", url, "to", file_path); + + try { + // Download to temp + const downloadFile = (async (url, temp_path) => { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Unexpected response ${res.statusText}`); + } + const destination = path.resolve(temp_path); + // Delete if previous download failed + if (fs.existsSync(temp_path)) { + fs.unlink(temp_path, (err) => { + if (err) throw err; + }); + } + const fileStream = fs.createWriteStream(destination, { flags: 'wx' }); + await finished(Readable.fromWeb(res.body).pipe(fileStream)); + }); + + await downloadFile(url, temp_path); + + // Move into asset place + console.debug("Download finished, moving file from", temp_path, "to", file_path); + fs.renameSync(temp_path, file_path); + response.sendStatus(200); + } + catch (error) { + console.log(error); + response.sendStatus(500); + } +}); + +/** + * HTTP POST handler function to delete the requested asset. + * + * @param {Object} request - HTTP Request object, expects a category and a filename + * @param {Object} response - HTTP Response only gives stats. + * + * @returns {void} + */ +app.post('/asset_delete', jsonParser, async (request, response) => { + const { Readable } = require('stream'); + const { finished } = require('stream/promises'); + const inputCategory = request.body.category; + const inputFilename = sanitize(request.body.filename); + const validCategories = ["bgm", "ambient"]; + + // Check category + let category = null; + for (i of validCategories) + if (i == inputCategory) + category = i; + + if (category === null) { + console.debug("Bad request: unsuported asset category."); + return response.sendStatus(400); + } + + // Sanitize filename + const safe_input = checkAssetFileName(inputFilename); + if (safe_input == '') + return response.sendFile(400); + + const file_path = path.join(directories.assets, category, safe_input) + console.debug("Request received to delete", category, file_path); + + try { + // Delete if previous download failed + if (fs.existsSync(file_path)) { + fs.unlink(file_path, (err) => { + if (err) throw err; + }); + console.debug("Asset deleted."); + } + else { + console.debug("Asset not found."); + response.sendStatus(400); + } + // Move into asset place + response.sendStatus(200); + } + catch (error) { + console.log(error); + response.sendStatus(500); + } +}); + + +/////////////////////////////// +/** + * HTTP POST handler function to retrieve a character background music list. + * + * @param {Object} request - HTTP Request object, expects a character name in the query. + * @param {Object} response - HTTP Response object will contain a list of audio file path. + * + * @returns {void} + */ +app.post('/get_character_assets_list', jsonParser, async (request, response) => { + const name = sanitize(request.query.name); + const inputCategory = request.query.category; + const validCategories = ["bgm", "ambient"] + + // Check category + let category = null + for (i of validCategories) + if (i == inputCategory) + category = i + + if (category === null) { + console.debug("Bad request: unsuported asset category."); + return response.sendStatus(400); + } + + const folderPath = path.join(directories.characters, name, category); + + let output = []; + try { + if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) { + const files = fs.readdirSync(folderPath) + .filter(filename => { + return filename != ".placeholder"; + }); + + for (i of files) + output.push(`/characters/${name}/${category}/${i}`); + + } + return response.send(output); + } + catch (err) { + console.log(err); + return response.sendStatus(500); + } +}); diff --git a/src/content-manager.js b/src/content-manager.js index e48eab877..e453d5f4a 100644 --- a/src/content-manager.js +++ b/src/content-manager.js @@ -1,10 +1,87 @@ const fs = require('fs'); -const path= require('path'); +const path = require('path'); const config = require(path.join(process.cwd(), './config.conf')); const contentDirectory = path.join(process.cwd(), 'default/content'); const contentLogPath = path.join(contentDirectory, 'content.log'); const contentIndexPath = path.join(contentDirectory, 'index.json'); +const unsafeExtensions = [ + ".php", + ".exe", + ".com", + ".dll", + ".pif", + ".application", + ".gadget", + ".msi", + ".jar", + ".cmd", + ".bat", + ".reg", + ".sh", + ".py", + ".js", + ".jse", + ".jsp", + ".pdf", + ".html", + ".htm", + ".hta", + ".vb", + ".vbs", + ".vbe", + ".cpl", + ".msc", + ".scr", + ".sql", + ".iso", + ".img", + ".dmg", + ".ps1", + ".ps1xml", + ".ps2", + ".ps2xml", + ".psc1", + ".psc2", + ".msh", + ".msh1", + ".msh2", + ".mshxml", + ".msh1xml", + ".msh2xml", + ".scf", + ".lnk", + ".inf", + ".reg", + ".doc", + ".docm", + ".docx", + ".dot", + ".dotm", + ".dotx", + ".xls", + ".xlsm", + ".xlsx", + ".xlt", + ".xltm", + ".xltx", + ".xlam", + ".ppt", + ".pptm", + ".pptx", + ".pot", + ".potm", + ".potx", + ".ppam", + ".ppsx", + ".ppsm", + ".pps", + ".ppam", + ".sldx", + ".sldm", + ".ws", +]; + function checkForNewContent() { try { if (config.skipContentCheck) { @@ -85,4 +162,5 @@ function getContentLog() { module.exports = { checkForNewContent, + unsafeExtensions, }