|
|
|
@ -25,7 +25,6 @@ const ErrorTypes = Object.freeze({
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @extends Events
|
|
|
|
|
*/
|
|
|
|
@ -125,13 +124,26 @@ export default class AddonManager extends Events {
|
|
|
|
|
hasSettings (addonId) {
|
|
|
|
|
try {
|
|
|
|
|
const addon = this.get(addonId);
|
|
|
|
|
if (!addon) throw new Error(`${toTitleCase(this.type)} "${addonId}" is not installed!`);
|
|
|
|
|
return Boolean(addon.sections?.settings);
|
|
|
|
|
return Boolean(addon?.sections?.settings);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return this._error(`An error occurred while checking for settings for "${addonId}"!`, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {string} addonId Addon ID
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
hasScreenshots (addonId) {
|
|
|
|
|
try {
|
|
|
|
|
const addon = this.get(addonId);
|
|
|
|
|
return Boolean(addon?.screenshots);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return this._error(`An error occurred while checking for screenshots for "${addonId}"!`, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @returns
|
|
|
|
@ -216,9 +228,6 @@ export default class AddonManager extends Events {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._setAddonIcon(addonId, manifest);
|
|
|
|
|
// this._setPreviewImages(addonId, manifest);
|
|
|
|
|
// this._setBanner(addonId, manifest);
|
|
|
|
|
|
|
|
|
|
this._items.set(addonId, new Addon());
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return this._error(`An error occurred while initializing "${addonId}"!`, err);
|
|
|
|
@ -292,7 +301,7 @@ export default class AddonManager extends Events {
|
|
|
|
|
async initialize () {
|
|
|
|
|
let addonId;
|
|
|
|
|
try {
|
|
|
|
|
await this._enableWatcher();
|
|
|
|
|
this._enableWatcher();
|
|
|
|
|
if (this._watcherEnabled) {
|
|
|
|
|
await this._watchFiles();
|
|
|
|
|
}
|
|
|
|
@ -459,79 +468,95 @@ export default class AddonManager extends Events {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Installs one or more addons.
|
|
|
|
|
* @param {string} addons Addon ID(s) or GitHub repository URLs
|
|
|
|
|
* Installs an addon.
|
|
|
|
|
* @param {string} addonId Addon ID or GitHub repository URL
|
|
|
|
|
*/
|
|
|
|
|
async install (addons) {
|
|
|
|
|
async install (addonId) {
|
|
|
|
|
try {
|
|
|
|
|
/*
|
|
|
|
|
* This is temporary until we get the API working to request this info from an endpoint.
|
|
|
|
|
*/
|
|
|
|
|
const community = [ 'spotify-in-discord', 'copy-raw-message', 'better-code-blocks', 'status-everywhere', 'open-links-in-discord', 'example-plugin-settings', 'channel-members-activity-icons', 'bring-back-gamer-text', 'heyzere' ];
|
|
|
|
|
addons = [ addons ].flat();
|
|
|
|
|
for (let addon of addons) {
|
|
|
|
|
let addonId;
|
|
|
|
|
for (const _addon of community) {
|
|
|
|
|
if (addon === _addon) {
|
|
|
|
|
addonId = _addon;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!addonId) {
|
|
|
|
|
if (!new RegExp(/^(((https?:\/\/)(((([a-zA-Z0-9][a-zA-Z0-9\-_]{1,252})\.){1,8}[a-zA-Z]{2,63})\/))|((ssh:\/\/)?git@)(((([a-zA-Z0-9][a-zA-Z0-9\-_]{1,252})\.){1,8}[a-zA-Z]{2,63})(:)))([a-zA-Z0-9][a-zA-Z0-9_-]{1,36})(\/)([a-zA-Z0-9][a-zA-Z0-9_-]{1,36})((\.git)?)$/).test(addon)) {
|
|
|
|
|
throw new Error('You must provide a valid GitHub repository URL or an addon ID from https://github.com/vizality-community!');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log(addonId);
|
|
|
|
|
let git;
|
|
|
|
|
if (vizality.manager.community[toPlural(this.type)].has(addonId)) {
|
|
|
|
|
console.log('uh');
|
|
|
|
|
({ git } = vizality.manager.community[toPlural(this.type)].get(addonId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The URL must end in git to get processed by isomorphic-git below
|
|
|
|
|
if (!addon.endsWith('.git')) {
|
|
|
|
|
addon = `${addon}.git`;
|
|
|
|
|
}
|
|
|
|
|
console.log('okay');
|
|
|
|
|
if (!new RegExp(/^(((https?:\/\/)(((([a-zA-Z0-9][a-zA-Z0-9\-_]{1,252})\.){1,8}[a-zA-Z]{2,63})\/))|((ssh:\/\/)?git@)(((([a-zA-Z0-9][a-zA-Z0-9\-_]{1,252})\.){1,8}[a-zA-Z]{2,63})(:)))([a-zA-Z0-9][a-zA-Z0-9_-]{1,36})(\/)([a-zA-Z0-9][a-zA-Z0-9_-]{1,36})((\.git)?)$/).test(git || addonId)) {
|
|
|
|
|
console.log('yes');
|
|
|
|
|
throw new Error('You must provide a valid GitHub repository URL or an addon ID from https://github.com/vizality-community!');
|
|
|
|
|
} else {
|
|
|
|
|
console.log('no');
|
|
|
|
|
git = addonId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addonId = addonId || addon.split('.git')[0].split('/')[addon.split('.git')[0].split('/').length - 1];
|
|
|
|
|
addonId = toKebabCase(addonId);
|
|
|
|
|
console.log('here', git);
|
|
|
|
|
/**
|
|
|
|
|
* The URL must end in git to get processed by isomorphic-git below.
|
|
|
|
|
*/
|
|
|
|
|
if (!git.endsWith('.git')) {
|
|
|
|
|
git = `${git}.git`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.isInstalled(addonId)) {
|
|
|
|
|
throw {
|
|
|
|
|
name: ErrorTypes.ADDON_ALREADY_INSTALLED,
|
|
|
|
|
message: Messages.VIZALITY_ADDON_ALREADY_INSTALLED_DESC.format({ type: this.type, addonId }),
|
|
|
|
|
stack: `\n${(new Error()).stack.replace('Error\n', '')}`,
|
|
|
|
|
addonId
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
addonId = git.split('.git')[0].split('/')[git.split('.git')[0].split('/').length - 1];
|
|
|
|
|
addonId = toKebabCase(addonId);
|
|
|
|
|
|
|
|
|
|
console.log(git);
|
|
|
|
|
console.log(addonId);
|
|
|
|
|
if (this.isInstalled(addonId)) {
|
|
|
|
|
throw {
|
|
|
|
|
name: ErrorTypes.ADDON_ALREADY_INSTALLED,
|
|
|
|
|
message: Messages.VIZALITY_ADDON_ALREADY_INSTALLED_DESC.format({ type: this.type, addonId }),
|
|
|
|
|
stack: `\n${(new Error()).stack.replace('Error\n', '')}`,
|
|
|
|
|
addonId
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existsSync(join(this.dir, `__installing__${addonId}`)) && lstatSync(join(this.dir, addonId)).isDirectory()) {
|
|
|
|
|
throw new Error(`${toTitleCase(this.type)} "${addonId}" looks like it's already being installed!`);
|
|
|
|
|
}
|
|
|
|
|
if (existsSync(join(this.dir, `__installing__${addonId}`)) && lstatSync(join(this.dir, addonId))?.isDirectory()) {
|
|
|
|
|
throw new Error(`${toTitleCase(this.type)} "${addonId}" looks like it's already being installed!`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existsSync(join(this.dir, addonId)) && lstatSync(join(this.dir, addonId)).isDirectory()) {
|
|
|
|
|
throw new Error(`${toTitleCase(this.type)} directory "${addonId}" already exists!`);
|
|
|
|
|
}
|
|
|
|
|
if (existsSync(join(this.dir, addonId)) && lstatSync(join(this.dir, addonId))?.isDirectory()) {
|
|
|
|
|
throw new Error(`${toTitleCase(this.type)} directory "${addonId}" already exists!`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await clone({
|
|
|
|
|
fs,
|
|
|
|
|
http,
|
|
|
|
|
singleBranch: true,
|
|
|
|
|
depth: 1,
|
|
|
|
|
dir: join(this.dir, `__installing__${addonId}`),
|
|
|
|
|
url: addon,
|
|
|
|
|
onProgress: evt => {
|
|
|
|
|
// console.log(evt);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
/*
|
|
|
|
|
* isomorphic-git creates the directory before it checks anything, whether there is
|
|
|
|
|
* a response or not, so let's remove it if there's an error here.
|
|
|
|
|
*/
|
|
|
|
|
await removeDirRecursive(resolve(this.dir, addonId));
|
|
|
|
|
throw new Error(`There was a problem while attempting to install "${addonId}"!`, err);
|
|
|
|
|
}
|
|
|
|
|
renameSync(join(this.dir, `__installing__${addonId}`), join(this.dir, addonId));
|
|
|
|
|
try {
|
|
|
|
|
await clone({
|
|
|
|
|
fs,
|
|
|
|
|
http,
|
|
|
|
|
singleBranch: true,
|
|
|
|
|
depth: 1,
|
|
|
|
|
dir: join(this.dir, `__installing__${addonId}`),
|
|
|
|
|
url: git,
|
|
|
|
|
onProgress: evt => {
|
|
|
|
|
// console.log(evt);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
/**
|
|
|
|
|
* @note isomorphic-git creates the directory before it checks anything, whether there is
|
|
|
|
|
* a response or not, so let's remove it if there's an error here.
|
|
|
|
|
*/
|
|
|
|
|
await removeDirRecursive(resolve(this.dir, addonId));
|
|
|
|
|
throw new Error(`There was a problem while attempting to install "${addonId}"!`, err);
|
|
|
|
|
}
|
|
|
|
|
renameSync(join(this.dir, `__installing__${addonId}`), join(this.dir, addonId));
|
|
|
|
|
/**
|
|
|
|
|
* Send a success toast.
|
|
|
|
|
*/
|
|
|
|
|
vizality.api.notifications.sendToast({
|
|
|
|
|
id: ErrorTypes.ADDON_ALREADY_INSTALLED,
|
|
|
|
|
header: Messages.VIZALITY_ADDON_SUCCESSFULLY_INSTALLED.format({ type: toTitleCase(this.type) }),
|
|
|
|
|
content: <AddonInfoMessage addon={addon} message={Messages.VIZALITY_ADDON_SUCCESSFULLY_INSTALLED_DESC.format({ type: toTitleCase(this.type) })} />,
|
|
|
|
|
icon: toTitleCase(this.type),
|
|
|
|
|
buttons: [
|
|
|
|
|
{
|
|
|
|
|
text: 'View',
|
|
|
|
|
onClick: () => vizality.api.routes.navigateTo(`/${toPlural(this.type)}/${addon.addonId}`)
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn(err);
|
|
|
|
|
let addon;
|
|
|
|
|
if (err.addonId) {
|
|
|
|
|
addon = this.get(err.addonId);
|
|
|
|
@ -594,20 +619,21 @@ export default class AddonManager extends Events {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets an addon's icon image URL.
|
|
|
|
|
* @private
|
|
|
|
|
* @param {string} addonId Addon ID
|
|
|
|
|
* @param {object} manifest Addon manifest
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async _setAddonIcon (addonId, manifest) {
|
|
|
|
|
try {
|
|
|
|
|
const validExtensions = [ '.png', '.jpg', '.jpeg' ];
|
|
|
|
|
if (manifest.icon) {
|
|
|
|
|
if (!manifest.icon.endsWith('.png') && !manifest.icon.endsWith('.jpg') && !manifest.icon.endsWith('.jpeg')) {
|
|
|
|
|
if (!validExtensions.some(ext => manifest.icon.endsWith(ext))) {
|
|
|
|
|
this._warn(`${toTitleCase(this.type)} icon must be of type .png, .jpg, or .jpeg.`);
|
|
|
|
|
} else {
|
|
|
|
|
return manifest.icon = `vz-${this.type}://${addonId}/${manifest.icon}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const validExtensions = [ '.png', '.jpg', '.jpeg' ];
|
|
|
|
|
if (validExtensions.some(ext => existsSync(resolve(this.dir, addonId, 'assets', `icon${ext}`)))) {
|
|
|
|
|
for (const ext of validExtensions) {
|
|
|
|
|
if (existsSync(resolve(this.dir, addonId, 'assets', `icon${ext}`))) {
|
|
|
|
@ -616,8 +642,7 @@ export default class AddonManager extends Events {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const addonIdHash = toHash(addonId);
|
|
|
|
|
return manifest.icon = Avatars[`DEFAULT_${this.type.toUpperCase()}_${(addonIdHash % 5) + 1}`];
|
|
|
|
|
return manifest.icon = this._getDefaultAddonIcon(addonId);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return this._error(err);
|
|
|
|
@ -625,34 +650,15 @@ export default class AddonManager extends Events {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets an addon's banner image URL.
|
|
|
|
|
* @param {string} addonId Addon ID
|
|
|
|
|
* @param {object} manifest Addon manifest
|
|
|
|
|
* Gets a default addon icon image URL.
|
|
|
|
|
* @private
|
|
|
|
|
* @param {string} addonId Addon ID
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
async _setBanner (addonId, manifest) {
|
|
|
|
|
_getDefaultAddonIcon (addonId) {
|
|
|
|
|
try {
|
|
|
|
|
if (manifest.icon) {
|
|
|
|
|
if (!manifest.icon.endsWith('.png') && !manifest.icon.endsWith('.jpg') && !manifest.icon.endsWith('.jpeg')) {
|
|
|
|
|
this._warn(`${toTitleCase(this.type)} icon must be of type .png, .jpg, or .jpeg.`);
|
|
|
|
|
} else {
|
|
|
|
|
return manifest.icon = `vz-${this.type}://${addonId}/${manifest.icon}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validExtensions = [ '.png', '.jpg', '.jpeg' ];
|
|
|
|
|
|
|
|
|
|
if (validExtensions.some(ext => existsSync(resolve(this.dir, addonId, 'assets', `icon${ext}`)))) {
|
|
|
|
|
for (const ext of validExtensions) {
|
|
|
|
|
if (existsSync(resolve(this.dir, addonId, 'assets', `icon${ext}`))) {
|
|
|
|
|
manifest.icon = `vz-${this.type}://${addonId}/assets/icon${ext}`;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const addonIdHash = toHash(addonId);
|
|
|
|
|
return manifest.icon = Avatars[`DEFAULT_${this.type.toUpperCase()}_${(addonIdHash % 5) + 1}`];
|
|
|
|
|
}
|
|
|
|
|
const addonIdHash = toHash(addonId);
|
|
|
|
|
return Avatars[`DEFAULT_${this.type.toUpperCase()}_${(addonIdHash % 5) + 1}`];
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return this._error(err);
|
|
|
|
|
}
|
|
|
|
@ -661,14 +667,16 @@ export default class AddonManager extends Events {
|
|
|
|
|
/**
|
|
|
|
|
* Enables the addon directory watcher.
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
async _enableWatcher () {
|
|
|
|
|
_enableWatcher () {
|
|
|
|
|
this._watcherEnabled = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Disables the addon directory watcher.
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async _disableWatcher () {
|
|
|
|
|
this._watcherEnabled = false;
|
|
|
|
@ -681,6 +689,7 @@ export default class AddonManager extends Events {
|
|
|
|
|
/**
|
|
|
|
|
* Initiates the addon directory watcher.
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async _watchFiles () {
|
|
|
|
|
this._watcher = watch(this.dir, {
|
|
|
|
@ -695,6 +704,7 @@ export default class AddonManager extends Events {
|
|
|
|
|
* @see {@link https://memorytin.com/2015/07/08/node-js-chokidar-wait-for-file-copy-to-complete-before-modifying/}
|
|
|
|
|
* @param {string} path Addon folder path
|
|
|
|
|
* @param {object} prev Previous folder stats info @see {@link https://nodejs.org/api/fs.html#fs_class_fs_stats}
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
const checkAddDirComplete = (path, prev) => {
|
|
|
|
|
try {
|
|
|
|
@ -718,10 +728,19 @@ export default class AddonManager extends Events {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
this._watcher
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
.on('addDir', (path, stat) => {
|
|
|
|
|
setTimeout(checkAddDirComplete, 2000, path, stat);
|
|
|
|
|
})
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
.on('unlinkDir', path => {
|
|
|
|
|
const addonId = path.replace(this.dir + sep, '');
|
|
|
|
|
Object.keys(require.cache).forEach(key => {
|
|
|
|
@ -736,6 +755,7 @@ export default class AddonManager extends Events {
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {string} addonId Addon ID
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
uninstall (addonId) {
|
|
|
|
|
try {
|
|
|
|
|