Merge branch 'staging' into feature/exorcism

This commit is contained in:
Cohee 2023-08-31 00:42:34 +03:00
commit 194278d171
12 changed files with 417 additions and 208 deletions

25
SECURITY.md Normal file
View File

@ -0,0 +1,25 @@
# Security Policy
We take the security of this project seriously. If you discover any security vulnerabilities or have concerns regarding the security of this repository, please reach out to us immediately. We appreciate your efforts in responsibly disclosing the issue and will make every effort to address it promptly.
## Reporting a Vulnerability
To report a security vulnerability, please follow these steps:
1. Go to the **Security** tab of this repository on GitHub.
2. Click on **"Report a vulnerability"**.
3. Provide a clear description of the vulnerability and its potential impact. Be as detailed as possible.
4. If applicable, include steps or a PoC (Proof of Concept) to reproduce the vulnerability.
5. Submit the report.
Once we receive the private report notification, we will promptly investigate and assess the reported vulnerability.
Please do not disclose any potential vulnerabilities in public repositories, issue trackers, or forums until we have had a chance to review and address the issue.
## Scope
This security policy applies to all the code and files within this repository and its dependencies actively maintained by us. If you encounter a security issue in a dependency that is not directly maintained by us, please follow responsible disclosure practices and report it to the respective project.
While we strive to ensure the security of this project, please note that there may be limitations on resources, response times, and mitigations.
Thank you for your help in making this project more secure.

17
jsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "node",
"strictNullChecks": true,
"strictFunctionTypes": true,
"checkJs": true,
"allowUmdGlobalAccess": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

View File

@ -404,6 +404,7 @@
.widthFitContent {
width: fit-content;
min-width: fit-content;
}
.flexGap5 {
@ -416,4 +417,4 @@
.opacity1 {
opacity: 1 !important;
}
}

View File

