You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vizality/renderer/src/setup/index.js

565 lines
19 KiB

/* eslint-disable no-undef */
const { existsSync, promises: { writeFile, mkdir, access, readdir, rm } } = require('fs');
const { exec: _exec, spawn } = require('child_process');
const { join, posix, sep } = require('path');
const { prompt } = require('inquirer');
const { promisify } = require('util');
const chalk = require('chalk');
const exec = promisify(_exec);
let release;
/**
* Colors used for log styling.
*/
const COLORS = {
Brand: '#ff006a',
Success: '#a5d479',
Warn: '#ebd334',
Info: '#7a85b8',
Error: '#f55353'
};
/**
* Dynamically create an assortment of functions that send colorful messages to console.
*/
Object.keys(COLORS).forEach(color => {
global[color.toLowerCase()] = (...message) => {
return console.log(chalk.hex(COLORS[color])(message) + chalk.reset());
};
});
/**
* Start the injection process automatically.
*/
(async () => {
try {
await startInjectionProcess();
} catch (err) {
if (err.code === 'EACCES') {
return error('Vizality was unable to be injected due to missing permissions. Try again with elevated permissions.');
}
error(`The setup process encountered an issue that prevented it from running correctly:\n${err}`);
info(`If the problem persists, please join our Discord server and reach out for support: ${chalk.hex(COLORS.Brand)('https://invite.vizality.com') + chalk.reset()}.`);
return promptForRestartOrExit();
}
})();
/**
* Prompts the user, giving them an option to restart the injection process or exit.
* @returns {Promise<void>}
*/
async function promptForRestartOrExit () {
const responses = await prompt([
{
type: 'list',
name: 'action',
message: `Would you like to start over or exit?`,
default: 'Inject',
choices: wrapChoicesInColor([
'Start Over',
'Exit'
])
}
]);
if (responses.action.includes('Start Over')) {
return startInjectionProcess();
}
warn('Process has been cancelled. Now exiting...');
return process.exit(0);
}
/**
* Checks whether a file or directory exists, and if so, whether the user has permissions for it.
* @param {path} path File or directory path
* @returns {boolean} Whether or not the file or directory exists, and if so, whether the user has permissions for it
*/
async function confirmExistAndHasPermissions (path) {
return access(path)
.then(() => true)
.catch(() => false);
}
/**
* Prompts the user to manually enter a Discord installation path if one can't be found automatically.
* @param {('inject'|'uninject'|'reinject')} action Action to be performed
* @returns {Promise<void>}
*/
async function promptManualPathCompletion (action) {
const responses = await prompt([
{
type: 'list',
name: 'action',
message: 'Would you like to enter the Discord directory path manually?',
default: 'Yes',
choices: wrapChoicesInColor([
'Yes',
'No'
])
},
{
type: 'input',
name: 'discordPath',
message: `Please enter the absolute path to your Discord directory:`,
when: responses => responses.action.includes('Yes')
}
]);
if (responses.action.includes('No')) {
return promptForRestartOrExit();
}
const discordPath = responses.discordPath?.trim();
if (discordPath) {
if (await confirmExistAndHasPermissions(discordPath)) {
let discordAppPath;
if (process.platform === 'win32') {
const discordDirectory = await readdir(discordPath);
/**
* Get the latest version's folder. Then find and return the app directory path.
*/
const latestVersionDirectory = discordDirectory.filter(path => path.startsWith('app-')).sort().reverse()[0];
discordAppPath = join(discordPath, latestVersionDirectory, 'resources', 'app');
} else if (process.platform === 'darwin') {
discordAppPath = join(discordPath, 'Contents', 'Resources', 'app');
} else {
discordAppPath = join(discordPath, 'resources', 'app');
}
if (!discordAppPath || !(await confirmExistAndHasPermissions(discordAppPath))) {
error(`${release}'s app directory couldn't be located and ${action}ion has been cancelled.`);
return promptForRestartOrExit();
}
info(`Discord app directory was successfully found at ${chalk.hex(COLORS.Brand)(discordAppPath) + chalk.reset()}`);
if (action === 'inject') {
await inject(discordAppPath);
} else if (action === 'uninject') {
await uninject(discordAppPath);
} else if (action === 'reinject') {
await reinject(discordAppPath);
} else {
return error(`Unrecognized action used. Process was cancelled.`);
}
} else {
error(`${release} installation couldn't be located at ${chalk.hex(COLORS.Brand)(discordAppPath) + chalk.reset()}.\nPlease make sure the path is correct and try again.`);
return promptForRestartOrExit();
}
} else {
error(`You must enter a valid path in order to ${action} Vizality.`);
return promptForRestartOrExit();
}
}
/**
* Determines the user's Discord app directory based on operating system and specified Discord release.
* @returns {Promise<string>} Discord app directory path
*/
async function getDiscordAppPath () {
/**
* Windows
*/
if (process.platform === 'win32') {
const discordPath = join(process.env.LOCALAPPDATA, release?.replace(' ', ''));
if (!discordPath || !(await confirmExistAndHasPermissions(discordPath))) {
info(`Unfortunately, the ${release} directory couldn't be located.`);
return promptManualPathCompletion('inject');
}
/**
* Read the Discord directory so that we can find the latest version's folder below.
*/
const discordDirectory = await readdir(discordPath);
/**
* Get the latest version's folder. Then find and return the app directory path.
*/
const latestVersionDirectory = discordDirectory.filter(path => path.startsWith('app-')).sort().reverse()[0];
return join(discordPath, latestVersionDirectory, 'resources', 'app');
/**
* Mac
*/
} else if (process.platform === 'darwin') {
const discordPath = join('/Applications', `${release}.app`);
if (!discordPath || !(await confirmExistAndHasPermissions(discordPath))) {
error(`Unfortunately, the ${release} directory couldn't be located.`);
return promptManualPathCompletion('inject');
}
return join(discordPath, 'Contents', 'Resources', 'app');
}
/**
* If all else fails, assume the user is on Linux and try to find the Discord process.
*/
const discordProcess = (await exec('ps x')).toString().split('\n').map(s => s.split(' ').filter(Boolean)).find(process => process[4] && (/discord$/i).test(process[4]) && process.includes('--type=renderer'));
/**
* If the process can't be found, try to determine the directory path by checking if any
* predefined directory paths exist.
*/
if (!discordProcess) {
warn('Cannot find Discord process, falling back to legacy path detection...');
/**
* Instead of using ~, use this to determine the user's home directory instead of the root's home directory.
*/
const homeDirectory = (await exec('grep $(logname) /etc/passwd | cut -d ":" -f6').toString()).trim();
const paths = [
`/usr/share/${release.toLowerCase().replace(' ', '-')}`,
`/usr/lib64/${release.toLowerCase().replace(' ', '-')}`,
`/opt/${release.toLowerCase().replace(' ', '-')}`,
`/opt/${release.replace(' ', '')}`,
`${homeDirectory}/.local/bin/${release.replace(' ', '')}/`
];
const discordPath = paths.find(async path => !(await confirmExistAndHasPermissions(path)));
if (!discordPath || !(await confirmExistAndHasPermissions(discordPath))) {
error(`Unfortunately, the ${release} directory couldn't be located.`);
return promptManualPathCompletion('inject');
}
return join(discordPath, 'resources', 'app');
}
const discordPath = discordProcess[4].split('/');
discordPath.splice(discordPath.length - 1, 1);
return join('/', ...discordPath, 'resources', 'app');
}
/**
* Run an npm command.
* Used in this script for installing Node package dependencies.
* @returns {Promise<void>}
*/
async function installDependencies () {
return new Promise((resolve, reject) => {
const command = spawn('npm', [ 'install', '--only=production', '--silent' ], {
cwd: join(__dirname, '..'),
stdio: 'inherit',
shell: true
});
command.on('close', () => resolve());
command.on('error', err => reject(err));
});
}
/**
* Checks for and ensures all node_modules package depedencies are installed.
* @returns {Promise<void>}
*/
async function ensureDependencies () {
try {
const nodeModulesPath = join(__dirname, '..', '..', '..', 'node_modules');
info('Checking dependencies. Please wait...');
/**
* Make sure all dependencies in our package file are installed.
*/
const { dependencies } = require('../../../package.json');
const missingDependencies = [];
const outdatedDependencies = [];
for (const dependency in dependencies) {
const dependencyPath = join(nodeModulesPath, dependency);
/**
* Let's make sure all the depdencies are installed.
*/
if (!existsSync(dependencyPath)) {
missingDependencies.push(dependency);
} else {
/**
* Let's check the package versions and make sure they're the metting the qualifications of
* the expected version specified in the package file.
*/
const dependencyPackage = require(`../../../node_modules/${dependency}/package.json`);
const expectedVersion = parseInt(dependencies[dependency].replace(/[^\d]/g, ''));
const installedVersion = parseInt(dependencyPackage.version.replace(/[^\d]/g, ''));
if (installedVersion < expectedVersion) {
outdatedDependencies.push(dependency);
}
}
}
const missingDependenciesMessage = missingDependencies.length && `${missingDependencies.length} missing`;
const outdatedDependenciesMessage = outdatedDependencies.length && `${outdatedDependencies.length} outdated`;
if (missingDependenciesMessage || outdatedDependenciesMessage) {
warn(`Found ${[ missingDependenciesMessage, outdatedDependenciesMessage ].filter(Boolean).join(' and ')} ${missingDependencies.length + outdatedDependencies.length > 1 ? 'packages' : 'package'}. Resolving...`);
}
/**
* Install the missing and updated dependencies.
*/
await installDependencies();
return info('Dependencies are up-to-date!');
} catch (err) {
error(`An error occured while installing the dependencies:\n${err.message}`);
return process.exit(0);
}
}
/**
* Uninjects Vizality from Discord.
* @param {path} discordAppPath Discord app path
* @param {boolean} vizality Whether or not the client mod is known to be Vizality
* @returns {Promise<void>}
*/
async function uninject (discordAppPath, vizality = true) {
/**
* Check for a valid Discord app directory.
*/
if (!discordAppPath || !existsSync(discordAppPath)) {
error(`It doesn't look like there's anything injected.`);
return promptForRestartOrExit();
}
/**
* Check for directory permissions.
*/
if (!(await confirmExistAndHasPermissions(discordAppPath))) {
error(`Vizality was unable to be uninjected due to missing permissions. Try again with elevated permissions.`);
return promptForRestartOrExit();
}
/**
* Delete the Discord app directory.
*/
await rm(discordAppPath, { recursive: true, force: true });
brand('-----------------------------------------------');
brand(`${vizality ? 'Vizality' : 'Client modification'} has been uninjected.`);
brand('-----------------------------------------------');
}
/**
* Injects Vizality into Discord.
* @param {path} discordAppPath Discord app path
* @returns {Promise<void>}
*/
async function inject (discordAppPath) {
const existsAndHasPermissions = await confirmExistAndHasPermissions(discordAppPath);
if (existsAndHasPermissions) {
const responses = await prompt([
{
type: 'list',
name: 'action',
message: `${`${chalk.hex(COLORS.Info)('It seems you already have a client modification in place.\n') + chalk.reset()}Would you like us to remove it and then try injecting again?`}`,
default: 'Yes',
choices: wrapChoicesInColor([
'Yes',
'No'
])
}
]);
if (responses.action.includes('Yes')) {
return reinject(discordAppPath, false);
}
warn('Vizality injection canceled. Now exiting...');
return process.exit(0);
}
await ensureDependencies();
await mkdir(discordAppPath);
Promise.all([
/**
* Relative to dist/setup.bundle.js
*/
writeFile(join(discordAppPath, 'index.js'), `require('${join(__dirname, '..', 'injector').replace(RegExp(sep.repeat(2), 'g'), '/')}');`),
writeFile(join(discordAppPath, 'package.json'), JSON.stringify({
main: 'index.js',
name: 'vizality'
}, null, 2))
]);
success('-----------------------------------------------');
success(`Vizality has been successfully injected!\nPlease restart ${release} to complete the process.`);
success('-----------------------------------------------');
}
/**
* Reinjects Vizality into Discord.
* @param {path} discordAppPath Discord app path
* @param {boolean} vizality Whether or not the client mod is known to be Vizality
* @returns {Promise<void>}
*/
async function reinject (discordAppPath, vizality = true) {
await uninject(discordAppPath, vizality);
await inject(discordAppPath);
}
/**
* Starts the setup process, allowing the user to inject, uninject, or reinject Vizality.
* @returns {Promise<void>}
*/
async function startInjectionProcess () {
try {
/**
* Start out with a nifty console message of Vizality. ( ͡° ͜ʖ ͡°)
*/
console.clear();
console.log(
'888 888 8888888 8888888888P d8888 888 8888888 88888888888 Y88b d88P\n' +
'888 888 888 d88P d88888 888 888 888 Y88b d88P \n' +
'888 888 888 d88P d88P888 888 888 888 Y88o88P \n' +
'Y88b d88P 888 d88P d88P 888 888 888 888 Y888P \n' +
' Y88b d88P 888 d88P d88P 888 888 888 888 888 \n' +
' Y88o88P 888 d88P d88P 888 888 888 888 888 \n' +
' Y888P 888 d88P d8888888888 888 888 888 888 \n' +
' Y8P 8888888 d8888888888 d88P 888 88888888 8888888 888 888\n'
);
brand(
'[=== m a k e ---- y o u r ---- v i s i o n ---- a ---- r e a l i t y ===]\n'
);
info(
'=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=\n' +
'*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*\n'
);
/**
* Not sure why someone would clone into System32... but let's not let them proceed to avoid any issues.
*/
if (process.platform === 'win32') {
if (__dirname.toLowerCase().split(sep).join(posix.sep).includes('/windows/system32')) {
error('It seems Vizality is installed in your System 32 directory. This can create issues and bloat your Windows installation. Please move your Vizality installation to a different directory.');
return process.exit(0);
}
}
/**
* Make sure the user is on Node v14+.
*/
const NODE_MAJOR_VERSION = process.versions.node.split('.')[0];
if (NODE_MAJOR_VERSION < 14) {
error(`It looks like you're on an outdated version of Node.js. Vizality requires you to run at least Node v14 or later. You can download a newer version here: https://nodejs.org`);
return process.exit(0);
}
/**
* Start a prompt to process whether the user wants to inject or uninject and which release
* of Discord they want to use.
*/
const responses = await prompt([
{
type: 'list',
name: 'action',
message: `Would you like to inject, uninject, or reinject Vizality?`,
default: 'Inject',
choices: wrapChoicesInColor([
'Inject',
'Uninject',
'Reinject'
])
},
{
type: 'list',
name: 'release',
message: `Which release of Discord would you like to inject Vizality into?`,
default: 'Discord Stable',
when: responses => responses.action.includes('Inject'),
choices: wrapChoicesInColor([
'Discord Stable',
'Discord PTB',
'Discord Canary'
])
},
{
type: 'list',
name: 'release',
message: `Which release of Discord would you like to uninject Vizality from?`,
default: 'Discord Stable',
when: responses => responses.action.includes('Uninject'),
choices: wrapChoicesInColor([
'Discord Stable',
'Discord PTB',
'Discord Canary'
])
},
{
type: 'list',
name: 'release',
message: `Which release of Discord would you like to reinject Vizality into?`,
default: 'Discord Stable',
when: responses => responses.action.includes('Reinject'),
choices: wrapChoicesInColor([
'Discord Stable',
'Discord PTB',
'Discord Canary'
])
}
]);
({ release, action } = responses);
/**
* Because we are using color insertions in our prompt, the output is not pretty.
* Let's check for includes and assign some corresponding plaintext values.
*/
switch (true) {
case release.includes('Stable'):
release = 'Discord';
break;
case release.includes('PTB'):
release = 'Discord PTB';
break;
case release.includes('Canary'):
release = 'Discord Canary';
break;
}
switch (true) {
case action.includes('Inject'):
action = 'inject';
break;
case action.includes('Uninject'):
action = 'uninject';
break;
case action.includes('Reinject'):
action = 'reinject';
break;
}
/**
* Try to automatically find the user's Discord directory based on the specified release.
*/
const discordAppPath = await getDiscordAppPath(release);
if (!discordAppPath) {
error(`${release}'s app directory couldn't be located. Please make sure the folder exists and you have the appropriate permissions.`);
return promptManualPathCompletion(action);
}
/**
* Perform the action based on the prompt.
*/
switch (action) {
case 'inject':
return inject(discordAppPath);
case 'uninject':
return uninject(discordAppPath);
case 'reinject':
return reinject(discordAppPath);
}
} catch (err) {
return error(`Something went wrong with the initial setup process:\n${err}`);
}
}
/**
* Wraps each item in a choice array in a color.
* @param {Array<string>} [choices] Choices
* @returns {Array}
*/
function wrapChoicesInColor (choices = []) {
const transformerChoices = [];
choices.forEach(choice => transformerChoices.push(`${chalk.hex(COLORS.Brand)(choice) + chalk.reset()}`));
return transformerChoices;
}