Jump to content
Jacobbuck.net

Recommended Posts

  • 3 weeks later...
Posted
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

 

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...