External extension import UI + backend

This commit is contained in:
BlipRanger
2023-07-11 18:24:04 -04:00
parent bb6ff352b3
commit 53d45356a4
3 changed files with 76 additions and 69 deletions

View File

@@ -2509,6 +2509,7 @@
<input id="extensions_connect" class="menu_button" type="submit" value="Connect"> <input id="extensions_connect" class="menu_button" type="submit" value="Connect">
</div> </div>
<input id="extensions_details" class="alignitemsflexstart menu_button" type="button" value="Manage extensions"> <input id="extensions_details" class="alignitemsflexstart menu_button" type="button" value="Manage extensions">
<div id="third_party_extension_button" title="Import Extension From Git Repo" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
</div> </div>
</div> </div>
<div id="extensions_settings" class="flex1 wide50p"> <div id="extensions_settings" class="flex1 wide50p">
@@ -2853,7 +2854,6 @@
<div id="rm_button_create" title="Create New Character" class="menu_button fa-solid fa-user-plus "></div> <div id="rm_button_create" title="Create New Character" class="menu_button fa-solid fa-user-plus "></div>
<div id="character_import_button" title="Import Character from File" class="menu_button fa-solid fa-file-arrow-up faSmallFontSquareFix"></div> <div id="character_import_button" title="Import Character from File" class="menu_button fa-solid fa-file-arrow-up faSmallFontSquareFix"></div>
<div id="external_import_button" title="Import content from external URL" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div> <div id="external_import_button" title="Import content from external URL" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
<div id="thrid_party_extension_button" title="Import extension from git repo" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
<div id="rm_button_group_chats" title="Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div> <div id="rm_button_group_chats" title="Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div>
<input id="character_search_bar" class="text_pole width100p" type="search" placeholder="Search..." maxlength="50" /> <input id="character_search_bar" class="text_pole width100p" type="search" placeholder="Search..." maxlength="50" />
<select id="character_sort_order" title="Characters sorting order"> <select id="character_sort_order" title="Characters sorting order">

View File

