diff --git a/public/css/st-tailwind.css b/public/css/st-tailwind.css
index 20c68542a..ecc55c074 100644
--- a/public/css/st-tailwind.css
+++ b/public/css/st-tailwind.css
@@ -491,6 +491,10 @@ textarea:disabled {
font-size: calc(var(--mainFontSize) * 1.2) !important;
}
+.fontsize90p {
+ font-size: calc(var(--mainFontSize) * 0.9) !important;
+}
+
.fontsize80p {
font-size: calc(var(--mainFontSize) * 0.8) !important;
}
diff --git a/public/script.js b/public/script.js
index cf536b331..c9722756a 100644
--- a/public/script.js
+++ b/public/script.js
@@ -7361,47 +7361,6 @@ export function cancelTtsPlay() {
}
}
-async function deleteMessageImage() {
- const value = await callPopup('
Delete image from message?
This action can\'t be undone.
', 'confirm');
-
- if (!value) {
- return;
- }
-
- const mesBlock = $(this).closest('.mes');
- const mesId = mesBlock.attr('mesid');
- const message = chat[mesId];
- delete message.extra.image;
- delete message.extra.inline_image;
- mesBlock.find('.mes_img_container').removeClass('img_extra');
- mesBlock.find('.mes_img').attr('src', '');
- await saveChatConditional();
-}
-
-function enlargeMessageImage() {
- const mesBlock = $(this).closest('.mes');
- const mesId = mesBlock.attr('mesid');
- const message = chat[mesId];
- const imgSrc = message?.extra?.image;
- const title = message?.extra?.title;
-
- if (!imgSrc) {
- return;
- }
-
- const img = document.createElement('img');
- img.classList.add('img_enlarged');
- img.src = imgSrc;
- const imgContainer = $('');
- imgContainer.prepend(img);
- imgContainer.addClass('img_enlarged_container');
- imgContainer.find('code').addClass('txt').text(title);
- const titleEmpty = !title || title.trim().length === 0;
- imgContainer.find('pre').toggle(!titleEmpty);
- addCopyToCodeBlocks(imgContainer);
- callPopup(imgContainer, 'text', '', { wide: true, large: true });
-}
-
function updateAlternateGreetingsHintVisibility(root) {
const numberOfGreetings = root.find('.alternate_greetings_list .alternate_greeting').length;
$(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
@@ -10392,9 +10351,6 @@ jQuery(async function () {
$('#char-management-dropdown').prop('selectedIndex', 0);
});
- $(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
- $(document).on('click', '.mes_img_delete', deleteMessageImage);
-
$(window).on('beforeunload', () => {
cancelTtsPlay();
if (streamingProcessor) {
diff --git a/public/scripts/chats.js b/public/scripts/chats.js
index 0b37e9345..3800f64e5 100644
--- a/public/scripts/chats.js
+++ b/public/scripts/chats.js
@@ -18,6 +18,8 @@ import {
saveSettingsDebounced,
showSwipeButtons,
this_chid,
+ saveChatConditional,
+ chat_metadata,
} from '../script.js';
import { selected_group } from './group-chats.js';
import { power_user } from './power-user.js';
@@ -29,9 +31,25 @@ import {
getStringHash,
humanFileSize,
saveBase64AsFile,
+ isValidUrl,
} from './utils.js';
+import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced, writeExtensionField } from './extensions.js';
+import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
+
+/**
+ * @typedef {Object} FileAttachment
+ * @property {string} url File URL
+ * @property {number} size File size
+ * @property {string} name File name
+ * @property {string} [text] File text
+ */
const fileSizeLimit = 1024 * 1024 * 10; // 10 MB
+const ATTACHMENT_SOURCE = {
+ GLOBAL: 'global',
+ CHAT: 'chat',
+ CHARACTER: 'character',
+};
const converters = {
'application/pdf': extractTextFromPDF,
@@ -39,6 +57,11 @@ const converters = {
'text/markdown': extractTextFromMarkdown,
};
+/**
+ * Determines if the file type has a converter function.
+ * @param {string} type MIME type
+ * @returns {boolean} True if the file type is convertible, false otherwise.
+ */
function isConvertible(type) {
return Object.keys(converters).includes(type);
}
@@ -275,9 +298,9 @@ async function onFileAttach() {
* @param {number} messageId Message ID
*/
async function deleteMessageFile(messageId) {
- const confirm = await callPopup('Are you sure you want to delete this file?', 'confirm');
+ const confirm = await callGenericPopup('Are you sure you want to delete this file?', POPUP_TYPE.CONFIRM);
- if (!confirm) {
+ if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
console.debug('Delete file cancelled');
return;
}
@@ -289,11 +312,15 @@ async function deleteMessageFile(messageId) {
return;
}
+ const url = message.extra.file.url;
+
delete message.extra.file;
$(`.mes[mesid="${messageId}"] .mes_file_container`).remove();
- saveChatDebounced();
+ await saveChatConditional();
+ await deleteFileFromServer(url);
}
+
/**
* Opens file from message in a modal.
* @param {number} messageId Message ID
@@ -306,14 +333,7 @@ async function viewMessageFile(messageId) {
return;
}
- const fileText = messageFile.text || (await getFileAttachment(messageFile.url));
-
- const modalTemplate = $('');
- modalTemplate.find('code').addClass('txt').text(fileText);
- modalTemplate.addClass('file_modal');
- addCopyToCodeBlocks(modalTemplate);
-
- callPopup(modalTemplate, 'text', '', { wide: true, large: true });
+ await openFilePopup(messageFile);
}
/**
@@ -348,7 +368,7 @@ function embedMessageFile(messageId, messageBlock) {
await populateFileAttachment(message, 'embed_file_input');
appendMediaToMessage(message, messageBlock);
- saveChatDebounced();
+ await saveChatConditional();
}
}
@@ -476,6 +496,366 @@ export function isExternalMediaAllowed() {
return !power_user.forbid_external_images;
}
+function enlargeMessageImage() {
+ const mesBlock = $(this).closest('.mes');
+ const mesId = mesBlock.attr('mesid');
+ const message = chat[mesId];
+ const imgSrc = message?.extra?.image;
+ const title = message?.extra?.title;
+
+ if (!imgSrc) {
+ return;
+ }
+
+ const img = document.createElement('img');
+ img.classList.add('img_enlarged');
+ img.src = imgSrc;
+ const imgContainer = $('');
+ imgContainer.prepend(img);
+ imgContainer.addClass('img_enlarged_container');
+ imgContainer.find('code').addClass('txt').text(title);
+ const titleEmpty = !title || title.trim().length === 0;
+ imgContainer.find('pre').toggle(!titleEmpty);
+ addCopyToCodeBlocks(imgContainer);
+ callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true });
+}
+
+async function deleteMessageImage() {
+ const value = await callGenericPopup('Delete image from message?
This action can\'t be undone.
', POPUP_TYPE.CONFIRM);
+
+ if (value !== POPUP_RESULT.AFFIRMATIVE) {
+ return;
+ }
+
+ const mesBlock = $(this).closest('.mes');
+ const mesId = mesBlock.attr('mesid');
+ const message = chat[mesId];
+ delete message.extra.image;
+ delete message.extra.inline_image;
+ mesBlock.find('.mes_img_container').removeClass('img_extra');
+ mesBlock.find('.mes_img').attr('src', '');
+ await saveChatConditional();
+}
+
+/**
+ * Deletes file from the server.
+ * @param {string} url Path to the file on the server
+ * @returns {Promise} True if file was deleted, false otherwise.
+ */
+async function deleteFileFromServer(url) {
+ try {
+ const result = await fetch('/api/files/delete', {
+ method: 'POST',
+ headers: getRequestHeaders(),
+ body: JSON.stringify({ path: url }),
+ });
+
+ if (!result.ok) {
+ const error = await result.text();
+ throw new Error(error);
+ }
+
+ return true;
+ } catch (error) {
+ toastr.error(String(error), 'Could not delete file');
+ console.error('Could not delete file', error);
+ return false;
+ }
+}
+
+/**
+ * Opens file attachment in a modal.
+ * @param {FileAttachment} attachment File attachment
+ */
+async function openFilePopup(attachment) {
+ const fileText = attachment.text || (await getFileAttachment(attachment.url));
+
+ const modalTemplate = $('');
+ modalTemplate.find('code').addClass('txt').text(fileText);
+ modalTemplate.addClass('file_modal').addClass('textarea_compact').addClass('fontsize90p');
+ addCopyToCodeBlocks(modalTemplate);
+
+ callGenericPopup(modalTemplate, POPUP_TYPE.TEXT, '', { wide: true, large: true });
+}
+
+/**
+ * Deletes an attachment from the server and the chat.
+ * @param {FileAttachment} attachment Attachment to delete
+ * @param {string} source Source of the attachment
+ * @param {function} callback Callback function
+ * @returns {Promise} A promise that resolves when the attachment is deleted.
+ */
+async function deleteAttachment(attachment, source, callback) {
+ const confirm = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM);
+
+ if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
+ return;
+ }
+
+ ensureAttachmentsExist();
+
+ switch (source) {
+ case 'global':
+ extension_settings.attachments = extension_settings.attachments.filter((a) => a.url !== attachment.url);
+ saveSettingsDebounced();
+ break;
+ case 'chat':
+ chat_metadata.attachments = chat_metadata.attachments.filter((a) => a.url !== attachment.url);
+ saveMetadataDebounced();
+ break;
+ case 'character':
+ characters[this_chid].data.extensions.attachments = characters[this_chid].data.extensions.attachments.filter((a) => a.url !== attachment.url);
+ await writeExtensionField(this_chid, 'attachments', characters[this_chid].data.extensions.attachments);
+ break;
+ }
+
+ await deleteFileFromServer(attachment.url);
+ callback();
+}
+
+/**
+ * Opens the attachment manager.
+ */
+async function openAttachmentManager() {
+ /**
+ *
+ * @param {FileAttachment[]} attachments List of attachments
+ * @param {string} source Source of the attachments
+ */
+ async function renderList(attachments, source) {
+ const sources = {
+ [ATTACHMENT_SOURCE.GLOBAL]: '.globalAttachmentsList',
+ [ATTACHMENT_SOURCE.CHARACTER]: '.characterAttachmentsList',
+ [ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsList',
+ };
+
+ template.find(sources[source]).empty();
+ for (const attachment of attachments) {
+ const attachmentTemplate = template.find('.attachmentListItemTemplate .attachmentListItem').clone();
+ attachmentTemplate.find('.attachmentListItemName').text(attachment.name);
+ attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size));
+ attachmentTemplate.find('.viewAttachmentButton').on('click', () => openFilePopup(attachment));
+ attachmentTemplate.find('.deleteAttachmentButton').on('click', () => deleteAttachment(attachment, source, renderAttachments));
+ template.find(sources[source]).append(attachmentTemplate);
+ }
+ }
+
+ async function renderAttachments() {
+ /** @type {FileAttachment[]} */
+ const globalAttachments = extension_settings.attachments ?? [];
+ /** @type {FileAttachment[]} */
+ const chatAttachments = chat_metadata.attachments ?? [];
+ /** @type {FileAttachment[]} */
+ const characterAttachments = characters[this_chid]?.data?.extensions?.attachments ?? [];
+
+ await renderList(globalAttachments, ATTACHMENT_SOURCE.GLOBAL);
+ await renderList(chatAttachments, ATTACHMENT_SOURCE.CHAT);
+ await renderList(characterAttachments, ATTACHMENT_SOURCE.CHARACTER);
+
+ const isNotCharacter = this_chid === undefined || selected_group;
+ const isNotInChat = getCurrentChatId() === undefined;
+ template.find('.characterAttachmentsBlock').toggle(!isNotCharacter);
+ template.find('.chatAttachmentsBlock').toggle(!isNotInChat);
+ }
+
+ const hasFandomPlugin = await isFandomPluginAvailable();
+ const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
+ template.find('.scrapeWebpageButton').on('click', function () {
+ openWebpageScraper(String($(this).data('attachment-manager-target')), renderAttachments);
+ });
+ template.find('.scrapeFandomButton').toggle(hasFandomPlugin).on('click', function () {
+ openFandomScraper(String($(this).data('attachment-manager-target')), renderAttachments);
+ });
+ template.find('.uploadFileButton').on('click', function () {
+ openFileUploader(String($(this).data('attachment-manager-target')), renderAttachments);
+ });
+ await renderAttachments();
+ callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
+}
+
+/**
+ * Scrapes a webpage for attachments.
+ * @param {string} target Target for the attachment
+ * @param {function} callback Callback function
+ */
+async function openWebpageScraper(target, callback) {
+ const template = $(await renderExtensionTemplateAsync('attachments', 'web-scrape', {}));
+ const link = await callGenericPopup(template, POPUP_TYPE.INPUT, '', { wide: false, large: false });
+
+ if (!link) {
+ return;
+ }
+
+ try {
+ if (!isValidUrl(link)) {
+ toastr.error('Invalid URL');
+ return;
+ }
+
+ const result = await fetch('/api/serpapi/visit', {
+ method: 'POST',
+ headers: getRequestHeaders(),
+ body: JSON.stringify({ url: link }),
+ });
+
+ const blob = await result.blob();
+ const domain = new URL(link).hostname;
+ const timestamp = Date.now();
+ const title = await getTitleFromHtmlBlob(blob) || 'webpage';
+ const file = new File([blob], `${title} - ${domain} - ${timestamp}.html`, { type: 'text/html' });
+ await uploadFileAttachmentToServer(file, target);
+ callback();
+ } catch (error) {
+ console.error('Scraping failed', error);
+ toastr.error('Check browser console for details.', 'Scraping failed');
+ }
+}
+
+/**
+ *
+ * @param {Blob} blob Blob of the HTML file
+ * @returns {Promise} Title of the HTML file
+ */
+async function getTitleFromHtmlBlob(blob) {
+ const text = await blob.text();
+ const titleMatch = text.match(/(.*?)<\/title>/i);
+ return titleMatch ? titleMatch[1] : '';
+}
+
+/**
+ * Scrapes a Fandom page for attachments.
+ * @param {string} target Target for the attachment
+ * @param {function} callback Callback function
+ */
+async function openFandomScraper(target, callback) {
+ toastr.info('Not implemented yet', target);
+ callback();
+}
+
+/**
+ * Uploads a file attachment.
+ * @param {string} target File upload target
+ * @param {function} callback Callback function
+ */
+async function openFileUploader(target, callback) {
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = '.txt, .md, .pdf, .html, .htm';
+ fileInput.onchange = async function () {
+ const file = fileInput.files[0];
+ if (!file) return;
+
+ await uploadFileAttachmentToServer(file, target);
+
+ callback();
+ };
+
+ fileInput.click();
+}
+
+/**
+ * Uploads a file attachment to the server.
+ * @param {File} file File to upload
+ * @param {string} target Target for the attachment
+ * @returns
+ */
+async function uploadFileAttachmentToServer(file, target) {
+ const isValid = await validateFile(file);
+
+ if (!isValid) {
+ return;
+ }
+
+ let base64Data = await getBase64Async(file);
+ const slug = getStringHash(file.name);
+ const uniqueFileName = `${Date.now()}_${slug}.txt`;
+
+ if (isConvertible(file.type)) {
+ try {
+ const converter = converters[file.type];
+ const fileText = await converter(file);
+ base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
+ } catch (error) {
+ toastr.error(String(error), 'Could not convert file');
+ console.error('Could not convert file', error);
+ }
+ } else {
+ const fileText = await file.text();
+ base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
+ }
+
+ const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data);
+
+ if (!fileUrl) {
+ return;
+ }
+
+ const attachment = {
+ url: fileUrl,
+ size: file.size,
+ name: file.name,
+ };
+
+ ensureAttachmentsExist();
+
+ switch (target) {
+ case ATTACHMENT_SOURCE.GLOBAL:
+ extension_settings.attachments.push(attachment);
+ saveSettingsDebounced();
+ break;
+ case ATTACHMENT_SOURCE.CHAT:
+ chat_metadata.attachments.push(attachment);
+ saveMetadataDebounced();
+ break;
+ case ATTACHMENT_SOURCE.CHARACTER:
+ characters[this_chid].data.extensions.attachments.push(attachment);
+ await writeExtensionField(this_chid, 'attachments', characters[this_chid].data.extensions.attachments);
+ break;
+ }
+}
+
+function ensureAttachmentsExist() {
+ if (!Array.isArray(extension_settings.attachments)) {
+ extension_settings.attachments = [];
+ }
+
+ if (!Array.isArray(chat_metadata.attachments)) {
+ chat_metadata.attachments = [];
+ }
+
+ if (this_chid !== undefined && characters[this_chid]) {
+ if (!characters[this_chid].data) {
+ characters[this_chid].data = {};
+ }
+
+ if (!characters[this_chid].data.extensions) {
+ characters[this_chid].data.extensions = {};
+ }
+
+ if (!Array.isArray(characters[this_chid]?.data?.extensions?.attachments)) {
+ characters[this_chid].data.extensions.attachments = [];
+ }
+ }
+}
+
+/**
+ * Probes the server to check if the Fandom plugin is available.
+ * @returns {Promise} True if the plugin is available, false otherwise.
+ */
+async function isFandomPluginAvailable() {
+ try {
+ const result = await fetch('/api/plugins/fandom/probe', {
+ method: 'POST',
+ headers: getRequestHeaders(),
+ });
+
+ return result.ok;
+ } catch (error) {
+ console.debug('Could not probe Fandom plugin', error);
+ return false;
+ }
+}
+
jQuery(function () {
$(document).on('click', '.mes_hide', async function () {
const messageBlock = $(this).closest('.mes');
@@ -506,6 +886,11 @@ jQuery(function () {
$('#file_form_input').trigger('click');
});
+ // Do not change. #manageAttachments is added by extension.
+ $(document).on('click', '#manageAttachments', function () {
+ openAttachmentManager();
+ });
+
$(document).on('click', '.mes_embed', function () {
const messageBlock = $(this).closest('.mes');
const messageId = Number(messageBlock.attr('mesid'));
@@ -597,6 +982,9 @@ jQuery(function () {
reloadCurrentChat();
});
+ $(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
+ $(document).on('click', '.mes_img_delete', deleteMessageImage);
+
$('#file_form_input').on('change', onFileAttach);
$('#file_form').on('reset', function () {
$('#file_form').addClass('displayNone');
diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js
index 26e9e58dd..4dc83b9a6 100644
--- a/public/scripts/extensions.js
+++ b/public/scripts/extensions.js
@@ -145,6 +145,7 @@ const extension_settings = {
variables: {
global: {},
},
+ attachments: [],
};
let modules = [];
diff --git a/public/scripts/extensions/attachments/buttons.html b/public/scripts/extensions/attachments/buttons.html
new file mode 100644
index 000000000..0e9adde85
--- /dev/null
+++ b/public/scripts/extensions/attachments/buttons.html
@@ -0,0 +1,9 @@
+
+
+ Attach a File
+
+
+
+
+ Open Data Bank
+
diff --git a/public/scripts/extensions/attachments/index.js b/public/scripts/extensions/attachments/index.js
new file mode 100644
index 000000000..a7f58dbc7
--- /dev/null
+++ b/public/scripts/extensions/attachments/index.js
@@ -0,0 +1,6 @@
+import { renderExtensionTemplateAsync } from '../../extensions.js';
+
+jQuery(async () => {
+ const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
+ $('#extensionsMenu').prepend(buttons);
+});
diff --git a/public/scripts/extensions/attachments/manager.html b/public/scripts/extensions/attachments/manager.html
new file mode 100644
index 000000000..3d5725743
--- /dev/null
+++ b/public/scripts/extensions/attachments/manager.html
@@ -0,0 +1,116 @@
+
+
+
+ Data Bank
+
+
+
+ These files will be available for extensions that support attachments (e.g. Vector Storage).
+
+
+ Supported file types: Plain Text, PDF, Markdown, HTML.
+
+
+
+
+ Global Attachments
+
+
+
+
+
+
+ From Fandom
+
+
+
+
+
+
+ These files are available for all characters in all chats.
+
+
+
+
+
+
+ Character Attachments
+
+
+
+
+
+ From Fandom
+
+
+
+
+
+
+ These files are available the current character in all chats they are in.
+
+
+
+
+
+
+
+ Chat Attachments
+
+
+
+
+
+
+ From Fandom
+
+
+
+
+
+
+ These files are available to all characters in the current chat.
+
+
+
+
+
+
diff --git a/public/scripts/extensions/attachments/manifest.json b/public/scripts/extensions/attachments/manifest.json
new file mode 100644
index 000000000..2037168c2
--- /dev/null
+++ b/public/scripts/extensions/attachments/manifest.json
@@ -0,0 +1,11 @@
+{
+ "display_name": "Chat Attachments",
+ "loading_order": 3,
+ "requires": [],
+ "optional": [],
+ "js": "index.js",
+ "css": "style.css",
+ "author": "Cohee1207",
+ "version": "1.0.0",
+ "homePage": "https://github.com/SillyTavern/SillyTavern"
+}
diff --git a/public/scripts/extensions/attachments/style.css b/public/scripts/extensions/attachments/style.css
new file mode 100644
index 000000000..f897a8283
--- /dev/null
+++ b/public/scripts/extensions/attachments/style.css
@@ -0,0 +1,20 @@
+.attachmentsList:empty {
+ width: 100%;
+ height: 100%;
+}
+
+.attachmentsList:empty::before {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ content: "No data";
+ font-weight: bolder;
+ width: 100%;
+ height: 100%;
+ opacity: 0.8;
+ min-height: 3rem;
+}
+
+.attachmentListItem {
+ padding: 10px;
+}
diff --git a/public/scripts/extensions/attachments/web-scrape.html b/public/scripts/extensions/attachments/web-scrape.html
new file mode 100644
index 000000000..0cc57482a
--- /dev/null
+++ b/public/scripts/extensions/attachments/web-scrape.html
@@ -0,0 +1,3 @@
+
+ Enter a web address to scrape:
+
diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js
index 8534fd0f6..945afa04f 100644
--- a/public/scripts/extensions/caption/index.js
+++ b/public/scripts/extensions/caption/index.js
@@ -270,14 +270,8 @@ jQuery(function () {
Generate Caption
`);
- const attachFileButton = $(`
-
-
- Attach a File
-
`);
$('#extensionsMenu').prepend(sendButton);
- $('#extensionsMenu').prepend(attachFileButton);
$(sendButton).on('click', () => {
const hasCaptionModule =
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
diff --git a/src/endpoints/files.js b/src/endpoints/files.js
index d011ae2f8..1c66273bd 100644
--- a/src/endpoints/files.js
+++ b/src/endpoints/files.js
@@ -1,4 +1,5 @@
const path = require('path');
+const fs = require('fs');
const writeFileSyncAtomic = require('write-file-atomic').sync;
const express = require('express');
const router = express.Router();
@@ -24,6 +25,7 @@ router.post('/upload', jsonParser, async (request, response) => {
const pathToUpload = path.join(request.user.directories.files, request.body.name);
writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');
const url = clientRelativePath(request.user.directories.root, pathToUpload);
+ console.log(`Uploaded file: ${url} from ${request.user.profile.handle}`);
return response.send({ path: url });
} catch (error) {
console.log(error);
@@ -31,4 +33,28 @@ router.post('/upload', jsonParser, async (request, response) => {
}
});
+router.post('/delete', jsonParser, async (request, response) => {
+ try {
+ if (!request.body.path) {
+ return response.status(400).send('No path specified');
+ }
+
+ const pathToDelete = path.join(request.user.directories.root, request.body.path);
+ if (!pathToDelete.startsWith(request.user.directories.files)) {
+ return response.status(400).send('Invalid path');
+ }
+
+ if (!fs.existsSync(pathToDelete)) {
+ return response.status(404).send('File not found');
+ }
+
+ fs.rmSync(pathToDelete);
+ console.log(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
+ return response.sendStatus(200);
+ } catch (error) {
+ console.log(error);
+ return response.sendStatus(500);
+ }
+});
+
module.exports = { router };
diff --git a/src/endpoints/serpapi.js b/src/endpoints/serpapi.js
index 0fc01b490..faae11750 100644
--- a/src/endpoints/serpapi.js
+++ b/src/endpoints/serpapi.js
@@ -5,10 +5,10 @@ const { jsonParser } = require('../express-common');
const router = express.Router();
-// Cosplay as Firefox
+// Cosplay as Chrome
const visitHeaders = {
'Accept': 'text/html',
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0',
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',