@decentnetwork/lan 0.1.227 → 0.1.228
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/daemon/ipc.d.ts +5 -1
- package/dist/daemon/ipc.js +8 -0
- package/dist/daemon/server.js +7 -0
- package/dist/ui/desktop/app.js +62 -12
- package/dist/ui/server.js +50 -0
- package/package.json +1 -1
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -44,6 +44,9 @@ export interface IpcHandlers {
|
|
|
44
44
|
/** Append a local-only chat history entry. Used for browser-local events such
|
|
45
45
|
* as WebRTC file receive; does not transmit anything to the peer. */
|
|
46
46
|
chatLogLocal: (userid: string, dir: "in" | "out", text: string) => Promise<void>;
|
|
47
|
+
/** Append a local-only file history entry after the UI server has saved a
|
|
48
|
+
* browser-received WebRTC file into downloads/. */
|
|
49
|
+
fileLogLocal: (userid: string, dir: "in" | "out", name: string, size: number) => Promise<void>;
|
|
47
50
|
/** Return persisted chat history. With `peer`, returns just that thread and
|
|
48
51
|
* honours `before`/`limit` pagination; without, every thread (legacy shape). */
|
|
49
52
|
chatHistory: (peer?: string, before?: number, limit?: number) => Promise<Record<string, unknown>>;
|
|
@@ -118,7 +121,7 @@ export interface IpcHandlers {
|
|
|
118
121
|
selfRestart: () => Promise<Record<string, unknown>>;
|
|
119
122
|
}
|
|
120
123
|
export interface IpcRequest {
|
|
121
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-log-local" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
124
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-log-local" | "file-log-local" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
122
125
|
address?: string;
|
|
123
126
|
hello?: string;
|
|
124
127
|
userid?: string;
|
|
@@ -127,6 +130,7 @@ export interface IpcRequest {
|
|
|
127
130
|
alias?: string;
|
|
128
131
|
name?: string;
|
|
129
132
|
description?: string;
|
|
133
|
+
size?: number;
|
|
130
134
|
path?: string;
|
|
131
135
|
before?: number;
|
|
132
136
|
limit?: number;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -202,6 +202,14 @@ export class IpcServer {
|
|
|
202
202
|
await this.handlers.chatLogLocal(req.userid, req.dir === "out" ? "out" : "in", req.text ?? "");
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
|
+
case "file-log-local": {
|
|
206
|
+
if (!req.userid)
|
|
207
|
+
throw new Error("userid is required");
|
|
208
|
+
if (!req.name)
|
|
209
|
+
throw new Error("name is required");
|
|
210
|
+
await this.handlers.fileLogLocal(req.userid, req.dir === "out" ? "out" : "in", req.name, Math.max(0, req.size ?? 0));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
205
213
|
case "chat-history":
|
|
206
214
|
return await this.handlers.chatHistory(req.userid, req.before, req.limit);
|
|
207
215
|
case "friends-list":
|
package/dist/daemon/server.js
CHANGED
|
@@ -386,6 +386,13 @@ export class DaemonServer {
|
|
|
386
386
|
return;
|
|
387
387
|
this.logChat(userid, dir, text);
|
|
388
388
|
},
|
|
389
|
+
fileLogLocal: async (userid, dir, name, size) => {
|
|
390
|
+
if (!name)
|
|
391
|
+
return;
|
|
392
|
+
this.messageStore?.appendFile(userid, dir, { name: sanitizeFileName(name), size });
|
|
393
|
+
this.friendMeta?.ensure(userid);
|
|
394
|
+
this.ipcEvents.emit("event", { type: "file", userid, dir, name });
|
|
395
|
+
},
|
|
389
396
|
chatHistory: async (peer, before, limit) => {
|
|
390
397
|
return { chats: this.messageStore?.history(peer, { before, limit }) ?? {} };
|
|
391
398
|
},
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -231,6 +231,22 @@ const dkApi = {
|
|
|
231
231
|
cancelSend: (userid, ids) => dkPost("/api/file-cancel", { userid, ids }),
|
|
232
232
|
retrySend: (userid, ids) => dkPost("/api/file-retry", { userid, ids }),
|
|
233
233
|
setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
|
|
234
|
+
saveWebrtcFile: async (userid, name, blob) => {
|
|
235
|
+
try {
|
|
236
|
+
const r = await fetch(
|
|
237
|
+
"/api/webrtc-file-save?userid=" + encodeURIComponent(userid) + "&name=" + encodeURIComponent(name),
|
|
238
|
+
{ method: "POST", body: blob }
|
|
239
|
+
);
|
|
240
|
+
const txt = await r.text();
|
|
241
|
+
try {
|
|
242
|
+
return JSON.parse(txt);
|
|
243
|
+
} catch {
|
|
244
|
+
return { ok: false, error: (txt || "HTTP " + r.status).slice(0, 120) };
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {
|
|
247
|
+
return { ok: false, error: String(e) };
|
|
248
|
+
}
|
|
249
|
+
},
|
|
234
250
|
sendFile: async (userid, file) => {
|
|
235
251
|
try {
|
|
236
252
|
const r = await fetch(
|
|
@@ -1190,9 +1206,14 @@ function dkRtcFileFromText(peerId, text) {
|
|
|
1190
1206
|
}
|
|
1191
1207
|
return null;
|
|
1192
1208
|
}
|
|
1193
|
-
function
|
|
1209
|
+
function dkCallFromText(text) {
|
|
1210
|
+
const m = /^WebRTC (audio|video) call: (incoming|outgoing)$/.exec(String(text || ""));
|
|
1211
|
+
return m ? { kind: m[1], direction: m[2] } : null;
|
|
1212
|
+
}
|
|
1213
|
+
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onCall, selMode, selected, onToggleSel, busy }) {
|
|
1194
1214
|
const mine = m.from === "me";
|
|
1195
1215
|
const rtcFile = !mine && !m.file ? dkRtcFileFromText(peer.userId || peer.id, m.text) : null;
|
|
1216
|
+
const callRec = !m.file ? dkCallFromText(m.text) : null;
|
|
1196
1217
|
return /* @__PURE__ */ React.createElement(
|
|
1197
1218
|
"div",
|
|
1198
1219
|
{
|
|
@@ -1222,7 +1243,16 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1222
1243
|
justifyContent: "center"
|
|
1223
1244
|
} }, selected && /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 3, color: "#fff" })),
|
|
1224
1245
|
!mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }),
|
|
1225
|
-
/* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") || rtcFile && (rtcFile.media === "image" || rtcFile.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } },
|
|
1246
|
+
/* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") || rtcFile && (rtcFile.media === "image" || rtcFile.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, callRec ? /* @__PURE__ */ React.createElement(
|
|
1247
|
+
"button",
|
|
1248
|
+
{
|
|
1249
|
+
onClick: () => onCall && onCall(peer.userId, callRec.kind === "video"),
|
|
1250
|
+
style: { display: "flex", alignItems: "center", gap: 10, padding: "9px 12px", borderRadius: 12, border: "1px solid var(--line)", background: mine ? "var(--bub-me)" : "var(--bub-them)", color: mine ? "#fff" : "var(--text)", cursor: "pointer", fontFamily: "var(--mono)", fontSize: 12.5 }
|
|
1251
|
+
},
|
|
1252
|
+
/* @__PURE__ */ React.createElement(Icon, { name: callRec.kind === "video" ? "video" : "phone", size: 17, stroke: 2, color: mine ? "#fff" : "var(--accent)" }),
|
|
1253
|
+
/* @__PURE__ */ React.createElement("span", null, callRec.direction === "incoming" ? "Incoming" : "Outgoing", " ", callRec.kind, " call"),
|
|
1254
|
+
/* @__PURE__ */ React.createElement("span", { style: { color: mine ? "rgba(255,255,255,0.75)" : "var(--faint)" } }, "\xB7 redial")
|
|
1255
|
+
) : rtcFile ? /* @__PURE__ */ React.createElement("div", { style: {
|
|
1226
1256
|
display: "flex",
|
|
1227
1257
|
flexDirection: "column",
|
|
1228
1258
|
gap: 6,
|
|
@@ -1664,6 +1694,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1664
1694
|
onDelete: (id) => doDelete([id]),
|
|
1665
1695
|
onCancel: doCancel,
|
|
1666
1696
|
onRetry: doRetry,
|
|
1697
|
+
onCall,
|
|
1667
1698
|
busy: m.id ? fileBusy[m.id] : void 0,
|
|
1668
1699
|
selMode,
|
|
1669
1700
|
selected: sel.has(m.id),
|
|
@@ -2008,13 +2039,13 @@ function dkRememberRtcFile(peerId, fileId, name, size, url) {
|
|
|
2008
2039
|
window.__dkRtcFiles.set(peerId + "|" + name + "|" + size, item);
|
|
2009
2040
|
return item;
|
|
2010
2041
|
}
|
|
2011
|
-
function useRtcFileController(selfId,
|
|
2042
|
+
function useRtcFileController(selfId, onReceivedFile) {
|
|
2012
2043
|
const [incoming, setIncoming] = React.useState([]);
|
|
2013
2044
|
const peersRef = React.useRef(/* @__PURE__ */ new Map());
|
|
2014
|
-
const
|
|
2045
|
+
const onReceivedFileRef = React.useRef(onReceivedFile);
|
|
2015
2046
|
React.useEffect(() => {
|
|
2016
|
-
|
|
2017
|
-
}, [
|
|
2047
|
+
onReceivedFileRef.current = onReceivedFile;
|
|
2048
|
+
}, [onReceivedFile]);
|
|
2018
2049
|
React.useEffect(() => {
|
|
2019
2050
|
if (!selfId || !window.RTCPeerConnection || !window.dkRtcSignalBus) return;
|
|
2020
2051
|
const bus = dkRtcSignalBus();
|
|
@@ -2055,7 +2086,7 @@ function useRtcFileController(selfId, onReceivedText) {
|
|
|
2055
2086
|
const dl = dkFileDownload(blob, meta.name || "agentnet-file");
|
|
2056
2087
|
dkRememberRtcFile(peerId, fileId, dl.name, blob.size, dl.url);
|
|
2057
2088
|
setIncoming((arr) => [{ id: fileId, peerId, name: dl.name, size: blob.size, url: dl.url }].concat(arr).slice(0, 8));
|
|
2058
|
-
if (
|
|
2089
|
+
if (onReceivedFileRef.current) onReceivedFileRef.current(peerId, dl.name, blob);
|
|
2059
2090
|
cleanup(fileId);
|
|
2060
2091
|
} else if (msg.type === "cancel") {
|
|
2061
2092
|
cleanup(fileId);
|
|
@@ -2224,13 +2255,17 @@ const CALL_ICE_SERVERS = [
|
|
|
2224
2255
|
{ urls: "turn:tokyo.fi.chat:3478", username: "allcom", credential: "allcompass" },
|
|
2225
2256
|
{ urls: "turn:tokyo.fi.chat:3478?transport=tcp", username: "allcom", credential: "allcompass" }
|
|
2226
2257
|
];
|
|
2227
|
-
function useCallController(selfId) {
|
|
2258
|
+
function useCallController(selfId, onCallLog) {
|
|
2228
2259
|
const [incoming, setIncoming] = React.useState(null);
|
|
2229
2260
|
const [active, setActive] = React.useState(null);
|
|
2230
2261
|
const [localStream, setLocalStream] = React.useState(null);
|
|
2231
2262
|
const [remoteStream, setRemoteStream] = React.useState(null);
|
|
2232
2263
|
const [sharing, setSharing] = React.useState(false);
|
|
2233
2264
|
const engineRef = React.useRef(null);
|
|
2265
|
+
const onCallLogRef = React.useRef(onCallLog);
|
|
2266
|
+
React.useEffect(() => {
|
|
2267
|
+
onCallLogRef.current = onCallLog;
|
|
2268
|
+
}, [onCallLog]);
|
|
2234
2269
|
React.useEffect(() => {
|
|
2235
2270
|
if (!selfId || engineRef.current) return;
|
|
2236
2271
|
if (!window.PeerWebRTC || !window.RTCPeerConnection) {
|
|
@@ -2248,7 +2283,10 @@ function useCallController(selfId) {
|
|
|
2248
2283
|
iceServers: CALL_ICE_SERVERS,
|
|
2249
2284
|
logger: (m) => console.log(m)
|
|
2250
2285
|
});
|
|
2251
|
-
engine.on("incomingCall", (info) =>
|
|
2286
|
+
engine.on("incomingCall", (info) => {
|
|
2287
|
+
setIncoming(info);
|
|
2288
|
+
if (onCallLogRef.current) onCallLogRef.current(info.peerId, !!info.video, "incoming");
|
|
2289
|
+
});
|
|
2252
2290
|
engine.on("stateChanged", (info, state) => setActive((a) => a && a.callId === info.callId ? { ...a, state } : a));
|
|
2253
2291
|
engine.on("localStream", (id, s) => {
|
|
2254
2292
|
setLocalStream(s);
|
|
@@ -2281,6 +2319,7 @@ function useCallController(selfId) {
|
|
|
2281
2319
|
return;
|
|
2282
2320
|
}
|
|
2283
2321
|
setActive({ callId: null, peerId, video: !!video, direction: "outgoing", state: "ringing" });
|
|
2322
|
+
if (onCallLogRef.current) onCallLogRef.current(peerId, !!video, "outgoing");
|
|
2284
2323
|
eng.call(peerId, { audio: true, video: !!video }).then((callId) => setActive((a) => a && a.peerId === peerId && a.callId === null ? { ...a, callId } : a)).catch((e) => {
|
|
2285
2324
|
setActive((a) => a && a.peerId === peerId ? null : a);
|
|
2286
2325
|
alert("Could not start call: " + (e && e.message || e));
|
|
@@ -2748,10 +2787,21 @@ function DkApp() {
|
|
|
2748
2787
|
const T = STR[t.lang] || STR.en;
|
|
2749
2788
|
const vars = dkTheme(t.theme, t.accent);
|
|
2750
2789
|
const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
|
|
2751
|
-
const callCtl = useCallController(me.userId)
|
|
2790
|
+
const callCtl = useCallController(me.userId, (peerId, video, direction) => {
|
|
2791
|
+
const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
|
|
2792
|
+
dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
|
|
2793
|
+
data.loadThread(peerId);
|
|
2794
|
+
data.refresh();
|
|
2795
|
+
});
|
|
2796
|
+
});
|
|
2752
2797
|
const onCall = (peerId, video) => callCtl.start(peerId, !!video);
|
|
2753
|
-
const rtcFileCtl = useRtcFileController(me.userId, (peerId,
|
|
2754
|
-
|
|
2798
|
+
const rtcFileCtl = useRtcFileController(me.userId, (peerId, name, blob) => {
|
|
2799
|
+
if (!blob) return;
|
|
2800
|
+
dkApi.saveWebrtcFile(peerId, name, blob).then((r) => {
|
|
2801
|
+
if (!r || r.ok === false) {
|
|
2802
|
+
return dkApi.logLocal(peerId, "in", "received file via WebRTC: " + name + " (" + dkFileSize(blob.size) + ")");
|
|
2803
|
+
}
|
|
2804
|
+
}).then(() => {
|
|
2755
2805
|
data.loadThread(peerId);
|
|
2756
2806
|
data.refresh();
|
|
2757
2807
|
});
|
package/dist/ui/server.js
CHANGED
|
@@ -346,6 +346,56 @@ export function startFriendUi(opts) {
|
|
|
346
346
|
}
|
|
347
347
|
return;
|
|
348
348
|
}
|
|
349
|
+
// Persist a file received by browser WebRTC DataChannel. Unlike the
|
|
350
|
+
// ordinary daemon filetransfer path, the bytes arrive in the browser, so
|
|
351
|
+
// the browser uploads the completed Blob here; we save it under
|
|
352
|
+
// downloads/ and ask the daemon to append a normal inbound file chip.
|
|
353
|
+
if (req.method === "POST" && url === "/api/webrtc-file-save") {
|
|
354
|
+
if (!opts.downloadsDir) {
|
|
355
|
+
sendJson(res, 404, { ok: false, error: "downloads disabled" });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const q = new URL(req.url || "", "http://x").searchParams;
|
|
359
|
+
const userid = q.get("userid") || "";
|
|
360
|
+
const rawName = q.get("name") || "agentnet-file";
|
|
361
|
+
const safe = rawName.replace(/[/\\]/g, "_").slice(0, 200) || "agentnet-file";
|
|
362
|
+
if (!userid) {
|
|
363
|
+
sendJson(res, 400, { ok: false, error: "userid required" });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const fs = await import("node:fs");
|
|
367
|
+
const fsp = await import("node:fs/promises");
|
|
368
|
+
await fsp.mkdir(opts.downloadsDir, { recursive: true });
|
|
369
|
+
let finalName = safe;
|
|
370
|
+
let finalPath = join(opts.downloadsDir, finalName);
|
|
371
|
+
const dot = safe.lastIndexOf(".");
|
|
372
|
+
const base = dot > 0 ? safe.slice(0, dot) : safe;
|
|
373
|
+
const ext = dot > 0 ? safe.slice(dot) : "";
|
|
374
|
+
for (let i = 1; existsSync(finalPath); i++) {
|
|
375
|
+
finalName = `${base}-${i}${ext}`;
|
|
376
|
+
finalPath = join(opts.downloadsDir, finalName);
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
await new Promise((resolve2, reject) => {
|
|
380
|
+
const ws = fs.createWriteStream(finalPath);
|
|
381
|
+
req.pipe(ws);
|
|
382
|
+
req.on("error", reject);
|
|
383
|
+
ws.on("error", reject);
|
|
384
|
+
ws.on("finish", () => resolve2());
|
|
385
|
+
});
|
|
386
|
+
const st = statSync(finalPath);
|
|
387
|
+
const r = await opts.call({ op: "file-log-local", userid, dir: "in", name: finalName, size: st.size });
|
|
388
|
+
if (!r.ok) {
|
|
389
|
+
sendJson(res, 502, r);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
sendJson(res, 200, { ok: true, name: finalName, size: st.size });
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
sendJson(res, 500, { ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
349
399
|
// Serve a received file by name, from <configDir>/downloads. Used both for
|
|
350
400
|
// download (?dl=1 → attachment) and inline preview/playback (no dl → the
|
|
351
401
|
// right media content-type + inline disposition + HTTP Range so <video>/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.228",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|