@@ -8494,23 +8494,21 @@ $(document).ready(function () {
} }
}); });
$('#thrid_party_extension_button').on('click', async () => { $('#third_party_extension_button').on('click', async () => {
const html = `<h3>Enter the URL of the content to import</h3> const html = `<h3>Enter the Git URL of the extension to import</h3>
Supported sources:<br> <br>
<ul class="justifyLeft"> <p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
<li>Chub characters (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li> <br>
<li>Chub lorebooks (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li> <p>Example: <tt> https://github.com/author/extension-name </tt></p>`
<li>More coming soon...</li>
<ul>`
const input = await callPopup(html, 'input'); const input = await callPopup(html, 'input');
if (!input) { if (!input) {
console.debug('Custom content import cancelled'); console.debug('Extension import cancelled');
return; return;
} }
const url = input.trim(); const url = input.trim();
console.debug('Custom content import started', url); console.debug('Extension import started', url);
const request = await fetch('/get_extension', { const request = await fetch('/get_extension', {
method: 'POST', method: 'POST',
@@ -8519,11 +8517,14 @@ $(document).ready(function () {
}); });
if (!request.ok) { if (!request.ok) {
toastr.info(request.statusText, 'Custom content import failed'); toastr.info(request.statusText, 'Extension import failed');
console.error('Custom content import failed', request.status, request.statusText); console.error('Extension import failed', request.status, request.statusText);
return; return;
} }
const response = await request.json();
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been imported successfully!`, 'Extension import successful');
console.debug(`Extension "${response.display_name}" has been imported successfully at ${response.extensionPath}`);
}); });
const $dropzone = $(document.body); const $dropzone = $(document.body);

110
server.js
View File

@@ -2711,10 +2711,27 @@ app.post('/poe_suggest', jsonParser, async function (request, response) {
}); });
/**
* Discover the extension folders
* If the folder is called third-party, search for subfolders instead
*/
app.get('/discover_extensions', jsonParser, function (_, response) { app.get('/discover_extensions', jsonParser, function (_, response) {
// get all folders in the extensions folder, except third-party
const extensions = fs const extensions = fs
.readdirSync(directories.extensions) .readdirSync(directories.extensions)
.filter(f => fs.statSync(path.join(directories.extensions, f)).isDirectory()); .filter(f => fs.statSync(path.join(directories.extensions, f)).isDirectory())
.filter(f => f !== 'third-party');
// get all folders in the third-party folder
const thirdPartyExtensions = fs
.readdirSync(path.join(directories.extensions, 'third-party'))
.filter(f => fs.statSync(path.join(directories.extensions, 'third-party', f)).isDirectory());
// add the third-party extensions to the extensions array
extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`));
console.log(extensions);
return response.send(extensions); return response.send(extensions);
}); });
@@ -4346,74 +4363,63 @@ async function getImageBuffers(zipFilePath) {
}); });
} }
/**
* This function is used ensure the list of extensions is up to date.
* @param {object} extensionList
* @returns
*/
function getExtensions(extensionList) {
const extensions = [];
// if the extension folder does not exist, git clone the extension, also check if the extension is up to date with the git repo, but don't pull if it is not up to date const simpleGit = require('simple-git');
for (const extension of extensionList) {
const extensionPath = path.join(directories.extensions, extension.name); /**
if (!fs.existsSync(extensionPath)) { * This function extracts the extension information from the manifest file.
console.log(`Extension ${extension.name} does not exist. Cloning...`); * @param {string} extensionPath - The path of the extension folder
try { * @returns {Object} - Returns the manifest data as an object
git.clone(extension.url, extensionPath); */
} catch (error) { async function getManifest(extensionPath) {
console.error(`Failed to clone extension ${extension.name}`); const manifestPath = path.join(extensionPath, 'manifest.json');
console.error(error);
} // Check if manifest.json exists
} else { if (!fs.existsSync(manifestPath)) {
console.log(`Extension ${extension.name} exists throw new Error(`Manifest file not found at ${manifestPath}`);
Checking if extension is up to date...`);
try {
git.fetch(extensionPath);
const status = git.status(extensionPath);
if (status.behind > 0) {
console.log(`Extension ${extension.name} is not up to date. Pulling...`);
git.pull(extensionPath);
}
} catch (error) {
console.error(`Failed to check if extension ${extension.name} is up to date`);
console.error(error);
}
}
extensions.push(extension.name);
} }
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
return manifest;
} }
/** /**
* This function is used to git clone a single extension into the thrid-party-extensions folder * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,
*/ * and return extension information and path.
*
* @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property.
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
*
* @returns {void}
*/
app.post('/get_extension', jsonParser, async (request, response) => { app.post('/get_extension', jsonParser, async (request, response) => {
if (!request.body.url) { if (!request.body.url) {
return response.sendStatus(400); return response.status(400).send('Bad Request: URL is required in the request body.');
} }
try { try {
const url = request.body.url; const url = request.body.url;
let result; const git = simpleGit();
// git clone and then get the resulting folder path of the extension // get the name of the repo from the url and create the extensions folder path
const extensionPath = git.clone(url, directories.extensions + '/third-party'); const extensionPath = path.join(directories.extensions, 'third-party', path.basename(url, '.git'));
// load the info from the manifest.json in the extension folder // Check if a folder already exists at the specified location
const manifest = JSON.parse(fs.readFileSync(extensionPath + '/manifest.json', 'utf8')); if (fs.existsSync(extensionPath)) {
// pull version, author, display_name return response.status(409).send(`Directory already exists at ${extensionPath}`);
const version = manifest.version; }
const author = manifest.author;
const display_name = manifest.display_name; await git.clone(url, extensionPath);
console.log(`Extension ${display_name} has been cloned`); console.log(`Extension has been cloned at ${extensionPath}`);
// return the version, author, display_name, and the path to the extension folder
// Load the info from the manifest.json in the extension folder
const { version, author, display_name } = await getManifest(extensionPath);
// Return the version, author, display_name, and the path to the extension folder
return response.send({ version, author, display_name, extensionPath }); return response.send({ version, author, display_name, extensionPath });
} catch (error) { } catch (error) {
console.log('Importing custom content failed', error); console.log('Importing custom content failed', error);
return response.sendStatus(500); return response.status(500).send(`Server Error: ${error.message}`);
} }
}); });