@ -4,8 +4,9 @@
"ja-jp",
"ko-kr",
"ru-ru",
"it-it",
"nl-nl"
"it-it",
"nl-nl",
"es-spa"
],
"zh-cn": {
"clickslidertips": "点击滑块右侧数字可手动输入",
@ -3483,5 +3484,135 @@
"Select this as default persona for the new chats.": "Selecteer dit als standaard persona voor de nieuwe chats.",
"Change persona image": "persona afbeelding wijzigen",
"Delete persona": "persona verwijderen"
}
}
},
"es-spa": {
"clickslidertips": "Haz click en el número al lado de la barra \npara seleccionar un número manualmente.",
"kobldpresets": "Configuraciones de KoboldAI",
"guikoboldaisettings": "Configuración actual de la interfaz de KoboldAI",
"novelaipreserts": "Configuraciones de NovelAI",
"default": "Predeterminado",
"openaipresets": "Configuraciones de OpenAI",
"text gen webio(ooba) presets": "Configuraciones de WebUI(ooba)",
"response legth(tokens)": "Largo de la respuesta de la IA (en Tokens)",
"select": "Seleccionar",
"context size(tokens)": "Tamaño del contexto (en Tokens)",
"unlocked": "Desbloqueado",
"only select modls support context sizes greater than 2048 tokens. proceed only is you know you're doing": "Solo algunos modelos tienen soporte para tamaños de más de 2048 tokens. Procede solo si sabes lo que estás haciendo.",
"rep.pen": "Rep. Pen.",
"rep.pen range": "Rango de Rep. Pen.",
"temperature": "Temperature",
"Encoder Rep. Pen.": "Encoder Rep. Pen.",
"No Repeat Ngram Size": "No Repeat Ngram Size",
"Min Length": "Largo mínimo",
"OpenAI Reverse Proxy": "Reverse Proxy de OpenAI",
"Alternative server URL (leave empty to use the default value).": "URL del server alternativo (deja vacío para usar el predeterminado)",
"Remove your real OAI API Key from the API panel BEFORE typing anything into this box": "Borra tu clave(API) real de OpenAI ANTES de escribir nada en este campo.",
"We cannot provide support for problems encountered while using an unofficial OpenAI proxy": "SillyTaven no puede dar soporte por problemas encontrados durante el uso de un proxy no-oficial de OpenAI",
"Legacy Streaming Processing": "Processo Streaming Legacy",
"Enable this if the streaming doesn't work with your proxy": "Habilita esta opción si el \"streaming\" no está funcionando.",
"Context Size (tokens)": "Tamaño del contexto (en Tokens)",
"Max Response Length (tokens)": "Tamaño máximo (en Tokens)",
"Temperature": "Temperatura",
"Frequency Penalty": "Frequency Penalty",
"Presence Penalty": "Presence Penalty",
"Top-p": "Top-p",
"Display bot response text chunks as they are generated": "Muestra el texto poco a poco al mismo tiempo que es generado.",
"Top A": "Top-a",
"Typical Sampling": "Typical Sampling",
"Tail Free Sampling": "Tail Free Sampling",
"Rep. Pen. Slope": "Rep. Pen. Slope",
"Single-line mode": "Modo \"Solo una línea\"",
"Top K": "Top-k",
"Top P": "Top-p",
"Do Sample": "Do Sample",
"Add BOS Token": "Añadir BOS Token",
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.": "Añade el \"bos_token\" al inicio del prompt. Desabilitar esto puede hacer las respuestas de la IA más creativas",
"Ban EOS Token": "Prohibir EOS Token",
"Ban the eos_token. This forces the model to never end the generation prematurely": "Prohibe el \"eos_token\". Esto obliga a la IA a no terminar su generación de forma prematura",
"Skip Special Tokens": "Saltarse Tokens Especiales",
"Beam search": "Beam Search",
"Number of Beams": "Number of Beams",
"Length Penalty": "Length Penalty",
"Early Stopping": "Early Stopping",
"Contrastive search": "Contrastive search",
"Penalty Alpha": "Penalty Alpha",
"Seed": "Seed",
"Inserts jailbreak as a last system message.": "Inserta el \"jailbreak\" como el último mensaje del Sistema",
"This tells the AI to ignore its usual content restrictions.": "Esto ayuda a la IA para ignorar sus restricciones de contenido",
"NSFW Encouraged": "Alentar \"NSFW\"",
"Tell the AI that NSFW is allowed.": "Le dice a la IA que el contenido NSFW (+18) está permitido",
"NSFW Prioritized": "Priorizar NSFW",
"NSFW prompt text goes first in the prompt to emphasize its effect.": "El \"prompt NSFW\" va antes para enfatizar su efecto",
"Streaming": "Streaming",
"Display the response bit by bit as it is generated.": "Enseña el texto poco a poco mientras es generado",
"When this is off, responses will be displayed all at once when they are complete.": "Cuando esto está deshabilitado, las respuestas se mostrarán de una vez cuando la generación se haya completado",
"Enhance Definitions": "Definiciones Mejoradas",
"Use OAI knowledge base to enhance definitions for public figures and known fictional characters": "Usa el conocimiento de OpenAI (GPT 3.5, GPT 4, ChatGPT) para mejorar las definiciones de figuras públicas y personajes ficticios",
"Wrap in Quotes": "Envolver En Comillas",
"Wrap entire user message in quotes before sending.": "Envuelve todo el mensaje en comillas antes de enviar",
"Leave off if you use quotes manually for speech.": "Déjalo deshabilitado si usas comillas manualmente para denotar diálogo",
"Main prompt": "Prompt Principal",
"The main prompt used to set the model behavior": "El prompt principal usado para definir el comportamiento de la IA",
"NSFW prompt": "Prompt NSFW",
"Prompt that is used when the NSFW toggle is on": "Prompt que es utilizado cuando \"Alentar NSFW\" está activado",
"Jailbreak prompt": "Jailbreak prompt",
"Prompt that is used when the Jailbreak toggle is on": "Prompt que es utilizado cuando Jailbreak Prompt está activado",
"Impersonation prompt": "Prompt \"Impersonar\"",
"Prompt that is used for Impersonation function": "Prompt que es utilizado para la función \"Impersonar\"",
"Logit Bias": "Logit Bias",
"Helps to ban or reenforce the usage of certain words": "Ayuda a prohibir o alentar el uso de algunas palabras",
"View / Edit bias preset": "Ver/Editar configuración de \"Logit Bias\"",
"Add bias entry": "Añadir bias",
"Jailbreak activation message": "Mensaje de activación de Jailbrak",
"Message to send when auto-jailbreak is on.": "Mensaje enviado cuando auto-jailbreak está activado",
"Jailbreak confirmation reply": "Mensaje de confirmación de Jailbreak",
"Bot must send this back to confirm jailbreak": "La IA debe enviar un mensaje para confirmar el jailbreak",
"Character Note": "Nota del personaje",
"Influences bot behavior in its responses": "Influencia el comportamiento de la IA y sus respuestas",
"API": "API",
"KoboldAI": "KoboldAI",
"Use Horde": "Usar AI Horde de KoboldAI",
"API url": "URL de la API",
"Register a Horde account for faster queue times": "Regístrate en KoboldAI para conseguir respuestas más rápido",
"Learn how to contribute your idle GPU cycles to the Hord": "Aprende cómo contribuir a AI Horde con tu GPU",
"Adjust context size to worker capabilities": "Ajustar tamaño del contexto a las capacidades del trabajador",
"Adjust response length to worker capabilities": "Ajustar tamaño de la respuesta a las capacidades del trabajador",
"API key": "API key",
"Register": "Registrarse",
"For privacy reasons": "Por motivos de privacidad, tu API será ocultada cuando se vuelva a cargar la página",
"Model": "Modelo IA",
"Hold Control / Command key to select multiple models.": "Presiona Ctrl/Command Key para seleccionar multiples modelos",
"Horde models not loaded": "Modelos del Horde no cargados",
"Not connected": "Desconectado",
"Novel API key": "API key de NovelAI",
"Follow": "Sigue",
"these directions": "estas instrucciones",
"to get your NovelAI API key.": "para conseguir tu NovelAI API key",
"Enter it in the box below": "Introduce tu NovelAI API key en el siguiente campo",
"Novel AI Model": "Modelo IA de NovelAI",
"Euterpe": "Euterpe",
"Krake": "Krake",
"No connection": "Desconectado",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "Asegúrate de usar el argumento --api cuando se ejecute",
"Blocking API url": "API URL",
"Streaming API url": "Streaming API URL",
"to get your OpenAI API key.": "para conseguir tu clave API de OpenAI",
"OpenAI Model": "Modelo AI de OpenAI",
"View API Usage Metrics": "Ver métricas de uso de la API",
"Bot": "Bot",
"Auto-connect to Last Server": "Auto-conectarse con el último servidor",
"View hidden API keys": "Ver claves API ocultas",
"Advanced Formatting": "Formateo avanzado",
"AutoFormat Overrides": "Autoformateo de overrides",
"Samplers Order": "Orden de Samplers",
"Samplers will be applied in a top-down order. Use with caution.": "Los Samplers serán aplicados de orden superior a inferior. \nUsa con precaución",
"Load koboldcpp order": "Cargar el orden de koboldcpp",
"Repetition Penalty": "Repetition Penalty",
"Epsilon Cutoff": "Epsilon Cutoff",
"Eta Cutoff": "Eta Cutoff",
"Rep. Pen. Range.": "Rep. Pen. Range.",
"Rep. Pen. Freq.": "Rep. Pen. Freq.",
"Rep. Pen. Presence": "Rep. Pen. Presence."
}
}

View File

