Peercord/Peercord Source/src/p2p/index.js
0% [█ █ █ █ █ █ █ █ █ █] 100% 439791ae4d v1.0.5
Lotta shit ngl
2026-06-16 15:25:36 -05:00

789 lines
30 KiB
JavaScript

const b4a = window.require('b4a');
import { generateUUID, Hyperswarm, Corestore, Hyperbee, sodium, fs, os, path, http } from './utils.js';
import * as Identity from './modules/identity.js';
import { handleData } from './handlers.js';
import { getAllMessages, processMessage, sendDMRequest, sendMessage, sendDM, sendEditMessage, sendDeleteMessage, acceptDMRequest, sendEphemeral, sendReadReceipt, sendDeliveredReceipt, sendOffline, sendTyping, sendReaction, _appendSignedMessage, _appendEncryptedMessage } from './modules/messaging.js';
import { createServer, joinServer, deleteServer, leaveServer, sendServerInvite, updateServerSettings, sendGroupChatAdd } from './modules/servers.js';
import { searchUser, queueFriendRequest, trackPeerCore } from './modules/discovery.js';
import { sendFile, sendDMFile, downloadFile } from './modules/files.js';
import { addWebRTCListener, removeWebRTCListener, sendWebRTCSignal } from './modules/webrtc.js';
class P2PNetwork {
constructor() {
this.swarm = null;
this.store = null;
this.localCore = null;
this.db = null;
this.serverDb = null;
this.dirDb = null;
this.pendingRequestsDb = null;
this.localFilesDb = null;
this.coresDb = null;
this.profilesDb = null;
this.seedHex = null;
this.coreKey = null;
this.myKey = null;
this.secretKey = null;
this.displayName = '';
this.username = '';
this.avatar = null;
this.bio = '';
this.connections = [];
this.storagePath = null;
this.peers = new Map();
this.peerCores = new Map();
this.knownProfiles = new Map();
this.userDirectory = new Map();
this.pendingWhois = new Map();
this.pendingFriendRequests = new Set();
this.messages = new Map();
this.deletedMessages = new Set();
this.dms = {};
this.servers =[];
this.serverMembers = {};
this.joinedTopics = new Set();
this.syncTimeout = null;
this._msgTimeout = null;
this._identityTimeout = null;
this.transfers = {};
this.webrtcListeners = new Set();
this.logicalClock = 0;
this.timeOffset = 0;
this.activeCalls = 0;
this.onInit = null;
this.onPeerUpdate = null;
this.onMessage = null;
this.onEphemeral = null;
this.onDMsUpdate = null;
this.onKnownProfilesUpdate = null;
this.onServersUpdate = null;
this.onServerMembersUpdate = null;
this.onSync = null;
this.onTransfersUpdate = null;
}
getAllMessages = () => getAllMessages(this);
processMessage = (msg) => processMessage(this, msg);
sendDMRequest = (targetKey, profile) => sendDMRequest(this, targetKey, profile);
acceptDMRequest = (targetKey) => acceptDMRequest(this, targetKey);
sendMessage = (channel, text, replyTo) => sendMessage(this, channel, text, replyTo);
sendDM = (targetKey, text, replyTo) => sendDM(this, targetKey, text, replyTo);
sendEditMessage = (targetId, newText) => sendEditMessage(this, targetId, newText);
sendDeleteMessage = (targetId) => sendDeleteMessage(this, targetId);
sendReaction = (targetId, emoji, isDM, targetKey) => sendReaction(this, targetId, emoji, isDM, targetKey);
sendEphemeral = (payload) => sendEphemeral(this, payload);
sendReadReceipt = (channel, messageId) => sendReadReceipt(this, channel, messageId);
sendDeliveredReceipt = (channel, messageId) => sendDeliveredReceipt(this, channel, messageId);
sendOffline = () => sendOffline(this);
sendTyping = (channel) => sendTyping(this, channel);
pruneFile = (msgId) => this._pruneFile(msgId);
getStorageStats = () => this._getStorageStats();
_appendSignedMessage = (payloadObj) => _appendSignedMessage(this, payloadObj);
_appendEncryptedMessage = (targetKey, payloadObj) => _appendEncryptedMessage(this, targetKey, payloadObj);
_downloadFile = (msgId, fileMeta, isSender) => downloadFile(this, msgId, fileMeta, isSender);
_emitMessages() {
if (!this.onMessage) return;
if (this._msgTimeout) clearTimeout(this._msgTimeout);
this._msgTimeout = setTimeout(() => {
this.onMessage(this.getAllMessages());
this._msgTimeout = null;
}, 50);
}
_wipeLocalServerData = async (topicHex) => {
this.servers = this.servers.filter(s => s.topicHex !== topicHex);
if (this.serverDb) await this.serverDb.del(topicHex);
delete this.serverMembers[topicHex];
const msgsToDelete =[];
for (const [msgId, msg] of this.messages.entries()) {
const ch = msg.payload?.channel;
if (ch === topicHex || (ch && ch.startsWith(topicHex + '-'))) {
msgsToDelete.push(msgId);
}
}
let localDeleted =[];
if (typeof window !== 'undefined') {
localDeleted = JSON.parse(localStorage.getItem('pear_local_deleted_msgs') || '[]');
}
for (const msgId of msgsToDelete) {
const msg = this.messages.get(msgId);
if (msg) {
if (msg.localPath && fs && fs.existsSync(msg.localPath)) {
try { fs.unlinkSync(msg.localPath); } catch (e) {}
}
if (msg.payload?.file?.coreKey) {
try {
const core = this.store.get({ key: b4a.from(msg.payload.file.coreKey, 'hex') });
await core.ready();
await core.clear(0, core.length);
} catch (e) {}
}
this.deletedMessages.add(msgId);
this.messages.delete(msgId);
if (!localDeleted.includes(msgId)) localDeleted.push(msgId);
if (this.transfers[msgId]) delete this.transfers[msgId];
}
}
if (typeof window !== 'undefined' && msgsToDelete.length > 0) {
localStorage.setItem('pear_local_deleted_msgs', JSON.stringify(localDeleted));
}
if (this.onTransfersUpdate) this.onTransfersUpdate(this.transfers);
this._emitMessages();
this._emitServers();
this._emitServerMembers();
};
_reloadCores = async () => {
this._emitSync();
};
createServer = (...args) => createServer(this, ...args);
joinServer = (...args) => joinServer(this, ...args);
deleteServer = (...args) => deleteServer(this, ...args);
leaveServer = (...args) => leaveServer(this, ...args);
sendServerInvite = (...args) => sendServerInvite(this, ...args);
updateServerSettings = (...args) => updateServerSettings(this, ...args);
sendGroupChatAdd = (...args) => sendGroupChatAdd(this, ...args);
searchUser = (username) => searchUser(this, username);
queueFriendRequest = (username) => queueFriendRequest(this, username);
trackPeerCore = (coreKeyHex) => trackPeerCore(this, coreKeyHex);
sendFile = (...args) => sendFile(this, ...args);
sendDMFile = (...args) => sendDMFile(this, ...args);
addWebRTCListener = (fn) => addWebRTCListener(this, fn);
removeWebRTCListener = (fn) => removeWebRTCListener(this, fn);
sendWebRTCSignal = (target, payload) => sendWebRTCSignal(this, target, payload);
updateProfile = (name, avatar, username, bio, connections) => Identity.updateProfile(this, name, avatar, username, bio, connections);
async closeDM(targetKey) {
if (this.dms[targetKey]) {
this.dms[targetKey].isOpen = false;
await this.db.put('dm:' + targetKey, this.dms[targetKey]);
if (this.onDMsUpdate) this.onDMsUpdate({ ...this.dms });
}
}
async removeFriend(targetKey) {
if (this.dms[targetKey]) {
delete this.dms[targetKey];
await this.db.del('dm:' + targetKey);
if (this.onDMsUpdate) this.onDMsUpdate({ ...this.dms });
}
}
async blockUser(targetKey) {
if (this.dms[targetKey]) {
this.dms[targetKey].status = 'blocked';
this.dms[targetKey].isOpen = false;
await this.db.put('dm:' + targetKey, this.dms[targetKey]);
if (this.onDMsUpdate) this.onDMsUpdate({ ...this.dms });
} else {
this.dms[targetKey] = { status: 'blocked', isOpen: false, profile: this.knownProfiles.get(targetKey) || { displayName: 'Unknown' } };
await this.db.put('dm:' + targetKey, this.dms[targetKey]);
if (this.onDMsUpdate) this.onDMsUpdate({ ...this.dms });
}
}
async exportAccount() {
const exportData = {
profile: {
displayName: this.displayName,
username: this.username,
avatar: this.avatar,
bio: this.bio,
connections: this.connections,
seedHex: this.seedHex
},
dms: this.dms,
servers: this.servers,
knownProfiles: Array.from(this.knownProfiles.entries()),
userDirectory: Array.from(this.userDirectory.entries()),
settings: {
theme: localStorage.getItem('peercord_theme'),
audioInput: localStorage.getItem('pear_audio_input'),
audioOutput: localStorage.getItem('pear_audio_output'),
videoInput: localStorage.getItem('pear_video_input'),
autoRestart: localStorage.getItem('pear_auto_restart'),
liveDecryption: localStorage.getItem('pear_live_decryption'),
ircMode: localStorage.getItem('pear_irc_mode'),
noiseSuppression: localStorage.getItem('pear_noise_suppression'),
closeToTray: localStorage.getItem('pear_close_to_tray'),
pinMembers: localStorage.getItem('pear_pin_members'),
notifyDMs: localStorage.getItem('pear_notify_dms'),
notifyHubs: localStorage.getItem('pear_notify_hubs'),
notifyMentions: localStorage.getItem('pear_notify_mentions'),
notifyCalls: localStorage.getItem('pear_notify_calls')
}
};
return JSON.stringify(exportData);
}
async importAccount(jsonString) {
const data = JSON.parse(jsonString);
for (const [key, value] of Object.entries(data.dms)) {
await this.db.put('dm:' + key, value);
}
for (const server of data.servers) {
await this.serverDb.put(server.topicHex, server);
}
for (const [key, value] of data.knownProfiles) {
await this.profilesDb.put(key, value);
}
for (const [key, value] of data.userDirectory) {
await this.dirDb.put(key, value);
}
if (data.settings) {
for (const [k, v] of Object.entries(data.settings)) {
if (v !== null && v !== undefined) {
const storageKey = k === 'theme' ? 'peercord_theme' : `pear_${k.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)}`;
localStorage.setItem(storageKey, v);
}
}
}
return data.profile;
}
_checkPendingRequests = (uname, pubKey, profile) => {
if (this.pendingFriendRequests.has(uname)) {
this.pendingFriendRequests.delete(uname);
this.pendingRequestsDb.del(uname);
this.sendDMRequest(pubKey, profile);
}
}
_emitKnownProfiles() {
if (this.onKnownProfilesUpdate) {
this.onKnownProfilesUpdate(Array.from(this.knownProfiles.entries()).map(([key, profile]) => ({ key, ...profile })));
}
}
_emitServers() {
if (this.onServersUpdate) this.onServersUpdate([...this.servers]);
}
_emitServerMembers() {
if (this.onServerMembersUpdate) {
const formatted = {};
for (const topic in this.serverMembers) {
formatted[topic] = Array.from(this.serverMembers[topic]);
}
this.onServerMembersUpdate(formatted);
}
}
_emitSync() {
if (this.onSync) this.onSync(true);
if (this.syncTimeout) clearTimeout(this.syncTimeout);
this.syncTimeout = setTimeout(() => {
if (this.onSync) this.onSync(false);
}, 500);
}
getBusyReasons() {
const reasons =[];
let activeUploads = 0;
let activeDownloads = 0;
let processing = 0;
for (const t of Object.values(this.transfers)) {
if (t.state === 'processing') {
processing++;
} else if (t.state === 'downloading') {
if (t.speed > 0 || (t.progress > 0 && t.progress < 1)) {
activeDownloads++;
}
} else if (t.state === 'uploading') {
if (t.progress < 1) {
activeUploads++;
}
}
}
if (processing > 0) reasons.push("Processing local files");
if (activeUploads > 0) reasons.push("Uploading files to peers");
if (activeDownloads > 0) reasons.push("Downloading files");
if (this.activeCalls > 0) reasons.push("Active voice/video call");
return reasons;
}
isBusy() {
return this.getBusyReasons().length > 0;
}
async _syncTimeWithServer() {
try {
if (!http) throw new Error("HTTP module not loaded");
await new Promise((resolve, reject) => {
const req = http.request({
hostname: '1.1.1.1',
method: 'HEAD',
port: 80,
timeout: 5000
}, (res) => {
const dateHeader = res.headers.date;
if (dateHeader) {
const serverTime = new Date(dateHeader).getTime();
const localTime = Date.now();
this.timeOffset = serverTime - localTime;
console.log(`[Time Sync] Offset calculated: ${this.timeOffset}ms`);
} else {
console.warn('[Time Sync] No date header found in response.');
}
resolve();
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Connection timed out'));
});
req.on('error', (err) => {
reject(err);
});
req.end();
});
} catch (err) {
console.warn('[Time Sync] Failed to reach time server, falling back to local system clock.', err.message || err);
this.timeOffset = 0;
}
}
async checkUsernameAvailable(username) {
const normalized = username.toLowerCase();
const tempSwarm = new Hyperswarm({ maxPeers: 3, maxClientConnections: 3, maxServerConnections: 0 });
const topic = b4a.alloc(32);
sodium.crypto_generichash(topic, b4a.from('peercord-user:' + normalized));
let isTaken = false;
tempSwarm.on('connection', (conn) => {
isTaken = true;
conn.destroy();
});
tempSwarm.join(topic, { client: true, server: false });
for (let i = 0; i < 30; i++) {
if (isTaken) break;
await new Promise(resolve => setTimeout(resolve, 100));
}
await tempSwarm.destroy();
return !isTaken;
}
async reconnect() {
if (!this.swarm) return;
console.log("[P2P] Network online event detected. Reconnecting...");
try {
// Hyperswarm handles reconnections automatically.
// Forcing a non-blocking flush is enough to kickstart the DHT without causing a UDP flood.
this.swarm.flush().catch(()=>{});
} catch (e) {
console.warn("[P2P] Reconnect flush failed:", e);
}
}
_broadcastIdentity() {
if (!this.swarm) return;
const pendingTargets = Object.entries(this.dms)
.filter(([_, data]) => data.status === 'pending_outgoing')
.map(([key]) => key);
const identityMsg = JSON.stringify({
type: 'identity',
displayName: this.displayName,
username: this.username,
avatar: this.avatar,
bio: this.bio,
connections: this.connections,
coreKey: this.coreKey,
topics: Array.from(this.joinedTopics),
pendingTargets
});
const payload = b4a.from(identityMsg);
for (const { conn } of this.peers.values()) {
try { conn.write(payload); } catch(e) {}
}
}
async initialize(seedHex, displayName, username, avatar = null, bio = '', connections = []) {
this.seedHex = seedHex;
this.displayName = displayName;
this.username = (username || 'unknown').toLowerCase();
this.avatar = avatar;
this.bio = bio;
this.connections = connections;
this._syncTimeWithServer().catch(() => {});
let instanceId = 'default';
if (typeof window !== 'undefined') {
instanceId = localStorage.getItem('pear_instance_id');
if (!instanceId) {
instanceId = generateUUID();
localStorage.setItem('pear_instance_id', instanceId);
}
const localDeleted = JSON.parse(localStorage.getItem('pear_local_deleted_msgs') || '[]');
localDeleted.forEach(id => this.deletedMessages.add(id));
}
let basePath = './p2p-storage';
if (os && path && typeof os.homedir === 'function') {
const home = os.homedir();
const appData = process.platform === 'win32'
? process.env.APPDATA
: (process.platform === 'darwin' ? path.join(home, 'Library', 'Application Support') : path.join(home, '.config'));
basePath = path.join(appData || home, 'Peercord', 'p2p-storage');
}
const hashBuf = b4a.alloc(32);
sodium.crypto_generichash(hashBuf, b4a.from(seedHex, 'hex'));
const accountHash = b4a.toString(hashBuf, 'hex').substring(0, 16);
this.storagePath = path.join(basePath, `${instanceId}-${accountHash}`);
if (fs && fs.existsSync) {
const badDownloadsPath = path.join(this.storagePath, 'downloads');
if (fs.existsSync(badDownloadsPath)) {
try { fs.rmSync(badDownloadsPath, { recursive: true, force: true }); } catch (e) {}
}
}
this.store = new Corestore(this.storagePath);
await this.store.ready();
const dbCore = this.store.get({ name: 'dm-db' }); await dbCore.ready();
this.db = new Hyperbee(dbCore, { keyEncoding: 'utf-8', valueEncoding: 'json' }); await this.db.ready();
const serverDbCore = this.store.get({ name: 'server-db' }); await serverDbCore.ready();
this.serverDb = new Hyperbee(serverDbCore, { keyEncoding: 'utf-8', valueEncoding: 'json' }); await this.serverDb.ready();
const dirDbCore = this.store.get({ name: 'directory-db' }); await dirDbCore.ready();
this.dirDb = new Hyperbee(dirDbCore, { keyEncoding: 'utf-8', valueEncoding: 'json' }); await this.dirDb.ready();
const pendingDbCore = this.store.get({ name: 'pending-requests-db' }); await pendingDbCore.ready();
this.pendingRequestsDb = new Hyperbee(pendingDbCore, { keyEncoding: 'utf-8', valueEncoding: 'json' }); await this.pendingRequestsDb.ready();
const localFilesDbCore = this.store.get({ name: 'local-files-db' }); await localFilesDbCore.ready();
this.localFilesDb = new Hyperbee(localFilesDbCore, { keyEncoding: 'utf-8', valueEncoding: 'utf-8' }); await this.localFilesDb.ready();
const coresDbCore = this.store.get({ name: 'peer-cores-db' }); await coresDbCore.ready();
this.coresDb = new Hyperbee(coresDbCore, { keyEncoding: 'utf-8', valueEncoding: 'utf-8' }); await this.coresDb.ready();
const profilesDbCore = this.store.get({ name: 'profiles-db' }); await profilesDbCore.ready();
this.profilesDb = new Hyperbee(profilesDbCore, { keyEncoding: 'utf-8', valueEncoding: 'json' }); await this.profilesDb.ready();
for await (const { key, value } of this.db.createReadStream({ gt: 'dm:', lt: 'dm:~' })) {
const pubKey = key.split(':')[1];
if (value.isOpen === undefined) value.isOpen = true;
this.dms[pubKey] = value;
if (value.profile) this.knownProfiles.set(pubKey, value.profile);
}
for await (const { key, value } of this.serverDb.createReadStream()) { this.servers.push({ topicHex: key, ...value }); }
for await (const { key, value } of this.dirDb.createReadStream()) { this.userDirectory.set(key, value); if (value.pubKey && value.profile) this.knownProfiles.set(value.pubKey, value.profile); }
for await (const { key } of this.pendingRequestsDb.createReadStream()) { this.pendingFriendRequests.add(key); }
for await (const { key, value } of this.profilesDb.createReadStream()) { this.knownProfiles.set(key, value); }
this.localCore = this.store.get({ name: 'user-messages', valueEncoding: 'json' }); await this.localCore.ready();
this.coreKey = b4a.toString(this.localCore.key, 'hex');
const seed = b4a.from(seedHex, 'hex');
const publicKey = b4a.alloc(32);
const secretKey = b4a.alloc(64);
sodium.crypto_sign_seed_keypair(publicKey, secretKey, seed);
this.myKey = b4a.toString(publicKey, 'hex');
this.secretKey = secretKey;
this.knownProfiles.set(this.myKey, { displayName: this.displayName, username: this.username, avatar: this.avatar, bio: this.bio, connections: this.connections });
// EMIT IMMEDIATELY BEFORE SWARM JOINS TO PREVENT UI BLOCKING
this._emitKnownProfiles();
if (this.onInit) this.onInit(this.myKey);
if (this.onDMsUpdate) this.onDMsUpdate({ ...this.dms });
this._emitServers();
for (let i = 0; i < this.localCore.length; i++) { this.processMessage(await this.localCore.get(i)); }
this._emitMessages();
const corePromises =[];
for await (const { key, value } of this.coresDb.createReadStream()) {
corePromises.push(this.trackPeerCore(value));
}
await Promise.all(corePromises);
// SETUP SWARM
this.swarm = new Hyperswarm({ keyPair: { publicKey, secretKey } });
this.swarm.on('connection', (conn, info) => {
conn.on('error', () => {}); // Prevent ECONNRESET crashes
this.store.replicate(conn);
const peerKey = b4a.toString(info.publicKey, 'hex');
// Preserve existing peer info if connection multiplexes
const existingPeer = this.peers.get(peerKey);
if (existingPeer) {
existingPeer.conn = conn;
} else {
this.peers.set(peerKey, { conn, displayName: 'Unknown', username: 'unknown', avatar: null, bio: '', connections: [], coreKey: null });
}
const pendingTargets = Object.entries(this.dms)
.filter(([_, data]) => data.status === 'pending_outgoing')
.map(([key]) => key);
const identityMsg = JSON.stringify({
type: 'identity',
displayName: this.displayName,
username: this.username,
avatar: this.avatar,
bio: this.bio,
connections: this.connections,
coreKey: this.coreKey,
topics: Array.from(this.joinedTopics),
pendingTargets
});
conn.write(b4a.from(identityMsg));
if (this.onPeerUpdate) this.onPeerUpdate(this.getPeerList());
conn.on('data', async (data) => handleData(this, peerKey, data, conn));
conn.on('close', () => {
// Only delete if this specific connection is still the active one
const currentPeer = this.peers.get(peerKey);
if (currentPeer && currentPeer.conn === conn) {
this.peers.delete(peerKey);
if (this.onPeerUpdate) this.onPeerUpdate(this.getPeerList());
}
});
});
// BACKGROUND JOINS TO PREVENT UDP FLOOD / NAT EXHAUSTION
(async () => {
const pace = () => new Promise(r => setTimeout(r, 400)); // 400ms between DHT lookups
if (this.username && this.username !== 'unknown') {
const myTopic = b4a.alloc(32);
sodium.crypto_generichash(myTopic, b4a.from('peercord-user:' + this.username));
this.swarm.join(myTopic, { client: false, server: true });
await pace();
}
for (const uname of this.pendingFriendRequests) {
const topic = b4a.alloc(32);
sodium.crypto_generichash(topic, b4a.from('peercord-user:' + uname));
this.swarm.join(topic, { client: true, server: false });
await pace();
}
for (const server of this.servers) {
await this._joinTopic(server.topicHex, true);
await pace();
}
const globalUpdateTopic = b4a.alloc(32);
sodium.crypto_generichash(globalUpdateTopic, b4a.from('peercord-global-updates'));
this.swarm.join(globalUpdateTopic, { client: true, server: true });
this.swarm.flush().then(() => {
console.log("[P2P] Swarm flushed and announced.");
}).catch(err => console.warn("[P2P] Swarm flush failed:", err));
})();
}
getPeerList() {
return Array.from(this.peers.entries()).map(([key, info]) => ({
key, displayName: info.displayName, username: info.username, avatar: info.avatar, bio: info.bio, connections: info.connections
}));
}
async _joinTopic(topicHex, skipFlush = false) {
if (!this.swarm) return;
if (this.joinedTopics.has(topicHex)) return;
this.joinedTopics.add(topicHex);
const topic = b4a.from(topicHex, 'hex');
this.swarm.join(topic, { client: true, server: true });
// Debounce identity broadcast to prevent TCP floods when joining many topics
if (this._identityTimeout) clearTimeout(this._identityTimeout);
this._identityTimeout = setTimeout(() => {
this._broadcastIdentity();
}, 1000);
// Request sync from existing members to fetch history immediately
this.sendEphemeral({ type: 'sync_request', topic: topicHex });
if (!skipFlush) {
// Don't await flush, it blocks the caller.
this.swarm.flush().catch(()=>{});
}
}
async close() {
if (this.swarm) {
for (const peer of this.swarm.connections) peer.destroy();
await this.swarm.destroy();
this.swarm = null;
}
if (this.db) { await this.db.close(); this.db = null; }
if (this.serverDb) { await this.serverDb.close(); this.serverDb = null; }
if (this.dirDb) { await this.dirDb.close(); this.dirDb = null; }
if (this.pendingRequestsDb) { await this.pendingRequestsDb.close(); this.pendingRequestsDb = null; }
if (this.localFilesDb) { await this.localFilesDb.close(); this.localFilesDb = null; }
if (this.coresDb) { await this.coresDb.close(); this.coresDb = null; }
if (this.profilesDb) { await this.profilesDb.close(); this.profilesDb = null; }
if (this.store) { await this.store.close(); this.store = null; }
this.peers.clear();
this.peerCores.clear();
this.knownProfiles.clear();
this.userDirectory.clear();
this.pendingWhois.clear();
this.pendingFriendRequests.clear();
this.messages.clear();
this.deletedMessages.clear();
this.dms = {};
this.servers =[];
this.serverMembers = {};
this.joinedTopics.clear();
this.transfers = {};
this.webrtcListeners.clear();
if (this._msgTimeout) clearTimeout(this._msgTimeout);
if (this._identityTimeout) clearTimeout(this._identityTimeout);
}
async wipeAllData() {
await this.close();
if (typeof window !== 'undefined') localStorage.removeItem('pear_discord_identity');
try {
if (this.storagePath && fs) await fs.promises.rm(this.storagePath, { recursive: true, force: true });
} catch (err) { console.error("Failed to delete storage directory:", err); }
window.location.reload();
}
async _getStorageStats() {
const stats = {
total: 0,
dms: {},
servers: {},
files: []
};
const guessServerName = (topicHex) => {
const s = this.servers.find(s => s.topicHex === topicHex);
if (s) return s.name;
for (const msg of this.messages.values()) {
if (msg.payload?.serverTopicHex === topicHex && msg.payload?.name) return msg.payload.name;
if (msg.payload?.type === 'server_invite' && msg.payload?.serverTopicHex === topicHex) return msg.payload.serverName;
if (msg.payload?.type === 'group_chat_add' && msg.payload?.topicHex === topicHex) return msg.payload.name;
}
return 'Unknown Hub';
};
for (const msg of this.messages.values()) {
if (msg.payload?.type === 'file' && msg.localPath) {
const size = msg.payload.file.size || 0;
stats.total += size;
const target = msg.recipient ? (msg.sender === this.myKey ? msg.recipient : msg.sender) : null;
let serverName = null;
let isGroupChat = false;
if (!msg.recipient && msg.payload.channel) {
const topicHex = msg.payload.channel.substring(0, 64);
serverName = guessServerName(topicHex);
const s = this.servers.find(s => s.topicHex === topicHex);
if (s) isGroupChat = s.isGroupChat;
}
const fileInfo = {
id: msg.payload.id,
name: msg.payload.file.name,
size: size,
coreKey: msg.payload.file.coreKey,
timestamp: msg.payload.timestamp,
channel: msg.payload.channel,
recipient: msg.recipient,
sender: msg.sender,
target: target,
serverName: serverName,
isGroupChat: isGroupChat
};
stats.files.push(fileInfo);
if (msg.recipient) {
stats.dms[target] = (stats.dms[target] || 0) + size;
} else if (msg.payload.channel) {
const topicHex = msg.payload.channel.substring(0, 64);
const channelName = msg.payload.channel.substring(65) || 'general';
if (!stats.servers[topicHex]) stats.servers[topicHex] = { total: 0, channels: {}, name: serverName, isGroupChat };
stats.servers[topicHex].total += size;
stats.servers[topicHex].channels[channelName] = (stats.servers[topicHex].channels[channelName] || 0) + size;
}
}
}
stats.files.sort((a, b) => b.size - a.size);
return stats;
}
async _pruneFile(msgId) {
const msg = this.messages.get(msgId);
if (!msg) return;
try {
if (msg.localPath && fs && fs.existsSync(msg.localPath)) {
try { fs.unlinkSync(msg.localPath); } catch (e) { console.error("Failed to delete physical file:", e); }
}
if (msg.payload?.file?.coreKey) {
try {
const core = this.store.get({ key: b4a.from(msg.payload.file.coreKey, 'hex') });
await core.ready();
await core.clear(0, core.length);
} catch (e) { console.error("Failed to clear hypercore:", e); }
}
this.deletedMessages.add(msgId);
this.messages.delete(msgId);
if (typeof window !== 'undefined') {
const localDeleted = JSON.parse(localStorage.getItem('pear_local_deleted_msgs') || '[]');
if (!localDeleted.includes(msgId)) {
localDeleted.push(msgId);
localStorage.setItem('pear_local_deleted_msgs', JSON.stringify(localDeleted));
}
}
if (this.transfers[msgId]) {
delete this.transfers[msgId];
if (this.onTransfersUpdate) this.onTransfersUpdate(this.transfers);
}
this._emitMessages();
} catch (err) {
console.error("Failed to prune file:", err);
}
}
}
export const network = new P2PNetwork();
export { initP2P, ADMIN_PUBLIC_KEY } from './utils.js';
export { generateIdentitySeed } from './modules/identity.js';