@decentnetwork/beagle 0.1.120 → 0.1.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/desktop/app.js +1 -1
- package/dist/desktop/vendor/peer-webrtc.js +82 -10
- package/dist/friend-store.d.ts +25 -0
- package/dist/friend-store.js +75 -0
- package/dist/server.js +10 -51
- package/package.json +2 -2
package/dist/desktop/app.js
CHANGED
|
@@ -57,6 +57,8 @@ var PeerWebRTC = (() => {
|
|
|
57
57
|
fitSignalForInvite: () => fitSignalForInvite,
|
|
58
58
|
looksLikeSignal: () => looksLikeSignal,
|
|
59
59
|
mediaDirection: () => mediaDirection,
|
|
60
|
+
orphanPayloadTypeLines: () => orphanPayloadTypeLines,
|
|
61
|
+
sanitizeSdp: () => sanitizeSdp,
|
|
60
62
|
shrinkSdpForInvite: () => shrinkSdpForInvite,
|
|
61
63
|
videoIsSending: () => videoIsSending
|
|
62
64
|
});
|
|
@@ -198,6 +200,29 @@ var PeerWebRTC = (() => {
|
|
|
198
200
|
}
|
|
199
201
|
return join(lines);
|
|
200
202
|
}
|
|
203
|
+
function orphanPayloadTypeLines(sdp) {
|
|
204
|
+
const out = [];
|
|
205
|
+
let pts;
|
|
206
|
+
for (const line of sdp.replace(/\r\n/g, "\n").split("\n")) {
|
|
207
|
+
if (line.startsWith("m=")) {
|
|
208
|
+
pts = new Set(line.split(" ").slice(3));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const m = pts && /^a=(?:rtpmap|rtcp-fb|fmtp):(\d+)\b/.exec(line);
|
|
212
|
+
if (m && !pts.has(m[1]))
|
|
213
|
+
out.push(line);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
function sanitizeSdp(sdp) {
|
|
218
|
+
const orphans = orphanPayloadTypeLines(sdp);
|
|
219
|
+
if (orphans.length === 0)
|
|
220
|
+
return sdp;
|
|
221
|
+
const drop = new Set(orphans);
|
|
222
|
+
const crlf = sdp.includes("\r\n");
|
|
223
|
+
const kept = sdp.replace(/\r\n/g, "\n").split("\n").filter((l) => !drop.has(l));
|
|
224
|
+
return kept.join(crlf ? "\r\n" : "\n");
|
|
225
|
+
}
|
|
201
226
|
|
|
202
227
|
// node_modules/@decentnetwork/peer-webrtc/dist/signal.js
|
|
203
228
|
var SDP_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -297,7 +322,7 @@ var PeerWebRTC = (() => {
|
|
|
297
322
|
var ICE_RESTART_MAX = 3;
|
|
298
323
|
var ICE_DISCONNECT_GRACE_MS = 4e3;
|
|
299
324
|
var ICE_FAILED_GRACE_MS = 12e4;
|
|
300
|
-
var _opts, _iceServers, _sessions, _restartOffersEnabled, _setOutgoingVideoTrack, setOutgoingVideoTrack_fn, _renegotiate, renegotiate_fn, _createOffer, createOffer_fn, _createAnswer, createAnswer_fn, _preferLeanVideoCodecs, preferLeanVideoCodecs_fn, _restartIce, restartIce_fn, _handleSignal, handleSignal_fn, _onOffer, onOffer_fn, _answerRenegotiation, answerRenegotiation_fn, _onAnswer, onAnswer_fn, _onCandidate, onCandidate_fn, _onBye, onBye_fn, _onAuxiliary, onAuxiliary_fn, _createSession, createSession_fn, _wirePeerConnection, wirePeerConnection_fn, _ensureRemoteStream, ensureRemoteStream_fn, _attachLocalMedia, attachLocalMedia_fn, _flushLocalCandidates, flushLocalCandidates_fn, _drainRemoteCandidates, drainRemoteCandidates_fn, _send, send_fn, _sendByeSafe, sendByeSafe_fn, _cleanup, cleanup_fn, _setState, setState_fn, _normId, normId_fn, _toInfo, _newCallId, newCallId_fn, _log, log_fn;
|
|
325
|
+
var _opts, _iceServers, _sessions, _restartOffersEnabled, _setOutgoingVideoTrack, setOutgoingVideoTrack_fn, _renegotiate, renegotiate_fn, _createOffer, createOffer_fn, _createAnswer, createAnswer_fn, _cleanOutbound, cleanOutbound_fn, _cleanInbound, cleanInbound_fn, _preferLeanVideoCodecs, preferLeanVideoCodecs_fn, _restartIce, restartIce_fn, _handleSignal, handleSignal_fn, _onOffer, onOffer_fn, _answerRenegotiation, answerRenegotiation_fn, _negotiationFailed, negotiationFailed_fn, _onAnswer, onAnswer_fn, _onCandidate, onCandidate_fn, _onBye, onBye_fn, _onAuxiliary, onAuxiliary_fn, _createSession, createSession_fn, _wirePeerConnection, wirePeerConnection_fn, _ensureRemoteStream, ensureRemoteStream_fn, _attachLocalMedia, attachLocalMedia_fn, _flushLocalCandidates, flushLocalCandidates_fn, _drainRemoteCandidates, drainRemoteCandidates_fn, _send, send_fn, _sendByeSafe, sendByeSafe_fn, _cleanup, cleanup_fn, _setState, setState_fn, _normId, normId_fn, _toInfo, _newCallId, newCallId_fn, _log, log_fn;
|
|
301
326
|
var CallEngine = class extends Emitter {
|
|
302
327
|
constructor(opts) {
|
|
303
328
|
var _a, _b;
|
|
@@ -309,10 +334,31 @@ var PeerWebRTC = (() => {
|
|
|
309
334
|
/** Create + send a fresh offer to renegotiate media (e.g. after adding a
|
|
310
335
|
* screen-share track). The peer applies it via #onOffer's renegotiation path. */
|
|
311
336
|
__privateAdd(this, _renegotiate);
|
|
312
|
-
/** createOffer with the lean codec set applied (see #preferLeanVideoCodecs)
|
|
337
|
+
/** createOffer with the lean codec set applied (see #preferLeanVideoCodecs)
|
|
338
|
+
* and the result sanitized (see #cleanOutbound). Every offer — initial,
|
|
339
|
+
* screen-share re-offer, ICE restart — goes through here. */
|
|
313
340
|
__privateAdd(this, _createOffer);
|
|
314
|
-
/** createAnswer with the lean codec set applied. */
|
|
341
|
+
/** createAnswer with the lean codec set applied and the result sanitized. */
|
|
315
342
|
__privateAdd(this, _createAnswer);
|
|
343
|
+
/**
|
|
344
|
+
* Strip orphan codec lines from a description WE produced, before it is
|
|
345
|
+
* applied locally and before it is sent.
|
|
346
|
+
*
|
|
347
|
+
* Firefox's setCodecPreferences drops VP9/AV1 from the m=video line but
|
|
348
|
+
* leaves their `a=fmtp:` / `a=rtcp-fb:` lines in place; Chrome's parser then
|
|
349
|
+
* rejects the whole re-offer ("Failed to parse codecs correctly") and the
|
|
350
|
+
* Firefox peer's screen share never shows up. Measured 2026-10-31 between
|
|
351
|
+
* the Firefox extension and a Chrome desktop, both on 0.2.16. The sanitizer
|
|
352
|
+
* is a no-op on a clean description (Chrome, Safari), so the same object
|
|
353
|
+
* comes back untouched there.
|
|
354
|
+
*/
|
|
355
|
+
__privateAdd(this, _cleanOutbound);
|
|
356
|
+
/**
|
|
357
|
+
* The same cleanup for a description the PEER sent, before setRemoteDescription:
|
|
358
|
+
* Firefox extensions already in the field (AMO 1.4.14 ships 0.2.16) still
|
|
359
|
+
* produce the orphan lines, and a receiver on this engine must accept them.
|
|
360
|
+
*/
|
|
361
|
+
__privateAdd(this, _cleanInbound);
|
|
316
362
|
/**
|
|
317
363
|
* Restrict video to the codecs the peers actually negotiate, so the SDP fits
|
|
318
364
|
* the 8192-byte Carrier invite channel. Chrome otherwise advertises VP9 and
|
|
@@ -335,6 +381,7 @@ var PeerWebRTC = (() => {
|
|
|
335
381
|
__privateAdd(this, _onOffer);
|
|
336
382
|
/** Apply a mid-call re-offer (renegotiation) and send the answer. */
|
|
337
383
|
__privateAdd(this, _answerRenegotiation);
|
|
384
|
+
__privateAdd(this, _negotiationFailed);
|
|
338
385
|
__privateAdd(this, _onAnswer);
|
|
339
386
|
__privateAdd(this, _onCandidate);
|
|
340
387
|
__privateAdd(this, _onBye);
|
|
@@ -450,7 +497,7 @@ var PeerWebRTC = (() => {
|
|
|
450
497
|
throw new Error(`accept: no pending incoming call ${callId}`);
|
|
451
498
|
}
|
|
452
499
|
await __privateMethod(this, _attachLocalMedia, attachLocalMedia_fn).call(this, session);
|
|
453
|
-
await session.pc.setRemoteDescription({ type: "offer", sdp: session.pendingOffer });
|
|
500
|
+
await session.pc.setRemoteDescription({ type: "offer", sdp: __privateMethod(this, _cleanInbound, cleanInbound_fn).call(this, session.pendingOffer, "offer") });
|
|
454
501
|
session.hasReceivedSdp = true;
|
|
455
502
|
session.pendingOffer = void 0;
|
|
456
503
|
await __privateMethod(this, _drainRemoteCandidates, drainRemoteCandidates_fn).call(this, session);
|
|
@@ -638,12 +685,30 @@ var PeerWebRTC = (() => {
|
|
|
638
685
|
_createOffer = new WeakSet();
|
|
639
686
|
createOffer_fn = async function(session, opts) {
|
|
640
687
|
__privateMethod(this, _preferLeanVideoCodecs, preferLeanVideoCodecs_fn).call(this, session.pc);
|
|
641
|
-
return session.pc.createOffer(opts);
|
|
688
|
+
return __privateMethod(this, _cleanOutbound, cleanOutbound_fn).call(this, await session.pc.createOffer(opts));
|
|
642
689
|
};
|
|
643
690
|
_createAnswer = new WeakSet();
|
|
644
691
|
createAnswer_fn = async function(session) {
|
|
645
692
|
__privateMethod(this, _preferLeanVideoCodecs, preferLeanVideoCodecs_fn).call(this, session.pc);
|
|
646
|
-
return session.pc.createAnswer();
|
|
693
|
+
return __privateMethod(this, _cleanOutbound, cleanOutbound_fn).call(this, await session.pc.createAnswer());
|
|
694
|
+
};
|
|
695
|
+
_cleanOutbound = new WeakSet();
|
|
696
|
+
cleanOutbound_fn = function(desc) {
|
|
697
|
+
var _a, _b;
|
|
698
|
+
const raw = (_a = desc.sdp) != null ? _a : "";
|
|
699
|
+
const sdp = sanitizeSdp(raw);
|
|
700
|
+
if (sdp === raw)
|
|
701
|
+
return desc;
|
|
702
|
+
__privateMethod(this, _log, log_fn).call(this, `${(_b = desc.type) != null ? _b : "sdp"}: removed ${orphanPayloadTypeLines(raw).length} orphan codec line(s) from our own description`);
|
|
703
|
+
return { type: desc.type, sdp };
|
|
704
|
+
};
|
|
705
|
+
_cleanInbound = new WeakSet();
|
|
706
|
+
cleanInbound_fn = function(raw, what) {
|
|
707
|
+
const sdp = sanitizeSdp(raw);
|
|
708
|
+
if (sdp !== raw) {
|
|
709
|
+
__privateMethod(this, _log, log_fn).call(this, `${what}: removed ${orphanPayloadTypeLines(raw).length} orphan codec line(s) from the peer's description`);
|
|
710
|
+
}
|
|
711
|
+
return sdp;
|
|
647
712
|
};
|
|
648
713
|
_preferLeanVideoCodecs = new WeakSet();
|
|
649
714
|
preferLeanVideoCodecs_fn = function(pc) {
|
|
@@ -733,14 +798,21 @@ var PeerWebRTC = (() => {
|
|
|
733
798
|
_answerRenegotiation = new WeakSet();
|
|
734
799
|
answerRenegotiation_fn = async function(session, sdp) {
|
|
735
800
|
try {
|
|
736
|
-
await session.pc.setRemoteDescription({ type: "offer", sdp });
|
|
801
|
+
await session.pc.setRemoteDescription({ type: "offer", sdp: __privateMethod(this, _cleanInbound, cleanInbound_fn).call(this, sdp, "offer") });
|
|
737
802
|
const answer = await __privateMethod(this, _createAnswer, createAnswer_fn).call(this, session);
|
|
738
803
|
await session.pc.setLocalDescription(answer);
|
|
739
804
|
await __privateMethod(this, _send, send_fn).call(this, session.peerId, { type: "answer", sdp: answer.sdp, callId: session.callId });
|
|
740
805
|
} catch (err) {
|
|
741
|
-
__privateMethod(this,
|
|
806
|
+
__privateMethod(this, _negotiationFailed, negotiationFailed_fn).call(this, session, "re-offer", err);
|
|
742
807
|
}
|
|
743
808
|
};
|
|
809
|
+
_negotiationFailed = new WeakSet();
|
|
810
|
+
negotiationFailed_fn = function(session, what, err) {
|
|
811
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
812
|
+
__privateMethod(this, _log, log_fn).call(this, `renegotiation failed (${what}) callId=${session.callId}: ${error.message}`);
|
|
813
|
+
console.warn(`[peer-webrtc] could not apply the peer's ${what} for call ${session.callId}: ${error.message}`);
|
|
814
|
+
this.emit("negotiationFailed", session.callId, error);
|
|
815
|
+
};
|
|
744
816
|
_onAnswer = new WeakSet();
|
|
745
817
|
onAnswer_fn = function(peerId, signal) {
|
|
746
818
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _normId, normId_fn).call(this, signal.callId));
|
|
@@ -749,14 +821,14 @@ var PeerWebRTC = (() => {
|
|
|
749
821
|
session.peerId = peerId;
|
|
750
822
|
void (async () => {
|
|
751
823
|
try {
|
|
752
|
-
await session.pc.setRemoteDescription({ type: "answer", sdp: signal.sdp });
|
|
824
|
+
await session.pc.setRemoteDescription({ type: "answer", sdp: __privateMethod(this, _cleanInbound, cleanInbound_fn).call(this, signal.sdp, "answer") });
|
|
753
825
|
session.hasReceivedSdp = true;
|
|
754
826
|
if (session.state !== "connected")
|
|
755
827
|
__privateMethod(this, _setState, setState_fn).call(this, session, "connecting");
|
|
756
828
|
await __privateMethod(this, _drainRemoteCandidates, drainRemoteCandidates_fn).call(this, session);
|
|
757
829
|
await __privateMethod(this, _flushLocalCandidates, flushLocalCandidates_fn).call(this, session);
|
|
758
830
|
} catch (err) {
|
|
759
|
-
__privateMethod(this,
|
|
831
|
+
__privateMethod(this, _negotiationFailed, negotiationFailed_fn).call(this, session, "answer", err);
|
|
760
832
|
}
|
|
761
833
|
})();
|
|
762
834
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface FriendStoreEntry {
|
|
2
|
+
requestedAt?: number;
|
|
3
|
+
acceptedAt?: number;
|
|
4
|
+
/** CryptoPunks id the friend advertised in its userinfo (Beagle's avatar). */
|
|
5
|
+
punkId?: number;
|
|
6
|
+
platform?: string;
|
|
7
|
+
appVersion?: string;
|
|
8
|
+
}
|
|
9
|
+
/** A punk id as the store or a backend may carry it: a finite non-negative
|
|
10
|
+
* number. Punk 0 is a real CryptoPunk; a string or a negative is not one. */
|
|
11
|
+
export declare function punkIdOf(v: unknown): number | undefined;
|
|
12
|
+
/** Parse the store file's JSON (an array, `{friends:[…]}`, or an object keyed
|
|
13
|
+
* by id — the SDK has written all three) into a map by userid. */
|
|
14
|
+
export declare function parseFriendStore(raw: unknown): Map<string, FriendStoreEntry>;
|
|
15
|
+
/** The friend store next to `keyFile`, by userid. Unreadable or absent (a
|
|
16
|
+
* fresh install has no friends yet): whatever we last read, else nothing —
|
|
17
|
+
* the list still renders, just without stamps or pushed avatars. */
|
|
18
|
+
export declare function readFriendStore(keyFile: string | undefined): Map<string, FriendStoreEntry>;
|
|
19
|
+
/** Which avatar to show for a friend. What they advertised over Carrier wins
|
|
20
|
+
* over the directory: it is what they are using on their device right now,
|
|
21
|
+
* it needs no gateway, and it is the only avatar a friend who never
|
|
22
|
+
* registered a beagles.eth name will ever have. The backend's own report
|
|
23
|
+
* first, then the SDK's store on disk (an older daemon reports none), then
|
|
24
|
+
* the beagles.eth record. */
|
|
25
|
+
export declare function pickPunk(pushed: unknown, stored: number | undefined, directory: number | null | undefined): number | null;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The SDK's own friend store (`<keyFile>.friends.json`, next to the identity)
|
|
2
|
+
// is the source of truth both backends write, and it is a file in our own
|
|
3
|
+
// config dir, so the UI server reads it for what the backend's friend list
|
|
4
|
+
// does not report. Two things live here:
|
|
5
|
+
//
|
|
6
|
+
// - the stamps (requestedAt / acceptedAt): a daemon backend reports only
|
|
7
|
+
// acceptedAt (as lastSeen) and nothing for a request we sent, so a friend
|
|
8
|
+
// just added had no stamp at all and sorted to the bottom;
|
|
9
|
+
// - what the friend pushed about itself (punk avatar, platform, app
|
|
10
|
+
// version): lan ≤ 0.1.308 dropped these from its friends-list, so on a
|
|
11
|
+
// daemon backend only a beagles.eth record could supply an avatar — on
|
|
12
|
+
// mac-dev 8 of 16 pushed punks never reached the screen (2026-10-31).
|
|
13
|
+
//
|
|
14
|
+
// Cached by mtime: the list is polled every 2.5s per tab.
|
|
15
|
+
import { readFileSync, statSync } from "node:fs";
|
|
16
|
+
/** A punk id as the store or a backend may carry it: a finite non-negative
|
|
17
|
+
* number. Punk 0 is a real CryptoPunk; a string or a negative is not one. */
|
|
18
|
+
export function punkIdOf(v) {
|
|
19
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Parse the store file's JSON (an array, `{friends:[…]}`, or an object keyed
|
|
22
|
+
* by id — the SDK has written all three) into a map by userid. */
|
|
23
|
+
export function parseFriendStore(raw) {
|
|
24
|
+
const recs = Array.isArray(raw)
|
|
25
|
+
? raw
|
|
26
|
+
: raw && typeof raw === "object" && Array.isArray(raw.friends)
|
|
27
|
+
? raw.friends
|
|
28
|
+
: Object.values(raw ?? {});
|
|
29
|
+
const map = new Map();
|
|
30
|
+
for (const r of recs) {
|
|
31
|
+
if (!r || typeof r !== "object")
|
|
32
|
+
continue;
|
|
33
|
+
const id = r.userid || r.pubkey;
|
|
34
|
+
if (!id)
|
|
35
|
+
continue;
|
|
36
|
+
map.set(id, {
|
|
37
|
+
requestedAt: typeof r.requestedAt === "number" ? r.requestedAt : undefined,
|
|
38
|
+
acceptedAt: typeof r.acceptedAt === "number" ? r.acceptedAt : undefined,
|
|
39
|
+
punkId: punkIdOf(r.punkId),
|
|
40
|
+
platform: typeof r.platform === "string" ? r.platform : undefined,
|
|
41
|
+
appVersion: typeof r.appVersion === "string" ? r.appVersion : undefined,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
let cache = null;
|
|
47
|
+
/** The friend store next to `keyFile`, by userid. Unreadable or absent (a
|
|
48
|
+
* fresh install has no friends yet): whatever we last read, else nothing —
|
|
49
|
+
* the list still renders, just without stamps or pushed avatars. */
|
|
50
|
+
export function readFriendStore(keyFile) {
|
|
51
|
+
const empty = new Map();
|
|
52
|
+
if (!keyFile)
|
|
53
|
+
return empty;
|
|
54
|
+
const path = `${keyFile}.friends.json`;
|
|
55
|
+
try {
|
|
56
|
+
const st = statSync(path);
|
|
57
|
+
if (cache && cache.path === path && cache.mtimeMs === st.mtimeMs)
|
|
58
|
+
return cache.map;
|
|
59
|
+
const map = parseFriendStore(JSON.parse(readFileSync(path, "utf-8")));
|
|
60
|
+
cache = { path, mtimeMs: st.mtimeMs, map };
|
|
61
|
+
return map;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return cache?.path === path ? cache.map : empty;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Which avatar to show for a friend. What they advertised over Carrier wins
|
|
68
|
+
* over the directory: it is what they are using on their device right now,
|
|
69
|
+
* it needs no gateway, and it is the only avatar a friend who never
|
|
70
|
+
* registered a beagles.eth name will ever have. The backend's own report
|
|
71
|
+
* first, then the SDK's store on disk (an older daemon reports none), then
|
|
72
|
+
* the beagles.eth record. */
|
|
73
|
+
export function pickPunk(pushed, stored, directory) {
|
|
74
|
+
return punkIdOf(pushed) ?? stored ?? directory ?? null;
|
|
75
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import * as peerAddr from "@decentnetwork/peer";
|
|
|
25
25
|
import { bridgeRoster } from "./bridge-roster.js";
|
|
26
26
|
import { isSupervised } from "./relaunch.js";
|
|
27
27
|
import { historyStats } from "./assistant-stats.js";
|
|
28
|
+
import { readFriendStore, pickPunk } from "./friend-store.js";
|
|
28
29
|
import { createDiscoverFetch } from "./discover-cache.js";
|
|
29
30
|
import { applyWalletBind } from "@decentnetwork/beagle-ui/wallet-proof";
|
|
30
31
|
// Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
|
|
@@ -61,50 +62,6 @@ async function ensFetchJson(path) {
|
|
|
61
62
|
clearTimeout(timer);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
/** requestedAt / acceptedAt per friend, read from the SDK's own friend store
|
|
65
|
-
* (`<keyFile>.friends.json`, next to the identity). The daemon's friends-list
|
|
66
|
-
* reports neither for a request we sent and only acceptedAt (as lastSeen)
|
|
67
|
-
* otherwise, so a friend just added had no stamp at all and sorted to the
|
|
68
|
-
* bottom — the third time this was reported. The store is the source of
|
|
69
|
-
* truth both backends write, and it is a file in our own config dir, so
|
|
70
|
-
* read it. Cached by mtime: the list is polled every 2.5s per tab. */
|
|
71
|
-
let friendStampsCache = null;
|
|
72
|
-
function friendStoreStamps(keyFile) {
|
|
73
|
-
const empty = new Map();
|
|
74
|
-
if (!keyFile)
|
|
75
|
-
return empty;
|
|
76
|
-
const path = `${keyFile}.friends.json`;
|
|
77
|
-
try {
|
|
78
|
-
const st = statSync(path);
|
|
79
|
-
if (friendStampsCache && friendStampsCache.mtimeMs === st.mtimeMs)
|
|
80
|
-
return friendStampsCache.map;
|
|
81
|
-
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
82
|
-
const recs = Array.isArray(raw)
|
|
83
|
-
? raw
|
|
84
|
-
: raw && typeof raw === "object" && Array.isArray(raw.friends)
|
|
85
|
-
? raw.friends
|
|
86
|
-
: Object.values(raw ?? {});
|
|
87
|
-
const map = new Map();
|
|
88
|
-
for (const r of recs) {
|
|
89
|
-
if (!r || typeof r !== "object")
|
|
90
|
-
continue;
|
|
91
|
-
const id = r.userid || r.pubkey;
|
|
92
|
-
if (!id)
|
|
93
|
-
continue;
|
|
94
|
-
map.set(id, {
|
|
95
|
-
requestedAt: typeof r.requestedAt === "number" ? r.requestedAt : undefined,
|
|
96
|
-
acceptedAt: typeof r.acceptedAt === "number" ? r.acceptedAt : undefined,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
friendStampsCache = { mtimeMs: st.mtimeMs, map };
|
|
100
|
-
return map;
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
// Unreadable or absent (a fresh install has no friends yet): whatever we
|
|
104
|
-
// last read, else nothing — the list still renders, just without stamps.
|
|
105
|
-
return friendStampsCache?.map ?? empty;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
65
|
/** The machine-local service-peer overlay. Absent file = nothing adopted yet,
|
|
109
66
|
* which is the normal state, not an error. */
|
|
110
67
|
function readLocalServicePeers(path) {
|
|
@@ -2071,7 +2028,7 @@ export function startBeagleServer(opts) {
|
|
|
2071
2028
|
beagleVer: beagleVersion(),
|
|
2072
2029
|
};
|
|
2073
2030
|
const fl = (flist.ok ? (flist.data?.friends ?? []) : []);
|
|
2074
|
-
const stamps =
|
|
2031
|
+
const stamps = readFriendStore(opts.callIce?.keyFile);
|
|
2075
2032
|
const peers = fl.map((f) => {
|
|
2076
2033
|
const uid = f.userid ?? "";
|
|
2077
2034
|
const sess = sessByUserid.get(uid);
|
|
@@ -2126,11 +2083,13 @@ export function startBeagleServer(opts) {
|
|
|
2126
2083
|
requestedAt: f.requestedAt ?? stamp?.requestedAt ?? 0,
|
|
2127
2084
|
wire: "163",
|
|
2128
2085
|
avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
|
|
2129
|
-
// What they advertised over Carrier wins over the directory
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2132
|
-
|
|
2133
|
-
|
|
2086
|
+
// What they advertised over Carrier wins over the directory (see
|
|
2087
|
+
// pickPunk). The daemon's list only carries the pushed punk from
|
|
2088
|
+
// lan 0.1.309; the SDK's store on disk covers an older daemon.
|
|
2089
|
+
punkId: pickPunk(f.punkId, stamp?.punkId, ensPub.get(uid)?.punkId),
|
|
2090
|
+
// The client they advertised with their profile, for the UI.
|
|
2091
|
+
platform: f.platform ?? stamp?.platform ?? null,
|
|
2092
|
+
appVersion: f.appVersion ?? stamp?.appVersion ?? null,
|
|
2134
2093
|
};
|
|
2135
2094
|
});
|
|
2136
2095
|
const pend = pending.ok ? (pending.data?.pending ?? []) : [];
|
|
@@ -2339,7 +2298,7 @@ export function startBeagleServer(opts) {
|
|
|
2339
2298
|
opts.call({ op: "friends-list" }),
|
|
2340
2299
|
ensPublicByUserid(300),
|
|
2341
2300
|
]);
|
|
2342
|
-
const stamps =
|
|
2301
|
+
const stamps = readFriendStore(opts.callIce?.keyFile);
|
|
2343
2302
|
const stats = historyStats(opts.messagesPath);
|
|
2344
2303
|
const fl = flist.ok ? (flist.data?.friends ?? []) : [];
|
|
2345
2304
|
const friends = fl.map((f) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.122",
|
|
4
4
|
"description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@decentnetwork/chat-components": "^0.1.3",
|
|
31
31
|
"@decentnetwork/lan": "^0.1.283",
|
|
32
32
|
"@decentnetwork/peer": "^0.1.168",
|
|
33
|
-
"@decentnetwork/peer-webrtc": "^0.2.
|
|
33
|
+
"@decentnetwork/peer-webrtc": "^0.2.17",
|
|
34
34
|
"js-yaml": "^4.1.0",
|
|
35
35
|
"ws": "^8.21.1",
|
|
36
36
|
"yargs": "^17.7.2"
|