jorge Posted Sunday at 08:36 PM Posted Sunday at 08:36 PM const { app, clipboard, dialog, ipcMain, protocol, shell, BrowserWindow, nativeImage, Menu, } = require('electron'); const shortcut = require('electron-localshortcut'); const Store = require('electron-store'); Store.initRenderer(); const config = new Store(); const path = require('path'); const { autoUpdate } = require('./features'); const https = require('https'); const log = require('electron-log'); const fse = require('fs-extra'); const md5File = require('md5-file'); const fs = require('fs'); const { checkFileExists } = require('./features/const'); Menu.setApplicationMenu(null); const launcherMode = config.get('launcherMode', true); const performanceMode = config.get('performanceMode', false); const gamePreload = path.join(__dirname, 'preload', 'global.js'); const settingsPreload = path.join(__dirname, 'preload', 'settings.js'); const launcherPreload = path.join(__dirname, 'preload', 'launcher.js'); let JSZip, pluginLoader; if (!performanceMode || launcherMode) { JSZip = require('jszip'); pluginLoader = require('./features/plugins').pluginLoader; } process.env.ELECTRON_ENABLE_LOGGING = '1'; log.info(` ------------------------------------------ Starting KirkaClient ${app.getVersion()}. Epoch Time: ${Date.now()} | ${(new Date()).toString()} User: ${config.get('user')} UserID: ${config.get('userID')} Directory: ${__dirname} Electron Version: ${process.versions.electron} Chromium Version: ${process.versions.chrome} `); let mainWindow; let settingsWindow; let launcherWindow; let launchMainClient = false; let CtrlW = false; const allowedScripts = []; const installedPlugins = []; const scriptCol = []; const pluginIdentifier = {}; const pluginIdentifier2 = {}; let pluginsLoaded = false; const icons = { linux: path.join(__dirname, 'media', 'icon.png'), win32: path.join(__dirname, 'media', 'icon.ico'), darwin: path.join(__dirname, 'media', 'icon.icns') }; const icon = icons[process.platform]; protocol.registerSchemesAsPrivileged([{ scheme: 'kirkaclient', privileges: { secure: true, corsEnabled: true }, }]); if (config.get('unlimitedFPS', false) && !launcherMode) { app.commandLine.appendSwitch('disable-frame-rate-limit'); app.commandLine.appendSwitch('disable-gpu-vsync'); } app.commandLine.appendSwitch('ignore-gpu-blacklist'); app.allowRendererProcessReuse = true; async function askUserToUpdate() { const options = { type: 'info', title: 'Update Available', message: 'KirkaClient has been completely rewritten, and is a lot faster and better. Please download the new ' + 'version from https://client.kirka.io. Click Ok to continue to the download page.', buttons: ['Ok'] }; await dialog.showMessageBox(options); await shell.openExternal('https://client.kirka.io'); app.quit(); } async function createWindow() { log.info('Creating main window'); mainWindow = new BrowserWindow({ width: 1280, height: 720, backgroundColor: '#000000', titleBarStyle: 'hidden', show: true, title: `KirkaClient v${app.getVersion()}`, acceptFirstMouse: true, icon: nativeImage.createFromPath(icon), webPreferences: { preload: gamePreload, devTools: !app.isPackaged }, }); createShortcutKeys(); await initAutoUpdater(mainWindow.webContents); mainWindow.on('close', function(e) { if (CtrlW) { e.preventDefault(); CtrlW = false; return; } app.quit(); }); if (config.get('fullScreenStart', true)) mainWindow.setFullScreen(true); mainWindow.webContents.on('new-window', (e, url) => { e.preventDefault(); mainWindow.loadURL(url); }); await mainWindow.loadURL('https://kirka.io/'); } function createShortcutKeys() { const contents = mainWindow.webContents; shortcut.register(mainWindow, 'Escape', () => contents.executeJavaScript('document.exitPointerLock()', true)); shortcut.register(mainWindow, 'F4', () => clipboard.writeText(contents.getURL())); shortcut.register(mainWindow, 'F5', () => contents.reload()); shortcut.register(mainWindow, 'Shift+F5', () => contents.reloadIgnoringCache()); shortcut.register(mainWindow, 'F6', () => joinByURL()); shortcut.register(mainWindow, 'F8', () => mainWindow.loadURL('https://kirka.io/')); shortcut.register(mainWindow, 'F11', () => mainWindow.setFullScreen(!mainWindow.isFullScreen())); // electronLocalshortcut.register(win, 'Control+Alt+C', () => clearCache()); if (config.get('controlW', true)) shortcut.register(mainWindow, 'Control+W', () => { CtrlW = true; }); } async function createLauncherWindow() { log.info('creating launcher window'); log.info('launcher preload', launcherPreload); log.info('icon', icon); launcherWindow = new BrowserWindow({ width: 1280, height: 720, backgroundColor: '#000000', show: true, title: 'KirkaClient Launcher', icon: nativeImage.createFromPath(icon), webPreferences: { preload: launcherPreload, devTools: !app.isPackaged }, }); await launcherWindow.loadFile(path.join(__dirname, 'launcher/launcher.html')); launcherWindow.webContents.openDevTools(); await initPlugins(launcherWindow.webContents); await initAutoUpdater(launcherWindow.webContents); ipcMain.on('launchClient', () => { launchMainClient = true; app.quit(); }); ipcMain.on('launchSettings', createSettings); const req = https.get('https://client.kirka.io/changelogs', (res) => { res.setEncoding('utf8'); let rawData = ''; res.on('data', (chunk) => { rawData += chunk; }); res.on('end', async() => { try { const changelog = JSON.parse(rawData); launcherWindow.webContents.send('changeLogs', changelog); } catch (e) { log.error(e.message); } }); }); req.on('error', (e) => { log.error(e.message); }); req.end(); } ipcMain.on('joinLink', joinByURL); async function joinByURL() { const urld = clipboard.readText(); if (urld.startsWith('https://kirka.io/games/')) await mainWindow.loadURL(urld); } app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); async function initAutoUpdater(webContents) { const req = https.get('https://client.kirka.io/api/v4', (res) => { res.setEncoding('utf8'); let rawData = ''; res.on('data', (chunk) => { rawData += chunk; }); res.on('end', async() => { try { const updateContent = JSON.parse(rawData); const didUpdate = await autoUpdate(webContents, updateContent); log.info(didUpdate); if (didUpdate) { config.set('update', true); const options = { buttons: ['Ok'], message: 'Update Complete! Client will now restart.' }; await dialog.showMessageBox(options); rebootClient(); } } catch (e) { log.error(e.message); } }); }); req.on('error', (e) => { log.error(e.message); }); req.end(); } ipcMain.on('show-settings', async function() { if (settingsWindow) { settingsWindow.focus(); return; } await createSettings(); }); ipcMain.on('reboot', () => { rebootClient(); }); ipcMain.on('installedPlugins', (ev) => { ev.returnValue = JSON.stringify(installedPlugins); }); ipcMain.handle('allowedScripts', () => { return JSON.stringify(pluginIdentifier); }); ipcMain.handle('scriptPath', () => { return path.join(app.getPath('appData'), '/kirkaclient/plugins'); }); ipcMain.handle('ensureIntegrity', async function() { await ensureIntegrity(); return JSON.stringify(allowedScripts); }); ipcMain.handle('canLoadPlugins', () => { return pluginsLoaded; }); ipcMain.handle('downloadPlugin', async(ev, uuid) => { log.info('[PLUGINS] Need to download', uuid); return await downloadPlugin(uuid); }); ipcMain.handle('uninstallPlugin', async(ev, uuid) => { log.info('[PLUGINS] Need to remove', uuid); if (!pluginIdentifier[uuid]) return { success: false }; const scriptPath = pluginIdentifier[uuid][1]; await fse.remove(scriptPath); installedPlugins.splice(installedPlugins.indexOf(uuid), 1); return { success: true }; }); ipcMain.handle('ask-confirmation', async(ev, title, message, details) => { const response = await dialog.showMessageBox({ title: title, message: message, detail: details, type: 'question', buttons: ['Ok', 'Cancel'], defaultId: 0, cancelId: 1 }); return response.response; }); ipcMain.handle('getDirectories', async(ev, source) => { return await getDirectories(source); }); async function installUpdate(pluginPath, uuid) { try { await fse.remove(pluginPath); } catch (e) { log.info(e); } await downloadPlugin(uuid); } async function unzipFile(zip) { const fileBuffer = await fse.readFile(zip); const pluginPath = path.join(app.getPath('appData'), '/kirkaclient/plugins'); const newZip = new JSZip(); const contents = await newZip.loadAsync(fileBuffer); for (const filename of Object.keys(contents.files)) { const content = await newZip.file(filename).async('nodebuffer'); const dest = path.join(pluginPath, filename); await fse.ensureDir(path.dirname(dest)); await fse.writeFile(dest, content); } } async function downloadPlugin(uuid) { return await new Promise(resolve => { const req = https.get(`https://client.kirka.io/api/v4/plugins/download/${uuid}?token=${encodeURIComponent(config.get('devToken'))}`, (res) => { res.setEncoding('binary'); let chunks = ''; log.info(`[PLUGINS] Update GET code: ${res.statusCode}`); if (res.statusCode !== 200) return resolve(false); const filename = res.headers['filename']; res.on('data', (chunk) => { chunks += chunk; }); res.on('end', async() => { try { const pluginsDir = path.join(app.getPath('appData'), '/kirkaclient/plugins/', filename); await fse.writeFile(pluginsDir, chunks, 'binary'); await unzipFile(pluginsDir); await fse.remove(pluginsDir); log.info(`[PLUGINS] File ${filename} downloaded`); resolve(true); } catch (e) { log.error(e); resolve(false); } }); }); req.on('error', error => { log.error(`[PLUGINS] Download Error: ${error}`); resolve(false); }); req.end(); }); } function ensureScriptIntegrity(filePath, scriptUUID) { if (!app.isPackaged) return { success: true }; return new Promise((resolve, reject) => { const hash = md5File.sync(filePath); const data = { hash: hash, uuid: scriptUUID }; const request = { method: 'POST', hostname: 'client.kirka.io', path: '/api/v4/plugins/updates', headers: { 'Content-Type': 'application/json' }, }; const req = https.request(request, res => { res.setEncoding('utf-8'); let chunks = ''; log.info(`[PLUGINS] Integrity check POST: ${res.statusCode} with payload ${JSON.stringify(data)}`); if (res.statusCode !== 200) { if (!app.isPackaged) resolve({ success: false }); else reject(); } else { res.on('data', (chunk) => { chunks += chunk; }); res.on('end', () => { const response = JSON.parse(chunks); const success = response.success; log.info(`Response on ${scriptUUID}: ${JSON.stringify(response, null, 2)}`); if (!success) reject(); resolve(response); }); } }); req.on('error', error => { log.error(`POST Error: ${error}`); reject(); }); req.write(JSON.stringify(data)); req.end(); }); } async function ensureIntegrity() { const oldAllowed = allowedScripts; allowedScripts.length = 0; const fileDir = path.join(app.getPath('appData'), 'kirkaclient', 'plugins'); await fse.ensureDir(fileDir); for (const scriptPath in oldAllowed) { try { const scriptUUID = pluginIdentifier2[scriptPath]; await ensureScriptIntegrity(scriptPath, scriptUUID); allowedScripts.push(scriptPath); log.info(`Ensured script: ${scriptPath}`); } catch (err) { log.info(err); } } } async function copyFolder(from, to, webContents) { let files; try { await fse.ensureDir(to); } catch (err) { log.info('[Copy Folder Error]:', err); log.info('from:', from, 'to:', to); return; } try { files = await fs.promises.readdir(from); } catch (err) { log.info(err); log.info(from, to); return; } for (const file of files) { const fromPath = path.join(from, file); const toPath = path.join(to, file); const stat = await fse.stat(fromPath); if (stat.isDirectory()) await copyFolder(fromPath, toPath, webContents); else { try { await fse.copyFile(fromPath, toPath); } catch (err) { log.info(err); } } } } async function copyNodeModules(srcDir, node_modules, incomplete_init, webContents) { // if (!app.isPackaged) // return; try { await fse.remove(node_modules); } catch (err) { log.info(err); } await fse.mkdir(node_modules, { recursive: true }); await fse.writeFile(incomplete_init, 'DO NOT DELETE THIS!'); log.info('copying from', srcDir, 'to', node_modules); webContents.send('copying'); await copyFolder(srcDir, node_modules, webContents); log.info('copying done'); webContents.send('copyProgress'); await fse.unlink(incomplete_init); } async function getDirectories(source) { return (await fse.readdir(source, { withFileTypes: true })) .filter(dirent => dirent.isDirectory() && dirent.name !== 'node_modules') .map(dirent => dirent.name); } async function initPlugins(webContents) { const fileDir = path.join(app.getPath('appData'), 'kirkaclient', 'plugins'); log.info('fileDir', fileDir); const node_modules = path.join(fileDir, 'node_modules'); const srcDir = path.join(__dirname, '../node_modules'); const incomplete_init = path.join(fileDir, 'node_modules.lock'); try { await fse.mkdir(fileDir); } catch (err) { log.info(err); } if (!await checkFileExists(node_modules) || await checkFileExists(incomplete_init)) { webContents.send('message', 'Configuring Plugins...'); await copyNodeModules(srcDir, node_modules, incomplete_init, webContents); } log.info('node_modules stuff done.'); log.info(await fse.readdir(fileDir)); const filenames = []; // get all directories inside a direcotry const dirs = await getDirectories(fileDir); log.info(dirs); for (const dir of dirs) { log.info(dir); const packageFile = path.join(fileDir, dir, 'package.json'); if (await checkFileExists(packageFile)) { const packageJson = JSON.parse((await fse.readFile(packageFile)).toString()); filenames.push([dir, packageJson]); } else log.info('No package.json'); } log.info('filenames', filenames); if (filenames.length === 0) webContents.send('pluginProgress', 0, 0, 0); let count = 0; for (const [dir, packageJson] of filenames) { try { count += 1; const pluginName = packageJson.name; const pluginPath = path.join(fileDir, dir); const scriptPath = path.join(pluginPath, packageJson.main); const pluginUUID = packageJson.uuid; const pluginVer = packageJson.version; webContents.send('message', `Loading ${pluginName} v${pluginVer} (${count}/${filenames.length})`); log.info('scriptPath:', scriptPath); const data = await ensureScriptIntegrity(scriptPath, pluginUUID); log.debug(data); if (data) { if (data.update) { webContents.send('message', 'Updating Plugin'); await installUpdate(pluginPath, pluginUUID); webContents.send('message', `Reloading Plugin: ${count}/${filenames.length}`); } } log.debug(packageJson); let script = await pluginLoader(pluginUUID, dir, packageJson); if (Array.isArray(script)) { webContents.send('message', 'Cache corrupted. Rebuilding...'); await copyNodeModules(srcDir, node_modules, incomplete_init, webContents); script = await pluginLoader(pluginUUID, dir, packageJson, false, true); } if (Array.isArray(script)) continue; if (!script.isPlatformMatching()) log.info(`Script ignored, platform not matching: ${script.scriptName}`); else { allowedScripts.push(scriptPath); installedPlugins.push(script.scriptUUID); pluginIdentifier[script.scriptUUID] = [script.scriptName, pluginPath]; pluginIdentifier2[script.scriptName] = script.scriptUUID; scriptCol.push(script); try { log.debug('[PLUGIN]:', script.scriptName, 'launching main'); script.launchMain(mainWindow); } catch (err) { log.info(err); dialog.showErrorBox(`Error in ${script.scriptName} Plugin. Uninstall it to skip this dialog.`, err); } log.info(`Loaded script: ${script.scriptName}- v${script.version}`); webContents.send('pluginProgress', filenames.length, count, ((count / filenames.length) * 100).toFixed(0)); } } catch (err) { log.info(err); } } pluginsLoaded = true; } async function createSettings() { settingsWindow = new BrowserWindow({ width: 1280, height: 720, show: true, frame: true, icon: nativeImage.createFromPath(icon), title: 'KirkaClient Settings', webPreferences: { nodeIntegration: true, contextIsolation: false, preload: settingsPreload, devTools: !app.isPackaged, } }); settingsWindow.removeMenu(); settingsWindow.webContents.openDevTools(); settingsWindow.on('close', () => { settingsWindow = null; }); await settingsWindow.loadFile(path.join(__dirname, 'settings/settings.html')); } function rebootClient() { app.relaunch(); app.quit(); } app.once('ready', async function() { if (!config.has('terms')) { const res = await dialog.showMessageBox({ type: 'info', title: 'Terms of Service', message: 'By using this client, you agree to our terms and services.\nThey can be found at https://client.kirka.io/terms', buttons: ['I agree', 'I disagree'], }); if (res.response === 1) app.quit(); else config.set('terms', true); } if (process.versions.electron !== '10.4.7' && process.platform === 'win32') return askUserToUpdate(); if (launcherMode) { log.info('Launcher mode'); await createLauncherWindow(); } else { log.info('Client mode'); await createWindow(); } }); app.on('will-quit', function() { if (launcherMode && launchMainClient) { config.set('launcherMode', false); log.info('Rebooting'); rebootClient(); } else { config.set('launcherMode', true); log.info('Quitting'); app.quit(); } }); // https://github.com/nodejs/node-gyp/issues/2673#issuecomment-1239619438 Quote
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.