393 lines
13 KiB
JavaScript
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();
|
|
}
|
|
});
|
|
} |