Files
DiscordClone/Frontend/Electron/main.cjs
2026-02-10 19:17:51 -06:00

296 lines
11 KiB
JavaScript

const { app, BrowserWindow, ipcMain, shell } = require('electron');
const path = require('path');
const https = require('https');
const http = require('http');
const { checkForUpdates } = require('./updater.cjs');
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
frame: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.cjs'),
sandbox: false
}
});
const isDev = process.env.npm_lifecycle_event === 'electron:dev';
if (isDev) {
win.loadURL('http://localhost:5173');
win.webContents.openDevTools();
} else {
// Production: Load the built file
// dist-react is in the same directory as main.cjs
win.loadFile(path.join(__dirname, 'dist-react', 'index.html'));
}
}
function createSplashWindow() {
const splash = new BrowserWindow({
width: 300,
height: 350,
frame: false,
resizable: false,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
splash.loadFile(path.join(__dirname, 'splash.html'));
return splash;
}
app.whenReady().then(async () => {
const isDev = !app.isPackaged;
if (isDev) {
createWindow();
} else {
const splash = createSplashWindow();
const noUpdate = await checkForUpdates(splash);
if (noUpdate === false) {
if (!splash.isDestroyed()) splash.close();
createWindow();
}
// If update downloaded, quitAndInstall handles restart
}
ipcMain.on('window-minimize', () => {
const win = BrowserWindow.getFocusedWindow();
if (win) win.minimize();
});
ipcMain.on('window-maximize', () => {
const win = BrowserWindow.getFocusedWindow();
if (win) {
if (win.isMaximized()) win.unmaximize();
else win.maximize();
}
});
ipcMain.on('window-close', () => {
const win = BrowserWindow.getFocusedWindow();
if (win) win.close();
});
// Helper to fetch metadata (Zero-Knowledge: Client fetches previews)
ipcMain.handle('fetch-metadata', async (event, url) => {
return new Promise((resolve) => {
// Check for direct video links to avoid downloading large files
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov'];
const imageExtensions = ['.gif', '.png', '.jpg', '.jpeg', '.webp'];
const lowerUrl = url.toLowerCase();
if (videoExtensions.some(ext => lowerUrl.endsWith(ext))) {
const filename = url.split('/').pop();
resolve({
title: filename,
siteName: new URL(url).hostname,
video: url,
image: null, // No thumbnail for now unless we generate one
description: 'Video File'
});
return;
}
if (imageExtensions.some(ext => lowerUrl.endsWith(ext))) {
const filename = url.split('/').pop();
resolve({
title: filename,
siteName: new URL(url).hostname,
video: null,
image: url, // Direct image/gif
description: 'Image File'
});
return;
}
const client = url.startsWith('https') ? https : http;
const req = client.get(url, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
// Simple Regex Parser for OG Tags to avoid dependencies
const getMeta = (prop) => {
const regex = new RegExp(`<meta\\s+(?:name|property)=["'](?:og:)?${prop}["']\\s+content=["'](.*?)["']`, 'i');
const match = data.match(regex);
return match ? match[1] : null;
};
const getTitle = () => {
const regex = /<title>(.*?)<\/title>/i;
const match = data.match(regex);
return match ? match[1] : null;
};
const metadata = {
title: getMeta('title') || getTitle(),
description: getMeta('description'),
image: getMeta('image'),
siteName: getMeta('site_name'),
themeColor: getMeta('theme-color')
};
resolve(metadata);
});
});
req.on('error', (err) => {
console.error('Metadata fetch error:', err);
resolve(null);
});
});
});
ipcMain.handle('open-external', async (event, url) => {
await shell.openExternal(url);
});
ipcMain.handle('get-screen-sources', async () => {
const { desktopCapturer } = require('electron');
const sources = await desktopCapturer.getSources({
types: ['window', 'screen'],
thumbnailSize: { width: 450, height: 250, borderRadius: '8px' },
fetchWindowIcons: true
});
return sources.map(source => ({
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL(),
appIcon: source.appIcon ? source.appIcon.toDataURL() : null
}));
});
// Crypto Handlers
const crypto = require('crypto');
ipcMain.handle('generate-keys', async () => {
const generateRSA = () => new Promise((resolve, reject) => {
crypto.generateKeyPair('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
}, (err, pub, priv) => { /* args are (err, publicKey, privateKey) */
if (err) reject(err); else resolve({ pub, priv });
});
});
const generateEd = () => new Promise((resolve, reject) => {
crypto.generateKeyPair('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
}, (err, pub, priv) => {
if (err) reject(err); else resolve({ pub, priv });
});
});
const [rsa, ed] = await Promise.all([generateRSA(), generateEd()]);
return {
rsaPub: rsa.pub,
rsaPriv: rsa.priv,
edPub: ed.pub,
edPriv: ed.priv
};
});
ipcMain.handle('random-bytes', (event, size) => {
return crypto.randomBytes(size).toString('hex');
});
ipcMain.handle('sha256', (event, data) => {
return crypto.createHash('sha256').update(data).digest('hex');
});
ipcMain.handle('sign-message', (event, privateKeyPem, message) => {
// message should be a string or buffer
return crypto.sign(null, Buffer.from(message), privateKeyPem).toString('hex');
});
ipcMain.handle('verify-signature', (event, publicKeyPem, message, signature) => {
return crypto.verify(null, Buffer.from(message), publicKeyPem, Buffer.from(signature, 'hex'));
});
ipcMain.handle('derive-auth-keys', (event, password, salt) => {
return new Promise((resolve, reject) => {
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) reject(err);
else {
const dak = derivedKey.subarray(0, 32).toString('hex');
const dek = derivedKey.subarray(32, 64);
resolve({ dak, dek });
}
});
});
});
ipcMain.handle('public-encrypt', (event, publicKeyPem, data) => {
// data can be string or buffer. Returns hex.
const buffer = Buffer.from(data);
return crypto.publicEncrypt({
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
}, buffer).toString('hex');
});
ipcMain.handle('private-decrypt', (event, privateKeyPem, encryptedHex) => {
const buffer = Buffer.from(encryptedHex, 'hex');
return crypto.privateDecrypt({
key: privateKeyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
}, buffer).toString(); // Assuming utf8 string output
});
ipcMain.handle('encrypt-data', (event, plaintext, key) => {
console.log('encrypt-data called with:', {
plaintextType: typeof plaintext,
isBuffer: Buffer.isBuffer(plaintext),
keyType: typeof key,
keyIsBuffer: Buffer.isBuffer(plaintext)
});
if (plaintext === undefined) throw new TypeError('plaintext is undefined');
if (key === undefined) throw new TypeError('key is undefined');
// Key can be hex string or buffer. 32 bytes for AES-256
const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
// Use Buffer concatenation to handle both string and Buffer inputs correctly without encoding confusion
const encryptedBuffer = Buffer.concat([
cipher.update(plaintext),
cipher.final()
]);
const tag = cipher.getAuthTag().toString('hex');
return { content: encryptedBuffer.toString('hex'), iv: iv.toString('hex'), tag };
});
ipcMain.handle('decrypt-data', (event, ciphertext, key, iv, tag, options = {}) => {
const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
const ivBuffer = Buffer.from(iv, 'hex');
const tagBuffer = Buffer.from(tag, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer);
decipher.setAuthTag(tagBuffer);
const outputEncoding = options.encoding || 'utf8';
// If encoding is 'buffer', don't pass an encoding to update/final
const updateEncoding = outputEncoding === 'buffer' ? undefined : outputEncoding;
let decrypted;
if (outputEncoding === 'buffer') {
decrypted = Buffer.concat([
decipher.update(ciphertext, 'hex'),
decipher.final()
]);
} else {
decrypted = decipher.update(ciphertext, 'hex', outputEncoding);
decrypted += decipher.final(outputEncoding);
}
return decrypted;
});
});