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">
</div>
<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 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="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="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>
<input id="character_search_bar" class="text_pole width100p" type="search" placeholder="Search..." maxlength="50" />
<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 () => {
const html = `<h3>Enter the URL of the content to import</h3>
Supported sources:<br>
<ul class="justifyLeft">
<li>Chub characters (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li>
<li>Chub lorebooks (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li>More coming soon...</li>
<ul>`
$('#third_party_extension_button').on('click', async () => {
const html = `<h3>Enter the Git URL of the extension to import</h3>
<br>
<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>
<br>
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Custom content import cancelled');
console.debug('Extension import cancelled');
return;
}
const url = input.trim();
console.debug('Custom content import started', url);
console.debug('Extension import started', url);
const request = await fetch('/get_extension', {
method: 'POST',
@@ -8519,11 +8517,14 @@ $(document).ready(function () {
});
if (!request.ok) {
toastr.info(request.statusText, 'Custom content import failed');
console.error('Custom content import failed', request.status, request.statusText);
toastr.info(request.statusText, 'Extension import failed');
console.error('Extension import failed', request.status, request.statusText);
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);

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) {
// get all folders in the extensions folder, except third-party
const extensions = fs
.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);
});
@@ -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
for (const extension of extensionList) {
const extensionPath = path.join(directories.extensions, extension.name);
if (!fs.existsSync(extensionPath)) {
console.log(`Extension ${extension.name} does not exist. Cloning...`);
try {
git.clone(extension.url, extensionPath);
} catch (error) {
console.error(`Failed to clone extension ${extension.name}`);
console.error(error);
}
} else {
console.log(`Extension ${extension.name} exists
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 simpleGit = require('simple-git');
/**
* This function extracts the extension information from the manifest file.
* @param {string} extensionPath - The path of the extension folder
* @returns {Object} - Returns the manifest data as an object
*/
async function getManifest(extensionPath) {
const manifestPath = path.join(extensionPath, 'manifest.json');
// Check if manifest.json exists
if (!fs.existsSync(manifestPath)) {
throw new Error(`Manifest file not found at ${manifestPath}`);
}
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) => {
if (!request.body.url) {
return response.sendStatus(400);
return response.status(400).send('Bad Request: URL is required in the request body.');
}
try {
const url = request.body.url;
let result;
const git = simpleGit();
// git clone and then get the resulting folder path of the extension
const extensionPath = git.clone(url, directories.extensions + '/third-party');
// get the name of the repo from the url and create the extensions folder path
const extensionPath = path.join(directories.extensions, 'third-party', path.basename(url, '.git'));
// load the info from the manifest.json in the extension folder
const manifest = JSON.parse(fs.readFileSync(extensionPath + '/manifest.json', 'utf8'));
// pull version, author, display_name
const version = manifest.version;
const author = manifest.author;
const display_name = manifest.display_name;
console.log(`Extension ${display_name} has been cloned`);
// return the version, author, display_name, and the path to the extension folder
// Check if a folder already exists at the specified location
if (fs.existsSync(extensionPath)) {
return response.status(409).send(`Directory already exists at ${extensionPath}`);
}
await git.clone(url, extensionPath);
console.log(`Extension has been cloned at ${extensionPath}`);
// 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 });
} catch (error) {
console.log('Importing custom content failed', error);
return response.sendStatus(500);
return response.status(500).send(`Server Error: ${error.message}`);
}
});