uniwrtc 1.0.9 → 1.1.0
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/NOSTR_INTEGRATION.md +6 -0
- package/package.json +4 -2
- package/src/main.js +98 -20
- package/src/nostr/nostrClient.js +243 -0
- package/src/nostr/useNostr.js +6 -0
- package/src/nostr/useWebRTC.js +6 -0
- package/src/services/nostrService.js +129 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniwrtc",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A universal WebRTC signaling service",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
],
|
|
21
21
|
"author": "",
|
|
22
22
|
"license": "MIT",
|
|
23
|
-
"dependencies": {
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"nostr-tools": "^2.9.0"
|
|
25
|
+
},
|
|
24
26
|
"devDependencies": {
|
|
25
27
|
"@playwright/test": "^1.57.0",
|
|
26
28
|
"vite": "^6.0.6"
|
package/src/main.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import './style.css';
|
|
2
2
|
import UniWRTCClient from '../client-browser.js';
|
|
3
|
+
import { createNostrClient } from './nostr/nostrClient.js';
|
|
3
4
|
|
|
4
5
|
// Make UniWRTCClient available globally for backwards compatibility
|
|
5
6
|
window.UniWRTCClient = UniWRTCClient;
|
|
6
7
|
|
|
8
|
+
// Nostr is the default transport (no toggle)
|
|
9
|
+
let nostrClient = null;
|
|
10
|
+
|
|
7
11
|
let client = null;
|
|
8
12
|
const peerConnections = new Map();
|
|
9
13
|
const dataChannels = new Map();
|
|
@@ -20,8 +24,8 @@ document.getElementById('app').innerHTML = `
|
|
|
20
24
|
<h2>Connection</h2>
|
|
21
25
|
<div class="connection-controls">
|
|
22
26
|
<div>
|
|
23
|
-
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">
|
|
24
|
-
<input type="text" id="
|
|
27
|
+
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Relay URL</label>
|
|
28
|
+
<input type="text" id="relayUrl" placeholder="wss://relay.damus.io" value="wss://relay.damus.io" style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-family: monospace; font-size: 12px;">
|
|
25
29
|
</div>
|
|
26
30
|
<div>
|
|
27
31
|
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Room / Session ID</label>
|
|
@@ -147,6 +151,67 @@ function updatePeerList() {
|
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
window.connect = async function() {
|
|
154
|
+
await connectNostr();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
async function connectNostr() {
|
|
158
|
+
const relayUrl = document.getElementById('relayUrl').value.trim();
|
|
159
|
+
const roomIdInput = document.getElementById('roomId');
|
|
160
|
+
const roomId = roomIdInput.value.trim();
|
|
161
|
+
|
|
162
|
+
if (!relayUrl) {
|
|
163
|
+
log('Please enter a relay URL', 'error');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const effectiveRoom = roomId || `room-${Math.random().toString(36).substring(2, 10)}`;
|
|
168
|
+
if (!roomId) roomIdInput.value = effectiveRoom;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
log(`Connecting to Nostr relay: ${relayUrl}...`, 'info');
|
|
172
|
+
|
|
173
|
+
if (nostrClient) {
|
|
174
|
+
await nostrClient.disconnect();
|
|
175
|
+
nostrClient = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
nostrClient = createNostrClient({
|
|
179
|
+
relayUrl,
|
|
180
|
+
room: effectiveRoom,
|
|
181
|
+
onState: (state) => {
|
|
182
|
+
if (state === 'connected') updateStatus(true);
|
|
183
|
+
if (state === 'disconnected') updateStatus(false);
|
|
184
|
+
},
|
|
185
|
+
onMessage: ({ from, text }) => {
|
|
186
|
+
displayChatMessage(text, from, false);
|
|
187
|
+
},
|
|
188
|
+
onPeer: ({ peerId }) => {
|
|
189
|
+
// Use the existing peer list UI as a simple "seen peers" list
|
|
190
|
+
if (!peerConnections.has(peerId)) {
|
|
191
|
+
peerConnections.set(peerId, null);
|
|
192
|
+
updatePeerList();
|
|
193
|
+
log(`Peer seen: ${peerId.substring(0, 6)}...`, 'success');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await nostrClient.connect();
|
|
199
|
+
|
|
200
|
+
const myPubkey = nostrClient.getPublicKey();
|
|
201
|
+
document.getElementById('clientId').textContent = myPubkey.substring(0, 16) + '...';
|
|
202
|
+
document.getElementById('sessionId').textContent = effectiveRoom;
|
|
203
|
+
log(`Joined Nostr room: ${effectiveRoom}`, 'success');
|
|
204
|
+
|
|
205
|
+
updateStatus(true);
|
|
206
|
+
|
|
207
|
+
log('Nostr connection established', 'success');
|
|
208
|
+
} catch (error) {
|
|
209
|
+
log(`Nostr connection error: ${error.message}`, 'error');
|
|
210
|
+
updateStatus(false);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function connectWebRTC() {
|
|
150
215
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
|
151
216
|
const roomId = document.getElementById('roomId').value.trim();
|
|
152
217
|
|
|
@@ -161,35 +226,35 @@ window.connect = async function() {
|
|
|
161
226
|
}
|
|
162
227
|
|
|
163
228
|
try {
|
|
164
|
-
|
|
229
|
+
log(`Connecting to ${serverUrl}...`, 'info');
|
|
165
230
|
|
|
166
231
|
// For Cloudflare, use /ws endpoint with room ID query param
|
|
167
232
|
let finalUrl = serverUrl;
|
|
168
233
|
if (serverUrl.includes('signal.peer.ooo')) {
|
|
169
|
-
|
|
170
|
-
|
|
234
|
+
finalUrl = `${serverUrl.replace(/\/$/, '')}/ws?room=${roomId}`;
|
|
235
|
+
log(`Using Cloudflare Durable Objects with session: ${roomId}`, 'info');
|
|
171
236
|
}
|
|
172
237
|
|
|
173
238
|
client = new UniWRTCClient(finalUrl, { autoReconnect: false });
|
|
174
239
|
|
|
175
240
|
client.on('connected', (data) => {
|
|
176
|
-
|
|
241
|
+
log(`Connected with client ID: ${data.clientId}`, 'success');
|
|
177
242
|
document.getElementById('clientId').textContent = data.clientId;
|
|
178
243
|
updateStatus(true);
|
|
179
244
|
|
|
180
245
|
// Auto-join the room
|
|
181
|
-
|
|
246
|
+
log(`Joining session: ${roomId}`, 'info');
|
|
182
247
|
client.joinSession(roomId);
|
|
183
248
|
});
|
|
184
249
|
|
|
185
250
|
client.on('joined', (data) => {
|
|
186
|
-
|
|
251
|
+
log(`Joined session: ${data.sessionId}`, 'success');
|
|
187
252
|
document.getElementById('sessionId').textContent = data.sessionId;
|
|
188
253
|
|
|
189
254
|
if (data.clients && data.clients.length > 0) {
|
|
190
|
-
|
|
255
|
+
log(`Found ${data.clients.length} existing peers`, 'info');
|
|
191
256
|
data.clients.forEach(peerId => {
|
|
192
|
-
|
|
257
|
+
log(`Creating connection to existing peer: ${peerId.substring(0, 6)}...`, 'info');
|
|
193
258
|
createPeerConnection(peerId, true);
|
|
194
259
|
});
|
|
195
260
|
}
|
|
@@ -198,11 +263,11 @@ window.connect = async function() {
|
|
|
198
263
|
client.on('peer-joined', (data) => {
|
|
199
264
|
// Only handle peers in our session
|
|
200
265
|
if (client.sessionId && data.sessionId !== client.sessionId) {
|
|
201
|
-
|
|
266
|
+
log(`Ignoring peer from different session: ${data.sessionId}`, 'warning');
|
|
202
267
|
return;
|
|
203
268
|
}
|
|
204
269
|
|
|
205
|
-
|
|
270
|
+
log(`Peer joined: ${data.peerId.substring(0, 6)}...`, 'success');
|
|
206
271
|
|
|
207
272
|
// Wait a bit to ensure both peers are ready
|
|
208
273
|
setTimeout(() => {
|
|
@@ -211,7 +276,7 @@ window.connect = async function() {
|
|
|
211
276
|
});
|
|
212
277
|
|
|
213
278
|
client.on('peer-left', (data) => {
|
|
214
|
-
|
|
279
|
+
log(`Peer left: ${data.peerId.substring(0, 6)}...`, 'warning');
|
|
215
280
|
const pc = peerConnections.get(data.peerId);
|
|
216
281
|
if (pc) {
|
|
217
282
|
pc.close();
|
|
@@ -222,17 +287,17 @@ window.connect = async function() {
|
|
|
222
287
|
});
|
|
223
288
|
|
|
224
289
|
client.on('offer', async (data) => {
|
|
225
|
-
|
|
290
|
+
log(`Received offer from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
226
291
|
const pc = peerConnections.get(data.peerId) || await createPeerConnection(data.peerId, false);
|
|
227
292
|
await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
|
|
228
293
|
const answer = await pc.createAnswer();
|
|
229
294
|
await pc.setLocalDescription(answer);
|
|
230
295
|
client.sendAnswer(answer, data.peerId);
|
|
231
|
-
|
|
296
|
+
log(`Sent answer to ${data.peerId.substring(0, 6)}...`, 'success');
|
|
232
297
|
});
|
|
233
298
|
|
|
234
299
|
client.on('answer', async (data) => {
|
|
235
|
-
|
|
300
|
+
log(`Received answer from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
236
301
|
const pc = peerConnections.get(data.peerId);
|
|
237
302
|
if (pc) {
|
|
238
303
|
await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
|
|
@@ -240,7 +305,7 @@ window.connect = async function() {
|
|
|
240
305
|
});
|
|
241
306
|
|
|
242
307
|
client.on('ice-candidate', async (data) => {
|
|
243
|
-
|
|
308
|
+
log(`Received ICE candidate from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
244
309
|
const pc = peerConnections.get(data.peerId);
|
|
245
310
|
if (pc && data.candidate) {
|
|
246
311
|
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
|
|
@@ -257,17 +322,21 @@ window.connect = async function() {
|
|
|
257
322
|
});
|
|
258
323
|
|
|
259
324
|
client.on('error', (data) => {
|
|
260
|
-
|
|
325
|
+
log(`Error: ${data.message}`, 'error');
|
|
261
326
|
});
|
|
262
327
|
|
|
263
328
|
await client.connect();
|
|
264
329
|
} catch (error) {
|
|
265
|
-
|
|
330
|
+
log(`Connection error: ${error.message}`, 'error');
|
|
266
331
|
updateStatus(false);
|
|
267
332
|
}
|
|
268
|
-
}
|
|
333
|
+
}
|
|
269
334
|
|
|
270
335
|
window.disconnect = function() {
|
|
336
|
+
if (nostrClient) {
|
|
337
|
+
nostrClient.disconnect().catch(() => {});
|
|
338
|
+
nostrClient = null;
|
|
339
|
+
}
|
|
271
340
|
if (client) {
|
|
272
341
|
client.disconnect();
|
|
273
342
|
client = null;
|
|
@@ -347,6 +416,15 @@ window.sendChatMessage = function() {
|
|
|
347
416
|
return;
|
|
348
417
|
}
|
|
349
418
|
|
|
419
|
+
if (nostrClient) {
|
|
420
|
+
nostrClient.sendMessage(message).catch((e) => {
|
|
421
|
+
log(`Failed to send via Nostr: ${e?.message || e}`, 'error');
|
|
422
|
+
});
|
|
423
|
+
displayChatMessage(message, 'You', true);
|
|
424
|
+
document.getElementById('chatMessage').value = '';
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
350
428
|
if (dataChannels.size === 0) {
|
|
351
429
|
log('No peer connections available. Wait for data channels to open.', 'error');
|
|
352
430
|
return;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
|
2
|
+
|
|
3
|
+
function bytesToHex(bytes) {
|
|
4
|
+
return Array.from(bytes)
|
|
5
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
6
|
+
.join('');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isHex64(s) {
|
|
10
|
+
return typeof s === 'string' && /^[0-9a-fA-F]{64}$/.test(s);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Minimal Nostr relay client using raw WebSocket protocol.
|
|
15
|
+
* - Publishes kind:1 events tagged with ['t', room] and ['room', room]
|
|
16
|
+
* - Subscribes to kind:1 events filtered by #t
|
|
17
|
+
*/
|
|
18
|
+
export function createNostrClient({ relayUrl, room, onMessage, onPeer, onState } = {}) {
|
|
19
|
+
if (!relayUrl) throw new Error('relayUrl is required');
|
|
20
|
+
if (!room) throw new Error('room is required');
|
|
21
|
+
|
|
22
|
+
const state = {
|
|
23
|
+
relayUrl,
|
|
24
|
+
room,
|
|
25
|
+
ws: null,
|
|
26
|
+
subId: `sub-${room}-${Math.random().toString(36).slice(2, 8)}`,
|
|
27
|
+
secretKeyHex: null,
|
|
28
|
+
pubkey: null,
|
|
29
|
+
seen: new Set(),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function ensureKeys() {
|
|
33
|
+
if (state.pubkey && state.secretKeyHex) return;
|
|
34
|
+
|
|
35
|
+
let stored = localStorage.getItem('nostr-secret-key');
|
|
36
|
+
// If stored value looks like an array string from prior buggy storage, clear it
|
|
37
|
+
if (stored && stored.includes(',')) {
|
|
38
|
+
localStorage.removeItem('nostr-secret-key');
|
|
39
|
+
stored = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!isHex64(stored)) {
|
|
43
|
+
const secretBytes = generateSecretKey();
|
|
44
|
+
state.secretKeyHex = bytesToHex(secretBytes);
|
|
45
|
+
localStorage.setItem('nostr-secret-key', state.secretKeyHex);
|
|
46
|
+
} else {
|
|
47
|
+
state.secretKeyHex = stored;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
state.pubkey = getPublicKey(state.secretKeyHex);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getPublicKeyHex() {
|
|
54
|
+
ensureKeys();
|
|
55
|
+
return state.pubkey;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setState(next) {
|
|
59
|
+
try {
|
|
60
|
+
onState?.(next);
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseIncoming(event) {
|
|
67
|
+
let msg;
|
|
68
|
+
try {
|
|
69
|
+
msg = JSON.parse(event.data);
|
|
70
|
+
} catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!Array.isArray(msg) || msg.length < 2) return;
|
|
75
|
+
const [type] = msg;
|
|
76
|
+
|
|
77
|
+
if (type === 'EVENT') {
|
|
78
|
+
const nostrEvent = msg[2];
|
|
79
|
+
if (!nostrEvent || typeof nostrEvent !== 'object') return;
|
|
80
|
+
if (nostrEvent.id && state.seen.has(nostrEvent.id)) return;
|
|
81
|
+
if (nostrEvent.id) state.seen.add(nostrEvent.id);
|
|
82
|
+
|
|
83
|
+
// Ensure it's for our room
|
|
84
|
+
const tags = Array.isArray(nostrEvent.tags) ? nostrEvent.tags : [];
|
|
85
|
+
const roomTag = tags.find((t) => Array.isArray(t) && t[0] === 'room');
|
|
86
|
+
if (!roomTag || roomTag[1] !== state.room) return;
|
|
87
|
+
|
|
88
|
+
// Content is JSON
|
|
89
|
+
let payload;
|
|
90
|
+
try {
|
|
91
|
+
payload = JSON.parse(nostrEvent.content);
|
|
92
|
+
} catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (payload?.type === 'peer-join' && payload.peerId) {
|
|
97
|
+
try {
|
|
98
|
+
onPeer?.({ peerId: payload.peerId });
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (payload?.type === 'message' && typeof payload.message === 'string') {
|
|
106
|
+
const from = (payload.peerId || nostrEvent.pubkey || 'peer').substring(0, 8) + '...';
|
|
107
|
+
try {
|
|
108
|
+
onMessage?.({ from, text: payload.message });
|
|
109
|
+
} catch {
|
|
110
|
+
// ignore
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sendRaw(frame) {
|
|
117
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) {
|
|
118
|
+
throw new Error('Relay not connected');
|
|
119
|
+
}
|
|
120
|
+
state.ws.send(JSON.stringify(frame));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function connect() {
|
|
124
|
+
ensureKeys();
|
|
125
|
+
|
|
126
|
+
setState('connecting');
|
|
127
|
+
|
|
128
|
+
const ws = new WebSocket(state.relayUrl);
|
|
129
|
+
state.ws = ws;
|
|
130
|
+
|
|
131
|
+
await new Promise((resolve, reject) => {
|
|
132
|
+
const onOpen = () => {
|
|
133
|
+
cleanup();
|
|
134
|
+
resolve();
|
|
135
|
+
};
|
|
136
|
+
const onError = () => {
|
|
137
|
+
cleanup();
|
|
138
|
+
reject(new Error('Failed to connect to relay'));
|
|
139
|
+
};
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
ws.removeEventListener('open', onOpen);
|
|
142
|
+
ws.removeEventListener('error', onError);
|
|
143
|
+
};
|
|
144
|
+
ws.addEventListener('open', onOpen);
|
|
145
|
+
ws.addEventListener('error', onError);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
ws.addEventListener('message', parseIncoming);
|
|
149
|
+
ws.addEventListener('close', () => setState('disconnected'));
|
|
150
|
+
|
|
151
|
+
// Subscribe to this room (topic-tag filtered)
|
|
152
|
+
const now = Math.floor(Date.now() / 1000);
|
|
153
|
+
const filter = {
|
|
154
|
+
kinds: [1],
|
|
155
|
+
'#t': [state.room],
|
|
156
|
+
since: now - 3600,
|
|
157
|
+
limit: 200,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
sendRaw(['REQ', state.subId, filter]);
|
|
161
|
+
|
|
162
|
+
// Announce presence
|
|
163
|
+
await sendJoin();
|
|
164
|
+
|
|
165
|
+
setState('connected');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function sendJoin() {
|
|
169
|
+
ensureKeys();
|
|
170
|
+
const created_at = Math.floor(Date.now() / 1000);
|
|
171
|
+
const tags = [
|
|
172
|
+
['room', state.room],
|
|
173
|
+
['t', state.room],
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const eventTemplate = {
|
|
177
|
+
kind: 1,
|
|
178
|
+
created_at,
|
|
179
|
+
tags,
|
|
180
|
+
content: JSON.stringify({
|
|
181
|
+
type: 'peer-join',
|
|
182
|
+
peerId: state.pubkey,
|
|
183
|
+
room: state.room,
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
}),
|
|
186
|
+
pubkey: state.pubkey,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const signed = finalizeEvent(eventTemplate, state.secretKeyHex);
|
|
190
|
+
sendRaw(['EVENT', signed]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function sendMessage(text) {
|
|
194
|
+
ensureKeys();
|
|
195
|
+
const created_at = Math.floor(Date.now() / 1000);
|
|
196
|
+
const tags = [
|
|
197
|
+
['room', state.room],
|
|
198
|
+
['t', state.room],
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const eventTemplate = {
|
|
202
|
+
kind: 1,
|
|
203
|
+
created_at,
|
|
204
|
+
tags,
|
|
205
|
+
content: JSON.stringify({
|
|
206
|
+
type: 'message',
|
|
207
|
+
message: text,
|
|
208
|
+
peerId: state.pubkey,
|
|
209
|
+
room: state.room,
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
}),
|
|
212
|
+
pubkey: state.pubkey,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const signed = finalizeEvent(eventTemplate, state.secretKeyHex);
|
|
216
|
+
sendRaw(['EVENT', signed]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function disconnect() {
|
|
220
|
+
const ws = state.ws;
|
|
221
|
+
state.ws = null;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
225
|
+
try {
|
|
226
|
+
ws.send(JSON.stringify(['CLOSE', state.subId]));
|
|
227
|
+
} catch {
|
|
228
|
+
// ignore
|
|
229
|
+
}
|
|
230
|
+
ws.close();
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
setState('disconnected');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
connect,
|
|
239
|
+
disconnect,
|
|
240
|
+
sendMessage,
|
|
241
|
+
getPublicKey: getPublicKeyHex,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { subscribe } from 'nostr-tools/relay';
|
|
2
|
+
import { SimplePool } from 'nostr-tools/pool';
|
|
3
|
+
|
|
4
|
+
// List of public Nostr relays
|
|
5
|
+
const DEFAULT_RELAYS = [
|
|
6
|
+
'wss://relay.damus.io',
|
|
7
|
+
'wss://relay.nostr.band',
|
|
8
|
+
'wss://nostr.wine',
|
|
9
|
+
'wss://relay.current.fyi'
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
let pool = new SimplePool();
|
|
13
|
+
let relayConnections = new Map();
|
|
14
|
+
let subscriptions = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add or connect to relays
|
|
18
|
+
*/
|
|
19
|
+
export async function addRelays(relayUrls = DEFAULT_RELAYS) {
|
|
20
|
+
for (const url of relayUrls) {
|
|
21
|
+
if (!relayConnections.has(url)) {
|
|
22
|
+
try {
|
|
23
|
+
await pool.ensureRelay(url);
|
|
24
|
+
relayConnections.set(url, true);
|
|
25
|
+
console.log(`Connected to relay: ${url}`);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`Failed to connect to relay ${url}:`, error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ensure relay connection is established
|
|
35
|
+
*/
|
|
36
|
+
export async function ensureRelayConnection(relayUrl = DEFAULT_RELAYS[0]) {
|
|
37
|
+
if (!relayConnections.has(relayUrl)) {
|
|
38
|
+
await addRelays([relayUrl]);
|
|
39
|
+
}
|
|
40
|
+
return relayConnections.get(relayUrl);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Crawl available relays
|
|
45
|
+
*/
|
|
46
|
+
export async function crawlRelays() {
|
|
47
|
+
return DEFAULT_RELAYS;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ensure connections to multiple relays
|
|
52
|
+
*/
|
|
53
|
+
export async function ensureConnections(relayUrls = DEFAULT_RELAYS) {
|
|
54
|
+
await addRelays(relayUrls);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Publish an event to all connected relays
|
|
59
|
+
*/
|
|
60
|
+
export async function publishEvent(event) {
|
|
61
|
+
const relayUrls = Array.from(relayConnections.keys());
|
|
62
|
+
if (relayUrls.length === 0) {
|
|
63
|
+
await addRelays();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const publishPromises = Array.from(relayConnections.keys()).map(url =>
|
|
68
|
+
pool.publish(url, event).catch(err => {
|
|
69
|
+
console.error(`Failed to publish to ${url}:`, err);
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
await Promise.all(publishPromises);
|
|
73
|
+
console.log('Event published to all relays');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Error publishing event:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Subscribe to events with a filter
|
|
81
|
+
*/
|
|
82
|
+
export async function subscribeToEvents(filter, onEvent, subscriptionId = 'default') {
|
|
83
|
+
if (relayConnections.size === 0) {
|
|
84
|
+
await addRelays();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const relayUrls = Array.from(relayConnections.keys());
|
|
89
|
+
|
|
90
|
+
const subscription = pool.subscribeMany(relayUrls, [filter], {
|
|
91
|
+
onevent: (event) => onEvent(event),
|
|
92
|
+
onclose: () => console.log(`Subscription ${subscriptionId} closed`),
|
|
93
|
+
oneose: () => console.log(`Subscription ${subscriptionId} received all events`)
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
subscriptions.set(subscriptionId, subscription);
|
|
97
|
+
console.log(`Subscribed with filter:`, filter);
|
|
98
|
+
return subscriptionId;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Error subscribing to events:', error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Unsubscribe from events
|
|
106
|
+
*/
|
|
107
|
+
export async function unsubscribeFromEvents(subscriptionId = 'default') {
|
|
108
|
+
const subscription = subscriptions.get(subscriptionId);
|
|
109
|
+
if (subscription) {
|
|
110
|
+
subscription.close();
|
|
111
|
+
subscriptions.delete(subscriptionId);
|
|
112
|
+
console.log(`Unsubscribed: ${subscriptionId}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Close all relay connections
|
|
118
|
+
*/
|
|
119
|
+
export async function closeAllConnections() {
|
|
120
|
+
subscriptions.forEach(sub => sub.close());
|
|
121
|
+
subscriptions.clear();
|
|
122
|
+
|
|
123
|
+
relayConnections.forEach((_, url) => {
|
|
124
|
+
pool.close(url);
|
|
125
|
+
});
|
|
126
|
+
relayConnections.clear();
|
|
127
|
+
|
|
128
|
+
console.log('Closed all relay connections');
|
|
129
|
+
}
|