mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-04-17 12:17:21 +02:00
Merge pull request #977 from city-unit/feature/exorcism
Feature/exorcism
This commit is contained in:
commit
6fb278266b
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,6 +6,7 @@ public/backgrounds/
|
|||||||
public/groups/
|
public/groups/
|
||||||
public/group chats/
|
public/group chats/
|
||||||
public/worlds/
|
public/worlds/
|
||||||
|
public/user/
|
||||||
public/css/bg_load.css
|
public/css/bg_load.css
|
||||||
public/themes/
|
public/themes/
|
||||||
public/OpenAI Settings/
|
public/OpenAI Settings/
|
||||||
|
@ -2378,10 +2378,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
|
|||||||
abortController = new AbortController();
|
abortController = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (main_api == 'novel' && quiet_prompt) {
|
|
||||||
quiet_prompt = adjustNovelInstructionPrompt(quiet_prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenAI doesn't need instruct mode. Use OAI main prompt instead.
|
// OpenAI doesn't need instruct mode. Use OAI main prompt instead.
|
||||||
const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
|
const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
|
||||||
const isImpersonate = type == "impersonate";
|
const isImpersonate = type == "impersonate";
|
||||||
@ -2470,6 +2466,11 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (quiet_prompt) {
|
||||||
|
quiet_prompt = substituteParams(quiet_prompt);
|
||||||
|
quiet_prompt = main_api == 'novel' ? adjustNovelInstructionPrompt(quiet_prompt) : quiet_prompt;
|
||||||
|
}
|
||||||
|
|
||||||
if (true === dryRun ||
|
if (true === dryRun ||
|
||||||
(online_status != 'no_connection' && this_chid != undefined && this_chid !== 'invalid-safety-id')) {
|
(online_status != 'no_connection' && this_chid != undefined && this_chid !== 'invalid-safety-id')) {
|
||||||
let textareaText;
|
let textareaText;
|
||||||
@ -5767,7 +5768,6 @@ export async function displayPastChats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
displayChats(''); // Display all by default
|
displayChats(''); // Display all by default
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
substituteParams,
|
|
||||||
saveSettingsDebounced,
|
saveSettingsDebounced,
|
||||||
systemUserName,
|
systemUserName,
|
||||||
hideSwipeButtons,
|
hideSwipeButtons,
|
||||||
@ -14,7 +13,8 @@ import {
|
|||||||
} from "../../../script.js";
|
} from "../../../script.js";
|
||||||
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
|
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
|
||||||
import { selected_group } from "../../group-chats.js";
|
import { selected_group } from "../../group-chats.js";
|
||||||
import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename } from "../../utils.js";
|
import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename, saveBase64AsFile } from "../../utils.js";
|
||||||
|
import { humanizedDateTime } from "../../RossAscends-mods.js";
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
// Wraps a string into monospace font-face span
|
// Wraps a string into monospace font-face span
|
||||||
@ -512,7 +512,7 @@ function getQuietPrompt(mode, trigger) {
|
|||||||
return trigger;
|
return trigger;
|
||||||
}
|
}
|
||||||
|
|
||||||
return substituteParams(stringFormat(extension_settings.sd.prompts[mode], trigger));
|
return stringFormat(extension_settings.sd.prompts[mode], trigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
function processReply(str) {
|
function processReply(str) {
|
||||||
@ -537,6 +537,7 @@ function processReply(str) {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getRawLastMessage() {
|
function getRawLastMessage() {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const lastMessage = context.chat.slice(-1)[0].mes,
|
const lastMessage = context.chat.slice(-1)[0].mes,
|
||||||
@ -565,6 +566,10 @@ async function generatePicture(_, trigger, message, callback) {
|
|||||||
const quiet_prompt = getQuietPrompt(generationType, trigger);
|
const quiet_prompt = getQuietPrompt(generationType, trigger);
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
|
|
||||||
|
// if context.characterId is not null, then we get context.characters[context.characterId].avatar, else we get groupId and context.groups[groupId].id
|
||||||
|
// sadly, groups is not an array, but is a dict with keys being index numbers, so we have to filter it
|
||||||
|
const characterName = context.characterId ? context.characters[context.characterId].name : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]].id.toString();
|
||||||
|
|
||||||
const prevSDHeight = extension_settings.sd.height;
|
const prevSDHeight = extension_settings.sd.height;
|
||||||
const prevSDWidth = extension_settings.sd.width;
|
const prevSDWidth = extension_settings.sd.width;
|
||||||
const aspectRatio = extension_settings.sd.width / extension_settings.sd.height;
|
const aspectRatio = extension_settings.sd.width / extension_settings.sd.height;
|
||||||
@ -580,8 +585,10 @@ async function generatePicture(_, trigger, message, callback) {
|
|||||||
// Round to nearest multiple of 64
|
// Round to nearest multiple of 64
|
||||||
extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
|
extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
|
||||||
const callbackOriginal = callback;
|
const callbackOriginal = callback;
|
||||||
callback = function (prompt, base64Image) {
|
callback = async function (prompt, base64Image) {
|
||||||
const imgUrl = `url(${base64Image})`;
|
const imagePath = base64Image;
|
||||||
|
const imgUrl = `url('${encodeURIComponent(base64Image)}')`;
|
||||||
|
|
||||||
if ('forceSetBackground' in window) {
|
if ('forceSetBackground' in window) {
|
||||||
forceSetBackground(imgUrl);
|
forceSetBackground(imgUrl);
|
||||||
} else {
|
} else {
|
||||||
@ -590,9 +597,9 @@ async function generatePicture(_, trigger, message, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof callbackOriginal === 'function') {
|
if (typeof callbackOriginal === 'function') {
|
||||||
callbackOriginal(prompt, base64Image);
|
callbackOriginal(prompt, imagePath);
|
||||||
} else {
|
} else {
|
||||||
sendMessage(prompt, base64Image);
|
sendMessage(prompt, imagePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -604,7 +611,7 @@ async function generatePicture(_, trigger, message, callback) {
|
|||||||
context.deactivateSendButtons();
|
context.deactivateSendButtons();
|
||||||
hideSwipeButtons();
|
hideSwipeButtons();
|
||||||
|
|
||||||
await sendGenerationRequest(generationType, prompt, callback);
|
await sendGenerationRequest(generationType, prompt, characterName, callback);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.trace(err);
|
console.trace(err);
|
||||||
throw new Error('SD prompt text generation failed.')
|
throw new Error('SD prompt text generation failed.')
|
||||||
@ -644,19 +651,31 @@ async function generatePrompt(quiet_prompt) {
|
|||||||
return processReply(reply);
|
return processReply(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendGenerationRequest(generationType, prompt, callback) {
|
async function sendGenerationRequest(generationType, prompt, characterName = null, callback) {
|
||||||
const prefix = generationType !== generationMode.BACKGROUND
|
const prefix = generationType !== generationMode.BACKGROUND
|
||||||
? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix())
|
? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix())
|
||||||
: extension_settings.sd.prompt_prefix;
|
: extension_settings.sd.prompt_prefix;
|
||||||
|
|
||||||
if (extension_settings.sd.horde) {
|
if (extension_settings.sd.horde) {
|
||||||
await generateHordeImage(prompt, prefix, callback);
|
await generateHordeImage(prompt, prefix, characterName, callback);
|
||||||
} else {
|
} else {
|
||||||
await generateExtrasImage(prompt, prefix, callback);
|
await generateExtrasImage(prompt, prefix, characterName, callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateExtrasImage(prompt, prefix, callback) {
|
/**
|
||||||
|
* Generates an "extras" image using a provided prompt and other settings,
|
||||||
|
* then saves the generated image and either invokes a callback or sends a message with the image.
|
||||||
|
*
|
||||||
|
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||||
|
* @param {string} prefix - Additional context or prefix to guide the image generation.
|
||||||
|
* @param {string} characterName - The name used to determine the sub-directory for saving.
|
||||||
|
* @param {function} [callback] - Optional callback function invoked with the prompt and saved image.
|
||||||
|
* If not provided, `sendMessage` is called instead.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the image generation and processing are complete.
|
||||||
|
*/
|
||||||
|
async function generateExtrasImage(prompt, prefix, characterName, callback) {
|
||||||
console.debug(extension_settings.sd);
|
console.debug(extension_settings.sd);
|
||||||
const url = new URL(getApiUrl());
|
const url = new URL(getApiUrl());
|
||||||
url.pathname = '/api/image';
|
url.pathname = '/api/image';
|
||||||
@ -680,14 +699,28 @@ async function generateExtrasImage(prompt, prefix, callback) {
|
|||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const data = await result.json();
|
const data = await result.json();
|
||||||
const base64Image = `data:image/jpeg;base64,${data.image}`;
|
//filename should be character name + human readable timestamp + generation mode
|
||||||
|
const filename = `${characterName}_${humanizedDateTime()}`;
|
||||||
|
const base64Image = await saveBase64AsFile(data.image, characterName, filename, "jpg");
|
||||||
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
|
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
|
||||||
} else {
|
} else {
|
||||||
callPopup('Image generation has failed. Please try again.', 'text');
|
callPopup('Image generation has failed. Please try again.', 'text');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateHordeImage(prompt, prefix, callback) {
|
/**
|
||||||
|
* Generates a "horde" image using the provided prompt and configuration settings,
|
||||||
|
* then saves the generated image and either invokes a callback or sends a message with the image.
|
||||||
|
*
|
||||||
|
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||||
|
* @param {string} prefix - Additional context or prefix to guide the image generation.
|
||||||
|
* @param {string} characterName - The name used to determine the sub-directory for saving.
|
||||||
|
* @param {function} [callback] - Optional callback function invoked with the prompt and saved image.
|
||||||
|
* If not provided, `sendMessage` is called instead.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the image generation and processing are complete.
|
||||||
|
*/
|
||||||
|
async function generateHordeImage(prompt, prefix, characterName, callback) {
|
||||||
const result = await fetch('/horde_generateimage', {
|
const result = await fetch('/horde_generateimage', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getRequestHeaders(),
|
headers: getRequestHeaders(),
|
||||||
@ -709,7 +742,8 @@ async function generateHordeImage(prompt, prefix, callback) {
|
|||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const data = await result.text();
|
const data = await result.text();
|
||||||
const base64Image = `data:image/webp;base64,${data}`;
|
const filename = `${characterName}_${humanizedDateTime()}`;
|
||||||
|
const base64Image = await saveBase64AsFile(data, characterName, filename, "webp");
|
||||||
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
|
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
|
||||||
} else {
|
} else {
|
||||||
toastr.error('Image generation has failed. Please try again.');
|
toastr.error('Image generation has failed. Please try again.');
|
||||||
@ -827,7 +861,7 @@ async function sdMessageButton(e) {
|
|||||||
const message_id = $mes.attr('mesid');
|
const message_id = $mes.attr('mesid');
|
||||||
const message = context.chat[message_id];
|
const message = context.chat[message_id];
|
||||||
const characterName = message?.name || context.name2;
|
const characterName = message?.name || context.name2;
|
||||||
const messageText = substituteParams(message?.mes);
|
const messageText = message?.mes;
|
||||||
const hasSavedImage = message?.extra?.image && message?.extra?.title;
|
const hasSavedImage = message?.extra?.image && message?.extra?.title;
|
||||||
|
|
||||||
if ($icon.hasClass(busyClass)) {
|
if ($icon.hasClass(busyClass)) {
|
||||||
@ -842,7 +876,7 @@ async function sdMessageButton(e) {
|
|||||||
message.extra.title = prompt;
|
message.extra.title = prompt;
|
||||||
|
|
||||||
console.log('Regenerating an image, using existing prompt:', prompt);
|
console.log('Regenerating an image, using existing prompt:', prompt);
|
||||||
await sendGenerationRequest(generationMode.FREE, prompt, saveGeneratedImage);
|
await sendGenerationRequest(generationMode.FREE, prompt, characterName, saveGeneratedImage);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("doing /sd raw last");
|
console.log("doing /sd raw last");
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
isDataURL,
|
isDataURL,
|
||||||
createThumbnail,
|
createThumbnail,
|
||||||
extractAllWords,
|
extractAllWords,
|
||||||
|
saveBase64AsFile
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap } from "./RossAscends-mods.js";
|
import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap } from "./RossAscends-mods.js";
|
||||||
import { loadMovingUIState, sortEntitiesList } from './power-user.js';
|
import { loadMovingUIState, sortEntitiesList } from './power-user.js';
|
||||||
@ -367,12 +368,22 @@ function updateGroupAvatar(group) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if isDataURLor if it's a valid local file url
|
||||||
|
function isValidImageUrl(url) {
|
||||||
|
console.trace(url);
|
||||||
|
// check if empty dict
|
||||||
|
if (Object.keys(url).length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isDataURL(url) || (url && url.startsWith("user"));
|
||||||
|
}
|
||||||
|
|
||||||
function getGroupAvatar(group) {
|
function getGroupAvatar(group) {
|
||||||
if (!group) {
|
if (!group) {
|
||||||
return $(`<div class="avatar"><img src="${default_avatar}"></div>`);
|
return $(`<div class="avatar"><img src="${default_avatar}"></div>`);
|
||||||
}
|
}
|
||||||
|
// if isDataURL or if it's a valid local file url
|
||||||
if (isDataURL(group.avatar_url)) {
|
if (isValidImageUrl(group.avatar_url)) {
|
||||||
return $(`<div class="avatar"><img src="${group.avatar_url}"></div>`);
|
return $(`<div class="avatar"><img src="${group.avatar_url}"></div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1079,8 +1090,7 @@ function select_group_chats(groupId, skipAnimation) {
|
|||||||
|
|
||||||
setMenuType(!!group ? 'group_edit' : 'group_create');
|
setMenuType(!!group ? 'group_edit' : 'group_create');
|
||||||
$("#group_avatar_preview").empty().append(getGroupAvatar(group));
|
$("#group_avatar_preview").empty().append(getGroupAvatar(group));
|
||||||
$("#rm_group_restore_avatar").toggle(!!group && isDataURL(group.avatar_url));
|
$("#rm_group_restore_avatar").toggle(!!group && isValidImageUrl(group.avatar_url));
|
||||||
$("#rm_group_chat_name").val(groupName);
|
|
||||||
$("#rm_group_filter").val("").trigger("input");
|
$("#rm_group_filter").val("").trigger("input");
|
||||||
$(`input[name="rm_group_activation_strategy"][value="${replyStrategy}"]`).prop('checked', true);
|
$(`input[name="rm_group_activation_strategy"][value="${replyStrategy}"]`).prop('checked', true);
|
||||||
|
|
||||||
@ -1125,6 +1135,15 @@ function select_group_chats(groupId, skipAnimation) {
|
|||||||
eventSource.emit('groupSelected', { detail: { id: openGroupId, group: group } });
|
eventSource.emit('groupSelected', { detail: { id: openGroupId, group: group } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the upload and processing of a group avatar.
|
||||||
|
* The selected image is read, cropped using a popup, processed into a thumbnail,
|
||||||
|
* and then uploaded to the server.
|
||||||
|
*
|
||||||
|
* @param {Event} event - The event triggered by selecting a file input, containing the image file to upload.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the processing and upload is complete.
|
||||||
|
*/
|
||||||
async function uploadGroupAvatar(event) {
|
async function uploadGroupAvatar(event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
|
|
||||||
@ -1147,16 +1166,22 @@ async function uploadGroupAvatar(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbnail = await createThumbnail(croppedImage, 96, 144);
|
let thumbnail = await createThumbnail(croppedImage, 96, 144);
|
||||||
|
//remove data:image/whatever;base64
|
||||||
|
thumbnail = thumbnail.replace(/^data:image\/[a-z]+;base64,/, "");
|
||||||
|
let _thisGroup = groups.find((x) => x.id == openGroupId);
|
||||||
|
// filename should be group id + human readable timestamp
|
||||||
|
const filename = `${_thisGroup.id}_${humanizedDateTime()}`;
|
||||||
|
let thumbnailUrl = await saveBase64AsFile(thumbnail, openGroupId.toString(), filename, 'jpg');
|
||||||
if (!openGroupId) {
|
if (!openGroupId) {
|
||||||
$('#group_avatar_preview img').attr('src', thumbnail);
|
$('#group_avatar_preview img').attr('src', thumbnailUrl);
|
||||||
$('#rm_group_restore_avatar').show();
|
$('#rm_group_restore_avatar').show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _thisGroup = groups.find((x) => x.id == openGroupId);
|
|
||||||
_thisGroup.avatar_url = thumbnail;
|
|
||||||
|
_thisGroup.avatar_url = thumbnailUrl;
|
||||||
$("#group_avatar_preview").empty().append(getGroupAvatar(_thisGroup));
|
$("#group_avatar_preview").empty().append(getGroupAvatar(_thisGroup));
|
||||||
$("#rm_group_restore_avatar").show();
|
$("#rm_group_restore_avatar").show();
|
||||||
await editGroup(openGroupId, true, true);
|
await editGroup(openGroupId, true, true);
|
||||||
@ -1303,7 +1328,7 @@ async function createGroup() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: name,
|
name: name,
|
||||||
members: members,
|
members: members,
|
||||||
avatar_url: isDataURL(avatar_url) ? avatar_url : default_avatar,
|
avatar_url: isValidImageUrl(avatar_url) ? avatar_url : default_avatar,
|
||||||
allow_self_responses: allow_self_responses,
|
allow_self_responses: allow_self_responses,
|
||||||
activation_strategy: activation_strategy,
|
activation_strategy: activation_strategy,
|
||||||
disabled_members: [],
|
disabled_members: [],
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getContext } from "./extensions.js";
|
import { getContext } from "./extensions.js";
|
||||||
|
import { getRequestHeaders } from "../script.js";
|
||||||
|
|
||||||
export function onlyUnique(value, index, array) {
|
export function onlyUnique(value, index, array) {
|
||||||
return array.indexOf(value) === index;
|
return array.indexOf(value) === index;
|
||||||
@ -554,6 +555,48 @@ export function extractDataFromPng(data, identifier = 'chara') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a base64 encoded image to the backend to be saved as a file.
|
||||||
|
*
|
||||||
|
* @param {string} base64Data - The base64 encoded image data.
|
||||||
|
* @param {string} characterName - The character name to determine the sub-directory for saving.
|
||||||
|
* @param {string} ext - The file extension for the image (e.g., 'jpg', 'png', 'webp').
|
||||||
|
*
|
||||||
|
* @returns {Promise<string>} - Resolves to the saved image's path on the server.
|
||||||
|
* Rejects with an error if the upload fails.
|
||||||
|
*/
|
||||||
|
export async function saveBase64AsFile(base64Data, characterName, filename = "", ext) {
|
||||||
|
// Construct the full data URL
|
||||||
|
const format = ext; // Extract the file extension (jpg, png, webp)
|
||||||
|
const dataURL = `data:image/${format};base64,${base64Data}`;
|
||||||
|
|
||||||
|
// Prepare the request body
|
||||||
|
const requestBody = {
|
||||||
|
image: dataURL,
|
||||||
|
ch_name: characterName,
|
||||||
|
filename: filename
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the data URL to your backend using fetch
|
||||||
|
const response = await fetch('/uploadimage', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
headers: {
|
||||||
|
...getRequestHeaders(),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the response is successful, get the saved image path from the server's response
|
||||||
|
if (response.ok) {
|
||||||
|
const responseData = await response.json();
|
||||||
|
return responseData.path;
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'Failed to upload the image to the server');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createThumbnail(dataUrl, maxWidth, maxHeight) {
|
export function createThumbnail(dataUrl, maxWidth, maxHeight) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
69
server.js
69
server.js
@ -297,6 +297,8 @@ const baseRequestArgs = { headers: { "Content-Type": "application/json" } };
|
|||||||
const directories = {
|
const directories = {
|
||||||
worlds: 'public/worlds/',
|
worlds: 'public/worlds/',
|
||||||
avatars: 'public/User Avatars',
|
avatars: 'public/User Avatars',
|
||||||
|
images: 'public/img/',
|
||||||
|
userImages: 'public/user/images/',
|
||||||
groups: 'public/groups/',
|
groups: 'public/groups/',
|
||||||
groupChats: 'public/group chats',
|
groupChats: 'public/group chats',
|
||||||
chats: 'public/chats/',
|
chats: 'public/chats/',
|
||||||
@ -2611,6 +2613,73 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the directory for the provided file path exists.
|
||||||
|
* If not, it will recursively create the directory.
|
||||||
|
*
|
||||||
|
* @param {string} filePath - The full path of the file for which the directory should be ensured.
|
||||||
|
*/
|
||||||
|
function ensureDirectoryExistence(filePath) {
|
||||||
|
const dirname = path.dirname(filePath);
|
||||||
|
if (fs.existsSync(dirname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ensureDirectoryExistence(dirname);
|
||||||
|
fs.mkdirSync(dirname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to handle image uploads.
|
||||||
|
* The image should be provided in the request body in base64 format.
|
||||||
|
* Optionally, a character name can be provided to save the image in a sub-folder.
|
||||||
|
*
|
||||||
|
* @route POST /uploadimage
|
||||||
|
* @param {Object} request.body - The request payload.
|
||||||
|
* @param {string} request.body.image - The base64 encoded image data.
|
||||||
|
* @param {string} [request.body.ch_name] - Optional character name to determine the sub-directory.
|
||||||
|
* @returns {Object} response - The response object containing the path where the image was saved.
|
||||||
|
*/
|
||||||
|
app.post('/uploadimage', jsonParser, async (request, response) => {
|
||||||
|
// Check for image data
|
||||||
|
if (!request.body || !request.body.image) {
|
||||||
|
return response.status(400).send({ error: "No image data provided" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracting the base64 data and the image format
|
||||||
|
const match = request.body.image.match(/^data:image\/(png|jpg|webp);base64,(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
return response.status(400).send({ error: "Invalid image format" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, format, base64Data] = match;
|
||||||
|
|
||||||
|
// Constructing filename and path
|
||||||
|
let filename = `${Date.now()}.${format}`;
|
||||||
|
if (request.body.filename) {
|
||||||
|
filename = `${request.body.filename}.${format}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if character is defined, save to a sub folder for that character
|
||||||
|
let pathToNewFile = path.join(directories.userImages, filename);
|
||||||
|
if (request.body.ch_name) {
|
||||||
|
pathToNewFile = path.join(directories.userImages, request.body.ch_name, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ensureDirectoryExistence(pathToNewFile);
|
||||||
|
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||||
|
await fs.promises.writeFile(pathToNewFile, imageBuffer);
|
||||||
|
// send the path to the image, relative to the client folder, which means removing the first folder from the path which is 'public'
|
||||||
|
pathToNewFile = pathToNewFile.split(path.sep).slice(1).join(path.sep);
|
||||||
|
response.send({ path: pathToNewFile });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response.status(500).send({ error: "Failed to save the image" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
app.post('/getgroups', jsonParser, (_, response) => {
|
app.post('/getgroups', jsonParser, (_, response) => {
|
||||||
const groups = [];
|
const groups = [];
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user