UNPKG

altv-pkg

Version:

Install alt:V Binaries Quickly

471 lines (385 loc) 16.1 kB
#!/usr/bin/env node const crypto = require('crypto'); const RPC = require('discord-rpc'); const chalk = require('chalk'); const fs = require('node:fs'); const path = require('node:path'); const { Readable } = require('node:stream'); const RC_FILE_NAME = '.altvpkgrc.json'; const CDN_ADDRESS = 'cdn.alt-mp.com'; const DISCORD_ID = '580868196270342175'; const ALTV_PREFIX = "ALTV-"; const BRANCH_RELEASE = "release"; const BRANCH_RC = "rc"; const BRANCH_RC2 = "rc2"; const BRANCH_DEV = "dev"; const args = process.argv; const rootPath = process.cwd(); let platform = process.platform == 'win32' ? 'x64_win32' : 'x64_linux'; let branch = null; const { loadJSModule, loadBytecodeModule, loadCSharpModule, loadJSV2Module, loadVoiceServer } = loadRuntimeConfig(); function authorizeDiscord() { console.log(chalk.greenBright('===== Authorizing via Discord =====')); return new Promise(async (resolve, reject) => { try { const client = new RPC.Client({ transport: 'ipc' }); client.on('ready', async () => { try { const { code } = await client.request('AUTHORIZE', { scopes: ['identify'], client_id: DISCORD_ID, prompt: 'none', }); resolve(code); } catch (e) { reject(e); return; } finally { client.destroy(); } }); await client.login({ clientId: DISCORD_ID }); } catch (e) { reject(e); } }); } async function authorizeCDN(code) { console.log(chalk.greenBright('===== Authorizing in CDN =====')); try { const res = await fetchJsonData('https://qa-auth.alt-mp.com/auth', { responseType: 'application/json', headers: { Authorization: code }, }); return res?.token; } catch (e) { if (e?.response?.status != 403) throw e; throw new Error('You do not have permissions to access this branch'); } } for (let i = 0; i < args.length; i++) { if (args[i] === BRANCH_RELEASE) { branch = BRANCH_RELEASE; continue; } if (args[i] === BRANCH_RC) { branch = BRANCH_RC; continue; } if (args[i] === BRANCH_RC2) { branch = BRANCH_RC2; continue; } if (args[i] === BRANCH_DEV) { branch = BRANCH_DEV; continue; } if (args[i].startsWith(ALTV_PREFIX)) { branch = args[i]; continue; } if (args[i].startsWith('qa')) { branch = args[i]; continue; } if (args[i] === 'windows') { platform = 'x64_win32'; continue; } if (args[i] === 'linux') { platform = 'x64_linux'; continue; } } if (!branch) { branch = BRANCH_RELEASE; console.log(chalk.yellowBright('Branch not specified, using release')); } /** * Fetch JSON data and return an object * * @param {string} url * @param {Object} headers * @return {Promise<Object>} */ async function fetchJsonData(url, headers) { const response = await fetch(url, headers).catch((err) => { throw err; }); if (!response || !response.ok) { throw new Error('Failed to download latest') } return response.json(); } async function getFilesFromCDN(urlPrefix, branch, platform, file, headers) { files = {}; fullUrl = `${urlPrefix}/${branch}/${platform}/${file}`; res = await fetchJsonData(fullUrl, { responseType: 'application/json', headers, }); for ([tmpFile, hash] of Object.entries(res.hashList)) { files[tmpFile] = `${urlPrefix}/${branch}/${platform}/${tmpFile}`; } return files; } async function start() { console.log(chalk.greenBright('===== altv-pkg =====')); console.log(chalk.whiteBright(`System: `), chalk.yellowBright(platform)); console.log(chalk.whiteBright(`Branch: `), chalk.yellowBright(branch)); let headers = undefined; let downloadDataBranch = BRANCH_RELEASE; if (branch.startsWith(ALTV_PREFIX)) { downloadDataBranch = BRANCH_DEV } else if (branch.startsWith(BRANCH_RC)) { downloadDataBranch = BRANCH_RC; } else { downloadDataBranch = branch; } const sharedFiles = {}; let res = await fetchJsonData(`https://${CDN_ADDRESS}/data/${downloadDataBranch}/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { sharedFiles[file] = `https://${CDN_ADDRESS}/data/${downloadDataBranch}/${file}`; } const linuxFiles = { ...sharedFiles }; res = await fetchJsonData(`https://${CDN_ADDRESS}/server/${branch}/x64_linux/update.json`, { responseType: 'application/json', headers, }); if (!res) return; for ([file, hash] of Object.entries(res.hashList)) { linuxFiles[file] = `https://${CDN_ADDRESS}/server/${branch}/x64_linux/${file}`; } const windowsFiles = { ...sharedFiles }; res = await fetchJsonData(`https://${CDN_ADDRESS}/server/${branch}/x64_win32/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { windowsFiles[file] = `https://${CDN_ADDRESS}/server/${branch}/x64_win32/${file}`; } const sharedUpdates = [`https://${CDN_ADDRESS}/data/${downloadDataBranch}/update.json`]; const linuxUpdates = [ ...sharedUpdates, `https://${CDN_ADDRESS}/server/${branch}/x64_linux/update.json`, ]; const windowsUpdates = [ ...sharedUpdates, `https://${CDN_ADDRESS}/server/${branch}/x64_win32/update.json`, ]; if (loadJSModule) { let jsModulesBranch = branch; try { tmpfiles = await getFilesFromCDN(`https://${CDN_ADDRESS}/js-module`, jsModulesBranch, `x64_linux`, `update.json`, headers) } catch (error) { console.log(chalk.yellowBright('Unable to get files from ${branch}.')); console.log(chalk.yellowBright('Will try to use ${downloadDataBranch}...')); jsModulesBranch = downloadDataBranch; tmpfiles = await getFilesFromCDN(`https://${CDN_ADDRESS}/js-module`, jsModulesBranch, `x64_linux`, `update.json`, headers) } for ([file, hash] of Object.entries(tmpfiles)) { linuxFiles[file] = hash; } linuxUpdates.push(`https://${CDN_ADDRESS}/js-module/${jsModulesBranch}/x64_linux/update.json`); jsModulesBranch = branch; try { tmpfiles = await getFilesFromCDN(`https://${CDN_ADDRESS}/js-module`, jsModulesBranch, `x64_win32`, `update.json`, headers) } catch (error) { console.log(chalk.yellowBright('Unable to get files from ${branch}.')); console.log(chalk.yellowBright('Will try to use ${downloadDataBranch}...')); jsModulesBranch = downloadDataBranch; tmpfiles = await getFilesFromCDN(`https://${CDN_ADDRESS}/js-module`, jsModulesBranch, `x64_win32`, `update.json`, headers) } for ([file, hash] of Object.entries(tmpfiles)) { windowsFiles[file] = hash; } windowsUpdates.push(`https://${CDN_ADDRESS}/js-module/${jsModulesBranch}/x64_win32/update.json`); } if (loadBytecodeModule) { res = await fetchJsonData(`https://${CDN_ADDRESS}/js-bytecode-module/${downloadDataBranch}/x64_linux/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { linuxFiles[file] = `https://${CDN_ADDRESS}/js-bytecode-module/${downloadDataBranch}/x64_linux/${file}`; } res = await fetchJsonData(`https://${CDN_ADDRESS}/js-bytecode-module/${downloadDataBranch}/x64_win32/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { windowsFiles[file] = `https://${CDN_ADDRESS}/js-bytecode-module/${downloadDataBranch}/x64_win32/${file}`; } linuxUpdates.push(`https://${CDN_ADDRESS}/js-bytecode-module/${downloadDataBranch}/x64_linux/update.json`); windowsUpdates.push(`https://${CDN_ADDRESS}/js-bytecode-module/${downloadDataBranch}/x64_win32/update.json`); } if (loadCSharpModule) { res = await fetchJsonData(`https://${CDN_ADDRESS}/coreclr-module/${downloadDataBranch}/x64_linux/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { linuxFiles[file] = `https://${CDN_ADDRESS}/coreclr-module/${downloadDataBranch}/x64_linux/${file}`; } res = await fetchJsonData(`https://${CDN_ADDRESS}/coreclr-module/${downloadDataBranch}/x64_win32/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { windowsFiles[file] = `https://${CDN_ADDRESS}/coreclr-module/${downloadDataBranch}/x64_win32/${file}`; } linuxUpdates.push(`https://${CDN_ADDRESS}/coreclr-module/${downloadDataBranch}/x64_linux/update.json`); windowsUpdates.push(`https://${CDN_ADDRESS}/coreclr-module/${downloadDataBranch}/x64_win32/update.json`); } if (loadJSV2Module) { res = await fetchJsonData(`https://${CDN_ADDRESS}/js-module-v2/${downloadDataBranch}/x64_linux/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { linuxFiles[file] = `https://${CDN_ADDRESS}/js-module-v2/${downloadDataBranch}/x64_linux/${file}`; } res = await fetchJsonData(`https://${CDN_ADDRESS}/js-module-v2/${downloadDataBranch}/x64_win32/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { windowsFiles[file] = `https://${CDN_ADDRESS}/js-module-v2/${downloadDataBranch}/x64_win32/${file}`; } linuxUpdates.push(`https://${CDN_ADDRESS}/js-module-v2/${downloadDataBranch}/x64_linux/update.json`); windowsUpdates.push(`https://${CDN_ADDRESS}/js-module-v2/${downloadDataBranch}/x64_win32/update.json`); } if (loadVoiceServer) { res = await fetchJsonData(`https://${CDN_ADDRESS}/voice-server/${branch}/x64_linux/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { linuxFiles[file] = `https://${CDN_ADDRESS}/voice-server/${branch}/x64_linux/${file}`; } res = await fetchJsonData(`https://${CDN_ADDRESS}/voice-server/${branch}/x64_win32/update.json`, { responseType: 'application/json', headers, }); for ([file, hash] of Object.entries(res.hashList)) { windowsFiles[file] = `https://${CDN_ADDRESS}/voice-server/${branch}/x64_win32/${file}`; } linuxUpdates.push(`https://${CDN_ADDRESS}/voice-server/${branch}/x64_linux/update.json`); windowsUpdates.push(`https://${CDN_ADDRESS}/voice-server/${branch}/x64_win32/update.json`); } const [filesUpdate, filesToUse] = platform == 'x64_win32' ? [windowsUpdates, windowsFiles] : [linuxUpdates, linuxFiles]; if (!fs.existsSync(path.join(rootPath, 'data'))) { fs.mkdirSync(path.join(rootPath, 'data')); } if (!fs.existsSync(path.join(rootPath, 'modules'))) { fs.mkdirSync(path.join(rootPath, 'modules')); } console.log(chalk.greenBright('===== Checking file hashes =====')); let filesToDownload = {}; let promises = []; let anyHashRejected = false; for (const url of filesUpdate) { const promise = new Promise(async (resolve, reject) => { /** @type {{ hashList: { [key: string]: string }}} */ const data = await fetchJsonData(url, { responseType: 'application/json', headers }); if (!data) { console.error(chalk.redBright(`Failed to check hash ${url}: ${error}`)); reject(); return; } for (let [file, hash] of Object.entries(data.hashList)) { if (getLocalFileHash(file) === hash) { console.log(chalk.cyanBright('✓'), chalk.whiteBright(file)); continue; } console.log(chalk.redBright('x'), chalk.whiteBright(file)); if (anyHashRejected) { return; } filesToDownload[file] = filesToUse[file]; } resolve(); }); promises.push(promise); } try { await Promise.all(promises); console.log(chalk.greenBright('===== File hash check complete =====')); } catch { console.log(chalk.redBright('===== File hash check corrupted -> download all =====')); filesToDownload = filesToUse; } const shouldIncludeRuntimeConfig = !fs.existsSync('AltV.Net.Host.runtimeconfig.json') && loadCSharpModule; if (Object.keys(filesToDownload).length) { promises = []; console.log(chalk.greenBright('===== Downloading =====')); for (const [file, url] of Object.entries(filesToDownload)) { // Avoid overwriting existing runtimeconfig.json file if (file == 'AltV.Net.Host.runtimeconfig.json' && !shouldIncludeRuntimeConfig) { continue; } console.log(chalk.whiteBright(`${file}`)); const promise = new Promise(async (resolve) => { const response = await fetch(url, { headers }).catch((err) => { return undefined; }); if (!response || !response.ok) { return resolve(); } const body = Readable.fromWeb(response.body); const fullPath = path.join(rootPath, file); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); const writeStream = fs.createWriteStream(fullPath); body.pipe(writeStream); body.on('close', () => { resolve(); }); body.on('error', (err) => { console.log(err); }); }); promises.push(promise); } await Promise.all(promises); } console.log(chalk.greenBright('===== Complete =====')); } function getLocalFileHash(file) { let fileBuffer; try { fileBuffer = fs.readFileSync(path.join(rootPath, file)); } catch { return '_'; } return crypto.createHash('sha1').update(fileBuffer).digest('hex'); } function loadRuntimeConfig() { let loadJSModule = true; let loadBytecodeModule = false; let loadCSharpModule = false; let loadJSV2Module = false; let loadVoiceServer = false; try { const data = fs.readFileSync(`./${RC_FILE_NAME}`, { encoding: 'utf8' }); const parsedData = JSON.parse(data); if (typeof parsedData.loadJSModule !== 'undefined') { loadJSModule = !!parsedData.loadJSModule; } loadBytecodeModule = !!parsedData.loadBytecodeModule; loadCSharpModule = !!parsedData.loadCSharpModule; loadJSV2Module = !!parsedData.loadJSV2Module; loadVoiceServer = !!parsedData.loadVoiceServer; } catch (e) { console.log(chalk.gray(`Configuration file '${RC_FILE_NAME}' could not be read. Continuing without...`)); } return { loadJSModule, loadBytecodeModule, loadCSharpModule, loadJSV2Module, loadVoiceServer }; } start();