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<boolean>} 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 @@ +<div id="attachFile" class="list-group-item flex-container flexGap5" title="Attach a file or image to a current chat."> + <div class="fa-fw fa-solid fa-paperclip extensionsMenuExtensionButton"></div> + <span data-i18n="Attach a File">Attach a File</span> +</div> + +<div id="manageAttachments" class="list-group-item flex-container flexGap5" title="View global, character, or data files."> + <div class="fa-fw fa-solid fa-book-open-reader extensionsMenuExtensionButton"></div> + <span data-i18n="Open Data Bank">Open Data Bank</span> +</div> 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 @@ +<div class="wide100p paddingTopBot5"> + <h2 class="marginBot5"> + <span data-i18n="Data Bank"> + Data Bank + </span> + </h2> + <div data-i18n="These files will be available for extensions that support attachments (e.g. Vector Storage)."> + These files will be available for extensions that support attachments (e.g. Vector Storage). + </div> + <div data-i18n="Supported file types: Plain Text, PDF, Markdown, HTML." class="marginTopBot5"> + Supported file types: Plain Text, PDF, Markdown, HTML. + </div> + <div class="justifyLeft globalAttachmentsBlock marginBot10"> + <h3 class="margin0 title_restorable"> + <span data-i18n="Global Attachments"> + Global Attachments + </span> + <div class="flex-container flexGap10"> + <div class="scrapeWebpageButton menu_button_icon menu_button" data-attachment-manager-target="global" title="Download a page from the web."> + <i class="fa-fw fa-solid fa-globe"></i> + <span data-i18n="From Web"> + From Web + </span> + </div> + <div class="scrapeFandomButton menu_button_icon menu_button" data-attachment-manager-target="global" title="Download a page from the Fandom wiki."> + <i class="fa-fw fa-solid fa-fan"></i> + <span data-i18n="From Fandom"> + From Fandom + </span> + </div> + <div class="uploadFileButton menu_button_icon menu_button" data-attachment-manager-target="global" title="Upload a file from your computer."> + <i class="fa-fw fa-solid fa-upload"></i> + <span data-i18n="From File"> + From File + </span> + </div> + </div> + </h3> + <small data-i18n="These files are available for all characters in all chats."> + These files are available for all characters in all chats. + </small> + <div class="globalAttachmentsList attachmentsList"></div> + <hr> + </div> + <div class="justifyLeft characterAttachmentsBlock marginBot10"> + <h3 class="margin0 title_restorable"> + <span data-i18n="Character Attachments">Character Attachments</span> + <div class="flex-container flexGap10"> + <div class="scrapeWebpageButton menu_button_icon menu_button" data-attachment-manager-target="character" title="Download a page from the web."> + <i class="fa-fw fa-solid fa-globe"></i> + <span data-i18n="From Web"> + From Web + </span> + </div> + <div class="scrapeFandomButton menu_button_icon menu_button" data-attachment-manager-target="character" title="Download a page from the Fandom wiki."> + <i class="fa-fw fa-solid fa-fan"></i> + <span data-i18n="From Fandom"> + From Fandom + </span> + </div> + <div class="uploadFileButton menu_button_icon menu_button" data-attachment-manager-target="character" title="Upload a file from your computer."> + <i class="fa-fw fa-solid fa-upload"></i> + <span data-i18n="From File"> + From File + </span> + </div> + </div> + </h3> + <small data-i18n="These files are available the current character in all chats they are in."> + These files are available the current character in all chats they are in. + </small> + <div class="characterAttachmentsList attachmentsList"></div> + <hr> + </div> + <div class="justifyLeft chatAttachmentsBlock marginBot10"> + <h3 class="margin0 title_restorable"> + <span data-i18n="Chat Attachments"> + Chat Attachments + </span> + <div class="flex-container flexGap10"> + <div class="scrapeWebpageButton menu_button_icon menu_button" data-attachment-manager-target="chat" title="Download a page from the web."> + <i class="fa-fw fa-solid fa-globe"></i> + <span data-i18n="From Web"> + From Web + </span> + </div> + <div class="scrapeFandomButton menu_button_icon menu_button" data-attachment-manager-target="chat" title="Download a page from the Fandom wiki."> + <i class="fa-fw fa-solid fa-fan"></i> + <span data-i18n="From Fandom"> + From Fandom + </span> + </div> + <div class="uploadFileButton menu_button_icon menu_button" data-attachment-manager-target="chat" title="Upload a file from your computer."> + <i class="fa-fw fa-solid fa-upload"></i> + <span data-i18n="From File"> + From File + </span> + </div> + </div> + </h3> + <small data-i18n="These files are available to all characters in the current chat."> + These files are available to all characters in the current chat. + </small> + <div class="chatAttachmentsList attachmentsList"></div> + </div> + + <div class="attachmentListItemTemplate template_element"> + <div class="attachmentListItem flex-container alignItemsCenter flexGap10"> + <div class="attachmentFileIcon fa-solid fa-file-alt"></div> + <div class="attachmentListItemName flex1"></div> + <small class="attachmentListItemSize"></small> + <div class="viewAttachmentButton right_menu_button fa-solid fa-magnifying-glass" title="View attachment content"></div> + <div class="deleteAttachmentButton right_menu_button fa-solid fa-trash" title="Delete attachment"></div> + </div> + </div> +</div> 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 @@ +<div data-i18n="Enter a web address to scrape:"> + Enter a web address to scrape: +</div> 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 () { <div class="fa-solid fa-image extensionsMenuExtensionButton"></div> Generate Caption </div>`); - const attachFileButton = $(` - <div id="attachFile" class="list-group-item flex-container flexGap5"> - <div class="fa-solid fa-paperclip extensionsMenuExtensionButton"></div> - Attach a File - </div>`); $('#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',