mirror of https://github.com/vizality/vizality
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.
565 lines
19 KiB
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;
|
|
}
|