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

393 lines
13 KiB
JavaScript

const b4a = window.require('b4a');
const crypto = window.require('crypto');
const Hyperswarm = window.require('hyperswarm');
export async function initNetwork() {
// Kept for legacy compatibility if imported elsewhere
}
class P2PNetwork {
constructor() {
this.swarm = null;
this.peers = new Set();
this.onPeerConnect = null;
this.onPeerDisconnect = null;
}
async initialize() {
try {
this.swarm = new Hyperswarm();
this.swarm.on('connection', (conn, info) => {
const peerKey = b4a.toString(info.publicKey, 'hex');
this.peers.add(peerKey);
if (this.onPeerConnect) this.onPeerConnect(peerKey);
conn.on('close', () => {
this.peers.delete(peerKey);
if (this.onPeerDisconnect) this.onPeerDisconnect(peerKey);
});
conn.on('data', (data) => {
console.log(`Received data from ${peerKey}:`, data.toString());
});
});
console.log('P2P Network Initialized');
} catch (err) {
console.error('Failed to initialize Hyperswarm.', err);
}
}
async joinGlobalServer() {
if (!this.swarm) return;
const globalTopicSeed = crypto.createHash('sha256').update('GLOBAL_MAIN_SERVER_V1').digest();
const discovery = this.swarm.join(globalTopicSeed, { client: true, server: true });
await discovery.flushed();
console.log('Joined Global Main Server Swarm');
}
}
export const networkLegacy = new P2PNetwork();
--- START OF FILE src/p2p/modules/discovery.js ---
const b4a = window.require('b4a');
import { generateUUID, sodium } from '../utils.js';
export async function searchUser(network, targetUsername) {
const normalized = targetUsername.toLowerCase();
if (network.userDirectory.has(normalized)) {
return network.userDirectory.get(normalized);
}
const topic = b4a.alloc(32);
sodium.crypto_generichash(topic, b4a.from('peercord-user:' + normalized));
network.swarm.join(topic, { client: true, server: false });
return new Promise((resolve) => {
let resolved = false;
const finish = (result) => {
if (resolved) return;
resolved = true;
network.swarm.leave(topic);
resolve(result);
};
const timeout = setTimeout(() => {
finish(null);
}, 5000);
const interval = setInterval(() => {
if (network.userDirectory.has(normalized)) {
clearTimeout(timeout);
clearInterval(interval);
finish(network.userDirectory.get(normalized));
}
}, 500);
const queryId = generateUUID();
network.pendingWhois.set(queryId, (result) => {
clearTimeout(timeout);
clearInterval(interval);
finish(result);
});
const msg = b4a.from(JSON.stringify({ type: 'whois', queryId, username: normalized }));
for (const { conn } of network.peers.values()) {
conn.write(msg);
}
});
}
export async function queueFriendRequest(network, targetUsername) {
const uname = targetUsername.toLowerCase();
network.pendingFriendRequests.add(uname);
await network.pendingRequestsDb.put(uname, { timestamp: Date.now() });
const topic = b4a.alloc(32);
sodium.crypto_generichash(topic, b4a.from('peercord-user:' + uname));
network.swarm.join(topic, { client: true, server: false });
}
export async function trackPeerCore(network, coreKeyHex) {
if (network.peerCores.has(coreKeyHex)) return;
const core = network.store.get({ key: b4a.from(coreKeyHex, 'hex'), valueEncoding: 'json' });
await core.ready();
network.peerCores.set(coreKeyHex, core);
let processedSeq = -1;
for (let i = 0; i < core.length; i++) {
const msg = await core.get(i);
network.processMessage(msg);
processedSeq = i;
}
core.on('append', async () => {
network._emitSync();
for (let i = processedSeq + 1; i < core.length; i++) {
const msg = await core.get(i);
network.processMessage(msg);
processedSeq = i;
}
});
}
--- START OF FILE src/p2p/modules/files.js ---
const b4a = window.require('b4a');
import { generateUUID, fs, path, os } from '../utils.js';
async function _hostFile(network, id, fileObj, fileCore) {
network.transfers[id] = { progress: 0, speed: 0, state: 'processing' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
let processedBytes = 0;
let lastTime = Date.now();
let lastBytes = 0;
const updateProcessingProgress = (chunkLength) => {
processedBytes += chunkLength;
const now = Date.now();
if (now - lastTime >= 250 || processedBytes >= fileObj.size) {
const timeDiff = (now - lastTime) / 1000;
const speed = timeDiff > 0 ? (processedBytes - lastBytes) / timeDiff : 0;
const progress = Math.min(1, processedBytes / fileObj.size);
network.transfers[id] = { progress, speed, state: 'processing' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
lastTime = now;
lastBytes = processedBytes;
}
};
if (fileObj.path && fs) {
const stream = fs.createReadStream(fileObj.path, { highWaterMark: 64 * 1024 });
for await (const chunk of stream) {
await fileCore.append(chunk);
updateProcessingProgress(chunk.length);
}
} else if (fileObj.fileObj && typeof fileObj.fileObj.stream === 'function') {
const stream = fileObj.fileObj.stream();
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
await fileCore.append(b4a.from(value));
updateProcessingProgress(value.length);
}
} else if (fileObj.buffer) {
const buf = b4a.from(fileObj.buffer);
const chunkSize = 64 * 1024;
for(let i=0; i<buf.length; i+=chunkSize) {
const chunk = buf.subarray(i, i+chunkSize);
await fileCore.append(chunk);
updateProcessingProgress(chunk.length);
}
}
const msg = network.messages.get(id);
if (msg) {
if (fileObj.path) {
msg.localPath = fileObj.path;
msg.isMediaInDB = false;
await network.localFilesDb.put(id, fileObj.path);
} else if (fileObj.fileObj && typeof URL !== 'undefined') {
msg.localBlobUrl = URL.createObjectURL(fileObj.fileObj);
msg.isMediaInDB = false;
} else if (fileObj.buffer && typeof URL !== 'undefined') {
const blob = new Blob([fileObj.buffer], { type: fileObj.type });
msg.localBlobUrl = URL.createObjectURL(blob);
msg.isMediaInDB = false;
} else {
msg.isMediaInDB = false;
}
network._emitMessages();
}
network.transfers[id] = { progress: 0, speed: 0, state: 'uploading' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
}
export async function sendFile(network, channel, text, fileObj) {
const id = generateUUID();
const fileCore = network.store.get({ name: 'file-' + id });
await fileCore.ready();
const coreKey = b4a.toString(fileCore.key, 'hex');
network.transfers[id] = { progress: 0, speed: 0, state: 'processing' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
await network._appendSignedMessage({
type: 'file', id, channel, text,
file: { name: fileObj.name, size: fileObj.size, mimeType: fileObj.type, coreKey },
timestamp: Date.now()
});
await _hostFile(network, id, fileObj, fileCore);
}
export async function sendDMFile(network, targetKey, text, fileObj) {
const id = generateUUID();
const fileCore = network.store.get({ name: 'file-' + id });
await fileCore.ready();
const coreKey = b4a.toString(fileCore.key, 'hex');
network.transfers[id] = { progress: 0, speed: 0, state: 'processing' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
await network._appendEncryptedMessage(targetKey, {
type: 'file', id, text,
file: { name: fileObj.name, size: fileObj.size, mimeType: fileObj.type, coreKey },
timestamp: Date.now()
});
await _hostFile(network, id, fileObj, fileCore);
}
export async function downloadFile(network, msgId, fileMeta, isSender) {
if (typeof window !== 'undefined') {
const localDeleted = JSON.parse(localStorage.getItem('pear_local_deleted_msgs') || '[]');
if (localDeleted.includes(msgId)) return;
}
if (isSender) {
if (network.transfers[msgId] && network.transfers[msgId].state === 'processing') {
return;
}
}
const storedPath = await network.localFilesDb.get(msgId);
if (storedPath && storedPath.value && fs && fs.existsSync(storedPath.value)) {
const msg = network.messages.get(msgId);
if (msg) {
msg.localPath = storedPath.value;
msg.isMediaInDB = fileMeta.mimeType?.startsWith('image/') || fileMeta.mimeType?.startsWith('video/');
network._emitMessages();
}
network.transfers[msgId] = { progress: 1, speed: 0, state: 'completed' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
return;
}
const core = network.store.get({ key: b4a.from(fileMeta.coreKey, 'hex') });
await core.ready();
const isMedia = fileMeta.mimeType?.startsWith('image/') || fileMeta.mimeType?.startsWith('video/');
let downloadsDir;
let filePath;
if (isMedia) {
downloadsDir = path.join(network.storagePath, 'downloads');
if (!fs.existsSync(downloadsDir)) fs.mkdirSync(downloadsDir, { recursive: true });
const safeName = fileMeta.name.replace(/[^a-zA-Z0-9.-]/g, '_');
filePath = path.join(downloadsDir, `${msgId}-${safeName}`);
} else {
downloadsDir = path.join(os.homedir(), 'Downloads');
if (!fs.existsSync(downloadsDir)) fs.mkdirSync(downloadsDir, { recursive: true });
const safeName = fileMeta.name.replace(/[^a-zA-Z0-9.\-_ ]/g, '');
filePath = path.join(downloadsDir, safeName);
const existingMsg = network.messages.get(msgId);
if (existingMsg && existingMsg.localPath) {
filePath = existingMsg.localPath;
} else if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath);
if (stats.size !== fileMeta.size) {
let baseName = path.basename(safeName, path.extname(safeName));
let ext = path.extname(safeName);
let counter = 1;
while (fs.existsSync(filePath)) {
filePath = path.join(downloadsDir, `${baseName} (${counter})${ext}`);
counter++;
}
}
}
}
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath);
if (stats.size >= fileMeta.size) {
const msg = network.messages.get(msgId);
if (msg) {
msg.localPath = filePath;
msg.isMediaInDB = isMedia;
network._emitMessages();
}
await network.localFilesDb.put(msgId, filePath);
network.transfers[msgId] = { progress: 1, speed: 0, state: 'completed' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
return;
} else {
try { fs.unlinkSync(filePath); } catch(e) {}
}
}
network.transfers[msgId] = { progress: 0, speed: 0, state: 'downloading' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
const readStream = core.createReadStream({ live: true });
const writeStream = fs.createWriteStream(filePath);
let downloadedBytes = 0;
let lastTime = Date.now();
let lastBytes = 0;
let isFinished = false;
const sendProgress = (progress, speed) => {
network.transfers[msgId] = { progress, speed, state: 'downloading' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
network.sendEphemeral({ type: 'transfer_progress', id: msgId, progress, speed });
};
writeStream.on('finish', async () => {
const msg = network.messages.get(msgId);
if (msg) {
msg.localPath = filePath;
msg.isMediaInDB = isMedia;
network._emitMessages();
}
await network.localFilesDb.put(msgId, filePath);
network.transfers[msgId] = { progress: 1, speed: 0, state: 'completed' };
if (network.onTransfersUpdate) network.onTransfersUpdate(network.transfers);
sendProgress(1, 0);
});
writeStream.on('error', (err) => {
console.error("File write error:", err);
});
if (fileMeta.size === 0) {
writeStream.end();
return;
}
readStream.on('data', (chunk) => {
if (isFinished) return;
downloadedBytes += chunk.length;
writeStream.write(chunk);
const now = Date.now();
if (now - lastTime >= 500 || downloadedBytes >= fileMeta.size) {
const timeDiff = (now - lastTime) / 1000;
const speed = timeDiff > 0 ? (downloadedBytes - lastBytes) / timeDiff : 0;
const progress = Math.min(1, downloadedBytes / fileMeta.size);
sendProgress(progress, Math.max(0, speed));
lastTime = now;
lastBytes = downloadedBytes;
}
if (downloadedBytes >= fileMeta.size) {
isFinished = true;
readStream.destroy();
writeStream.end();
}
});
}