wireweave 0.1.1 → 0.1.3
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/README.md +46 -35
- package/package.json +14 -4
- package/src/bans.js +45 -0
- package/src/channels.js +107 -0
- package/src/chat.js +95 -0
- package/src/fsm.js +51 -0
- package/src/index.js +12 -0
- package/src/media.js +81 -0
- package/src/message.js +22 -0
- package/src/pages.js +77 -0
- package/src/roles.js +58 -0
- package/src/servers.js +103 -0
- package/src/settings.js +72 -0
- package/src/voice.js +348 -0
- package/src/wireweave.js +89 -0
- package/src/legacy/README.md +0 -71
- package/src/legacy/index.js +0 -49
- package/src/legacy/nostr-auth.js +0 -217
- package/src/legacy/nostr-bans.js +0 -59
- package/src/legacy/nostr-channels.js +0 -145
- package/src/legacy/nostr-chat.js +0 -185
- package/src/legacy/nostr-fsm.js +0 -5
- package/src/legacy/nostr-media.js +0 -97
- package/src/legacy/nostr-message.js +0 -12
- package/src/legacy/nostr-network.js +0 -182
- package/src/legacy/nostr-pages.js +0 -152
- package/src/legacy/nostr-roles.js +0 -74
- package/src/legacy/nostr-servers.js +0 -223
- package/src/legacy/nostr-settings.js +0 -97
- package/src/legacy/nostr-state-patch.js +0 -19
- package/src/legacy/nostr-voice-camera.js +0 -63
- package/src/legacy/nostr-voice-rtc.js +0 -199
- package/src/legacy/nostr-voice-sfu.js +0 -128
- package/src/legacy/nostr-voice.js +0 -257
package/src/legacy/nostr-chat.js
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
const profiles = new Map();
|
|
2
|
-
const fetching = new Set();
|
|
3
|
-
|
|
4
|
-
const chat = {
|
|
5
|
-
activeChannelId: null,
|
|
6
|
-
|
|
7
|
-
get messages() { return state.chatMessages || []; },
|
|
8
|
-
set messages(v) { state.chatMessages = v; },
|
|
9
|
-
|
|
10
|
-
async send(content) {
|
|
11
|
-
if (!auth.isLoggedIn() || !state.currentChannelId) return;
|
|
12
|
-
const trimmed = content.trim();
|
|
13
|
-
if (!trimmed) return;
|
|
14
|
-
const chanHex = await _hexChannelId(state.currentChannelId, state.currentServerId);
|
|
15
|
-
const relayHint = (state.nostrRelays || [])[0] || '';
|
|
16
|
-
const template = {
|
|
17
|
-
kind: 42,
|
|
18
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
19
|
-
tags: [['e', chanHex, relayHint, 'root']],
|
|
20
|
-
content: trimmed,
|
|
21
|
-
};
|
|
22
|
-
const signedEvent = await auth.sign(template);
|
|
23
|
-
nostrNet.publish(signedEvent);
|
|
24
|
-
chat._addMessage(chat._eventToMsg(signedEvent));
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
sendImage(file) {
|
|
28
|
-
if (!file) {
|
|
29
|
-
var input = document.createElement('input');
|
|
30
|
-
input.type = 'file'; input.accept = 'image/*,video/*';
|
|
31
|
-
input.onchange = function() { if (input.files[0]) nostrMedia.sendMedia(input.files[0]).catch(function(e){ message.add('Upload failed: ' + e.message); }); };
|
|
32
|
-
input.click(); return;
|
|
33
|
-
}
|
|
34
|
-
nostrMedia.sendMedia(file).catch(function(e){ message.add('Upload failed: ' + e.message); });
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
async loadHistory(channelId) {
|
|
38
|
-
if (chat.activeChannelId) {
|
|
39
|
-
nostrNet.unsubscribe('chat-' + chat.activeChannelId);
|
|
40
|
-
nostrNet.unsubscribe('chat-live-' + chat.activeChannelId);
|
|
41
|
-
}
|
|
42
|
-
chat.activeChannelId = channelId;
|
|
43
|
-
state.chatMessages = [];
|
|
44
|
-
const chanHex = await _hexChannelId(channelId, state.currentServerId);
|
|
45
|
-
const collected = [];
|
|
46
|
-
nostrNet.subscribe(
|
|
47
|
-
'chat-' + channelId,
|
|
48
|
-
[{ kinds: [42], '#e': [chanHex], limit: 50 }],
|
|
49
|
-
function(event) { collected.push(chat._eventToMsg(event)); },
|
|
50
|
-
function() {
|
|
51
|
-
collected.sort(function(a, b) { return a.timestamp - b.timestamp; });
|
|
52
|
-
state.chatMessages = collected;
|
|
53
|
-
chat._updateMembers(collected);
|
|
54
|
-
ui.render.all();
|
|
55
|
-
}
|
|
56
|
-
);
|
|
57
|
-
nostrNet.subscribe(
|
|
58
|
-
'chat-live-' + channelId,
|
|
59
|
-
[{ kinds: [42], '#e': [chanHex], since: Math.floor(Date.now() / 1000) }],
|
|
60
|
-
function(event) { chat._addMessage(chat._eventToMsg(event)); },
|
|
61
|
-
function() {}
|
|
62
|
-
);
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
async sendAnnouncement(text) {
|
|
66
|
-
if (!auth.isLoggedIn() || !state.currentChannelId) return;
|
|
67
|
-
if (!window.serverRoles || !serverRoles.isAdmin(state.currentServerId)) return;
|
|
68
|
-
const trimmed = text.trim();
|
|
69
|
-
if (!trimmed) return;
|
|
70
|
-
const chanHex = await _hexChannelId(state.currentChannelId, state.currentServerId);
|
|
71
|
-
const relayHint = (state.nostrRelays || [])[0] || '';
|
|
72
|
-
const template = {
|
|
73
|
-
kind: 42,
|
|
74
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
75
|
-
tags: [['e', chanHex, relayHint, 'root'], ['t', 'announcement']],
|
|
76
|
-
content: trimmed,
|
|
77
|
-
};
|
|
78
|
-
const signedEvent = await auth.sign(template);
|
|
79
|
-
nostrNet.publish(signedEvent);
|
|
80
|
-
chat._addMessage(chat._eventToMsg(signedEvent));
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
_eventToMsg(event) {
|
|
84
|
-
const tTags = (event.tags || []).filter(t => t[0] === 't').map(t => t[1]);
|
|
85
|
-
chat._fetchProfile(event.pubkey);
|
|
86
|
-
return {
|
|
87
|
-
id: event.id,
|
|
88
|
-
type: 'text',
|
|
89
|
-
userId: event.pubkey,
|
|
90
|
-
content: event.content,
|
|
91
|
-
timestamp: event.created_at * 1000,
|
|
92
|
-
tags: tTags,
|
|
93
|
-
};
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
_addMessage(msg) {
|
|
97
|
-
if (state.chatMessages && state.chatMessages.find(function(m) { return m.id === msg.id; })) return;
|
|
98
|
-
const current = state.chatMessages ? state.chatMessages.slice() : [];
|
|
99
|
-
current.push(msg);
|
|
100
|
-
state.chatMessages = current;
|
|
101
|
-
chat._updateMembers(current);
|
|
102
|
-
ui.render.all();
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
_updateMembers(msgs) {
|
|
106
|
-
const seen = new Map();
|
|
107
|
-
(msgs || []).forEach(function(m) {
|
|
108
|
-
if (m.userId && !seen.has(m.userId)) seen.set(m.userId, chat.resolveProfile(m.userId));
|
|
109
|
-
});
|
|
110
|
-
state.roomMembers = Array.from(seen.entries()).map(function([id, username]) {
|
|
111
|
-
return { id, username, online: true };
|
|
112
|
-
});
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
async deleteMessage(id) {
|
|
116
|
-
const template = {
|
|
117
|
-
kind: 5,
|
|
118
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
119
|
-
tags: [['e', id]],
|
|
120
|
-
content: 'deleted',
|
|
121
|
-
};
|
|
122
|
-
const signedEvent = await auth.sign(template);
|
|
123
|
-
nostrNet.publish(signedEvent);
|
|
124
|
-
state.chatMessages = (state.chatMessages || []).filter(function(m) { return m.id !== id; });
|
|
125
|
-
ui.render.all();
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
editMessage() {
|
|
129
|
-
if (typeof ui !== 'undefined' && ui.showToast) ui.showToast('Nostr messages cannot be edited');
|
|
130
|
-
else console.warn('Nostr messages cannot be edited');
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
resolveProfile(pubkey) {
|
|
134
|
-
if (profiles.has(pubkey)) {
|
|
135
|
-
const p = profiles.get(pubkey);
|
|
136
|
-
return p.name || auth.npubShort(pubkey);
|
|
137
|
-
}
|
|
138
|
-
chat._fetchProfile(pubkey);
|
|
139
|
-
return auth.npubShort(pubkey);
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
_fetchProfile(pubkey) {
|
|
143
|
-
if (fetching.has(pubkey)) return;
|
|
144
|
-
fetching.add(pubkey);
|
|
145
|
-
nostrNet.subscribe(
|
|
146
|
-
'profile-' + pubkey,
|
|
147
|
-
[{ kinds: [0], authors: [pubkey], limit: 1 }],
|
|
148
|
-
function(event) {
|
|
149
|
-
try {
|
|
150
|
-
const p = JSON.parse(event.content);
|
|
151
|
-
profiles.set(pubkey, p);
|
|
152
|
-
if (pubkey === state.nostrPubkey) state.nostrProfile = p;
|
|
153
|
-
} catch (e) {}
|
|
154
|
-
},
|
|
155
|
-
function() {
|
|
156
|
-
nostrNet.unsubscribe('profile-' + pubkey);
|
|
157
|
-
fetching.delete(pubkey);
|
|
158
|
-
chat._updateMembers(state.chatMessages);
|
|
159
|
-
ui.render.all();
|
|
160
|
-
}
|
|
161
|
-
);
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
linkify(html) {
|
|
165
|
-
return html.replace(/https?:\/\/[^\s<>"]+/g, function(url) {
|
|
166
|
-
if (!window.nostrMedia) return '<a href="' + url + '" target="_blank" rel="noopener">' + url + '</a>';
|
|
167
|
-
var kind = nostrMedia.isMedia(url);
|
|
168
|
-
if (kind === 'image') return '<a href="' + url + '" target="_blank" rel="noopener"><img src="' + url + '" style="max-width:100%;max-height:300px;border-radius:8px;margin-top:4px;display:block" loading="lazy"></a>';
|
|
169
|
-
if (kind === 'video') return '<video src="' + url + '" controls style="max-width:100%;max-height:300px;border-radius:8px;margin-top:4px;display:block"></video>';
|
|
170
|
-
return '<a href="' + url + '" target="_blank" rel="noopener">' + url + '</a>';
|
|
171
|
-
});
|
|
172
|
-
},
|
|
173
|
-
|
|
174
|
-
handleTextMessage(msg) { chat._addMessage(msg); },
|
|
175
|
-
handleImageMessage() {},
|
|
176
|
-
handleFileShared() {},
|
|
177
|
-
|
|
178
|
-
updateProfile(pubkey, profile) {
|
|
179
|
-
profiles.set(pubkey, profile);
|
|
180
|
-
ui.render.all();
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
window.__zellous.chat = chat;
|
|
185
|
-
window.chat = chat;
|
package/src/legacy/nostr-fsm.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
var voiceMachine = XState.createMachine({initial:'idle',states:{idle:{on:{connect:'connecting'}},connecting:{on:{connected:'connected',fail:'idle'}},connected:{on:{disconnect:'disconnecting'}},disconnecting:{on:{done:'idle'}}}});
|
|
2
|
-
var peerMachine = XState.createMachine({initial:'new',states:{new:{on:{offer:'offering',recv_offer:'answering'}},offering:{on:{recv_answer:'connected',fail:'new',restart:'offering'}},answering:{on:{sent_answer:'connected',recv_answer:'connected',fail:'new'}},connected:{on:{disconnect:'reconnecting',close:'closed'}},reconnecting:{on:{offer:'offering',recv_answer:'connected',close:'closed'}},closed:{}}});
|
|
3
|
-
var cameraMachine = XState.createMachine({initial:'idle',states:{idle:{on:{enable:'requesting'}},requesting:{on:{enabled:'active',denied:'idle'}},active:{on:{disable:'idle',error:'idle'}},error:{on:{enable:'requesting'}}}});
|
|
4
|
-
window.__zellous.fsm = { voiceMachine: voiceMachine, peerMachine: peerMachine, cameraMachine: cameraMachine };
|
|
5
|
-
window.nostrFsm = window.__zellous.fsm;
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
var nostrMedia = {
|
|
2
|
-
_BLOSSOM_SERVERS: [
|
|
3
|
-
'https://blossom.nostr.build',
|
|
4
|
-
'https://files.sovbit.host',
|
|
5
|
-
'https://nostrcheck.me',
|
|
6
|
-
],
|
|
7
|
-
|
|
8
|
-
async _hashFile(file) {
|
|
9
|
-
const buf = await file.arrayBuffer();
|
|
10
|
-
const hash = await crypto.subtle.digest('SHA-256', buf);
|
|
11
|
-
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
async _generateBlossomAuth(file, uploadUrl) {
|
|
15
|
-
if (!state.nostrPubkey) throw new Error('Not authenticated');
|
|
16
|
-
const hash = await nostrMedia._hashFile(file);
|
|
17
|
-
const auth_event = {
|
|
18
|
-
kind: 24242,
|
|
19
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
20
|
-
tags: [
|
|
21
|
-
['t', 'upload'],
|
|
22
|
-
['x', hash],
|
|
23
|
-
['expiration', String(Math.floor(Date.now() / 1000) + 600)],
|
|
24
|
-
],
|
|
25
|
-
content: 'Upload ' + file.name,
|
|
26
|
-
};
|
|
27
|
-
const signed = await window.auth.sign(auth_event);
|
|
28
|
-
return { header: 'Nostr ' + btoa(JSON.stringify(signed)), hash };
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
async _uploadBlossom(file, serverUrl) {
|
|
32
|
-
const uploadUrl = serverUrl + '/upload';
|
|
33
|
-
const { header, hash } = await nostrMedia._generateBlossomAuth(file, uploadUrl);
|
|
34
|
-
const res = await fetch(uploadUrl, {
|
|
35
|
-
method: 'PUT',
|
|
36
|
-
body: file,
|
|
37
|
-
headers: {
|
|
38
|
-
'Authorization': header,
|
|
39
|
-
'Content-Type': file.type || 'application/octet-stream',
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
if (!res.ok) throw new Error('Status ' + res.status);
|
|
43
|
-
const data = await res.json();
|
|
44
|
-
const url = data.url || (data.content && JSON.parse(data.content).url);
|
|
45
|
-
if (!url) throw new Error('No URL in response');
|
|
46
|
-
return url;
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
async upload(file) {
|
|
50
|
-
var errors = [];
|
|
51
|
-
for (var i = 0; i < nostrMedia._BLOSSOM_SERVERS.length; i++) {
|
|
52
|
-
try {
|
|
53
|
-
var url = await nostrMedia._uploadBlossom(file, nostrMedia._BLOSSOM_SERVERS[i]);
|
|
54
|
-
return { url: url, type: file.type, name: file.name, size: file.size };
|
|
55
|
-
} catch(e) { errors.push(nostrMedia._BLOSSOM_SERVERS[i] + ': ' + e.message); }
|
|
56
|
-
}
|
|
57
|
-
throw new Error('Upload failed: ' + errors.join('; '));
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
isMedia(url) {
|
|
61
|
-
if (!url || typeof url !== 'string') return null;
|
|
62
|
-
var imgRx = /\.(png|jpe?g|gif|webp|svg|avif)(\?|$)/i;
|
|
63
|
-
var vidRx = /\.(mp4|webm|mov|ogg)(\?|$)/i;
|
|
64
|
-
if (imgRx.test(url)) return 'image';
|
|
65
|
-
if (vidRx.test(url)) return 'video';
|
|
66
|
-
return null;
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
extractUrls(text) {
|
|
70
|
-
if (!text) return [];
|
|
71
|
-
var urlRx = /https?:\/\/[^\s<>"]+/g;
|
|
72
|
-
return (text.match(urlRx) || []);
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
async sendMedia(file) {
|
|
76
|
-
if (file.size > 20 * 1024 * 1024) throw new Error('File too large (max 20 MB)');
|
|
77
|
-
var result = await nostrMedia.upload(file);
|
|
78
|
-
var chanHex = await _hexChannelId(state.currentChannelId, state.currentServerId);
|
|
79
|
-
var relayHint = (state.nostrRelays || [])[0] || '';
|
|
80
|
-
var imetaTag = ['imeta', 'url ' + result.url, 'm ' + result.type];
|
|
81
|
-
if (result.size) imetaTag.push('size ' + result.size);
|
|
82
|
-
var template = {
|
|
83
|
-
kind: 42,
|
|
84
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
85
|
-
tags: [['e', chanHex, relayHint, 'root'], imetaTag],
|
|
86
|
-
content: result.url
|
|
87
|
-
};
|
|
88
|
-
var signed = await auth.sign(template);
|
|
89
|
-
nostrNet.publish(signed);
|
|
90
|
-
chat._addMessage(chat._eventToMsg(signed));
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
window.__zellous.media = nostrMedia;
|
|
95
|
-
window.nostrMedia = nostrMedia;
|
|
96
|
-
if (!window.__debug) window.__debug = {};
|
|
97
|
-
Object.defineProperty(window.__debug, 'media', { get: function() { return { servers: nostrMedia._NIP96_SERVERS }; }, configurable: true });
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
var message = window.message || {
|
|
2
|
-
handlers: {},
|
|
3
|
-
handle: function(m) { var h = message.handlers[m.type]; if (h) h(m); },
|
|
4
|
-
add: function(text, audioData, userId, username) {
|
|
5
|
-
var id = Date.now() + Math.random();
|
|
6
|
-
var msgs = (state.messages || []).concat([{ id: id, text: text, time: Date.now(), userId: userId, username: username }]);
|
|
7
|
-
state.messages = msgs.length > 50 ? msgs.slice(-50) : msgs;
|
|
8
|
-
if (window.ui) ui.render.messages();
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
window.__zellous.message = message;
|
|
12
|
-
window.message = message;
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
var nostrNet = {
|
|
2
|
-
relays: new Map(),
|
|
3
|
-
subs: new Map(),
|
|
4
|
-
pendingEvents: [],
|
|
5
|
-
seenIds: new Set(),
|
|
6
|
-
|
|
7
|
-
_makeRelayActor: function() {
|
|
8
|
-
var machine = XState.createMachine({initial:'connecting',states:{connecting:{on:{connected:'connected',fail:'error'}},connected:{on:{fail:'error',disconnect:'disconnected'}},disconnected:{on:{reconnect:'connecting'}},error:{on:{reconnect:'connecting'}}}});
|
|
9
|
-
var actor = XState.createActor(machine);
|
|
10
|
-
actor.start();
|
|
11
|
-
return actor;
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
connect: function() {
|
|
15
|
-
var relays = (state.nostrRelays || []);
|
|
16
|
-
for (var i = 0; i < relays.length; i++) nostrNet._openRelay(relays[i]);
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
disconnect: function() {
|
|
20
|
-
nostrNet.relays.forEach(function(relay) {
|
|
21
|
-
if (relay.ws) { relay.ws.onclose = null; relay.ws.onerror = null; relay.ws.close(); }
|
|
22
|
-
});
|
|
23
|
-
nostrNet.relays.clear();
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
_openRelay: function(url) {
|
|
27
|
-
var existing = nostrNet.relays.get(url);
|
|
28
|
-
if (existing && existing.ws && (existing.ws.readyState === 0 || existing.ws.readyState === 1)) return;
|
|
29
|
-
var relay = existing || { ws: null, status: 'connecting', actor: null, reconnectDelay: 1000, failCount: 0, subIds: new Set(), latencyMs: null, _reqSentAt: null };
|
|
30
|
-
relay.status = 'connecting';
|
|
31
|
-
relay.latencyMs = null;
|
|
32
|
-
if(!relay.actor) relay.actor = nostrNet._makeRelayActor();
|
|
33
|
-
else relay.actor.send({type:'reconnect'});
|
|
34
|
-
nostrNet.relays.set(url, relay);
|
|
35
|
-
var ws;
|
|
36
|
-
try { ws = new WebSocket(url); } catch (e) {
|
|
37
|
-
relay.status = 'error'; relay.actor.send({type:'fail'}); nostrNet._updateRelayStatus(url, 'error');
|
|
38
|
-
if (window.ui) ui.render.all(); return;
|
|
39
|
-
}
|
|
40
|
-
relay.ws = ws;
|
|
41
|
-
ws.onopen = function() {
|
|
42
|
-
relay.status = 'connected'; relay.actor.send({type:'connected'}); relay._openedAt = Date.now();
|
|
43
|
-
nostrNet._updateRelayStatus(url, 'connected');
|
|
44
|
-
var wasConnected = state.isConnected; state.isConnected = true;
|
|
45
|
-
if (!wasConnected && window.ui) ui.render.all();
|
|
46
|
-
nostrNet.subs.forEach(function(sub, subId) {
|
|
47
|
-
var req = ['REQ', subId].concat(sub.filters);
|
|
48
|
-
ws.send(JSON.stringify(req)); relay.subIds.add(subId);
|
|
49
|
-
if (!relay._reqSentAt) relay._reqSentAt = Date.now();
|
|
50
|
-
});
|
|
51
|
-
nostrNet._drainPending();
|
|
52
|
-
};
|
|
53
|
-
ws.onmessage = function(e) {
|
|
54
|
-
if (relay._reqSentAt && relay.latencyMs === null) {
|
|
55
|
-
relay.latencyMs = Date.now() - relay._reqSentAt; relay._reqSentAt = null;
|
|
56
|
-
nostrNet._updateRelayStatus(url, 'connected');
|
|
57
|
-
}
|
|
58
|
-
try { nostrNet._handleMessage(url, e.data); } catch (_) {}
|
|
59
|
-
};
|
|
60
|
-
ws.onerror = function() {
|
|
61
|
-
relay.status = 'error'; nostrNet._updateRelayStatus(url, 'error');
|
|
62
|
-
if (window.ui) ui.render.all();
|
|
63
|
-
};
|
|
64
|
-
ws.onclose = function() {
|
|
65
|
-
relay.status = 'closed'; relay.actor.send({type:'fail'}); nostrNet._updateRelayStatus(url, 'error');
|
|
66
|
-
var anyOpen = false;
|
|
67
|
-
nostrNet.relays.forEach(function(r) { if (r.ws && r.ws.readyState === 1) anyOpen = true; });
|
|
68
|
-
if (!anyOpen) state.isConnected = false;
|
|
69
|
-
if (window.ui) ui.render.all();
|
|
70
|
-
var sustained = relay._openedAt && (Date.now() - relay._openedAt) > 5000;
|
|
71
|
-
if (sustained) { relay.failCount = 0; relay.reconnectDelay = 1000; }
|
|
72
|
-
else { relay.failCount = (relay.failCount || 0) + 1; relay.reconnectDelay = Math.min(relay.reconnectDelay * 2, 30000); }
|
|
73
|
-
relay._openedAt = null;
|
|
74
|
-
nostrNet._reconnect(url, relay.reconnectDelay);
|
|
75
|
-
};
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
_reconnect: function(url, delay) {
|
|
79
|
-
setTimeout(function() {
|
|
80
|
-
var relay = nostrNet.relays.get(url); if (!relay) return;
|
|
81
|
-
relay.status = 'connecting'; nostrNet._openRelay(url);
|
|
82
|
-
}, delay || 1000);
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
_handleMessage: function(url, rawData) {
|
|
86
|
-
var msg = JSON.parse(rawData);
|
|
87
|
-
if (!Array.isArray(msg) || msg.length < 2) return;
|
|
88
|
-
var type = msg[0], subId = msg[1], sub;
|
|
89
|
-
if (type === 'EVENT') {
|
|
90
|
-
var event = msg[2]; if (!event || !event.id) return;
|
|
91
|
-
if (event.created_at > Math.floor(Date.now() / 1000) + 300) return;
|
|
92
|
-
if (nostrNet.seenIds.has(event.id)) return;
|
|
93
|
-
var valid = true;
|
|
94
|
-
if (window.NostrTools && NostrTools.verifyEvent) {
|
|
95
|
-
try { valid = NostrTools.verifyEvent(event); } catch (_) { valid = false; }
|
|
96
|
-
}
|
|
97
|
-
if (!valid) return;
|
|
98
|
-
nostrNet.seenIds.add(event.id); nostrNet._trimSeenIds();
|
|
99
|
-
sub = nostrNet.subs.get(subId);
|
|
100
|
-
if (sub && sub.onEvent) sub.onEvent(event);
|
|
101
|
-
} else if (type === 'EOSE') {
|
|
102
|
-
sub = nostrNet.subs.get(subId); if (sub && sub.onEose) sub.onEose();
|
|
103
|
-
} else if (type === 'NOTICE') {
|
|
104
|
-
console.warn('[nostr relay notice]', msg[1]);
|
|
105
|
-
} else if (type === 'OK' && !msg[2]) {
|
|
106
|
-
console.error('[nostr relay rejected event]', msg[1], msg[3] || '');
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
_normSubId: function(subId) { return subId.length > 64 ? subId.slice(0, 64) : subId; },
|
|
111
|
-
|
|
112
|
-
subscribe: function(subId, filters, onEvent, onEose) {
|
|
113
|
-
subId = nostrNet._normSubId(subId);
|
|
114
|
-
nostrNet.subs.set(subId, { filters: filters, onEvent: onEvent, onEose: onEose, relays: new Set() });
|
|
115
|
-
nostrNet.relays.forEach(function(relay, url) {
|
|
116
|
-
if (relay.ws && relay.ws.readyState === 1) {
|
|
117
|
-
var req = ['REQ', subId].concat(filters); relay.ws.send(JSON.stringify(req));
|
|
118
|
-
relay.subIds.add(subId); nostrNet.subs.get(subId).relays.add(url);
|
|
119
|
-
if (!relay._reqSentAt) relay._reqSentAt = Date.now();
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
return subId;
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
unsubscribe: function(subId) {
|
|
126
|
-
subId = nostrNet._normSubId(subId);
|
|
127
|
-
nostrNet.relays.forEach(function(relay) {
|
|
128
|
-
if (relay.ws && relay.ws.readyState === 1 && relay.subIds.has(subId)) {
|
|
129
|
-
relay.ws.send(JSON.stringify(['CLOSE', subId])); relay.subIds.delete(subId);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
nostrNet.subs.delete(subId);
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
publish: function(event) {
|
|
136
|
-
var sent = false;
|
|
137
|
-
nostrNet.relays.forEach(function(relay) {
|
|
138
|
-
if (relay.ws && relay.ws.readyState === 1) { relay.ws.send(JSON.stringify(['EVENT', event])); sent = true; }
|
|
139
|
-
});
|
|
140
|
-
if (!sent) nostrNet.pendingEvents.push(event);
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
_drainPending: function() {
|
|
144
|
-
var pending = nostrNet.pendingEvents.splice(0);
|
|
145
|
-
for (var i = 0; i < pending.length; i++) nostrNet.publish(pending[i]);
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
isConnected: function() {
|
|
149
|
-
var open = false;
|
|
150
|
-
nostrNet.relays.forEach(function(r) { if (r.ws && r.ws.readyState === 1) open = true; });
|
|
151
|
-
return open;
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
_updateRelayStatus: function(url, status) {
|
|
155
|
-
var m = new Map(state.nostrRelayStatus || []); m.set(url, status); state.nostrRelayStatus = m;
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
_trimSeenIds: function() {
|
|
159
|
-
if (nostrNet.seenIds.size > 10000) {
|
|
160
|
-
var arr = Array.from(nostrNet.seenIds); nostrNet.seenIds = new Set(arr.slice(arr.length - 5000));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
window.__zellous.net = nostrNet;
|
|
165
|
-
window.network = nostrNet;
|
|
166
|
-
window.nostrNet = nostrNet;
|
|
167
|
-
|
|
168
|
-
function _healRelays() {
|
|
169
|
-
nostrNet.relays.forEach(function(relay, url) {
|
|
170
|
-
if (!relay.ws || relay.ws.readyState === 2 || relay.ws.readyState === 3) nostrNet._openRelay(url);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
window.addEventListener('online', function() {
|
|
174
|
-
nostrNet.relays.forEach(function(relay) { relay.reconnectDelay = 1000; });
|
|
175
|
-
_healRelays();
|
|
176
|
-
});
|
|
177
|
-
document.addEventListener('visibilitychange', function() {
|
|
178
|
-
if (document.visibilityState === 'visible') _healRelays();
|
|
179
|
-
});
|
|
180
|
-
window.__debugNet = { get relays() {
|
|
181
|
-
var out = []; nostrNet.relays.forEach(function(r, url) { out.push({url: url, status: r.status, actorState: r.actor ? r.actor.getSnapshot().value : null, latencyMs: r.latencyMs}); }); return out;
|
|
182
|
-
}};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
var serverPages = {
|
|
2
|
-
_store: new Map(),
|
|
3
|
-
_subs: new Map(),
|
|
4
|
-
|
|
5
|
-
_sanitize(html) {
|
|
6
|
-
const el = document.createElement('div');
|
|
7
|
-
el.innerHTML = html;
|
|
8
|
-
el.querySelectorAll('script,iframe,object,embed,form,input,button').forEach(n => n.remove());
|
|
9
|
-
el.querySelectorAll('*').forEach(n => {
|
|
10
|
-
[...n.attributes].forEach(a => {
|
|
11
|
-
if (/^on/i.test(a.name) || (a.name === 'href' && /^javascript:/i.test(a.value)) || (a.name === 'src' && /^javascript:/i.test(a.value))) n.removeAttribute(a.name);
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
return el.innerHTML;
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
_pageKey(serverId, slug) { return 'zellous-page:' + serverId + ':' + slug; },
|
|
18
|
-
|
|
19
|
-
getPages(serverId) { return Array.from((serverPages._store.get(serverId) || new Map()).values()); },
|
|
20
|
-
|
|
21
|
-
subscribe(serverId) {
|
|
22
|
-
if (serverPages._subs.has(serverId)) return;
|
|
23
|
-
var creator = serverId ? serverId.split(':')[0] : null;
|
|
24
|
-
if (!creator) return;
|
|
25
|
-
var subId = 'pages-' + serverId;
|
|
26
|
-
serverPages._subs.set(serverId, subId);
|
|
27
|
-
nostrNet.subscribe(subId,
|
|
28
|
-
[{ kinds: [30078], authors: [creator] }],
|
|
29
|
-
function(event) {
|
|
30
|
-
if (event.pubkey !== creator) return;
|
|
31
|
-
var dTag = (event.tags.find(function(t) { return t[0] === 'd'; }) || [])[1] || '';
|
|
32
|
-
var prefix = 'zellous-page:' + serverId + ':';
|
|
33
|
-
if (!dTag.startsWith(prefix)) return;
|
|
34
|
-
var slug = dTag.slice(prefix.length);
|
|
35
|
-
if (!slug) return;
|
|
36
|
-
try {
|
|
37
|
-
var data = JSON.parse(event.content);
|
|
38
|
-
var pages = serverPages._store.get(serverId) || new Map();
|
|
39
|
-
if (data.deleted) { pages.delete(slug); } else {
|
|
40
|
-
pages.set(slug, { slug: slug, title: data.title || slug, html: serverPages._sanitize(data.html || ''), updatedAt: event.created_at });
|
|
41
|
-
}
|
|
42
|
-
serverPages._store.set(serverId, pages);
|
|
43
|
-
if (window.uiChannels) uiChannels.render();
|
|
44
|
-
} catch(e) {}
|
|
45
|
-
},
|
|
46
|
-
function() {}
|
|
47
|
-
);
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
unsubscribe(serverId) {
|
|
51
|
-
var subId = serverPages._subs.get(serverId);
|
|
52
|
-
if (subId) { nostrNet.unsubscribe(subId); serverPages._subs.delete(serverId); }
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
async publish(serverId, slug, title, html) {
|
|
56
|
-
if (!serverRoles.isAdmin(serverId)) throw new Error('Admin only');
|
|
57
|
-
var safe = serverPages._sanitize(html);
|
|
58
|
-
var dTag = serverPages._pageKey(serverId, slug);
|
|
59
|
-
var signed = await auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag]], content: JSON.stringify({ title: title, html: safe }) });
|
|
60
|
-
await nostrNet.publish(signed);
|
|
61
|
-
var pages = serverPages._store.get(serverId) || new Map();
|
|
62
|
-
pages.set(slug, { slug: slug, title: title, html: safe, updatedAt: Math.floor(Date.now() / 1000) });
|
|
63
|
-
serverPages._store.set(serverId, pages);
|
|
64
|
-
if (window.uiChannels) uiChannels.render();
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
async deletePage(serverId, slug) {
|
|
68
|
-
if (!serverRoles.isAdmin(serverId)) throw new Error('Admin only');
|
|
69
|
-
var dTag = serverPages._pageKey(serverId, slug);
|
|
70
|
-
var signed = await auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag]], content: JSON.stringify({ deleted: true }) });
|
|
71
|
-
await nostrNet.publish(signed);
|
|
72
|
-
var pages = serverPages._store.get(serverId) || new Map();
|
|
73
|
-
pages.delete(slug);
|
|
74
|
-
serverPages._store.set(serverId, pages);
|
|
75
|
-
if (window.uiChannels) uiChannels.render();
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
showEditModal(serverId, existing) {
|
|
79
|
-
document.getElementById('pageEditModal')?.remove();
|
|
80
|
-
var modal = document.createElement('div');
|
|
81
|
-
modal.id = 'pageEditModal';
|
|
82
|
-
modal.className = 'modal-overlay';
|
|
83
|
-
modal.style.display = 'flex';
|
|
84
|
-
var slug = existing ? existing.slug : 'page-' + Date.now().toString(36);
|
|
85
|
-
modal.innerHTML = '<div class="modal" style="max-width:680px;width:95%">' +
|
|
86
|
-
'<div class="modal-header"><span class="modal-title">' + (existing ? 'Edit Page' : 'New Page') + '</span>' +
|
|
87
|
-
'<button class="modal-close" id="pageModalClose">×</button></div>' +
|
|
88
|
-
'<div class="modal-body" style="display:flex;flex-direction:column;gap:12px">' +
|
|
89
|
-
'<input id="pageModalTitle" class="modal-input" placeholder="Page title" value="' + escHtml(existing ? existing.title : '') + '">' +
|
|
90
|
-
'<div id="pageModalEditor" class="page-editor" contenteditable="true" style="min-height:220px;background:var(--bg-raised);border:1px solid var(--border);border-radius:6px;padding:12px;overflow-y:auto;font-size:14px;line-height:1.6"></div>' +
|
|
91
|
-
'<div class="page-editor-toolbar" style="display:flex;gap:6px;flex-wrap:wrap">' +
|
|
92
|
-
'<button class="page-tb-btn" data-cmd="bold"><b>B</b></button>' +
|
|
93
|
-
'<button class="page-tb-btn" data-cmd="italic"><i>I</i></button>' +
|
|
94
|
-
'<button class="page-tb-btn" data-cmd="underline"><u>U</u></button>' +
|
|
95
|
-
'<button class="page-tb-btn" data-cmd="insertUnorderedList">•</button>' +
|
|
96
|
-
'<button class="page-tb-btn" data-cmd="insertOrderedList">1.</button>' +
|
|
97
|
-
'</div>' +
|
|
98
|
-
'</div>' +
|
|
99
|
-
'<div class="modal-footer">' +
|
|
100
|
-
(existing ? '<button class="modal-btn danger" id="pageModalDelete">Delete</button>' : '') +
|
|
101
|
-
'<button class="modal-btn secondary" id="pageModalCancel">Cancel</button>' +
|
|
102
|
-
'<button class="modal-btn primary" id="pageModalSave">Save</button>' +
|
|
103
|
-
'</div></div>';
|
|
104
|
-
document.body.appendChild(modal);
|
|
105
|
-
var editor = document.getElementById('pageModalEditor');
|
|
106
|
-
if (existing) editor.innerHTML = existing.html;
|
|
107
|
-
modal.querySelectorAll('.page-tb-btn').forEach(function(btn) {
|
|
108
|
-
btn.addEventListener('mousedown', function(e) { e.preventDefault(); document.execCommand(btn.dataset.cmd, false, null); editor.focus(); });
|
|
109
|
-
});
|
|
110
|
-
document.getElementById('pageModalClose').addEventListener('click', function() { modal.remove(); });
|
|
111
|
-
document.getElementById('pageModalCancel').addEventListener('click', function() { modal.remove(); });
|
|
112
|
-
if (existing) {
|
|
113
|
-
document.getElementById('pageModalDelete').addEventListener('click', async function() {
|
|
114
|
-
if (!confirm('Delete this page?')) return;
|
|
115
|
-
try { await serverPages.deletePage(serverId, slug); modal.remove(); } catch(e) { alert(e.message); }
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
document.getElementById('pageModalSave').addEventListener('click', async function() {
|
|
119
|
-
var title = document.getElementById('pageModalTitle').value.trim();
|
|
120
|
-
if (!title) { alert('Title required'); return; }
|
|
121
|
-
try { await serverPages.publish(serverId, slug, title, editor.innerHTML); modal.remove(); } catch(e) { alert(e.message); }
|
|
122
|
-
});
|
|
123
|
-
modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
renderPageView(serverId, slug) {
|
|
127
|
-
var pages = serverPages._store.get(serverId) || new Map();
|
|
128
|
-
var page = pages.get(slug);
|
|
129
|
-
var area = document.getElementById('pageView');
|
|
130
|
-
if (!area) return;
|
|
131
|
-
var isAdmin = window.serverRoles && serverRoles.isAdmin(serverId);
|
|
132
|
-
area.innerHTML = '<div class="page-view-header">' +
|
|
133
|
-
'<span class="page-view-title">' + escHtml(page ? page.title : slug) + '</span>' +
|
|
134
|
-
(isAdmin ? '<button class="modal-btn secondary page-edit-btn" id="pageEditBtn">Edit</button>' : '') +
|
|
135
|
-
'</div>' +
|
|
136
|
-
'<div class="page-view-body">' + (page ? page.html : '<p style="color:var(--text-muted)">No content yet.</p>') + '</div>';
|
|
137
|
-
if (isAdmin) {
|
|
138
|
-
document.getElementById('pageEditBtn').addEventListener('click', function() {
|
|
139
|
-
serverPages.showEditModal(serverId, page || { slug: slug, title: slug, html: '' });
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
window.__zellous.pages = serverPages;
|
|
146
|
-
window.serverPages = serverPages;
|
|
147
|
-
if (!window.__debug) window.__debug = {};
|
|
148
|
-
Object.defineProperty(window.__debug, 'pages', { get: function() {
|
|
149
|
-
var out = {};
|
|
150
|
-
serverPages._store.forEach(function(pages, srv) { out[srv] = Array.from(pages.keys()); });
|
|
151
|
-
return out;
|
|
152
|
-
}, configurable: true });
|