wireweave 0.1.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/LICENSE +21 -0
- package/README.md +72 -0
- package/package.json +52 -0
- package/src/auth.js +97 -0
- package/src/debug.js +20 -0
- package/src/index.js +3 -0
- package/src/legacy/README.md +71 -0
- package/src/legacy/index.js +49 -0
- package/src/legacy/nostr-auth.js +217 -0
- package/src/legacy/nostr-bans.js +59 -0
- package/src/legacy/nostr-channels.js +145 -0
- package/src/legacy/nostr-chat.js +185 -0
- package/src/legacy/nostr-fsm.js +5 -0
- package/src/legacy/nostr-media.js +97 -0
- package/src/legacy/nostr-message.js +12 -0
- package/src/legacy/nostr-network.js +182 -0
- package/src/legacy/nostr-pages.js +152 -0
- package/src/legacy/nostr-roles.js +74 -0
- package/src/legacy/nostr-servers.js +223 -0
- package/src/legacy/nostr-settings.js +97 -0
- package/src/legacy/nostr-state-patch.js +19 -0
- package/src/legacy/nostr-voice-camera.js +63 -0
- package/src/legacy/nostr-voice-rtc.js +199 -0
- package/src/legacy/nostr-voice-sfu.js +128 -0
- package/src/legacy/nostr-voice.js +257 -0
- package/src/relay-pool.js +170 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
var DEFAULT_CATEGORIES = [
|
|
2
|
+
{ id: 'general', name: 'TEXT CHANNELS', position: 0 },
|
|
3
|
+
{ id: 'voice', name: 'VOICE CHANNELS', position: 1 }
|
|
4
|
+
];
|
|
5
|
+
|
|
6
|
+
var DEFAULT_CHANNELS = [
|
|
7
|
+
{ id: 'general', name: 'general', type: 'text', categoryId: 'general', position: 0 },
|
|
8
|
+
{ id: 'announcements', name: 'announcements', type: 'announcement', categoryId: 'general', position: 1 },
|
|
9
|
+
{ id: 'general-voice', name: 'General', type: 'voice', categoryId: 'voice', position: 0 }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
async function _hexChannelId(channelId, serverId) {
|
|
13
|
+
var input = (serverId || '') + ':' + channelId;
|
|
14
|
+
var buf = new TextEncoder().encode(input);
|
|
15
|
+
var hash = await crypto.subtle.digest('SHA-256', buf);
|
|
16
|
+
return Array.from(new Uint8Array(hash)).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
var channelManager = {
|
|
20
|
+
isOwner: function() {
|
|
21
|
+
return state.nostrPubkey && state.currentServerId && state.nostrPubkey === state.currentServerId.split(':')[0];
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
loadChannels: function(serverId, onReady) {
|
|
25
|
+
var parts = serverId.split(':');
|
|
26
|
+
var ownerPubkey = parts[0];
|
|
27
|
+
var dTag = 'zellous-channels:' + serverId;
|
|
28
|
+
nostrNet.subscribe(
|
|
29
|
+
'channels-' + serverId,
|
|
30
|
+
[{ kinds: [30078], authors: [ownerPubkey], '#d': [dTag] }],
|
|
31
|
+
function(event) {
|
|
32
|
+
var hasTag = event.tags && event.tags.some(function(t) { return t[0] === 'd' && t[1] === dTag; });
|
|
33
|
+
if (!hasTag) return;
|
|
34
|
+
try {
|
|
35
|
+
var data = JSON.parse(event.content);
|
|
36
|
+
state.channels = data.channels || [];
|
|
37
|
+
state.categories = data.categories || [];
|
|
38
|
+
ui.render.all();
|
|
39
|
+
} catch (e) { console.warn('[nostr-channels] parse error:', e.message); }
|
|
40
|
+
},
|
|
41
|
+
function() {
|
|
42
|
+
if (!state.channels.length) channelManager._setDefaults();
|
|
43
|
+
if (onReady) onReady();
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
_setDefaults: function() {
|
|
49
|
+
state.channels = DEFAULT_CHANNELS.map(function(c) { return Object.assign({}, c); });
|
|
50
|
+
state.categories = DEFAULT_CATEGORIES.map(function(c) { return Object.assign({}, c); });
|
|
51
|
+
ui.render.all();
|
|
52
|
+
if (channelManager.isOwner()) channelManager._publishChannelList().catch(function(e) { console.warn('[nostr-channels] publish defaults failed:', e.message); });
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
_publishChannelList: async function() {
|
|
56
|
+
if (!channelManager.isOwner()) return;
|
|
57
|
+
var serverId = state.currentServerId;
|
|
58
|
+
var template = {
|
|
59
|
+
kind: 30078,
|
|
60
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
61
|
+
tags: [['d', 'zellous-channels:' + serverId]],
|
|
62
|
+
content: JSON.stringify({ channels: state.channels, categories: state.categories })
|
|
63
|
+
};
|
|
64
|
+
var signed = await auth.sign(template);
|
|
65
|
+
await nostrNet.publish(signed);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
create: async function(name, type, categoryId) {
|
|
69
|
+
var id = 'ch-' + Date.now();
|
|
70
|
+
state.channels = state.channels.concat([{
|
|
71
|
+
id: id, name: name, type: type || 'text',
|
|
72
|
+
categoryId: categoryId || 'general', position: state.channels.length
|
|
73
|
+
}]);
|
|
74
|
+
await channelManager._publishChannelList();
|
|
75
|
+
ui.render.all();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
rename: async function(id, name) {
|
|
79
|
+
state.channels = state.channels.map(function(c) {
|
|
80
|
+
return c.id === id ? Object.assign({}, c, { name: name }) : c;
|
|
81
|
+
});
|
|
82
|
+
await channelManager._publishChannelList();
|
|
83
|
+
ui.render.all();
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
remove: async function(id) {
|
|
87
|
+
state.channels = state.channels.filter(function(c) { return c.id !== id; });
|
|
88
|
+
await channelManager._publishChannelList();
|
|
89
|
+
ui.render.all();
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
createCategory: async function(name) {
|
|
93
|
+
var id = 'cat-' + Date.now();
|
|
94
|
+
state.categories = state.categories.concat([{ id: id, name: name, position: state.categories.length }]);
|
|
95
|
+
await channelManager._publishChannelList();
|
|
96
|
+
ui.render.all();
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
renameCategory: async function(id, name) {
|
|
100
|
+
state.categories = state.categories.map(function(c) {
|
|
101
|
+
return c.id === id ? Object.assign({}, c, { name: name }) : c;
|
|
102
|
+
});
|
|
103
|
+
await channelManager._publishChannelList();
|
|
104
|
+
ui.render.all();
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
deleteCategory: async function(id) {
|
|
108
|
+
state.categories = state.categories.filter(function(c) { return c.id !== id; });
|
|
109
|
+
state.channels = state.channels.map(function(c) {
|
|
110
|
+
return c.categoryId === id ? Object.assign({}, c, { categoryId: null }) : c;
|
|
111
|
+
});
|
|
112
|
+
await channelManager._publishChannelList();
|
|
113
|
+
ui.render.all();
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
reorderChannels: async function(catId, ids) {
|
|
117
|
+
ids.forEach(function(chId, idx) {
|
|
118
|
+
state.channels = state.channels.map(function(c) {
|
|
119
|
+
return c.id === chId ? Object.assign({}, c, { position: idx, categoryId: catId }) : c;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
await channelManager._publishChannelList();
|
|
123
|
+
ui.render.all();
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
reorderCategories: async function(ids) {
|
|
127
|
+
ids.forEach(function(catId, idx) {
|
|
128
|
+
state.categories = state.categories.map(function(c) {
|
|
129
|
+
return c.id === catId ? Object.assign({}, c, { position: idx }) : c;
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
await channelManager._publishChannelList();
|
|
133
|
+
ui.render.all();
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
hideContextMenu: function() {
|
|
137
|
+
var m = document.getElementById('channelContextMenu');
|
|
138
|
+
if (m) m.remove();
|
|
139
|
+
var cm = document.getElementById('categoryContextMenu');
|
|
140
|
+
if (cm) cm.remove();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
window.__zellous.channels = channelManager;
|
|
145
|
+
window.channelManager = channelManager;
|
|
@@ -0,0 +1,185 @@
|
|
|
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;
|
|
@@ -0,0 +1,5 @@
|
|
|
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;
|
|
@@ -0,0 +1,97 @@
|
|
|
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 });
|
|
@@ -0,0 +1,12 @@
|
|
|
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;
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
}};
|