-
- {data.profile?.avatar ?

: data.profile?.displayName?.substring(0, 2).toUpperCase()}
+ {pendingIncoming.length > 0 && (
+
+
Incoming Requests — {pendingIncoming.length}
+
+ {pendingIncoming.map(([pubKey, data]) => (
+
+
+
+ {data.profile?.avatar ?

: data.profile?.displayName?.substring(0, 2).toUpperCase()}
+
+
+ {data.profile?.displayName}
+ @{data.profile?.username}
+
+
+
+
+
+
-
- {data.profile?.displayName}
- @{data.profile?.username} • Incoming Contact Request
-
-
-
-
-
-
+ ))}
- ))}
+
+ )}
- {pendingOutgoing.map(([pubKey, data]) => (
-
-
-
- {data.profile?.avatar ?

: data.profile?.displayName?.substring(0, 2).toUpperCase()}
+ {pendingOutgoing.length > 0 && (
+
+
Outgoing Requests — {pendingOutgoing.length}
+
+ {pendingOutgoing.map(([pubKey, data]) => (
+
+
+
+ {data.profile?.avatar ?

: data.profile?.displayName?.substring(0, 2).toUpperCase()}
+
+
+ {data.profile?.displayName}
+ @{data.profile?.username}
+
+
+
+
+
-
- {data.profile?.displayName}
- @{data.profile?.username} • Outgoing Contact Request
-
-
-
-
-
+ ))}
- ))}
+
+ )}
- {pendingIncoming.length === 0 && pendingOutgoing.length === 0 && (
-
No pending requests.
- )}
-
+ {pendingIncoming.length === 0 && pendingOutgoing.length === 0 && (
+
No pending requests.
+ )}
)}
diff --git a/Peercord Source/src/components/MainApp.jsx b/Peercord Source/src/components/MainApp.jsx
index c2c388e..63546da 100644
--- a/Peercord Source/src/components/MainApp.jsx
+++ b/Peercord Source/src/components/MainApp.jsx
@@ -61,10 +61,17 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
const [isFocused, setIsFocused] = useState(true);
const activeStateRef = useRef({ view: 'dms', dm: 'friends', channel: 'general-chat', focused: true });
+ // FIX: Stable reference for WebRTC listener to prevent dropped calls during React re-renders
+ const callStateRef = useRef({ activeCall, activeGroupCall, activeVc, knownUsers, dms, servers, profile });
+
useEffect(() => {
activeStateRef.current = { view: activeView, dm: activeDm, channel: activeChannel, focused: isFocused };
}, [activeView, activeDm, activeChannel, isFocused]);
+ useEffect(() => {
+ callStateRef.current = { activeCall, activeGroupCall, activeVc, knownUsers, dms, servers, profile };
+ }, [activeCall, activeGroupCall, activeVc, knownUsers, dms, servers, profile]);
+
useEffect(() => {
const onFocus = () => setIsFocused(true);
const onBlur = () => setIsFocused(false);
@@ -142,49 +149,6 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
localStorage.setItem('pear_delivered_receipts', JSON.stringify(deliveredReceipts));
},[deliveredReceipts]);
- // Secure P2P Account Sync Responder
- useEffect(() => {
- if (!myKey) return;
- let syncSwarm;
- try {
- const b4a = window.require('b4a');
- const sodium = window.require('sodium-native');
- const Hyperswarm = window.require('hyperswarm');
-
- syncSwarm = new Hyperswarm();
- const syncTopic = b4a.alloc(32);
- sodium.crypto_generichash(syncTopic, b4a.from('peercord-sync:' + myKey));
-
- syncSwarm.on('connection', (conn) => {
- conn.on('data', async (data) => {
- try {
- const msg = JSON.parse(b4a.toString(data));
- if (msg.type === 'account_sync_request') {
- const sigBuf = b4a.from(msg.signature, 'hex');
- const msgBuf = b4a.from('sync-request:' + msg.tempKey);
- const pubBuf = b4a.from(myKey, 'hex');
-
- if (sodium.crypto_sign_verify_detached(sigBuf, msgBuf, pubBuf)) {
- console.log("[Sync] Valid sync request received. Exporting account...");
- const exportData = await network.exportAccount();
- const payload = b4a.from(JSON.stringify({ type: 'account_sync_reply', data: exportData }));
- conn.write(payload);
- }
- }
- } catch (e) {}
- });
- });
-
- syncSwarm.join(syncTopic, { server: true, client: false });
- } catch (e) {
- console.error("Failed to start sync swarm", e);
- }
-
- return () => {
- if (syncSwarm) syncSwarm.destroy();
- };
- }, [myKey]);
-
useEffect(() => {
if (!initialized.current && typeof window !== 'undefined') {
initialized.current = true;
@@ -429,8 +393,10 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
}
},[servers, dms, activeView, activeDm]);
+ // FIX: Use stable ref for WebRTC listener to prevent dropped calls
useEffect(() => {
const handleWebRTC = (peerKey, payload) => {
+ const { activeCall, activeGroupCall, activeVc, knownUsers, dms, servers, profile } = callStateRef.current;
const notifyCalls = localStorage.getItem('pear_notify_calls') !== 'false';
if (payload.type === 'webrtc-init') {
@@ -474,9 +440,10 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
}
}
};
+
network.addWebRTCListener(handleWebRTC);
return () => network.removeWebRTCListener(handleWebRTC);
- },[activeCall, activeGroupCall, activeVc, knownUsers, dms, servers]);
+ }, []);
const handleSaveProfile = (newName, newAvatar, newUsername, newBio, newConnections) => {
const updatedProfile = {
diff --git a/Peercord Source/src/components/OnlineUsers.jsx b/Peercord Source/src/components/OnlineUsers.jsx
index be96e86..c9fc820 100644
--- a/Peercord Source/src/components/OnlineUsers.jsx
+++ b/Peercord Source/src/components/OnlineUsers.jsx
@@ -104,9 +104,7 @@ export default function OnlineUsers({ onlinePeers, knownUsers, dms, myKey, profi
user={selectedUser}
onClose={() => setSelectedUser(null)}
onSendDM={selectedUser.key !== myKey ? (u) => {
- if (!dms[u.key]) {
- network.sendDMRequest(u.key, { displayName: u.displayName, username: u.username, avatar: u.avatar, bio: u.bio, connections: u.connections });
- }
+ network.openDM(u.key, { displayName: u.displayName, username: u.username, avatar: u.avatar, bio: u.bio, connections: u.connections });
if (onNavigateToDM) onNavigateToDM(u.key);
} : null}
/>
diff --git a/Peercord Source/src/components/SetupScreen.jsx b/Peercord Source/src/components/SetupScreen.jsx
index bcaa9f2..0a19baf 100644
--- a/Peercord Source/src/components/SetupScreen.jsx
+++ b/Peercord Source/src/components/SetupScreen.jsx
@@ -77,7 +77,8 @@ export default function SetupScreen({ setProfile }) {
sodium.crypto_sign_seed_keypair(realPubKey, realSecKey, seedBuf);
const realPubKeyHex = b4a.toString(realPubKey, 'hex');
- const tempSwarm = new Hyperswarm();
+ // FIX: Added ephemeral: true to prevent this background swarm from exhausting the router NAT table
+ const tempSwarm = new Hyperswarm({ ephemeral: true });
const syncTopic = b4a.alloc(32);
sodium.crypto_generichash(syncTopic, b4a.from('peercord-sync:' + realPubKeyHex));
@@ -89,18 +90,25 @@ export default function SetupScreen({ setProfile }) {
const sigBuf = b4a.alloc(sodium.crypto_sign_BYTES);
sodium.crypto_sign_detached(sigBuf, msgBuf, realSecKey);
- conn.write(b4a.from(JSON.stringify({
- type: 'account_sync_request',
- tempKey: tempKeyHex,
- signature: b4a.toString(sigBuf, 'hex')
- })));
+ const Protomux = window.require('protomux');
+ const cenc = window.require('compact-encoding');
+
+ const mux = Protomux.from(conn);
+ const channel = mux.createChannel({ protocol: 'peercord/app' });
+ if (!channel) return;
- conn.on('data', async (data) => {
- try {
- const msg = JSON.parse(b4a.toString(data));
- if (msg.type === 'account_sync_reply' && msg.data) {
+ const appEncoding = {
+ preencode(state, m) { cenc.string.preencode(state, JSON.stringify(m)); },
+ encode(state, m) { cenc.string.encode(state, JSON.stringify(m)); },
+ decode(state) { return JSON.parse(cenc.string.decode(state)); }
+ };
+
+ const appMessage = channel.addMessage({
+ encoding: appEncoding,
+ onmessage: async (msg) => {
+ if (msg.type === 'ephemeral' && msg.payload?.type === 'account_sync_reply' && msg.payload.data) {
setSyncStatus('Syncing data...');
- const importedProfile = await network.importAccount(msg.data);
+ const importedProfile = await network.importAccount(msg.payload.data);
synced = true;
tempSwarm.destroy();
@@ -108,10 +116,21 @@ export default function SetupScreen({ setProfile }) {
localStorage.setItem('pear_discord_identity', JSON.stringify(importedProfile));
setProfile(importedProfile);
}
- } catch (err) {
- console.error("Sync parse error", err);
}
});
+
+ channel.open();
+
+ try {
+ appMessage.send({
+ type: 'ephemeral',
+ payload: {
+ type: 'account_sync_request',
+ tempKey: tempKeyHex,
+ signature: b4a.toString(sigBuf, 'hex')
+ }
+ });
+ } catch (e) {}
});
tempSwarm.join(syncTopic, { client: true, server: false });
diff --git a/Peercord Source/src/p2p/handlers.js b/Peercord Source/src/p2p/handlers.js
index a902ad3..bcef91a 100644
--- a/Peercord Source/src/p2p/handlers.js
+++ b/Peercord Source/src/p2p/handlers.js
@@ -1,21 +1,19 @@
const b4a = window.require('b4a');
-export async function handleData(network, peerKey, data, conn) {
+export async function handleData(network, peerKey, parsed, send) {
try {
- const parsed = JSON.parse(b4a.toString(data));
-
switch (parsed.type) {
case 'identity':
await handleIdentity(network, peerKey, parsed);
break;
case 'whois':
- handleWhois(network, parsed, conn);
+ handleWhois(network, parsed, send);
break;
case 'whois_reply':
handleWhoisReply(network, parsed);
break;
case 'ephemeral':
- handleEphemeral(network, peerKey, parsed, conn);
+ handleEphemeral(network, peerKey, parsed, send);
break;
default:
// Could be a standard message core, which is handled by replication, not this handler.
@@ -96,12 +94,11 @@ async function handleIdentity(network, peerKey, parsed) {
}
}
-function handleWhois(network, parsed, conn) {
+function handleWhois(network, parsed, send) {
const uname = parsed.username;
if (network.userDirectory.has(uname)) {
const cached = network.userDirectory.get(uname);
- const reply = b4a.from(JSON.stringify({ type: 'whois_reply', queryId: parsed.queryId, username: uname, pubKey: cached.pubKey, profile: cached.profile }));
- conn.write(reply);
+ send({ type: 'whois_reply', queryId: parsed.queryId, username: uname, pubKey: cached.pubKey, profile: cached.profile });
}
}
@@ -114,7 +111,7 @@ function handleWhoisReply(network, parsed) {
network._checkPendingRequests(parsed.username, parsed.pubKey, parsed.profile);
}
-function handleEphemeral(network, peerKey, parsed, conn) {
+function handleEphemeral(network, peerKey, parsed, send) {
const { payload } = parsed;
if (!payload) return;
@@ -124,7 +121,7 @@ function handleEphemeral(network, peerKey, parsed, conn) {
.filter(([_, data]) => data.status === 'pending_outgoing')
.map(([key]) => key);
- const identityMsg = JSON.stringify({
+ const identityMsg = {
type: 'identity',
displayName: network.displayName,
username: network.username,
@@ -134,8 +131,8 @@ function handleEphemeral(network, peerKey, parsed, conn) {
coreKey: network.coreKey,
topics: Array.from(network.joinedTopics),
pendingTargets
- });
- try { conn.write(b4a.from(identityMsg)); } catch(e) {}
+ };
+ try { send(identityMsg); } catch(e) {}
}
return;
}
diff --git a/Peercord Source/src/p2p/index.js b/Peercord Source/src/p2p/index.js
index d4482cf..9906614 100644
--- a/Peercord Source/src/p2p/index.js
+++ b/Peercord Source/src/p2p/index.js
@@ -1,5 +1,5 @@
const b4a = window.require('b4a');
-import { generateUUID, Hyperswarm, Corestore, Hyperbee, sodium, fs, os, path, http } from './utils.js';
+import { generateUUID, Hyperswarm, Corestore, Hyperbee, sodium, fs, os, path, http, Protomux, cenc, DHT } 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';
@@ -40,6 +40,8 @@ class P2PNetwork {
this.pendingFriendRequests = new Set();
this.messages = new Map();
+ this.reactions = new Map(); // targetId -> { emoji: [senders] }
+ this.processedSigs = new Set(); // signature -> true (prevents double processing)
this.deletedMessages = new Set();
this.dms = {};
this.servers =[];
@@ -133,6 +135,7 @@ class P2PNetwork {
}
this.deletedMessages.add(msgId);
this.messages.delete(msgId);
+ this.reactions.delete(msgId);
if (!localDeleted.includes(msgId)) localDeleted.push(msgId);
if (this.transfers[msgId]) delete this.transfers[msgId];
}
@@ -173,6 +176,21 @@ class P2PNetwork {
updateProfile = (name, avatar, username, bio, connections) => Identity.updateProfile(this, name, avatar, username, bio, connections);
+ async openDM(targetKey, profile) {
+ if (this.dms[targetKey]) {
+ this.dms[targetKey].isOpen = true;
+ if (profile) {
+ this.dms[targetKey].profile = { ...this.dms[targetKey].profile, ...profile };
+ this.knownProfiles.set(targetKey, this.dms[targetKey].profile);
+ this._emitKnownProfiles();
+ }
+ await this.db.put('dm:' + targetKey, this.dms[targetKey]);
+ if (this.onDMsUpdate) this.onDMsUpdate({ ...this.dms });
+ } else {
+ await this.sendDMRequest(targetKey, profile);
+ }
+ }
+
async closeDM(targetKey) {
if (this.dms[targetKey]) {
this.dms[targetKey].isOpen = false;
@@ -374,7 +392,9 @@ class P2PNetwork {
async checkUsernameAvailable(username) {
const normalized = username.toLowerCase();
- const tempSwarm = new Hyperswarm({ maxPeers: 3, maxClientConnections: 3, maxServerConnections: 0 });
+ // FIX: Explicitly pass ephemeral DHT to prevent router exhaustion
+ const dht = new DHT({ ephemeral: true });
+ const tempSwarm = new Hyperswarm({ dht, maxPeers: 3, maxClientConnections: 3, maxServerConnections: 0 });
const topic = b4a.alloc(32);
sodium.crypto_generichash(topic, b4a.from('peercord-user:' + normalized));
@@ -418,21 +438,20 @@ class P2PNetwork {
.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,
+ const identityMsg = {
+ 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) {}
+ };
+
+ for (const { send } of this.peers.values()) {
+ if (send) send(identityMsg);
}
}
@@ -534,47 +553,104 @@ class P2PNetwork {
}
await Promise.all(corePromises);
- // SETUP SWARM
+ // Compact-encoding codec for the JSON app protocol. Built on cenc.string
+ // (utf-8) so it works regardless of whether this compact-encoding build
+ // ships a dedicated `json` codec.
+ const appEncoding = {
+ preencode(state, m) { cenc.string.preencode(state, JSON.stringify(m)); },
+ encode(state, m) { cenc.string.encode(state, JSON.stringify(m)); },
+ decode(state) { return JSON.parse(cenc.string.decode(state)); }
+ };
+
+ // FIX: Explicitly create an ephemeral DHT instance to guarantee we don't route traffic for others
+ // Also limit maxPeers to protect cheap home routers from NAT exhaustion
+ const dht = new DHT({ ephemeral: true });
this.swarm = new Hyperswarm({
keyPair: { publicKey, secretKey },
- ephemeral: true, // CRITICAL FIX: Prevents node from becoming a routing node and exhausting router NAT table
- maxPeers: 128
+ dht,
+ maxPeers: 24,
+ maxClientConnections: 12,
+ maxServerConnections: 12
});
this.swarm.on('connection', (conn, info) => {
conn.on('error', () => {}); // Prevent ECONNRESET crashes
this.store.replicate(conn);
const peerKey = b4a.toString(info.publicKey, 'hex');
-
+
+ // The hyperswarm connection is a Noise stream that corestore wraps in
+ // Protomux for replication framing. Raw conn.write would corrupt that
+ // mux, so the JSON app protocol rides on its own Protomux channel that
+ // shares the same connection with replication.
+ const mux = Protomux.from(conn);
+ const channel = mux.createChannel({ protocol: 'peercord/app' });
+
+ // createChannel returns null if a channel for this protocol already
+ // exists on the connection (e.g. a duplicate/multiplexed link). Bail
+ // gracefully but keep the connection alive for replication.
+ if (!channel) {
+ if (this.onPeerUpdate) this.onPeerUpdate(this.getPeerList());
+ return;
+ }
+
+ const appMessage = channel.addMessage({
+ encoding: appEncoding,
+ onmessage: (msg) => {
+ // Intercept Account Sync Requests directly on the main swarm
+ if (msg.type === 'ephemeral' && msg.payload?.type === 'account_sync_request') {
+ try {
+ const sigBuf = b4a.from(msg.payload.signature, 'hex');
+ const msgBuf = b4a.from('sync-request:' + msg.payload.tempKey);
+ const pubBuf = b4a.from(this.myKey, 'hex');
+
+ if (sodium.crypto_sign_verify_detached(sigBuf, msgBuf, pubBuf)) {
+ console.log("[Sync] Valid sync request received. Exporting account...");
+ this.exportAccount().then(exportData => {
+ send({ type: 'ephemeral', payload: { type: 'account_sync_reply', data: exportData } });
+ });
+ }
+ } catch (e) {
+ console.error("Sync request error:", e);
+ }
+ return;
+ }
+ handleData(this, peerKey, msg, send);
+ }
+ });
+
+ const send = (obj) => {
+ try { appMessage.send(obj); } catch (e) {}
+ };
+
+ channel.open();
+
// Preserve existing peer info if connection multiplexes
const existingPeer = this.peers.get(peerKey);
if (existingPeer) {
existingPeer.conn = conn;
+ existingPeer.send = send;
} else {
- this.peers.set(peerKey, { conn, displayName: 'Unknown', username: 'unknown', avatar: null, bio: '', connections: [], coreKey: null });
+ this.peers.set(peerKey, { conn, send, 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,
+ send({
+ 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);
@@ -587,7 +663,14 @@ class P2PNetwork {
// BACKGROUND JOINS TO PREVENT UDP FLOOD / NAT EXHAUSTION
(async () => {
- const pace = () => new Promise(r => setTimeout(r, 1000)); // 1000ms between DHT lookups
+ // FIX: Increased pacing to 3 seconds to protect router NAT tables
+ const pace = () => new Promise(r => setTimeout(r, 3000));
+
+ // Join the sync topic on the main swarm instead of creating a second swarm
+ const syncTopic = b4a.alloc(32);
+ sodium.crypto_generichash(syncTopic, b4a.from('peercord-sync:' + this.myKey));
+ this.swarm.join(syncTopic, { server: true, client: false });
+ await pace();
if (this.username && this.username !== 'unknown') {
const myTopic = b4a.alloc(32);
@@ -668,6 +751,8 @@ class P2PNetwork {
this.pendingWhois.clear();
this.pendingFriendRequests.clear();
this.messages.clear();
+ this.reactions.clear();
+ this.processedSigs.clear();
this.deletedMessages.clear();
this.dms = {};
this.servers =[];
@@ -774,6 +859,7 @@ class P2PNetwork {
this.deletedMessages.add(msgId);
this.messages.delete(msgId);
+ this.reactions.delete(msgId);
if (typeof window !== 'undefined') {
const localDeleted = JSON.parse(localStorage.getItem('pear_local_deleted_msgs') || '[]');
diff --git a/Peercord Source/src/p2p/modules/discovery.js b/Peercord Source/src/p2p/modules/discovery.js
index 83bfd4f..3ec9660 100644
--- a/Peercord Source/src/p2p/modules/discovery.js
+++ b/Peercord Source/src/p2p/modules/discovery.js
@@ -41,9 +41,9 @@ export async function searchUser(network, targetUsername) {
finish(result);
});
- const msg = b4a.from(JSON.stringify({ type: 'whois', queryId, username: normalized }));
- for (const { conn } of network.peers.values()) {
- try { conn.write(msg); } catch(e) {}
+ const msg = { type: 'whois', queryId, username: normalized };
+ for (const { send } of network.peers.values()) {
+ if (send) send(msg);
}
});
}
diff --git a/Peercord Source/src/p2p/modules/messaging.js b/Peercord Source/src/p2p/modules/messaging.js
index c6ef076..545e9ea 100644
--- a/Peercord Source/src/p2p/modules/messaging.js
+++ b/Peercord Source/src/p2p/modules/messaging.js
@@ -45,6 +45,13 @@ export function getAllMessages(network) {
const isInvite = m.payload.type === 'server_invite';
const isFile = m.payload.type === 'file';
+ // Deep clone reactions to ensure React detects the state change and re-renders
+ const rawReactions = network.reactions.get(m.payload.id) || {};
+ const clonedReactions = {};
+ for (const [emoji, users] of Object.entries(rawReactions)) {
+ clonedReactions[emoji] = [...users];
+ }
+
return {
id: m.payload.id,
channel: m.recipient ? m.recipient : m.payload.channel,
@@ -58,7 +65,7 @@ export function getAllMessages(network) {
logicalTime: m.payload.logicalTime || 0,
edited: m.payload.edited || false,
replyTo: m.payload.replyTo || null,
- reactions: m.reactions || {},
+ reactions: clonedReactions,
sender: m.sender,
senderName: known ? known.displayName : 'Unknown',
senderAvatar: known ? known.avatar : null,
@@ -74,7 +81,26 @@ export function getAllMessages(network) {
}
export async function processMessage(network, msg) {
- if (!msg || !msg.sender) return;
+ if (!msg || !msg.sender || !msg.signature) return;
+
+ // FIX: Prevent double-processing of messages caused by Hypercore's download+append event race condition
+ if (network.processedSigs.has(msg.signature)) return;
+ network.processedSigs.add(msg.signature);
+
+ const applyReaction = (targetId, emoji, sender) => {
+ if (!network.reactions.has(targetId)) network.reactions.set(targetId, {});
+ const msgReactions = network.reactions.get(targetId);
+ if (!msgReactions[emoji]) msgReactions[emoji] = [];
+
+ const idx = msgReactions[emoji].indexOf(sender);
+ if (idx > -1) {
+ msgReactions[emoji].splice(idx, 1);
+ if (msgReactions[emoji].length === 0) delete msgReactions[emoji];
+ } else {
+ msgReactions[emoji].push(sender);
+ }
+ network._emitMessages();
+ };
if (msg.recipient) {
if (msg.recipient !== network.myKey && msg.sender !== network.myKey) return;
@@ -114,20 +140,7 @@ export async function processMessage(network, msg) {
}
if (decrypted.type === 'reaction') {
- const targetMsg = network.messages.get(decrypted.targetId);
- if (targetMsg) {
- if (!targetMsg.reactions) targetMsg.reactions = {};
- if (!targetMsg.reactions[decrypted.emoji]) targetMsg.reactions[decrypted.emoji] = [];
-
- const idx = targetMsg.reactions[decrypted.emoji].indexOf(msg.sender);
- if (idx > -1) {
- targetMsg.reactions[decrypted.emoji].splice(idx, 1);
- if (targetMsg.reactions[decrypted.emoji].length === 0) delete targetMsg.reactions[decrypted.emoji];
- } else {
- targetMsg.reactions[decrypted.emoji].push(msg.sender);
- }
- network._emitMessages();
- }
+ applyReaction(decrypted.targetId, decrypted.emoji, msg.sender);
return;
}
@@ -138,6 +151,20 @@ export async function processMessage(network, msg) {
network.knownProfiles.set(msg.sender, msg.payload.profile);
network._emitKnownProfiles();
if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms });
+ } else if (network.dms[msg.sender].status === 'pending_outgoing') {
+ // Mutual request! Auto-accept.
+ network.dms[msg.sender].status = 'accepted';
+ network.dms[msg.sender].isOpen = true;
+ if (msg.payload.profile) {
+ network.dms[msg.sender].profile = { ...network.dms[msg.sender].profile, ...msg.payload.profile };
+ network.knownProfiles.set(msg.sender, network.dms[msg.sender].profile);
+ network._emitKnownProfiles();
+ }
+ await network.db.put('dm:' + msg.sender, network.dms[msg.sender]);
+ if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms });
+
+ // Send an accept back just in case
+ acceptDMRequest(network, msg.sender);
}
} else if (msg.payload.type === 'dm_accept' && msg.sender !== network.myKey) {
if (network.dms[msg.sender] && network.dms[msg.sender].status === 'pending_outgoing') {
@@ -147,6 +174,13 @@ export async function processMessage(network, msg) {
if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms });
}
} else if (msg.payload.type === 'dm_chat' || msg.payload.type === 'file') {
+ if (network.dms[msg.sender] && network.dms[msg.sender].status === 'pending_outgoing') {
+ network.dms[msg.sender].status = 'accepted';
+ network.dms[msg.sender].isOpen = true;
+ await network.db.put('dm:' + msg.sender, network.dms[msg.sender]);
+ if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms });
+ }
+
if (!network.deletedMessages.has(msg.payload.id) && !network.messages.has(msg.payload.id)) {
network.messages.set(msg.payload.id, msg);
network._emitMessages();
@@ -323,18 +357,8 @@ export async function processMessage(network, msg) {
}
}
- if (canReact && targetMsg) {
- if (!targetMsg.reactions) targetMsg.reactions = {};
- if (!targetMsg.reactions[emoji]) targetMsg.reactions[emoji] = [];
-
- const idx = targetMsg.reactions[emoji].indexOf(msg.sender);
- if (idx > -1) {
- targetMsg.reactions[emoji].splice(idx, 1);
- if (targetMsg.reactions[emoji].length === 0) delete targetMsg.reactions[emoji];
- } else {
- targetMsg.reactions[emoji].push(msg.sender);
- }
- network._emitMessages();
+ if (canReact) {
+ applyReaction(targetId, emoji, msg.sender);
}
return;
}
@@ -363,6 +387,7 @@ export async function processMessage(network, msg) {
if (canDelete) {
network.deletedMessages.add(targetId);
network.messages.delete(targetId);
+ network.reactions.delete(targetId);
network._emitMessages();
}
return;
@@ -532,9 +557,9 @@ export async function sendReaction(network, targetId, emoji, isDM = false, targe
export function sendEphemeral(network, payload) {
if (!network.swarm) return;
- const msg = b4a.from(JSON.stringify({ type: 'ephemeral', payload }));
- for (const { conn } of network.peers.values()) {
- try { conn.write(msg); } catch(e) {}
+ const msg = { type: 'ephemeral', payload };
+ for (const { send } of network.peers.values()) {
+ if (send) send(msg);
}
}
diff --git a/Peercord Source/src/p2p/modules/webrtc.js b/Peercord Source/src/p2p/modules/webrtc.js
index 677bbc1..7e50c46 100644
--- a/Peercord Source/src/p2p/modules/webrtc.js
+++ b/Peercord Source/src/p2p/modules/webrtc.js
@@ -11,8 +11,7 @@ export function removeWebRTCListener(network, fn) {
export function sendWebRTCSignal(network, targetKey, payload) {
if (!network.swarm) return;
const peerInfo = network.peers.get(targetKey);
- if (peerInfo && peerInfo.conn) {
- const msg = b4a.from(JSON.stringify({ type: 'ephemeral', payload }));
- peerInfo.conn.write(msg);
+ if (peerInfo && peerInfo.send) {
+ peerInfo.send({ type: 'ephemeral', payload });
}
}
\ No newline at end of file
diff --git a/Peercord Source/src/p2p/network.js b/Peercord Source/src/p2p/network.js
index ee90c29..27aea57 100644
--- a/Peercord Source/src/p2p/network.js
+++ b/Peercord Source/src/p2p/network.js
@@ -1,393 +1,4 @@
-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
= 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();
- }
- });
-}
\ No newline at end of file
+// DEPRECATED - This file is no longer used and has been emptied to prevent confusion.
+// All networking logic is now handled in src/p2p/index.js
+export async function initNetwork() {}
+export const networkLegacy = {};
\ No newline at end of file
diff --git a/Peercord Source/src/p2p/utils.js b/Peercord Source/src/p2p/utils.js
index c8b4be7..c235c46 100644
--- a/Peercord Source/src/p2p/utils.js
+++ b/Peercord Source/src/p2p/utils.js
@@ -1,6 +1,6 @@
const b4a = window.require('b4a');
-export let Hyperswarm, Corestore, Hyperbee, sodium, fs, os, path, http;
+export let Hyperswarm, Corestore, Hyperbee, sodium, fs, os, path, http, Protomux, cenc, DHT;
// The PUBLIC key is 100% safe to be in the open-source code.
// It is mathematically impossible to derive your private seed from it.
@@ -16,6 +16,9 @@ export async function initP2P() {
os = req('os');
path = req('path');
http = req('http');
+ Protomux = req('protomux');
+ cenc = req('compact-encoding');
+ DHT = req('hyperdht');
}
export function generateUUID() {