Peercord/Peercord Source/index.js
0% [█ █ █ █ █ █ █ █ █ █] 100% 29e61f07f2 Full source
2026-06-14 21:28:04 -05:00

360 lines
12 KiB
JavaScript

import { app, BrowserWindow, ipcMain, desktopCapturer, protocol, net } from 'electron';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import fs from 'fs';
import { spawn } from 'child_process';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// We need to read package.json for version and upgrade link
const pkgPath = path.join(__dirname, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
// Register custom protocol BEFORE app is ready to ensure localStorage persistence
// FIX: Added stream: true to allow <video> and <audio> tags to stream media properly
protocol.registerSchemesAsPrivileged([
{ scheme: 'peercord', privileges: { standard: true, secure: true, supportFetchAPI: true, bypassCSP: true, corsEnabled: true, stream: true } }
]);
// Force app name and userData path BEFORE app.whenReady()
// This ensures localStorage and user data is identical regardless of which .exe launches the app
app.name = 'Peercord';
const appDataPath = path.join(app.getPath('appData'), 'Peercord');
app.setPath('userData', appDataPath);
// Enforce Single Instance Lock
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
process.exit(0);
}
// Release the Windows directory lock!
if (!fs.existsSync(appDataPath)) {
fs.mkdirSync(appDataPath, { recursive: true });
}
try {
process.chdir(appDataPath);
} catch (e) {
console.error("Failed to change CWD:", e);
}
function getAppDir() {
// Since we disabled ASAR, app.isPackaged will be FALSE even in production!
// We must detect dev mode by checking if the executable is named 'electron'
const execName = path.basename(process.execPath).toLowerCase();
const isDev = execName === 'electron.exe' || execName === 'electron';
if (isDev) return null;
if (process.platform === 'linux' && process.env.APPIMAGE) return process.env.APPIMAGE;
if (process.platform === 'darwin') return path.join(process.resourcesPath, '..', '..');
// Return the DIRECTORY containing the binary for Windows and Linux
// PearRuntime expects the directory to hash and verify against the seeded drive!
return path.dirname(process.execPath);
}
let globalWin = null;
let isWindowReady = false;
let logQueue = [];
// Focus existing window if a second instance is launched
app.on('second-instance', (event, commandLine, workingDirectory) => {
if (globalWin) {
if (globalWin.isMinimized()) globalWin.restore();
globalWin.focus();
}
});
// Custom logger to pipe Main Process logs to the React F10 Console
function logToRenderer(level, ...args) {
const formattedArgs = args.map(a => {
if (a instanceof Error) return `${a.name}: ${a.message}\n${a.stack}`;
if (typeof a === 'object') {
try { return JSON.stringify(a, Object.getOwnPropertyNames(a)); } catch(e) { return String(a); }
}
return String(a);
});
console[level](...formattedArgs);
// Queue logs if the React window hasn't finished loading yet!
if (globalWin && isWindowReady && !globalWin.isDestroyed()) {
globalWin.webContents.send('main-log', { level, args: formattedArgs });
} else {
logQueue.push({ level, args: formattedArgs });
}
}
// Flush the log queue the millisecond React says it's ready
ipcMain.on('renderer-ready', () => {
isWindowReady = true;
logQueue.forEach(log => {
if (globalWin && !globalWin.isDestroyed()) {
globalWin.webContents.send('main-log', log);
}
});
logQueue = [];
});
async function boot() {
// Handle Squirrel.Windows startup events to prevent multiple background launches
if (process.platform === 'win32') {
const cmd = process.argv[1];
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated' || cmd === '--squirrel-uninstall' || cmd === '--squirrel-obsolete') {
app.quit();
return;
}
}
// Prevents "GPU process exited unexpectedly: exit_code=1" on Windows
app.disableHardwareAcceleration();
await app.whenReady();
const appPath = app.getAppPath();
// Handle custom protocol for consistent localStorage origin
// This prevents Linux from wiping localStorage when the executable path changes
protocol.handle('peercord', (request) => {
// Safely serve local files (images/videos) bypassing Electron's file:// restrictions
if (request.url.startsWith('peercord://local/')) {
let filePath = decodeURIComponent(request.url.replace('peercord://local/', ''));
if (process.platform === 'win32' && filePath.startsWith('/')) {
filePath = filePath.substring(1); // Remove leading slash on Windows
}
return net.fetch(pathToFileURL(filePath).href);
}
let url = request.url.replace('peercord://app/', '');
url = url.split('?')[0].split('#')[0];
url = decodeURIComponent(url);
if (url.startsWith('/')) url = url.substring(1);
let filePath = path.join(appPath, url);
// Fallback to dist/ if not found (helps with absolute paths in index.html)
if (!fs.existsSync(filePath)) {
const distPath = path.join(appPath, 'dist', url);
if (fs.existsSync(distPath)) filePath = distPath;
}
return net.fetch(pathToFileURL(filePath).href);
});
// Dynamically select the correct icon format based on the OS
const iconFile = process.platform === 'win32' ? 'icon.ico' : 'icon.png';
const win = new BrowserWindow({
width: 1100,
height: 750,
title: "Peercord",
icon: path.join(appPath, 'assets', iconFile),
frame: false,
resizable: true,
maximizable: true,
thickFrame: true,
backgroundColor: '#313338',
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
globalWin = win;
// Load via custom protocol instead of file:// to prevent localStorage wipes on Linux
win.loadURL('peercord://app/dist/index.html');
// ALLOW F12 TO OPEN DEVTOOLS
win.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12') {
win.webContents.toggleDevTools();
event.preventDefault();
}
});
// Bridge for TitleBar.jsx custom controls
ipcMain.on('window-action', (event, action) => {
if (action === 'minimize') win.minimize();
if (action === 'maximize') win.isMaximized() ? win.restore() : win.maximize();
if (action === 'close') win.close();
});
// Sync window state back to React for the maximize/restore icon
win.on('maximize', () => win.webContents.send('window-state-changed', true));
win.on('unmaximize', () => win.webContents.send('window-state-changed', false));
// Bridge for ScreenShareModal.jsx desktop sources
ipcMain.handle('get-desktop-sources', async () => {
const sources = await desktopCapturer.getSources({
types:['window', 'screen'],
thumbnailSize: { width: 320, height: 180 }
});
return sources.map(s => ({
id: s.id,
name: s.name,
thumbnailDataURL: s.thumbnail.toDataURL()
}));
});
// ---------------------------------------------------------
// PEAR OTA UPDATER LOGIC
// ---------------------------------------------------------
// Initialize Embedded Pear Runtime AFTER window creation so logs are queued properly
let pear = null;
try {
const { default: PearRuntime } = await import('pear-runtime');
const resolvedAppDir = getAppDir();
logToRenderer('info', '[Pear] Resolved App Directory for Updater:', resolvedAppDir);
pear = new PearRuntime({
...pkg, // Spread pkg to ensure updates: true is passed
dir: path.join(app.getPath('userData'), 'pear-data'),
app: resolvedAppDir
});
pear.on('error', (err) => logToRenderer('error', '[Pear Error]', err));
} catch (e) {
logToRenderer('error', '[Pear] Failed to initialize PearRuntime (Likely missing native modules for this OS. Did you build Linux on Windows?):', e.message, e.stack);
}
if (pear && pear.updater) {
logToRenderer('info', '[Pear] Updater initialized. Current version:', pkg.version);
pear.updater.on('updating', () => {
logToRenderer('info', '[Pear] updating event fired. Downloading update...');
if (win && !win.isDestroyed()) win.webContents.send('pear-updating');
});
pear.updater.on('updated', () => {
logToRenderer('info', '[Pear] updated event fired. Update downloaded and ready.');
if (win && !win.isDestroyed()) win.webContents.send('pear-updated');
});
pear.updater.on('error', (err) => {
logToRenderer('error', '[Pear] Updater Error:', err);
if (win && !win.isDestroyed()) {
win.webContents.send('pear-error', err instanceof Error ? err.message : String(err));
}
});
} else {
logToRenderer('warn', '[Pear] Updater not available on pear object');
}
// Safe restart for Gossip protocol (reboots to find new seeder)
ipcMain.on('normal-restart', () => {
app.relaunch();
app.quit();
});
ipcMain.on('apply-update', async () => {
logToRenderer('info', '[Pear] apply-update requested by renderer');
try {
const baseDir = getAppDir();
if (!baseDir) throw new Error("Cannot apply update in dev mode");
const nextDir = path.join(appDataPath, 'pear-data', 'pear-runtime', 'next');
if (!fs.existsSync(nextDir)) throw new Error("Update cache directory not found");
const versions = fs.readdirSync(nextDir).filter(v => fs.statSync(path.join(nextDir, v)).isDirectory());
if (versions.length === 0) throw new Error("No downloaded updates found");
versions.sort((a, b) => parseFloat(b) - parseFloat(a));
const latestVersion = versions[0];
const archDir = `${process.platform}-${process.arch}`;
const updateAppPath = path.join(nextDir, latestVersion, 'by-arch', archDir, 'app', 'peercord');
if (!fs.existsSync(updateAppPath)) throw new Error(`Update files not found at ${updateAppPath}`);
if (process.platform === 'win32') {
logToRenderer('info', '[Pear] Windows detected. Using detached script to bypass OS file locks...');
const batPath = path.join(app.getPath('temp'), `peercord-update-${Date.now()}.bat`);
const vbsPath = path.join(app.getPath('temp'), `peercord-update-${Date.now()}.vbs`);
const batContent = `
@echo off
:wait
tasklist /FI "PID eq ${process.pid}" /NH | findstr /C:"${process.pid}" > nul
if %ERRORLEVEL% == 0 (
timeout /t 1 /nobreak > nul
goto wait
)
xcopy /E /Y /I /H /C "${updateAppPath}\\*" "${baseDir}\\"
start "" "${process.execPath}"
del "%~1"
del "%~f0"
`;
const vbsContent = `
Dim WshShell
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run "cmd.exe /c """"" & WScript.Arguments(0) & """ """ & WScript.Arguments(1) & """""", 0, False
`;
fs.writeFileSync(batPath, batContent);
fs.writeFileSync(vbsPath, vbsContent);
const child = spawn('wscript.exe', [vbsPath, batPath, vbsPath], {
detached: true,
stdio: 'ignore',
windowsHide: true
});
child.unref();
logToRenderer('info', '[Pear] Detached script spawned. Quitting app to allow swap...');
app.quit();
} else {
logToRenderer('info', '[Pear] macOS/Linux detected. Using detached bash script for reliable directory swap...');
const shPath = path.join(app.getPath('temp'), `peercord-update-${Date.now()}.sh`);
const shContent = `#!/bin/bash
# Wait for the Electron process to exit
while kill -0 ${process.pid} 2>/dev/null; do
sleep 0.5
done
# Copy the new app directory contents into place, overwriting existing files
cp -rf "${updateAppPath}/." "${baseDir}/"
# Ensure the new binary is executable
chmod -R 755 "${baseDir}"
# Launch the new app
"${process.execPath}" &
# Delete this script
rm "$0"
`;
fs.writeFileSync(shPath, shContent);
fs.chmodSync(shPath, '755');
const child = spawn(shPath, [], {
detached: true,
stdio: 'ignore'
});
child.unref();
logToRenderer('info', '[Pear] Detached script spawned. Quitting app to allow swap...');
app.quit();
}
} catch (err) {
logToRenderer('error', '[Pear] Failed to apply update:', err);
if (globalWin && !globalWin.isDestroyed()) {
globalWin.webContents.send('pear-error', err instanceof Error ? err.message : String(err));
}
}
});
}
boot().catch(console.error);