Implement persona thumbnails (#4210) * Implement persona thumbnails * Dear Firefox, fix your overzealous image cache * Add cache busting for avatar uploads when overwriting existing files

cdefddef6c0f7fe4655619204429f0276b9a2b28

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
9 files changed, +92 -22Showing whitespace changes
default/config.yaml+1 -1
@@ -139,7 +139,7 @@ thumbnails:
139 # JPG thumbnail quality (0-100)139 # JPG thumbnail quality (0-100)
140 quality: 95140 quality: 95
141 # Maximum thumbnail dimensions per type [width, height]141 # Maximum thumbnail dimensions per type [width, height]
142 dimensions: { 'bg': [160, 90], 'avatar': [96, 144] }142 dimensions: { 'bg': [160, 90], 'avatar': [96, 144], 'persona': [96, 144] }
143143
144# PERFORMANCE-RELATED CONFIGURATION144# PERFORMANCE-RELATED CONFIGURATION
145performance:145performance:
public/script.js+22 -7
@@ -2598,7 +2598,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2598 mes.swipes = [mes.mes];2598 mes.swipes = [mes.mes];
2599 }2599 }
26002600
2601 let avatarImg = getUserAvatar(user_avatar);2601 let avatarImg = getThumbnailUrl('persona', user_avatar);
2602 const isSystem = mes.is_system;2602 const isSystem = mes.is_system;
2603 const title = mes.title;2603 const title = mes.title;
2604 generatedPromptCache = '';2604 generatedPromptCache = '';
@@ -5575,7 +5575,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
55755575
5576 // Lock user avatar to a persona.5576 // Lock user avatar to a persona.
5577 if (avatar in power_user.personas) {5577 if (avatar in power_user.personas) {
5578 message.force_avatar = getUserAvatar(avatar);5578 message.force_avatar = getThumbnailUrl('persona', avatar);
5579 }5579 }
55805580
5581 if (messageBias) {5581 if (messageBias) {
@@ -7204,7 +7204,7 @@ async function read_avatar_load(input) {
7204 await delay(DEFAULT_SAVE_EDIT_TIMEOUT);7204 await delay(DEFAULT_SAVE_EDIT_TIMEOUT);
72057205
7206 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));7206 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));
7207 await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {7207 await fetch(getThumbnailUrl('avatar', formData.get('avatar_url').toString()), {
7208 method: 'GET',7208 method: 'GET',
7209 cache: 'no-cache',7209 cache: 'no-cache',
7210 headers: {7210 headers: {
@@ -7233,8 +7233,15 @@ async function read_avatar_load(input) {
7233 }7233 }
7234}7234}
72357235
7236export function getThumbnailUrl(type, file) {7236/**
7237 return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;7237 * Gets the URL for a thumbnail of a specific type and file.
7238 * @param {import('../src/endpoints/thumbnails.js').ThumbnailType} type The type of the thumbnail to get
7239 * @param {string} file The file name or path for which to get the thumbnail URL
7240 * @param {boolean} [t=false] Whether to add a cache-busting timestamp to the URL
7241 * @returns {string} The URL for the thumbnail
7242 */
7243export function getThumbnailUrl(type, file, t = false) {
7244 return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}${t ? `&t=${Date.now()}` : ''}`;
7238}7245}
72397246
7240export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) {7247export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) {
@@ -7274,7 +7281,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
7274 }7281 }
7275 else if (entity.type === 'persona') {7282 else if (entity.type === 'persona') {
7276 avatarTemplate.attr({ 'data-pid': id, 'data-chid': null });7283 avatarTemplate.attr({ 'data-pid': id, 'data-chid': null });
7277 avatarTemplate.find('img').attr('src', getUserAvatar(entity.item.avatar));7284 avatarTemplate.find('img').attr('src', getThumbnailUrl('persona', entity.item.avatar));
7278 avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`);7285 avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`);
7279 }7286 }
72807287
@@ -12147,9 +12154,17 @@ jQuery(async function () {
12147 $('body').append(newElement);12154 $('body').append(newElement);
12148 newElement.fadeIn(animation_duration);12155 newElement.fadeIn(animation_duration);
12149 const zoomedAvatarImgElement = $(`.zoomed_avatar[forChar="${charname}"] img`);12156 const zoomedAvatarImgElement = $(`.zoomed_avatar[forChar="${charname}"] img`);
12150 if (messageElement.attr('is_user') == 'true' || (messageElement.attr('is_system') == 'true' && !isValidCharacter)) { //handle user and system avatars12157 if (messageElement.attr('is_user') == 'true' || (messageElement.attr('is_system') == 'true' && !isValidCharacter)) {
12158 //handle user and system avatars
12159 const isValidPersona = decodeURIComponent(targetAvatarImg) in power_user.personas;
12160 if (isValidPersona) {
12161 const personaSrc = getUserAvatar(targetAvatarImg);
12162 zoomedAvatarImgElement.attr('src', personaSrc);
12163 zoomedAvatarImgElement.attr('data-izoomify-url', personaSrc);
12164 } else {
12151 zoomedAvatarImgElement.attr('src', thumbURL);12165 zoomedAvatarImgElement.attr('src', thumbURL);
12152 zoomedAvatarImgElement.attr('data-izoomify-url', thumbURL);12166 zoomedAvatarImgElement.attr('data-izoomify-url', thumbURL);
12167 }
12153 } else if (messageElement.attr('is_user') == 'false') { //handle char avatars12168 } else if (messageElement.attr('is_user') == 'false') { //handle char avatars
12154 zoomedAvatarImgElement.attr('src', avatarSrc);12169 zoomedAvatarImgElement.attr('src', avatarSrc);
12155 zoomedAvatarImgElement.attr('data-izoomify-url', avatarSrc);12170 zoomedAvatarImgElement.attr('data-izoomify-url', avatarSrc);
public/scripts/data-maid.js+4 -0
@@ -45,6 +45,10 @@ class DataMaidDialog {
45 name: t`Background Thumbnails`,45 name: t`Background Thumbnails`,
46 description: t`Thumbnails for missing or deleted backgrounds.`,46 description: t`Thumbnails for missing or deleted backgrounds.`,
47 },47 },
48 personaThumbnails: {
49 name: t`Persona Thumbnails`,
50 description: t`Thumbnails for missing or deleted personas.`,
51 },
48 chatBackups: {52 chatBackups: {
49 name: t`Chat Backups`,53 name: t`Chat Backups`,
50 description: t`Automatically generated chat backups.`,54 description: t`Automatically generated chat backups.`,
public/scripts/personas.js+5 -7
@@ -37,6 +37,7 @@ import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from '
37import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';37import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
38import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';38import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
39import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';39import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
40import { isFirefox } from './browser-fixes.js';
4041
41/**42/**
42 * @typedef {object} PersonaConnection A connection between a character and a character or group entity43 * @typedef {object} PersonaConnection A connection between a character and a character or group entity
@@ -124,7 +125,7 @@ function reloadUserAvatar(force = false) {
124 }125 }
125126
126 if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') {127 if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') {
127 avatarImg.attr('src', getUserAvatar(user_avatar));128 avatarImg.attr('src', getThumbnailUrl('persona', user_avatar));
128 }129 }
129 });130 });
130}131}
@@ -179,7 +180,6 @@ function verifyPersonaSearchSortRule() {
179 * @returns {JQuery<HTMLElement>} Avatar block180 * @returns {JQuery<HTMLElement>} Avatar block
180 */181 */
181function getUserAvatarBlock(avatarId) {182function getUserAvatarBlock(avatarId) {
182 const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
183 const template = $('#user_avatar_template .avatar-container').clone();183 const template = $('#user_avatar_template .avatar-container').clone();
184 const personaName = power_user.personas[avatarId];184 const personaName = power_user.personas[avatarId];
185 const personaDescription = power_user.persona_descriptions[avatarId]?.description;185 const personaDescription = power_user.persona_descriptions[avatarId]?.description;
@@ -189,10 +189,7 @@ function getUserAvatarBlock(avatarId) {
189 template.attr('data-avatar-id', avatarId);189 template.attr('data-avatar-id', avatarId);
190 template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId);190 template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId);
191 template.toggleClass('default_persona', avatarId === power_user.default_persona);191 template.toggleClass('default_persona', avatarId === power_user.default_persona);
192 let avatarUrl = getUserAvatar(avatarId);192 const avatarUrl = getThumbnailUrl('persona', avatarId, isFirefox());
193 if (isFirefox) {
194 avatarUrl += '?t=' + Date.now();
195 }
196 template.find('img').attr('src', avatarUrl);193 template.find('img').attr('src', avatarUrl);
197194
198 // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this.195 // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this.
@@ -386,6 +383,7 @@ async function changeUserAvatar(e) {
386 const name = formData.get('overwrite_name');383 const name = formData.get('overwrite_name');
387 if (name) {384 if (name) {
388 await fetch(getUserAvatar(String(name)), { cache: 'no-cache' });385 await fetch(getUserAvatar(String(name)), { cache: 'no-cache' });
386 await fetch(getThumbnailUrl('persona', String(name)), { cache: 'no-cache' });
389 reloadUserAvatar(true);387 reloadUserAvatar(true);
390 }388 }
391389
@@ -1657,7 +1655,7 @@ async function syncUserNameToPersona() {
1657 for (const mes of chat) {1655 for (const mes of chat) {
1658 if (mes.is_user) {1656 if (mes.is_user) {
1659 mes.name = name1;1657 mes.name = name1;
1660 mes.force_avatar = getUserAvatar(user_avatar);1658 mes.force_avatar = getThumbnailUrl('persona', user_avatar);
1661 }1659 }
1662 }1660 }
16631661
src/constants.js+1 -0
@@ -18,6 +18,7 @@ export const USER_DIRECTORY_TEMPLATE = Object.freeze({
18 thumbnails: 'thumbnails',18 thumbnails: 'thumbnails',
19 thumbnailsBg: 'thumbnails/bg',19 thumbnailsBg: 'thumbnails/bg',
20 thumbnailsAvatar: 'thumbnails/avatar',20 thumbnailsAvatar: 'thumbnails/avatar',
21 thumbnailsPersona: 'thumbnails/persona',
21 worlds: 'worlds',22 worlds: 'worlds',
22 user: 'user',23 user: 'user',
23 avatars: 'User Avatars',24 avatars: 'User Avatars',
src/endpoints/avatars.js+10 -2
@@ -9,12 +9,13 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
9import { getImages, tryParse } from '../util.js';9import { getImages, tryParse } from '../util.js';
10import { getFileNameValidationFunction } from '../middleware/validateFileName.js';10import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
11import { applyAvatarCropResize } from './characters.js';11import { applyAvatarCropResize } from './characters.js';
12import { invalidateThumbnail } from './thumbnails.js';
1213
13export const router = express.Router();14export const router = express.Router();
1415
15router.post('/get', function (request, response) {16router.post('/get', function (request, response) {
16 var images = getImages(request.user.directories.avatars);17 const images = getImages(request.user.directories.avatars);
17 response.send(JSON.stringify(images));18 response.send(images);
18});19});
1920
20router.post('/delete', getFileNameValidationFunction('avatar'), function (request, response) {21router.post('/delete', getFileNameValidationFunction('avatar'), function (request, response) {
@@ -29,6 +30,7 @@ router.post('/delete', getFileNameValidationFunction('avatar'), function (reques
2930
30 if (fs.existsSync(fileName)) {31 if (fs.existsSync(fileName)) {
31 fs.unlinkSync(fileName);32 fs.unlinkSync(fileName);
33 invalidateThumbnail(request.user.directories, 'persona', sanitize(request.body.avatar));
32 return response.send({ result: 'ok' });34 return response.send({ result: 'ok' });
33 }35 }
3436
@@ -44,6 +46,12 @@ router.post('/upload', getFileNameValidationFunction('overwrite_name'), async (r
44 const rawImg = await Jimp.read(pathToUpload);46 const rawImg = await Jimp.read(pathToUpload);
45 const image = await applyAvatarCropResize(rawImg, crop);47 const image = await applyAvatarCropResize(rawImg, crop);
4648
49 // Remove previous thumbnail and bust cache if overwriting
50 if (request.body.overwrite_name) {
51 invalidateThumbnail(request.user.directories, 'persona', sanitize(request.body.overwrite_name));
52 response.setHeader('Clear-Site-Data', '"cache"');
53 }
54
47 const filename = request.body.overwrite_name || `${Date.now()}.png`;55 const filename = request.body.overwrite_name || `${Date.now()}.png`;
48 const pathToNewFile = path.join(request.user.directories.avatars, filename);56 const pathToNewFile = path.join(request.user.directories.avatars, filename);
49 writeFileAtomicSync(pathToNewFile, image);57 writeFileAtomicSync(pathToNewFile, image);
src/endpoints/data-maid.js+32 -0
@@ -18,6 +18,7 @@ const sha256 = str => crypto.createHash('sha256').update(str).digest('hex');
18 * @property {string[]} groupChats - List of loose group chats18 * @property {string[]} groupChats - List of loose group chats
19 * @property {string[]} avatarThumbnails - List of loose avatar thumbnails19 * @property {string[]} avatarThumbnails - List of loose avatar thumbnails
20 * @property {string[]} backgroundThumbnails - List of loose background thumbnails20 * @property {string[]} backgroundThumbnails - List of loose background thumbnails
21 * @property {string[]} personaThumbnails - List of loose persona thumbnails
21 * @property {string[]} chatBackups - List of chat backups22 * @property {string[]} chatBackups - List of chat backups
22 * @property {string[]} settingsBackups - List of settings backups23 * @property {string[]} settingsBackups - List of settings backups
23 */24 */
@@ -39,6 +40,7 @@ const sha256 = str => crypto.createHash('sha256').update(str).digest('hex');
39 * @property {DataMaidSanitizedRecord[]} groupChats - List of sanitized loose group chats40 * @property {DataMaidSanitizedRecord[]} groupChats - List of sanitized loose group chats
40 * @property {DataMaidSanitizedRecord[]} avatarThumbnails - List of sanitized loose avatar thumbnails41 * @property {DataMaidSanitizedRecord[]} avatarThumbnails - List of sanitized loose avatar thumbnails
41 * @property {DataMaidSanitizedRecord[]} backgroundThumbnails - List of sanitized loose background thumbnails42 * @property {DataMaidSanitizedRecord[]} backgroundThumbnails - List of sanitized loose background thumbnails
43 * @property {DataMaidSanitizedRecord[]} personaThumbnails - List of sanitized loose persona thumbnails
42 * @property {DataMaidSanitizedRecord[]} chatBackups - List of sanitized chat backups44 * @property {DataMaidSanitizedRecord[]} chatBackups - List of sanitized chat backups
43 * @property {DataMaidSanitizedRecord[]} settingsBackups - List of sanitized settings backups45 * @property {DataMaidSanitizedRecord[]} settingsBackups - List of sanitized settings backups
44 */46 */
@@ -106,6 +108,7 @@ export class DataMaidService {
106 groupChats: await this.#collectGroupChats(),108 groupChats: await this.#collectGroupChats(),
107 avatarThumbnails: await this.#collectAvatarThumbnails(),109 avatarThumbnails: await this.#collectAvatarThumbnails(),
108 backgroundThumbnails: await this.#collectBackgroundThumbnails(),110 backgroundThumbnails: await this.#collectBackgroundThumbnails(),
111 personaThumbnails: await this.#collectPersonaThumbnails(),
109 chatBackups: await this.#collectChatBackups(),112 chatBackups: await this.#collectChatBackups(),
110 settingsBackups: await this.#collectSettingsBackups(),113 settingsBackups: await this.#collectSettingsBackups(),
111 };114 };
@@ -145,6 +148,7 @@ export class DataMaidService {
145 groupChats: await Promise.all(report.groupChats.map(i => this.#sanitizeRecord(i, false))),148 groupChats: await Promise.all(report.groupChats.map(i => this.#sanitizeRecord(i, false))),
146 avatarThumbnails: await Promise.all(report.avatarThumbnails.map(i => this.#sanitizeRecord(i, false))),149 avatarThumbnails: await Promise.all(report.avatarThumbnails.map(i => this.#sanitizeRecord(i, false))),
147 backgroundThumbnails: await Promise.all(report.backgroundThumbnails.map(i => this.#sanitizeRecord(i, false))),150 backgroundThumbnails: await Promise.all(report.backgroundThumbnails.map(i => this.#sanitizeRecord(i, false))),
151 personaThumbnails: await Promise.all(report.personaThumbnails.map(i => this.#sanitizeRecord(i, false))),
148 chatBackups: await Promise.all(report.chatBackups.map(i => this.#sanitizeRecord(i, false))),152 chatBackups: await Promise.all(report.chatBackups.map(i => this.#sanitizeRecord(i, false))),
149 settingsBackups: await Promise.all(report.settingsBackups.map(i => this.#sanitizeRecord(i, false))),153 settingsBackups: await Promise.all(report.settingsBackups.map(i => this.#sanitizeRecord(i, false))),
150 };154 };
@@ -416,6 +420,34 @@ export class DataMaidService {
416 }420 }
417421
418 /**422 /**
423 * Collects loose persona thumbnails from the provided directories.
424 * @returns {Promise<string[]>} List of paths to loose persona thumbnails
425 */
426 async #collectPersonaThumbnails() {
427 const result = [];
428
429 try {
430 const knownPersonas = new Set();
431 const personas = await fs.promises.readdir(this.directories.avatars, { withFileTypes: true });
432 for (const file of personas) {
433 if (file.isFile()) {
434 knownPersonas.add(file.name);
435 }
436 }
437 const personaThumbnails = await fs.promises.readdir(this.directories.thumbnailsPersona, { withFileTypes: true });
438 for (const file of personaThumbnails) {
439 if (file.isFile() && !knownPersonas.has(file.name)) {
440 result.push(path.join(this.directories.thumbnailsPersona, file.name));
441 }
442 }
443 } catch (error) {
444 console.error('[Data Maid] Error collecting persona thumbnails:', error);
445 }
446
447 return result;
448 }
449
450 /**
419 * Collects chat backups from the provided directories.451 * Collects chat backups from the provided directories.
420 * @returns {Promise<string[]>} List of paths to chat backups452 * @returns {Promise<string[]>} List of paths to chat backups
421 */453 */
src/endpoints/thumbnails.js+16 -5
@@ -14,16 +14,21 @@ const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean'
14const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number'))));14const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number'))));
15const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png';15const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png';
1616
17/**
18 * @typedef {'bg' | 'avatar' | 'persona'} ThumbnailType
19 */
20
17/** @type {Record<string, number[]>} */21/** @type {Record<string, number[]>} */
18export const dimensions = {22export const dimensions = {
19 'bg': getConfigValue('thumbnails.dimensions.bg', [160, 90]),23 'bg': getConfigValue('thumbnails.dimensions.bg', [160, 90]),
20 'avatar': getConfigValue('thumbnails.dimensions.avatar', [96, 144]),24 'avatar': getConfigValue('thumbnails.dimensions.avatar', [96, 144]),
25 'persona': getConfigValue('thumbnails.dimensions.persona', [96, 144]),
21};26};
2227
23/**28/**
24 * Gets a path to thumbnail folder based on the type.29 * Gets a path to thumbnail folder based on the type.
25 * @param {import('../users.js').UserDirectoryList} directories User directories30 * @param {import('../users.js').UserDirectoryList} directories User directories
26 * @param {'bg' | 'avatar'} type Thumbnail type31 * @param {ThumbnailType} type Thumbnail type
27 * @returns {string} Path to the thumbnails folder32 * @returns {string} Path to the thumbnails folder
28 */33 */
29function getThumbnailFolder(directories, type) {34function getThumbnailFolder(directories, type) {
@@ -36,6 +41,9 @@ function getThumbnailFolder(directories, type) {
36 case 'avatar':41 case 'avatar':
37 thumbnailFolder = directories.thumbnailsAvatar;42 thumbnailFolder = directories.thumbnailsAvatar;
38 break;43 break;
44 case 'persona':
45 thumbnailFolder = directories.thumbnailsPersona;
46 break;
39 }47 }
4048
41 return thumbnailFolder;49 return thumbnailFolder;
@@ -44,7 +52,7 @@ function getThumbnailFolder(directories, type) {
44/**52/**
45 * Gets a path to the original images folder based on the type.53 * Gets a path to the original images folder based on the type.
46 * @param {import('../users.js').UserDirectoryList} directories User directories54 * @param {import('../users.js').UserDirectoryList} directories User directories
47 * @param {'bg' | 'avatar'} type Thumbnail type55 * @param {ThumbnailType} type Thumbnail type
48 * @returns {string} Path to the original images folder56 * @returns {string} Path to the original images folder
49 */57 */
50function getOriginalFolder(directories, type) {58function getOriginalFolder(directories, type) {
@@ -57,6 +65,9 @@ function getOriginalFolder(directories, type) {
57 case 'avatar':65 case 'avatar':
58 originalFolder = directories.characters;66 originalFolder = directories.characters;
59 break;67 break;
68 case 'persona':
69 originalFolder = directories.avatars;
70 break;
60 }71 }
6172
62 return originalFolder;73 return originalFolder;
@@ -65,7 +76,7 @@ function getOriginalFolder(directories, type) {
65/**76/**
66 * Removes the generated thumbnail from the disk.77 * Removes the generated thumbnail from the disk.
67 * @param {import('../users.js').UserDirectoryList} directories User directories78 * @param {import('../users.js').UserDirectoryList} directories User directories
68 * @param {'bg' | 'avatar'} type Type of the thumbnail79 * @param {ThumbnailType} type Type of the thumbnail
69 * @param {string} file Name of the file80 * @param {string} file Name of the file
70 */81 */
71export function invalidateThumbnail(directories, type, file) {82export function invalidateThumbnail(directories, type, file) {
@@ -82,7 +93,7 @@ export function invalidateThumbnail(directories, type, file) {
82/**93/**
83 * Generates a thumbnail for the given file.94 * Generates a thumbnail for the given file.
84 * @param {import('../users.js').UserDirectoryList} directories User directories95 * @param {import('../users.js').UserDirectoryList} directories User directories
85 * @param {'bg' | 'avatar'} type Type of the thumbnail96 * @param {ThumbnailType} type Type of the thumbnail
86 * @param {string} file Name of the file97 * @param {string} file Name of the file
87 * @returns98 * @returns
88 */99 */
@@ -188,7 +199,7 @@ router.get('/', async function (request, response) {
188 return response.sendStatus(400);199 return response.sendStatus(400);
189 }200 }
190201
191 if (!(type == 'bg' || type == 'avatar')) {202 if (!(type === 'bg' || type === 'avatar' || type === 'persona')) {
192 return response.sendStatus(400);203 return response.sendStatus(400);
193 }204 }
194205
src/users.js+1 -0
@@ -71,6 +71,7 @@ const STORAGE_KEYS = {
71 * @property {string} thumbnails - The directory where the thumbnails are stored71 * @property {string} thumbnails - The directory where the thumbnails are stored
72 * @property {string} thumbnailsBg - The directory where the background thumbnails are stored72 * @property {string} thumbnailsBg - The directory where the background thumbnails are stored
73 * @property {string} thumbnailsAvatar - The directory where the avatar thumbnails are stored73 * @property {string} thumbnailsAvatar - The directory where the avatar thumbnails are stored
74 * @property {string} thumbnailsPersona - The directory where the persona thumbnails are stored
74 * @property {string} worlds - The directory where the WI are stored75 * @property {string} worlds - The directory where the WI are stored
75 * @property {string} user - The directory where the user's public data is stored76 * @property {string} user - The directory where the user's public data is stored
76 * @property {string} avatars - The directory where the avatars are stored77 * @property {string} avatars - The directory where the avatars are stored