@ -2099,8 +2099,8 @@
<h4 data-i18n="Context Template">
Context Template
</h4>
<div class="preset_buttons">
<select id="context_presets" data-preset-manager-for="context" class="flex1"></select>
<div class="preset_buttons flex-container">
<select id="context_presets" data-preset-manager-for="context" class="flex1 widthFitContent"></select>
<input type="file" hidden data-preset-manager-file="context" accept=".json, .settings">
<i id="context_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset for Instruct Mode."></i>
<i data-newbie-hidden data-preset-manager-update="context" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
@ -2926,6 +2926,10 @@
<input id="encode_tags" type="checkbox" />
<span data-i18n="Show tags in responses">Show &lt;tags&gt; in responses</span>
</label>
<label data-newbie-hidden class="checkbox_label" for="disable_group_trimming" title="Allow AI messages in groups to contain lines spoken by other group members.">
<input id="disable_group_trimming" type="checkbox" />
<span data-i18n="Show impersonated replies in groups">Show impersonated replies in groups</span>
</label>
<label data-newbie-hidden class="checkbox_label" for="console_log_prompts">
<input id="console_log_prompts" type="checkbox" />
<span data-i18n="Log prompts to console">Log prompts to console</span>
@ -4322,6 +4326,9 @@
<i class="fa-lg fa-solid fa-note-sticky"></i>
<span data-i18n="Author's Note">Author's Note</span>
</a>
<div data-newbie-hidden id="options_advanced">
</div>
<a id="option_back_to_main">
<i class="fa-lg fa-solid fa-left-long"></i>
<span data-i18n="Back to parent chat">Back to parent chat</span>
@ -4435,4 +4442,4 @@
</script>
</body>
</html>
</html>

View File

@ -1895,7 +1895,17 @@ export function extractMessageBias(message) {
}
}
/**
* Removes impersonated group member lines from the group member messages.
* Doesn't do anything if group reply trimming is disabled.
* @param {string} getMessage Group message
* @returns Cleaned-up group message
*/
function cleanGroupMessage(getMessage) {
if (power_user.disable_group_trimming) {
return getMessage;
}
const group = groups.find((x) => x.id == selected_group);
if (group && Array.isArray(group.members) && group.members) {
@ -2535,6 +2545,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
let examplesString = '';
let chatString = '';
let cyclePrompt = '';
function getMessagesTokenCount() {
const encodeString = [
storyString,
@ -2542,6 +2554,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
chatString,
allAnchors,
quiet_prompt,
cyclePrompt,
].join('').replace(/\r/gm, '');
return getTokenCount(encodeString, power_user.token_padding);
}
@ -2552,7 +2565,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
pinExmString = examplesString = mesExamplesArray.join('');
}
let cyclePrompt = '';
if (isContinue) {
cyclePrompt = chat2.shift();
}
@ -2801,11 +2813,11 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
return;
}
const anchorDepth = Math.abs(index - finalMesSend.length + 1);
const anchorDepth = Math.abs(index - finalMesSend.length);
// NOTE: Depth injected here!
const extensionAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, anchorDepth);
if (anchorDepth > 0 && extensionAnchor && extensionAnchor.length) {
if (anchorDepth >= 0 && extensionAnchor && extensionAnchor.length) {
mesItem.extensionPrompts.push(extensionAnchor);
}
});

View File

