vibedate 0.4.0 → 0.4.1
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/cli.d.ts +1 -1
- package/dist/cli.js +415 -36
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ interface ParsedArgs {
|
|
|
9
9
|
readonly live: boolean;
|
|
10
10
|
/** `live --dating`: pick-a-handle mode vs omegle auto-pair. Default false. */
|
|
11
11
|
readonly dating: boolean;
|
|
12
|
-
/** `discover --any` / `live --any`: match every league (not just ±1). Default false. */
|
|
12
|
+
/** `discover --any` / `live --any` / `open --any`: match every league (not just ±1). Default false. */
|
|
13
13
|
readonly any: boolean;
|
|
14
14
|
/** Positional argument for `handle`/`find` (e.g. `@name`). */
|
|
15
15
|
readonly arg: string | undefined;
|
package/dist/cli.js
CHANGED
|
@@ -110,6 +110,7 @@ function createPairing() {
|
|
|
110
110
|
|
|
111
111
|
// src/server.ts
|
|
112
112
|
import http from "http";
|
|
113
|
+
import { randomUUID } from "crypto";
|
|
113
114
|
import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
|
|
114
115
|
|
|
115
116
|
// src/web-app-html.ts
|
|
@@ -545,6 +546,80 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
545
546
|
background: var(--danger); color: #2a0a0c;
|
|
546
547
|
}
|
|
547
548
|
.video-modal .vhangup:hover{ filter: brightness(1.06); }
|
|
549
|
+
.live-panel .lp-legend{ font-size: .66rem; color: var(--muted-2); margin-top: 8px; line-height: 1.4; }
|
|
550
|
+
.incoming-call{
|
|
551
|
+
position: fixed; inset: 0; z-index: 65;
|
|
552
|
+
display: flex; align-items: center; justify-content: center;
|
|
553
|
+
background: rgba(12,7,15,.6); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
|
|
554
|
+
opacity: 0; pointer-events: none; transition: opacity var(--dur-med) ease;
|
|
555
|
+
}
|
|
556
|
+
.incoming-call.is-open{ opacity: 1; pointer-events: auto; }
|
|
557
|
+
.incoming-call .ic-card{
|
|
558
|
+
background: linear-gradient(165deg, var(--bg-card), var(--bg-card-2));
|
|
559
|
+
border: 1px solid var(--border-2); border-radius: 24px; box-shadow: var(--shadow-3);
|
|
560
|
+
padding: 30px 28px; max-width: 320px; text-align: center;
|
|
561
|
+
transform: scale(.85) translateY(10px); transition: transform var(--dur-med) var(--ease-bounce);
|
|
562
|
+
}
|
|
563
|
+
.incoming-call.is-open .ic-card{ transform: scale(1) translateY(0); }
|
|
564
|
+
.incoming-call h3{ margin: 0 0 6px; font-size: 1.15rem; font-weight: 800; word-break: break-all; }
|
|
565
|
+
.incoming-call p{ color: var(--muted); font-size: .85rem; margin: 0 0 18px; }
|
|
566
|
+
.incoming-call .ic-actions{ display: flex; gap: 10px; }
|
|
567
|
+
|
|
568
|
+
/* ---- live text chat (browser <-> PeerLink over /live/message) ---- */
|
|
569
|
+
.live-actions{ display: flex; gap: 6px; flex-shrink: 0; }
|
|
570
|
+
.live-row .cbtn{
|
|
571
|
+
border: 1px solid var(--border-2); border-radius: 9px; padding: 7px 12px;
|
|
572
|
+
font-weight: 700; font-size: .76rem; background: transparent; color: var(--muted);
|
|
573
|
+
transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
|
|
574
|
+
flex-shrink: 0;
|
|
575
|
+
}
|
|
576
|
+
.live-row .cbtn:hover{ color: var(--fg); border-color: var(--muted-2); }
|
|
577
|
+
.live-row .cbtn:active{ transform: scale(.95); }
|
|
578
|
+
.live-row .cbtn.has-unread{ color: var(--coral); border-color: var(--coral); }
|
|
579
|
+
.chat-panel{
|
|
580
|
+
position: fixed; right: 18px; bottom: 18px; z-index: 45;
|
|
581
|
+
width: min(320px, calc(100vw - 36px));
|
|
582
|
+
background: linear-gradient(180deg, var(--bg-card), var(--bg-card-2));
|
|
583
|
+
border: 1px solid var(--border-2); border-radius: 16px; box-shadow: var(--shadow-2);
|
|
584
|
+
display: none; flex-direction: column; overflow: hidden;
|
|
585
|
+
}
|
|
586
|
+
.chat-panel.is-open{ display: flex; animation: rise var(--dur-med) var(--ease-out); }
|
|
587
|
+
.chat-panel .cp-head{
|
|
588
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
589
|
+
padding: 11px 14px; border-bottom: 1px solid var(--border);
|
|
590
|
+
}
|
|
591
|
+
.chat-panel .cp-title{ font-size: .84rem; font-weight: 700; word-break: break-all; }
|
|
592
|
+
.chat-panel .cp-sub{ font-size: .68rem; color: var(--muted-2); margin-top: 2px; }
|
|
593
|
+
.chat-panel .cp-close{
|
|
594
|
+
border: 0; background: transparent; color: var(--muted-2);
|
|
595
|
+
font-size: 1.15rem; line-height: 1; padding: 4px; flex-shrink: 0;
|
|
596
|
+
}
|
|
597
|
+
.chat-panel .cp-close:hover{ color: var(--fg); }
|
|
598
|
+
.chat-panel .cp-msgs{
|
|
599
|
+
padding: 12px 14px; min-height: 120px; max-height: 260px; overflow-y: auto;
|
|
600
|
+
display: flex; flex-direction: column; gap: 7px;
|
|
601
|
+
}
|
|
602
|
+
.chat-panel .cp-empty{ font-size: .76rem; color: var(--muted-2); text-align: center; margin: auto; }
|
|
603
|
+
.cp-msg{
|
|
604
|
+
max-width: 85%; padding: 7px 11px; border-radius: 13px;
|
|
605
|
+
font-size: .82rem; line-height: 1.4; word-break: break-word; white-space: pre-wrap;
|
|
606
|
+
}
|
|
607
|
+
.cp-msg.them{ align-self: flex-start; background: rgba(248,239,232,.07); border: 1px solid var(--border); }
|
|
608
|
+
.cp-msg.you{ align-self: flex-end; background: rgba(255,122,104,.16); border: 1px solid rgba(255,122,104,.32); }
|
|
609
|
+
.cp-msg.sys{ align-self: center; background: transparent; border: 0; color: var(--muted-2); font-size: .72rem; padding: 2px 6px; }
|
|
610
|
+
.chat-panel .cp-inputrow{ display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--border); }
|
|
611
|
+
.chat-panel .cp-inputrow input{
|
|
612
|
+
flex: 1; border: 1px solid var(--border-2); border-radius: 10px; background: rgba(0,0,0,.22);
|
|
613
|
+
color: var(--fg); padding: 9px 11px; font: inherit; font-size: .82rem; outline: none; min-width: 0;
|
|
614
|
+
}
|
|
615
|
+
.chat-panel .cp-inputrow input:focus{ border-color: var(--coral); }
|
|
616
|
+
.chat-panel .cp-send{
|
|
617
|
+
border: 0; border-radius: 10px; padding: 9px 14px; font-weight: 700; font-size: .8rem;
|
|
618
|
+
background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
|
|
619
|
+
transition: filter var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
|
|
620
|
+
}
|
|
621
|
+
.chat-panel .cp-send:hover{ filter: brightness(1.06); }
|
|
622
|
+
.chat-panel .cp-send:active{ transform: scale(.95); }
|
|
548
623
|
</style>
|
|
549
624
|
</head>
|
|
550
625
|
<body>
|
|
@@ -689,9 +764,36 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
689
764
|
</div>
|
|
690
765
|
</div>
|
|
691
766
|
|
|
692
|
-
<aside class="live-panel" id="livePanel" aria-label="Live
|
|
693
|
-
<div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live
|
|
767
|
+
<aside class="live-panel" id="livePanel" aria-label="Live peers">
|
|
768
|
+
<div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live peers</div>
|
|
694
769
|
<div id="liveRows"></div>
|
|
770
|
+
<div class="lp-legend">✓ usage verified · 🔑 identity verified — call someone, or wait for a call</div>
|
|
771
|
+
</aside>
|
|
772
|
+
|
|
773
|
+
<div class="incoming-call" id="incomingCall" role="dialog" aria-modal="true" aria-label="Incoming call">
|
|
774
|
+
<div class="ic-card">
|
|
775
|
+
<h3 id="incomingCaller">@peer</h3>
|
|
776
|
+
<p>is calling you — video chat</p>
|
|
777
|
+
<div class="ic-actions">
|
|
778
|
+
<button class="btn btn-ghost" id="declineBtn" type="button">Decline</button>
|
|
779
|
+
<button class="btn btn-primary" id="acceptBtn" type="button">Accept</button>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<aside class="chat-panel" id="chatPanel" aria-label="Text chat">
|
|
785
|
+
<div class="cp-head">
|
|
786
|
+
<div>
|
|
787
|
+
<div class="cp-title" id="chatTitle">@peer</div>
|
|
788
|
+
<div class="cp-sub" id="chatSub">live over the P2P link · text only</div>
|
|
789
|
+
</div>
|
|
790
|
+
<button class="cp-close" id="chatClose" type="button" aria-label="Close chat">×</button>
|
|
791
|
+
</div>
|
|
792
|
+
<div class="cp-msgs" id="chatMsgs"></div>
|
|
793
|
+
<div class="cp-inputrow">
|
|
794
|
+
<input id="chatInput" type="text" maxlength="4000" placeholder="message…" autocomplete="off">
|
|
795
|
+
<button class="cp-send" id="chatSend" type="button">Send</button>
|
|
796
|
+
</div>
|
|
695
797
|
</aside>
|
|
696
798
|
|
|
697
799
|
<div class="video-modal" id="videoModal" role="dialog" aria-modal="true" aria-label="Video call">
|
|
@@ -998,9 +1100,15 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
998
1100
|
var remoteVideo = document.getElementById("remoteVideo");
|
|
999
1101
|
var remoteLabel = document.getElementById("remoteLabel");
|
|
1000
1102
|
var hangupBtn = document.getElementById("hangupBtn");
|
|
1103
|
+
var incomingCallEl = document.getElementById("incomingCall");
|
|
1104
|
+
var incomingCaller = document.getElementById("incomingCaller");
|
|
1105
|
+
var acceptBtn = document.getElementById("acceptBtn");
|
|
1106
|
+
var declineBtn = document.getElementById("declineBtn");
|
|
1001
1107
|
|
|
1002
1108
|
// One in-flight call's state. remoteHandle === null means idle.
|
|
1003
1109
|
var rtc = { pc: null, localStream: null, remoteHandle: null, role: null };
|
|
1110
|
+
// An incoming offer awaiting the user's accept/decline (never auto-answered).
|
|
1111
|
+
var pendingOffer = null;
|
|
1004
1112
|
var knownPeers = [];
|
|
1005
1113
|
var idleIdx = 0;
|
|
1006
1114
|
|
|
@@ -1117,7 +1225,8 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1117
1225
|
}
|
|
1118
1226
|
|
|
1119
1227
|
// Idle listener: when not in a call, watch known peers round-robin so an
|
|
1120
|
-
// incoming offer (the peer clicked THEIR
|
|
1228
|
+
// incoming offer (the peer clicked THEIR Call button) is noticed. Offers are
|
|
1229
|
+
// never auto-answered \u2014 the caller is named and the user accepts or declines.
|
|
1121
1230
|
async function idleLoop(){
|
|
1122
1231
|
while (true) {
|
|
1123
1232
|
if (rtc.remoteHandle || knownPeers.length === 0) { await sleep(1000); continue; }
|
|
@@ -1129,7 +1238,12 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1129
1238
|
var data = await res.json();
|
|
1130
1239
|
var f = data && data.frame;
|
|
1131
1240
|
if (f && f.t === "rtc-offer" && !rtc.pc && !rtc.remoteHandle) {
|
|
1132
|
-
|
|
1241
|
+
// One prompt at a time: a re-offer from the SAME caller refreshes
|
|
1242
|
+
// the stored sdp; an offer from someone else while a prompt is up
|
|
1243
|
+
// is dropped (their call simply fails to connect).
|
|
1244
|
+
if (pendingOffer && pendingOffer.handle !== peer.handle) continue;
|
|
1245
|
+
pendingOffer = { handle: peer.handle, sdp: f.sdp };
|
|
1246
|
+
showIncomingPrompt(peer.handle);
|
|
1133
1247
|
}
|
|
1134
1248
|
}
|
|
1135
1249
|
} catch(e){ /* abort/timeout \u2014 loop to next peer */ }
|
|
@@ -1137,6 +1251,22 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1137
1251
|
}
|
|
1138
1252
|
idleLoop();
|
|
1139
1253
|
|
|
1254
|
+
function showIncomingPrompt(handle){
|
|
1255
|
+
incomingCaller.textContent = handle;
|
|
1256
|
+
incomingCallEl.classList.add("is-open");
|
|
1257
|
+
}
|
|
1258
|
+
function clearIncomingPrompt(){
|
|
1259
|
+
pendingOffer = null;
|
|
1260
|
+
incomingCallEl.classList.remove("is-open");
|
|
1261
|
+
}
|
|
1262
|
+
acceptBtn.addEventListener("click", function(){
|
|
1263
|
+
var p = pendingOffer;
|
|
1264
|
+
clearIncomingPrompt();
|
|
1265
|
+
if (!p) return;
|
|
1266
|
+
answerIncomingOffer(p.handle, p.sdp).catch(function(){ hangup(); });
|
|
1267
|
+
});
|
|
1268
|
+
declineBtn.addEventListener("click", clearIncomingPrompt);
|
|
1269
|
+
|
|
1140
1270
|
function hangup(){
|
|
1141
1271
|
try { if (rtc.pc) rtc.pc.close(); } catch(e){}
|
|
1142
1272
|
rtc.pc = null;
|
|
@@ -1152,7 +1282,171 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1152
1282
|
}
|
|
1153
1283
|
hangupBtn.addEventListener("click", hangup);
|
|
1154
1284
|
|
|
1155
|
-
|
|
1285
|
+
/* ---- Live text chat: msg frames over the same PeerLinks, via /live/message ----
|
|
1286
|
+
* One long-poll loop per connected peer surfaces incoming texts immediately,
|
|
1287
|
+
* no matter which conversation is open. Outbound texts POST {handle, text};
|
|
1288
|
+
* the server re-validates through the frame allowlist and mints id/at. */
|
|
1289
|
+
var chatPanel = document.getElementById("chatPanel");
|
|
1290
|
+
var chatTitle = document.getElementById("chatTitle");
|
|
1291
|
+
var chatSub = document.getElementById("chatSub");
|
|
1292
|
+
var chatMsgs = document.getElementById("chatMsgs");
|
|
1293
|
+
var chatInput = document.getElementById("chatInput");
|
|
1294
|
+
var chatSend = document.getElementById("chatSend");
|
|
1295
|
+
var chatClose = document.getElementById("chatClose");
|
|
1296
|
+
|
|
1297
|
+
var chatWith = null; // handle of the open conversation (null = closed)
|
|
1298
|
+
var conversations = {}; // handle -> [{from:"you"|"them"|"sys", text}]
|
|
1299
|
+
var unread = {}; // handle -> count of unseen incoming messages
|
|
1300
|
+
var chatLoops = {}; // handle -> true while its poll loop is running
|
|
1301
|
+
var MAX_CHAT_KEPT = 200; // per-conversation local cap (mirrors the server)
|
|
1302
|
+
|
|
1303
|
+
function fetchMessage(handle, timeoutMs){
|
|
1304
|
+
var ctrl = new AbortController();
|
|
1305
|
+
var t = setTimeout(function(){ ctrl.abort(); }, timeoutMs);
|
|
1306
|
+
return fetch("/live/message?handle=" + encodeURIComponent(handle), { signal: ctrl.signal })
|
|
1307
|
+
.then(function(r){ clearTimeout(t); return r; })
|
|
1308
|
+
.catch(function(e){ clearTimeout(t); throw e; });
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function postChat(handle, text){
|
|
1312
|
+
return fetch("/live/message", {
|
|
1313
|
+
method: "POST",
|
|
1314
|
+
headers: { "content-type": "application/json" },
|
|
1315
|
+
body: JSON.stringify({ handle: handle, text: text })
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function peerKnown(handle){
|
|
1320
|
+
for (var i = 0; i < knownPeers.length; i++){ if (knownPeers[i].handle === handle) return true; }
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function pushChat(handle, entry){
|
|
1325
|
+
var conv = conversations[handle] || (conversations[handle] = []);
|
|
1326
|
+
conv.push(entry);
|
|
1327
|
+
if (conv.length > MAX_CHAT_KEPT) conv.splice(0, conv.length - MAX_CHAT_KEPT);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// One long-poll loop per connected peer: started when the peer is first
|
|
1331
|
+
// seen, stopped when it vanishes (its mailbox on the server is gone).
|
|
1332
|
+
function ensureChatLoop(handle){
|
|
1333
|
+
if (chatLoops[handle]) return;
|
|
1334
|
+
chatLoops[handle] = true;
|
|
1335
|
+
(async function(){
|
|
1336
|
+
while (peerKnown(handle)) {
|
|
1337
|
+
var res;
|
|
1338
|
+
try { res = await fetchMessage(handle, 30000); }
|
|
1339
|
+
catch(e){ await sleep(1000); continue; }
|
|
1340
|
+
if (!res || res.status !== 200) { await sleep(1000); continue; }
|
|
1341
|
+
var data = await res.json();
|
|
1342
|
+
var m = data && data.message;
|
|
1343
|
+
if (!m) continue; // timed out empty
|
|
1344
|
+
pushChat(handle, { from: "them", text: m.text });
|
|
1345
|
+
if (chatWith === handle) {
|
|
1346
|
+
renderChat();
|
|
1347
|
+
} else {
|
|
1348
|
+
unread[handle] = (unread[handle] || 0) + 1;
|
|
1349
|
+
renderLiveRows(knownPeers);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
delete chatLoops[handle];
|
|
1353
|
+
})();
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function renderChat(){
|
|
1357
|
+
chatMsgs.innerHTML = "";
|
|
1358
|
+
var conv = (chatWith && conversations[chatWith]) || [];
|
|
1359
|
+
if (conv.length === 0) {
|
|
1360
|
+
var empty = document.createElement("div");
|
|
1361
|
+
empty.className = "cp-empty";
|
|
1362
|
+
empty.textContent = "No messages yet \u2014 say hi.";
|
|
1363
|
+
chatMsgs.appendChild(empty);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
conv.forEach(function(m){
|
|
1367
|
+
var el = document.createElement("div");
|
|
1368
|
+
el.className = "cp-msg " + (m.from === "you" ? "you" : m.from === "sys" ? "sys" : "them");
|
|
1369
|
+
// textContent only \u2014 a peer's text is never parsed as HTML.
|
|
1370
|
+
el.textContent = m.text;
|
|
1371
|
+
chatMsgs.appendChild(el);
|
|
1372
|
+
});
|
|
1373
|
+
chatMsgs.scrollTop = chatMsgs.scrollHeight;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function openChat(handle){
|
|
1377
|
+
chatWith = handle;
|
|
1378
|
+
delete unread[handle];
|
|
1379
|
+
chatTitle.textContent = handle;
|
|
1380
|
+
chatSub.textContent = peerKnown(handle)
|
|
1381
|
+
? "live over the P2P link \xB7 text only"
|
|
1382
|
+
: "peer offline \xB7 messages will not deliver";
|
|
1383
|
+
chatPanel.classList.add("is-open");
|
|
1384
|
+
renderChat();
|
|
1385
|
+
renderLiveRows(knownPeers);
|
|
1386
|
+
chatInput.focus();
|
|
1387
|
+
}
|
|
1388
|
+
chatClose.addEventListener("click", function(){
|
|
1389
|
+
chatWith = null;
|
|
1390
|
+
chatPanel.classList.remove("is-open");
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
function sendChat(){
|
|
1394
|
+
var target = chatWith;
|
|
1395
|
+
var text = chatInput.value.trim();
|
|
1396
|
+
if (!target || text === "") return;
|
|
1397
|
+
chatInput.value = "";
|
|
1398
|
+
postChat(target, text).then(function(res){
|
|
1399
|
+
// Honest failure \u2014 never show a message as sent when it wasn't.
|
|
1400
|
+
pushChat(target, res && res.status === 200
|
|
1401
|
+
? { from: "you", text: text }
|
|
1402
|
+
: { from: "sys", text: "(not delivered \u2014 peer offline?)" });
|
|
1403
|
+
if (chatWith === target) renderChat();
|
|
1404
|
+
}).catch(function(){
|
|
1405
|
+
pushChat(target, { from: "sys", text: "(not delivered \u2014 server unreachable)" });
|
|
1406
|
+
if (chatWith === target) renderChat();
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
chatSend.addEventListener("click", sendChat);
|
|
1410
|
+
chatInput.addEventListener("keydown", function(e){ if (e.key === "Enter") sendChat(); });
|
|
1411
|
+
|
|
1412
|
+
// Render the live-peers rows: one per connected peer with its verification
|
|
1413
|
+
// marks, a Chat button (opens the conversation), and a Call button that
|
|
1414
|
+
// rings THAT peer specifically.
|
|
1415
|
+
function renderLiveRows(peers){
|
|
1416
|
+
liveRows.innerHTML = "";
|
|
1417
|
+
peers.forEach(function(p){
|
|
1418
|
+
var row = document.createElement("div");
|
|
1419
|
+
row.className = "live-row";
|
|
1420
|
+
var who = document.createElement("div");
|
|
1421
|
+
// Marks from the peer's hello, shown only when present: \u2713 usage
|
|
1422
|
+
// verified (real local logs), \u{1F511} identity-verified (signed hello).
|
|
1423
|
+
var marks = (p.verified === true ? " \u2713" : "") + (p.identityVerified === true ? " \u{1F511}" : "");
|
|
1424
|
+
var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle + marks;
|
|
1425
|
+
var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
|
|
1426
|
+
who.appendChild(h); who.appendChild(s);
|
|
1427
|
+
var actions = document.createElement("div");
|
|
1428
|
+
actions.className = "live-actions";
|
|
1429
|
+
var chatBtn = document.createElement("button");
|
|
1430
|
+
chatBtn.className = "cbtn" + (unread[p.handle] ? " has-unread" : "");
|
|
1431
|
+
chatBtn.type = "button";
|
|
1432
|
+
chatBtn.textContent = "Chat" + (unread[p.handle] ? " (" + unread[p.handle] + ")" : "");
|
|
1433
|
+
chatBtn.addEventListener("click", function(){ openChat(p.handle); });
|
|
1434
|
+
var btn = document.createElement("button");
|
|
1435
|
+
btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Call";
|
|
1436
|
+
btn.addEventListener("click", function(){
|
|
1437
|
+
if (rtc.remoteHandle || pendingOffer) return; // already in a call / prompt
|
|
1438
|
+
btn.disabled = true;
|
|
1439
|
+
// DIRECTED call: the rtc-offer is relayed only to this peer's handle.
|
|
1440
|
+
startCallAsOfferer(p.handle).catch(function(){ hangup(); });
|
|
1441
|
+
});
|
|
1442
|
+
actions.appendChild(chatBtn);
|
|
1443
|
+
actions.appendChild(btn);
|
|
1444
|
+
row.appendChild(who);
|
|
1445
|
+
row.appendChild(actions);
|
|
1446
|
+
liveRows.appendChild(row);
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1156
1450
|
async function refreshPeers(){
|
|
1157
1451
|
try {
|
|
1158
1452
|
var res = await fetch("/api/live/peers");
|
|
@@ -1160,26 +1454,15 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1160
1454
|
var data = await res.json();
|
|
1161
1455
|
var peers = (data && data.peers) || [];
|
|
1162
1456
|
knownPeers = peers;
|
|
1457
|
+
peers.forEach(function(p){ ensureChatLoop(p.handle); });
|
|
1458
|
+
if (chatWith) {
|
|
1459
|
+
chatSub.textContent = peerKnown(chatWith)
|
|
1460
|
+
? "live over the P2P link \xB7 text only"
|
|
1461
|
+
: "peer offline \xB7 messages will not deliver";
|
|
1462
|
+
}
|
|
1163
1463
|
if (peers.length === 0) { livePanel.classList.remove("is-open"); return; }
|
|
1164
1464
|
livePanel.classList.add("is-open");
|
|
1165
|
-
|
|
1166
|
-
peers.forEach(function(p){
|
|
1167
|
-
var row = document.createElement("div");
|
|
1168
|
-
row.className = "live-row";
|
|
1169
|
-
var who = document.createElement("div");
|
|
1170
|
-
var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle;
|
|
1171
|
-
var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
|
|
1172
|
-
who.appendChild(h); who.appendChild(s);
|
|
1173
|
-
var btn = document.createElement("button");
|
|
1174
|
-
btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Video";
|
|
1175
|
-
btn.addEventListener("click", function(){
|
|
1176
|
-
if (rtc.remoteHandle) return; // already in a call
|
|
1177
|
-
btn.disabled = true;
|
|
1178
|
-
startCallAsOfferer(p.handle).catch(function(){ btn.disabled = false; });
|
|
1179
|
-
});
|
|
1180
|
-
row.appendChild(who); row.appendChild(btn);
|
|
1181
|
-
liveRows.appendChild(row);
|
|
1182
|
-
});
|
|
1465
|
+
renderLiveRows(peers);
|
|
1183
1466
|
} catch(e){}
|
|
1184
1467
|
}
|
|
1185
1468
|
refreshPeers();
|
|
@@ -1190,19 +1473,37 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1190
1473
|
</html>`;
|
|
1191
1474
|
|
|
1192
1475
|
// src/server.ts
|
|
1476
|
+
var MAX_QUEUED_MESSAGES = 200;
|
|
1193
1477
|
function createLiveBridge() {
|
|
1194
1478
|
const boxes = /* @__PURE__ */ new Map();
|
|
1195
1479
|
const bridge = {
|
|
1196
1480
|
get peers() {
|
|
1197
|
-
return [...boxes.values()].map((m) =>
|
|
1481
|
+
return [...boxes.values()].map((m) => {
|
|
1482
|
+
const h = m.link.hello;
|
|
1483
|
+
return {
|
|
1484
|
+
handle: h.handle,
|
|
1485
|
+
league: h.league,
|
|
1486
|
+
harness: h.harness,
|
|
1487
|
+
...h.verified !== void 0 ? { verified: h.verified } : {},
|
|
1488
|
+
...h.identityVerified !== void 0 ? { identityVerified: h.identityVerified } : {}
|
|
1489
|
+
};
|
|
1490
|
+
});
|
|
1198
1491
|
},
|
|
1199
1492
|
addLink(link) {
|
|
1200
1493
|
const handle2 = link.hello.handle;
|
|
1201
|
-
boxes.set(handle2, { link, incoming: [] });
|
|
1494
|
+
boxes.set(handle2, { link, incoming: [], messages: [] });
|
|
1202
1495
|
link.onSignal((f) => {
|
|
1203
1496
|
const mb = boxes.get(handle2);
|
|
1204
1497
|
if (mb) mb.incoming.push(f);
|
|
1205
1498
|
});
|
|
1499
|
+
link.onMessage((m) => {
|
|
1500
|
+
const mb = boxes.get(handle2);
|
|
1501
|
+
if (!mb) return;
|
|
1502
|
+
mb.messages.push(m);
|
|
1503
|
+
if (mb.messages.length > MAX_QUEUED_MESSAGES) {
|
|
1504
|
+
mb.messages.splice(0, mb.messages.length - MAX_QUEUED_MESSAGES);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1206
1507
|
link.onClose(() => {
|
|
1207
1508
|
const cur = boxes.get(handle2);
|
|
1208
1509
|
if (cur && cur.link === link) boxes.delete(handle2);
|
|
@@ -1223,6 +1524,22 @@ function createLiveBridge() {
|
|
|
1223
1524
|
if (cur.incoming.length > 0) return cur.incoming.shift() ?? null;
|
|
1224
1525
|
}
|
|
1225
1526
|
return null;
|
|
1527
|
+
},
|
|
1528
|
+
sendMessage(handle2, text) {
|
|
1529
|
+
boxes.get(handle2)?.link.send(text);
|
|
1530
|
+
},
|
|
1531
|
+
async pollMessage(handle2, timeoutMs) {
|
|
1532
|
+
const mb = boxes.get(handle2);
|
|
1533
|
+
if (!mb) return null;
|
|
1534
|
+
if (mb.messages.length > 0) return mb.messages.shift() ?? null;
|
|
1535
|
+
const deadline = Date.now() + timeoutMs;
|
|
1536
|
+
while (Date.now() < deadline) {
|
|
1537
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1538
|
+
const cur = boxes.get(handle2);
|
|
1539
|
+
if (!cur) return null;
|
|
1540
|
+
if (cur.messages.length > 0) return cur.messages.shift() ?? null;
|
|
1541
|
+
}
|
|
1542
|
+
return null;
|
|
1226
1543
|
}
|
|
1227
1544
|
};
|
|
1228
1545
|
return bridge;
|
|
@@ -1396,11 +1713,62 @@ async function handle(req, res, opts) {
|
|
|
1396
1713
|
sendJson(res, 200, { ok: true });
|
|
1397
1714
|
return;
|
|
1398
1715
|
}
|
|
1716
|
+
if (req.method === "GET" && pathname === "/live/message") {
|
|
1717
|
+
const live = opts.live;
|
|
1718
|
+
if (!live) {
|
|
1719
|
+
sendJson(res, 200, { message: null, reason: "live-not-attached" });
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
const handle2 = url.searchParams.get("handle") ?? "";
|
|
1723
|
+
if (handle2 === "") {
|
|
1724
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const message = await live.pollMessage(handle2, 25e3);
|
|
1728
|
+
if (req.destroyed || res.writableEnded) return;
|
|
1729
|
+
sendJson(res, 200, { message });
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
if (req.method === "POST" && pathname === "/live/message") {
|
|
1733
|
+
const live = opts.live;
|
|
1734
|
+
if (!live) {
|
|
1735
|
+
sendJson(res, 400, { error: "live-not-attached" });
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const body = await readBody(req);
|
|
1739
|
+
let parsed = {};
|
|
1740
|
+
try {
|
|
1741
|
+
parsed = JSON.parse(body);
|
|
1742
|
+
} catch {
|
|
1743
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const handle2 = typeof parsed["handle"] === "string" ? parsed["handle"] : "";
|
|
1747
|
+
if (handle2 === "") {
|
|
1748
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const text = parsed["text"];
|
|
1752
|
+
if (typeof text !== "string") {
|
|
1753
|
+
sendJson(res, 400, { error: "missing text" });
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
const reParsed = parseFrame(
|
|
1757
|
+
JSON.stringify({ t: "msg", id: randomUUID(), text, at: Date.now() })
|
|
1758
|
+
);
|
|
1759
|
+
if (reParsed === null || reParsed.t !== "msg") {
|
|
1760
|
+
sendJson(res, 400, { error: "invalid message text" });
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
live.sendMessage(handle2, reParsed.text);
|
|
1764
|
+
sendJson(res, 200, { ok: true });
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1399
1767
|
sendJson(res, 404, { error: "not found" });
|
|
1400
1768
|
}
|
|
1401
1769
|
|
|
1402
1770
|
// src/cli.ts
|
|
1403
|
-
var VERSION = "0.4.
|
|
1771
|
+
var VERSION = "0.4.1";
|
|
1404
1772
|
function parsePort(raw) {
|
|
1405
1773
|
const n = Number(raw);
|
|
1406
1774
|
if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
|
|
@@ -1698,7 +2066,7 @@ async function cmdDiscover(live, any) {
|
|
|
1698
2066
|
);
|
|
1699
2067
|
return 0;
|
|
1700
2068
|
}
|
|
1701
|
-
async function cmdOpen(port) {
|
|
2069
|
+
async function cmdOpen(port, any) {
|
|
1702
2070
|
const profile = loadProfile();
|
|
1703
2071
|
let live;
|
|
1704
2072
|
let session;
|
|
@@ -1712,7 +2080,14 @@ async function cmdOpen(port) {
|
|
|
1712
2080
|
${LIVE_NOTICE}
|
|
1713
2081
|
`);
|
|
1714
2082
|
const hello = buildHello(profile);
|
|
1715
|
-
|
|
2083
|
+
const { topics, acceptLeague } = discoveryScope(profile.league, any);
|
|
2084
|
+
void startDiscovery({
|
|
2085
|
+
hello,
|
|
2086
|
+
topics,
|
|
2087
|
+
acceptLeague,
|
|
2088
|
+
isBlocked: blockedChecker(),
|
|
2089
|
+
onLink: (link) => live.addLink(link)
|
|
2090
|
+
}).then((s) => {
|
|
1716
2091
|
session = s;
|
|
1717
2092
|
}).catch(() => {
|
|
1718
2093
|
});
|
|
@@ -1721,9 +2096,11 @@ async function cmdOpen(port) {
|
|
|
1721
2096
|
vibedating local web app \u2192 ${started.url}
|
|
1722
2097
|
`);
|
|
1723
2098
|
if (live) {
|
|
1724
|
-
process2.stdout.write(
|
|
2099
|
+
process2.stdout.write(
|
|
2100
|
+
any ? " \u2022 live video + chat available for connected peers (ANY league \u2014 --any)\n" : " \u2022 live video + chat available for connected peers (your league + adjacent; --any = everyone)\n"
|
|
2101
|
+
);
|
|
1725
2102
|
} else {
|
|
1726
|
-
process2.stdout.write(" \u2022 connect first (`vibedating connect`) to enable live video\n");
|
|
2103
|
+
process2.stdout.write(" \u2022 connect first (`vibedating connect`) to enable live video + chat\n");
|
|
1727
2104
|
}
|
|
1728
2105
|
process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n");
|
|
1729
2106
|
process2.stdout.write(" (Ctrl+C to stop)\n\n");
|
|
@@ -1986,8 +2363,9 @@ Usage:
|
|
|
1986
2363
|
vibedating block <@handle> Block a handle \u2014 their hello is dropped (never recorded/paired)
|
|
1987
2364
|
vibedating unblock <@handle> Remove a handle from the blocklist
|
|
1988
2365
|
vibedating blocklist List blocked handles
|
|
1989
|
-
vibedating open [--port N]
|
|
1990
|
-
+ live
|
|
2366
|
+
vibedating open [--port N] [--any] Serve the local web app (default: random port)
|
|
2367
|
+
+ live video + chat with connected peers
|
|
2368
|
+
(your league + adjacent; --any = every league)
|
|
1991
2369
|
vibedating mcp Run the stdio MCP server (profile, matches)
|
|
1992
2370
|
vibedating --version
|
|
1993
2371
|
vibedating --help
|
|
@@ -2001,9 +2379,10 @@ Privacy:
|
|
|
2001
2379
|
(real local logs) \xB7 ~ unverified \xB7 \u{1F511} identity-verified (signed hello).
|
|
2002
2380
|
|
|
2003
2381
|
Matching:
|
|
2004
|
-
discover/live match your league + adjacent (\xB11) tiers by default, so
|
|
2005
|
-
leagues and cross-league friends still connect. --any matches every
|
|
2006
|
-
find / live --to look for one specific handle instead of the first
|
|
2382
|
+
discover/live/open match your league + adjacent (\xB11) tiers by default, so
|
|
2383
|
+
thin leagues and cross-league friends still connect. --any matches every
|
|
2384
|
+
league. find / live --to look for one specific handle instead of the first
|
|
2385
|
+
random peer.
|
|
2007
2386
|
|
|
2008
2387
|
Env:
|
|
2009
2388
|
VIBEDATING_TOKENS=<n> Self-report a token count (e.g. 23400000 or 12M)
|
|
@@ -2036,7 +2415,7 @@ async function main(argv) {
|
|
|
2036
2415
|
case "discover":
|
|
2037
2416
|
return cmdDiscover(parsed.live, parsed.any);
|
|
2038
2417
|
case "open":
|
|
2039
|
-
return cmdOpen(parsed.port);
|
|
2418
|
+
return cmdOpen(parsed.port, parsed.any);
|
|
2040
2419
|
case "live":
|
|
2041
2420
|
return cmdLive(parsed.dating, parsed.any, parsed.to);
|
|
2042
2421
|
case "find":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibedate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Dating by tokens — matched by usage league across agentic CLIs. Raw usage stays local; only your league is shared. CLI + local web app + MCP. Local-first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|