@ -4,7 +4,7 @@ import { callPopup, event_types, eventSource, is_send_press, main_api, substitut
import { is_group_generating } from "./group-chats.js";
import { TokenHandler } from "./openai.js";
import { power_user } from "./power-user.js";
import { debounce, waitUntilCondition } from "./utils.js";
import { debounce, waitUntilCondition, escapeHtml } from "./utils.js";
function debouncePromise(func, delay) {
let timeoutId;
@ -1291,7 +1291,7 @@ PromptManagerModule.prototype.renderPromptManager = function () {
const prompts = [...this.serviceSettings.prompts]
.filter(prompt => prompt && !prompt?.system_prompt)
.sort((promptA, promptB) => promptA.name.localeCompare(promptB.name))
.reduce((acc, prompt) => acc + `<option value="${prompt.identifier}">${prompt.name}</option>`, '');
.reduce((acc, prompt) => acc + `<option value="${prompt.identifier}">${escapeHtml(prompt.name)}</option>`, '');
const footerHtml = `
<div class="${this.configuration.prefix}prompt_manager_footer">
@ -1440,13 +1440,14 @@ PromptManagerModule.prototype.renderPromptManagerListItems = function () {
toggleSpanHtml = `<span class="fa-solid"></span>`;
}
const encodedName = escapeHtml(prompt.name);
listItemHtml += `
<li class="${prefix}prompt_manager_prompt ${draggableClass} ${enabledClass} ${markerClass}" data-pm-identifier="${prompt.identifier}">
<span class="${prefix}prompt_manager_prompt_name" data-pm-name="${prompt.name}">
<span class="${prefix}prompt_manager_prompt_name" data-pm-name="${encodedName}">
${prompt.marker ? '<span class="fa-solid fa-thumb-tack" title="Marker"></span>' : ''}
${!prompt.marker && prompt.system_prompt ? '<span class="fa-solid fa-square-poll-horizontal" title="Global Prompt"></span>' : ''}
${!prompt.marker && !prompt.system_prompt ? '<span class="fa-solid fa-user" title="User Prompt"></span>' : ''}
${this.isPromptInspectionAllowed(prompt) ? `<a class="prompt-manager-inspect-action">${prompt.name}</a>` : prompt.name}
${this.isPromptInspectionAllowed(prompt) ? `<a class="prompt-manager-inspect-action">${encodedName}</a>` : encodedName}
</span>
<span>
<span class="prompt_manager_prompt_controls">

View File

@ -1021,9 +1021,9 @@ export function initRossMods() {
}
if (event.key == "Escape") { //closes various panels
//dont override Escape hotkey functions from script.js
//"close edit box" and "cancel stream generation".
if ($("#curEditTextarea").is(":visible") || $("#mes_stop").is(":visible")) {
console.debug('escape key, but deferring to script.js routines')
return
@ -1060,13 +1060,11 @@ export function initRossMods() {
.not('#left-nav-panel')
.not('#right-nav-panel')
.not('#floatingPrompt')
console.log(visibleDrawerContent)
$(visibleDrawerContent).parent().find('.drawer-icon').trigger('click');
return
}
if ($("#floatingPrompt").is(":visible")) {
console.log('saw AN visible, trying to close')
$("#ANClose").trigger('click');
return
}
@ -1097,7 +1095,7 @@ export function initRossMods() {
if (event.ctrlKey && /^[1-9]$/.test(event.key)) {
// Your code here
// This will eventually be to trigger quick replies
event.preventDefault();
console.log("Ctrl +" + event.key + " pressed!");
}

View File

@ -385,7 +385,7 @@ jQuery(async () => {
const buttonHtml = $(await $.get(`${extensionFolderPath}/menuButton.html`));
buttonHtml.on('click', onCfgMenuItemClick)
buttonHtml.insertAfter("#option_toggle_AN");
buttonHtml.appendTo("#options_advanced");
// Hook events
eventSource.on(event_types.CHAT_CHANGED, async () => {

View File

@ -165,6 +165,7 @@ let power_user = {
continue_on_send: false,
trim_spaces: true,
relaxed_api_urls: false,
disable_group_trimming: false,
default_instruct: '',
instruct: {
@ -789,6 +790,7 @@ function loadPowerUserSettings(settings, data) {
$("#trim_sentences_checkbox").prop("checked", power_user.trim_sentences);
$("#include_newline_checkbox").prop("checked", power_user.include_newline);
$('#render_formulas').prop("checked", power_user.render_formulas);
$('#disable_group_trimming').prop("checked", power_user.disable_group_trimming);
$("#markdown_escape_strings").val(power_user.markdown_escape_strings);
$("#fast_ui_mode").prop("checked", power_user.fast_ui_mode);
$("#waifuMode").prop("checked", power_user.waifuMode);
@ -2160,6 +2162,11 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#disable_group_trimming').on('input', function () {
power_user.disable_group_trimming = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#debug_menu').on('click', function () {
showDebugMenu();
});

View File

@ -14,6 +14,10 @@ export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= tot
*/
export const navigation_option = { none: 0, previous: 1, last: 2, };
export function escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/**
* Determines if a value is unique in an array.
* @param {any} value Current value.

382
server.js
View File

@ -1,10 +1,77 @@
#!/usr/bin/env node
// native node modules
const child_process = require('child_process')
const crypto = require('crypto');
const fs = require('fs');
const http = require("http");
const https = require('https');
const path = require('path');
const readline = require('readline');
const util = require('util');
const { Readable } = require('stream');
const { finished } = require('stream/promises');
const { TextEncoder, TextDecoder } = require('util');
// cli/fs related library imports
const commandExistsSync = require('command-exists').sync;
const open = require('open');
const sanitize = require('sanitize-filename');
const simpleGit = require('simple-git');
const writeFileAtomicSync = require('write-file-atomic').sync;
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
// express/server related library imports
const cors = require('cors');
const doubleCsrf = require('csrf-csrf').doubleCsrf;
const express = require('express');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const multer = require("multer");
const responseTime = require('response-time');
// net related library imports
const axios = require('axios');
const DeviceDetector = require("device-detector-js");
const fetch = require('node-fetch').default;
const ipaddr = require('ipaddr.js');
const ipMatching = require('ip-matching');
const json5 = require('json5');
const RESTClient = require('node-rest-client').Client;
const WebSocket = require('ws');
// image processing related library imports
const exif = require('piexifjs');
const encode = require('png-chunks-encode');
const extract = require('png-chunks-extract');
const jimp = require('jimp');
const mime = require('mime-types');
const PNGtext = require('png-chunk-text');
const webp = require('webp-converter');
const yauzl = require('yauzl');
// tokenizing related library imports
const { SentencePieceProcessor } = require("@agnai/sentencepiece-js");
const tiktoken = require('@dqbd/tiktoken');
const { Tokenizer } = require('@agnai/web-tokenizers');
// misc/other imports
const _ = require('lodash');
const { generateRequestUrl, normaliseResponse } = require('google-translate-api-browser');
// Create files before running anything else
createDefaultFiles();
// local library imports
const AIHorde = require("./src/horde");
const basicAuthMiddleware = require('./src/middleware/basicAuthMiddleware');
const characterCardParser = require('./src/character-card-parser.js');
const contentManager = require('./src/content-manager');
const novelai = require('./src/novelai');
const statsHelpers = require('./statsHelpers.js');
function createDefaultFiles() {
const fs = require('fs');
const path = require('path');
const files = {
settings: 'public/settings.json',
bg_load: 'public/css/bg_load.css',
@ -24,13 +91,9 @@ function createDefaultFiles() {
}
}
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const net = require("net");
// work around a node v20 bug: https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
if (net.setDefaultAutoSelectFamily) {
net.setDefaultAutoSelectFamily(false);
}
// @ts-ignore work around a node v20 bug: https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
const cliArguments = yargs(hideBin(process.argv))
.option('disableCsrf', {
@ -49,57 +112,21 @@ const cliArguments = yargs(hideBin(process.argv))
type: 'string',
default: 'certs/privkey.pem',
describe: 'Path to your private key file.'
}).argv;
}).parseSync();
// change all relative paths
const path = require('path');
const directory = process.pkg ? path.dirname(process.execPath) : __dirname;
console.log(process.pkg ? 'Running from binary' : 'Running from source');
const directory = process['pkg'] ? path.dirname(process.execPath) : __dirname;
console.log(process['pkg'] ? 'Running from binary' : 'Running from source');
process.chdir(directory);
const express = require('express');
const compression = require('compression');
const app = express();
const responseTime = require('response-time');
const simpleGit = require('simple-git');
app.use(compression());
app.use(responseTime());
const fs = require('fs');
const writeFileAtomicSync = require('write-file-atomic').sync;
const readline = require('readline');
const open = require('open');
const multer = require("multer");
const http = require("http");
const https = require('https');
const basicAuthMiddleware = require('./src/middleware/basicAuthMiddleware');
const contentManager = require('./src/content-manager');
const extract = require('png-chunks-extract');
const encode = require('png-chunks-encode');
const PNGtext = require('png-chunk-text');
const jimp = require('jimp');
const sanitize = require('sanitize-filename');
const mime = require('mime-types');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const ipaddr = require('ipaddr.js');
const json5 = require('json5');
const exif = require('piexifjs');
const webp = require('webp-converter');
const DeviceDetector = require("device-detector-js");
const { TextEncoder, TextDecoder } = require('util');
const utf8Encode = new TextEncoder();
const commandExistsSync = require('command-exists').sync;
// impoort from statsHelpers.js
const statsHelpers = require('./statsHelpers.js');
const characterCardParser = require('./src/character-card-parser.js');
const config = require(path.join(process.cwd(), './config.conf'));
const server_port = process.env.SILLY_TAVERN_PORT || config.port;
@ -120,25 +147,16 @@ const enableExtensions = config.enableExtensions;
const listen = config.listen;
const allowKeysExposure = config.allowKeysExposure;
const axios = require('axios');
const tiktoken = require('@dqbd/tiktoken');
const WebSocket = require('ws');
function getHordeClient() {
const AIHorde = require("./src/horde");
const ai_horde = new AIHorde({
client_agent: getVersion()?.agent || 'SillyTavern:UNKNOWN:Cohee#1207',
});
return ai_horde;
}
const ipMatching = require('ip-matching');
const yauzl = require('yauzl');
const restClient = new RESTClient();
const Client = require('node-rest-client').Client;
const client = new Client();
client.on('error', (err) => {
restClient.on('error', (err) => {
console.error('An error occurred:', err);
});
@ -183,8 +201,6 @@ function get_mancer_headers() {
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const { SentencePieceProcessor } = require("@agnai/sentencepiece-js");
const { Tokenizer } = require('@agnai/web-tokenizers');
const CHARS_PER_TOKEN = 3.35;
let spp_llama;
@ -333,8 +349,6 @@ const directories = {
// CSRF Protection //
if (cliArguments.disableCsrf === false) {
const doubleCsrf = require('csrf-csrf').doubleCsrf;
const CSRF_SECRET = crypto.randomBytes(8).toString('hex');
const COOKIES_SECRET = crypto.randomBytes(8).toString('hex');
@ -352,7 +366,7 @@ if (cliArguments.disableCsrf === false) {
app.get("/csrf-token", (req, res) => {
res.json({
"token": generateToken(res)
"token": generateToken(res, req)
});
});
@ -368,7 +382,6 @@ if (cliArguments.disableCsrf === false) {
}
// CORS Settings //
const cors = require('cors');
const CORS = cors({
origin: 'null',
methods: ['OPTIONS']
@ -385,7 +398,7 @@ function getIpFromRequest(req) {
let clientIp = req.connection.remoteAddress;
let ip = ipaddr.parse(clientIp);
// Check if the IP address is IPv4-mapped IPv6 address
if (ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) {
if (ip.kind() === 'ipv6' && ip instanceof ipaddr.IPv6 && ip.isIPv4MappedAddress()) {
const ipv4 = ip.toIPv4Address().toString();
clientIp = ipv4;
} else {
@ -422,7 +435,7 @@ app.use(function (req, res, next) {
});
app.use(express.static(process.cwd() + "/public", { refresh: true }));
app.use(express.static(process.cwd() + "/public", {}));
app.use('/backgrounds', (req, res) => {
const filePath = decodeURIComponent(path.join(process.cwd(), 'public/backgrounds', req.url.replace(/%20/g, ' ')));
@ -456,7 +469,7 @@ app.get("/notes/*", function (request, response) {
app.get('/deviceinfo', function (request, response) {
const userAgent = request.header('user-agent');
const deviceDetector = new DeviceDetector();
const deviceInfo = deviceDetector.parse(userAgent);
const deviceInfo = deviceDetector.parse(userAgent || "");
return response.send(deviceInfo);
});
app.get('/version', function (_, response) {
@ -465,7 +478,7 @@ app.get('/version', function (_, response) {
})
//**************Kobold api
app.post("/generate", jsonParser, async function (request, response_generate = response) {
app.post("/generate", jsonParser, async function (request, response_generate) {
if (!request.body) return response_generate.sendStatus(400);
const request_prompt = request.body.prompt;
@ -536,10 +549,9 @@ app.post("/generate", jsonParser, async function (request, response_generate = r
const MAX_RETRIES = 50;
const delayAmount = 2500;
let fetch, url, response;
let url, response;
for (let i = 0; i < MAX_RETRIES; i++) {
try {
fetch = require('node-fetch').default;
url = request.body.streaming ? `${api_server}/extra/generate/stream` : `${api_server}/v1/generate`;
response = await fetch(url, { method: 'POST', timeout: 0, ...args });
@ -596,7 +608,7 @@ app.post("/generate", jsonParser, async function (request, response_generate = r
});
//************** Text generation web UI
app.post("/generate_textgenerationwebui", jsonParser, async function (request, response_generate = response) {
app.post("/generate_textgenerationwebui", jsonParser, async function (request, response_generate) {
if (!request.body) return response_generate.sendStatus(400);
console.log(request.body);
@ -610,6 +622,10 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
});
if (request.header('X-Response-Streaming')) {
const streamingUrlHeader = request.header('X-Streaming-URL');
if (streamingUrlHeader === undefined) return response_generate.sendStatus(400);
const streamingUrl = streamingUrlHeader.replace("localhost", "127.0.0.1");
response_generate.writeHead(200, {
'Content-Type': 'text/plain;charset=utf-8',
'Transfer-Encoding': 'chunked',
@ -617,7 +633,6 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
});
async function* readWebsocket() {
const streamingUrl = request.header('X-Streaming-URL').replace("localhost", "127.0.0.1");
const websocket = new WebSocket(streamingUrl);
websocket.on('open', async function () {
@ -648,7 +663,7 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
websocket.once('error', reject);
websocket.once('message', (data, isBinary) => {
websocket.removeListener('error', reject);
resolve(data, isBinary);
resolve(data);
});
});
} catch (err) {
@ -711,7 +726,7 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
console.log("Endpoint response:", data);
return response_generate.send(data);
} catch (error) {
retval = { error: true, status: error.status, response: error.statusText };
let retval = { error: true, status: error.status, response: error.statusText };
console.log("Endpoint error:", error);
try {
retval.response = await error.json();
@ -837,12 +852,12 @@ function getVersion() {
try {
const pkgJson = require('./package.json');
pkgVersion = pkgJson.version;
if (!process.pkg && commandExistsSync('git')) {
gitRevision = require('child_process')
if (!process['pkg'] && commandExistsSync('git')) {
gitRevision = child_process
.execSync('git rev-parse --short HEAD', { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'ignore'] })
.toString().trim();
gitBranch = require('child_process')
gitBranch = child_process
.execSync('git rev-parse --abbrev-ref HEAD', { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'ignore'] })
.toString().trim();
}
@ -887,13 +902,11 @@ function convertToV2(char) {
function unsetFavFlag(char) {
const _ = require('lodash');
_.set(char, 'fav', false);
_.set(char, 'data.extensions.fav', false);
}
function readFromV2(char) {
const _ = require('lodash');
if (_.isUndefined(char.data)) {
console.warn('Spec v2 data missing');
return char;
@ -948,16 +961,14 @@ function readFromV2(char) {
//***************** Main functions
function charaFormatData(data) {
// This is supposed to save all the foreign keys that ST doesn't care about
const _ = require('lodash');
const char = tryParse(data.json_data) || {};
// This function uses _.cond() to create a series of conditional checks that return the desired output based on the input data.
// It checks if data.alternate_greetings is an array, a string, or neither, and acts accordingly.
const getAlternateGreetings = data => _.cond([
[d => Array.isArray(d.alternate_greetings), d => d.alternate_greetings],
[d => typeof d.alternate_greetings === 'string', d => [d.alternate_greetings]],
[_.stubTrue, _.constant([])]
])(data);
// Checks if data.alternate_greetings is an array, a string, or neither, and acts accordingly. (expected to be an array of strings)
const getAlternateGreetings = data => {
if (Array.isArray(data.alternate_greetings)) return data.alternate_greetings
if (typeof data.alternate_greetings === 'string') return [data.alternate_greetings]
return []
}
// Spec V1 fields
_.set(char, 'name', data.ch_name);
@ -1087,9 +1098,10 @@ app.post("/renamecharacter", jsonParser, async function (request, response) {
const newChatsPath = path.join(chatsPath, newInternalName);
try {
const _ = require('lodash');
// Read old file, replace name int it
const rawOldData = await charaRead(oldAvatarPath);
if (rawOldData === false || rawOldData === undefined) throw new Error("Failed to read character file");
const oldData = getCharaCardV2(json5.parse(rawOldData));
_.set(oldData, 'data.name', newName);
_.set(oldData, 'name', newName);
@ -1178,26 +1190,22 @@ app.post("/editcharacterattribute", jsonParser, async function (request, respons
try {
const avatarPath = path.join(charactersPath, request.body.avatar_url);
charaRead(avatarPath).then((char) => {
char = JSON.parse(char);
//check if the field exists
if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {
console.error('Error: invalid field.');
response.status(400).send('Error: invalid field.');
return;
}
char[request.body.field] = request.body.value;
char.data[request.body.field] = request.body.value;
char = JSON.stringify(char);
return { char };
}).then(({ char }) => {
charaWrite(avatarPath, char, (request.body.avatar_url).replace('.png', ''), response, 'Character saved');
}).catch((err) => {
console.error('An error occured, character edit invalidated.', err);
});
}
catch {
console.error('An error occured, character edit invalidated.');
let charJSON = await charaRead(avatarPath);
if (typeof charJSON !== 'string') throw new Error("Failed to read character file");
let char = JSON.parse(charJSON)
//check if the field exists
if (char[request.body.field] === undefined && char.data[request.body.field] === undefined) {
console.error('Error: invalid field.');
response.status(400).send('Error: invalid field.');
return;
}
char[request.body.field] = request.body.value;
char.data[request.body.field] = request.body.value;
let newCharJSON = JSON.stringify(char);
await charaWrite(avatarPath, newCharJSON, (request.body.avatar_url).replace('.png', ''), response, 'Character saved');
} catch (err) {
console.error('An error occured, character edit invalidated.', err);
}
});
@ -1237,6 +1245,10 @@ app.post("/deletecharacter", jsonParser, async function (request, response) {
return response.sendStatus(200);
});
/**
* @param {express.Response | undefined} response
* @param {{file_name: string} | string} mes
*/
async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok', crop = undefined) {
try {
// Read the image, resize, and save it as a PNG into the buffer
@ -1841,8 +1853,7 @@ function getImages(path) {
//***********Novel.ai API
app.post("/getstatus_novelai", jsonParser, function (request, response_getstatus_novel = response) {
app.post("/getstatus_novelai", jsonParser, async function (request, response_getstatus_novel) {
if (!request.body) return response_getstatus_novel.sendStatus(400);
const api_key_novel = readSecret(SECRET_KEYS.NOVEL);
@ -1850,27 +1861,30 @@ app.post("/getstatus_novelai", jsonParser, function (request, response_getstatus
return response_getstatus_novel.sendStatus(401);
}
var data = {};
var args = {
data: data,
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + api_key_novel }
};
client.get(api_novelai + "/user/subscription", args, function (data, response) {
if (response.statusCode == 200) {
//console.log(data);
response_getstatus_novel.send(data);//data);
try {
const response = await fetch(api_novelai + "/user/subscription", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer " + api_key_novel,
},
});
if (response.ok) {
const data = await response.json();
return response_getstatus_novel.send(data);
} else if (response.status == 401) {
console.log('NovelAI Access Token is incorrect.');
return response_getstatus_novel.send({ error: true });
}
else {
if (response.statusCode == 401) {
console.log('Access Token is incorrect.');
}
console.log(data);
response_getstatus_novel.send({ error: true });
console.log('NovelAI returned an error:', response.statusText);
return response_getstatus_novel.send({ error: true });
}
}).on('error', function () {
response_getstatus_novel.send({ error: true });
});
} catch (error) {
console.log(error);
return response_getstatus_novel.send({ error: true });
}
});
app.post("/generate_novelai", jsonParser, async function (request, response_generate_novel = response) {
@ -1888,7 +1902,6 @@ app.post("/generate_novelai", jsonParser, async function (request, response_gene
controller.abort();
});
const novelai = require('./src/novelai');
const isNewModel = (request.body.model.includes('clio') || request.body.model.includes('kayra'));
const badWordsList = novelai.getBadWordsList(request.body.model);
@ -1943,7 +1956,7 @@ app.post("/generate_novelai", jsonParser, async function (request, response_gene
"order": request.body.order
}
};
const util = require('util');
console.log(util.inspect(data, { depth: 4 }))
const args = {
@ -1953,7 +1966,6 @@ app.post("/generate_novelai", jsonParser, async function (request, response_gene
};
try {
const fetch = require('node-fetch').default;
const url = request.body.streaming ? `${api_novelai}/ai/generate-stream` : `${api_novelai}/ai/generate`;
const response = await fetch(url, { method: 'POST', timeout: 0, ...args });
@ -2304,7 +2316,6 @@ app.post("/exportchat", jsonParser, async function (request, response) {
}
}
const readline = require('readline');
const readStream = fs.createReadStream(filename);
const rl = readline.createInterface({
input: readStream,
@ -3105,7 +3116,7 @@ app.get('/thumbnail', jsonParser, async function (request, response) {
});
/* OpenAI */
app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_openai = response) {
app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_openai) {
if (!request.body) return response_getstatus_openai.sendStatus(400);
let api_url;
@ -3133,14 +3144,14 @@ app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_
...headers,
},
};
client.get(api_url + "/models", args, function (data, response) {
restClient.get(api_url + "/models", args, function (data, response) {
if (response.statusCode == 200) {
response_getstatus_openai.send(data);
if (request.body.use_openrouter) {
let models = [];
data.data.forEach(model => {
const context_length = model.context_length;
const tokens_dollar = parseFloat(1 / (1000 * model.pricing.prompt));
const tokens_dollar = Number(1 / (1000 * model.pricing.prompt));
const tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
models[model.id] = {
tokens_per_dollar: tokens_rounded + 'k',
@ -3283,7 +3294,6 @@ function convertClaudePrompt(messages, addHumanPrefix, addAssistantPostfix) {
}
async function sendScaleRequest(request, response) {
const fetch = require('node-fetch').default;
const api_url = new URL(request.body.api_url_scale).toString();
const api_key_scale = readSecret(SECRET_KEYS.SCALE);
@ -3397,7 +3407,6 @@ app.post("/generate_altscale", jsonParser, function (request, response_generate_
});
async function sendClaudeRequest(request, response) {
const fetch = require('node-fetch').default;
const api_url = new URL(request.body.reverse_proxy || api_claude).toString();
const api_key_claude = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.CLAUDE);
@ -3658,7 +3667,7 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op
makeRequest(config, response_generate_openai, request);
});
app.post("/tokenize_openai", jsonParser, function (request, response_tokenize_openai = response) {
app.post("/tokenize_openai", jsonParser, function (request, response_tokenize_openai) {
if (!request.body) return response_tokenize_openai.sendStatus(400);
let num_tokens = 0;
@ -3766,7 +3775,7 @@ async function sendAI21Request(request, response) {
}
app.post("/tokenize_ai21", jsonParser, function (request, response_tokenize_ai21 = response) {
app.post("/tokenize_ai21", jsonParser, function (request, response_tokenize_ai21) {
if (!request.body) return response_tokenize_ai21.sendStatus(400);
const options = {
method: 'POST',
@ -3973,7 +3982,6 @@ app.post("/tokenize_via_api", jsonParser, async function (request, response) {
// ** REST CLIENT ASYNC WRAPPERS **
async function postAsync(url, args) {
const fetch = require('node-fetch').default;
const response = await fetch(url, { method: 'POST', timeout: 0, ...args });
if (response.ok) {
@ -3986,7 +3994,7 @@ async function postAsync(url, args) {
function getAsync(url, args) {
return new Promise((resolve, reject) => {
client.get(url, args, (data, response) => {
restClient.get(url, args, (data, response) => {
if (response.statusCode >= 400) {
reject(data);
}
@ -4063,23 +4071,24 @@ if (listen && !config.whitelistMode && !config.basicAuthMode) {
}
}
if (true === cliArguments.ssl)
if (true === cliArguments.ssl) {
https.createServer(
{
cert: fs.readFileSync(cliArguments.certPath),
key: fs.readFileSync(cliArguments.keyPath)
}, app)
.listen(
tavernUrl.port || 443,
Number(tavernUrl.port) || 443,
tavernUrl.hostname,
setupTasks
);
else
} else {
http.createServer(app).listen(
tavernUrl.port || 80,
Number(tavernUrl.port) || 80,
tavernUrl.hostname,
setupTasks
);
}
async function convertWebp() {
const files = fs.readdirSync(directories.characters).filter(e => e.endsWith(".webp"));
@ -4190,7 +4199,7 @@ function migrateSecrets() {
try {
let modified = false;
const fileContents = fs.readFileSync(SETTINGS_FILE);
const fileContents = fs.readFileSync(SETTINGS_FILE, 'utf8');
const settings = JSON.parse(fileContents);
const oaiKey = settings?.api_key_openai;
const hordeKey = settings?.horde_settings?.api_key;
@ -4242,7 +4251,7 @@ app.post('/readsecretstate', jsonParser, (_, response) => {
}
try {
const fileContents = fs.readFileSync(SECRETS_FILE);
const fileContents = fs.readFileSync(SECRETS_FILE, 'utf8');
const secrets = JSON.parse(fileContents);
const state = {};
@ -4301,7 +4310,7 @@ app.post('/viewsecrets', jsonParser, async (_, response) => {
}
try {
const fileContents = fs.readFileSync(SECRETS_FILE);
const fileContents = fs.readFileSync(SECRETS_FILE, 'utf-8');
const secrets = JSON.parse(fileContents);
return response.send(secrets);
} catch (error) {
@ -4364,6 +4373,7 @@ app.post('/horde_generateimage', jsonParser, async (request, response) => {
{
sampler_name: request.body.sampler,
hires_fix: request.body.enable_hr,
// @ts-ignore - use_gfpgan param is not in the type definition, need to update to new ai_horde @ https://github.com/ZeldaFan0225/ai_horde/blob/main/index.ts
use_gfpgan: request.body.restore_faces,
cfg_scale: request.body.scale,
steps: request.body.steps,
@ -4390,6 +4400,7 @@ app.post('/horde_generateimage', jsonParser, async (request, response) => {
if (check.done) {
const result = await ai_horde.getImageGenerationStatus(generation.id);
if (result.generations === undefined) return response.sendStatus(500);
return response.send(result.generations[0].img);
}
@ -4452,8 +4463,6 @@ app.post('/libre_translate', jsonParser, async (request, response) => {
});
app.post('/google_translate', jsonParser, async (request, response) => {
const { generateRequestUrl, normaliseResponse } = require('google-translate-api-browser');
const text = request.body.text;
const lang = request.body.lang;
@ -4499,7 +4508,6 @@ app.post('/deepl_translate', jsonParser, async (request, response) => {
console.log('Input text: ' + text);
const fetch = require('node-fetch').default;
const params = new URLSearchParams();
params.append('text', text);
params.append('target_lang', lang);
@ -4545,7 +4553,6 @@ app.post('/novel_tts', jsonParser, async (request, response) => {
}
try {
const fetch = require('node-fetch').default;
const url = `${api_novelai}/ai/generate-voice?text=${encodeURIComponent(text)}&voice=-1&seed=${encodeURIComponent(voice)}&opus=false&version=v2`;
const result = await fetch(url, {
method: 'GET',
@ -4718,7 +4725,7 @@ app.post('/import_custom', jsonParser, async (request, response) => {
return response.sendStatus(404);
}
response.set('Content-Type', result.fileType);
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);
@ -4729,8 +4736,6 @@ app.post('/import_custom', jsonParser, async (request, response) => {
});
async function downloadChubLorebook(id) {
const fetch = require('node-fetch').default;
const result = await fetch('https://api.chub.ai/api/lorebooks/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -4754,8 +4759,6 @@ async function downloadChubLorebook(id) {
}
async function downloadChubCharacter(id) {
const fetch = require('node-fetch').default;
const result = await fetch('https://api.chub.ai/api/characters/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -4776,6 +4779,11 @@ async function downloadChubCharacter(id) {
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;
@ -4880,7 +4888,7 @@ function writeSecret(key, value) {
writeFileAtomicSync(SECRETS_FILE, emptyFile, "utf-8");
}
const fileContents = fs.readFileSync(SECRETS_FILE);
const fileContents = fs.readFileSync(SECRETS_FILE, 'utf-8');
const secrets = JSON.parse(fileContents);
secrets[key] = value;
writeFileAtomicSync(SECRETS_FILE, JSON.stringify(secrets), "utf-8");
@ -4891,7 +4899,7 @@ function readSecret(key) {
return undefined;
}
const fileContents = fs.readFileSync(SECRETS_FILE);
const fileContents = fs.readFileSync(SECRETS_FILE, 'utf-8');
const secrets = JSON.parse(fileContents);
return secrets[key];
}
@ -4972,7 +4980,7 @@ async function getImageBuffers(zipFilePath) {
/**
* This function extracts the extension information from the manifest file.
* @param {string} extensionPath - The path of the extension folder
* @returns {Object} - Returns the manifest data as an object
* @returns {Promise<Object>} - Returns the manifest data as an object
*/
async function getManifest(extensionPath) {
const manifestPath = path.join(extensionPath, 'manifest.json');
@ -4987,6 +4995,7 @@ async function getManifest(extensionPath) {
}
async function checkIfRepoIsUpToDate(extensionPath) {
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
const git = simpleGit();
await git.cwd(extensionPath).fetch('origin');
const currentBranch = await git.cwd(extensionPath).branch();
@ -5017,6 +5026,7 @@ async function checkIfRepoIsUpToDate(extensionPath) {
* @returns {void}
*/
app.post('/get_extension', jsonParser, async (request, response) => {
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
const git = simpleGit();
if (!request.body.url) {
return response.status(400).send('Bad Request: URL is required in the request body.');
@ -5062,6 +5072,7 @@ app.post('/get_extension', jsonParser, async (request, response) => {
* @returns {void}
*/
app.post('/update_extension', jsonParser, async (request, response) => {
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
const git = simpleGit();
if (!request.body.extensionName) {
return response.status(400).send('Bad Request: extensionName is required in the request body.');
@ -5107,6 +5118,7 @@ app.post('/update_extension', jsonParser, async (request, response) => {
* @returns {void}
*/
app.post('/get_extension_version', jsonParser, async (request, response) => {
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
const git = simpleGit();
if (!request.body.extensionName) {
return response.status(400).send('Bad Request: extensionName is required in the request body.');
@ -5249,16 +5261,15 @@ function checkAssetFileName(inputFilename) {
* @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"];
const fetch = require('node-fetch').default;
// Check category
let category = null;
for (i of validCategories)
for (let i of validCategories)
if (i == inputCategory)
category = i;
@ -5270,7 +5281,7 @@ app.post('/asset_download', jsonParser, async (request, response) => {
// Sanitize filename
const safe_input = checkAssetFileName(inputFilename);
if (safe_input == '')
return response.sendFile(400);
return response.sendStatus(400);
const temp_path = path.join(directories.assets, "temp", safe_input)
const file_path = path.join(directories.assets, category, safe_input)
@ -5278,23 +5289,19 @@ app.post('/asset_download', jsonParser, async (request, response) => {
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);
const res = await fetch(url);
if (!res.ok || res.body === null) {
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(res.body.pipe(fileStream));
// Move into asset place
console.debug("Download finished, moving file from", temp_path, "to", file_path);
@ -5316,15 +5323,13 @@ app.post('/asset_download', jsonParser, async (request, response) => {
* @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)
for (let i of validCategories)
if (i == inputCategory)
category = i;
@ -5336,7 +5341,7 @@ app.post('/asset_delete', jsonParser, async (request, response) => {
// Sanitize filename
const safe_input = checkAssetFileName(inputFilename);
if (safe_input == '')
return response.sendFile(400);
return response.sendStatus(400);
const file_path = path.join(directories.assets, category, safe_input)
console.debug("Request received to delete", category, file_path);
@ -5373,13 +5378,14 @@ app.post('/asset_delete', jsonParser, async (request, response) => {
* @returns {void}
*/
app.post('/get_character_assets_list', jsonParser, async (request, response) => {
const name = sanitize(request.query.name);
if (request.query.name === undefined) return response.sendStatus(400);
const name = sanitize(request.query.name.toString());
const inputCategory = request.query.category;
const validCategories = ["bgm", "ambient"]
// Check category
let category = null
for (i of validCategories)
for (let i of validCategories)
if (i == inputCategory)
category = i
@ -5398,7 +5404,7 @@ app.post('/get_character_assets_list', jsonParser, async (request, response) =>
return filename != ".placeholder";
});
for (i of files)
for (let i of files)
output.push(`/characters/${name}/${category}/${i}`);
}