wireweave 0.1.2 → 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 +11 -4
- package/src/bans.js +45 -0
- package/src/channels.js +107 -0
- package/src/index.js +9 -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/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
|
@@ -1,145 +0,0 @@
|
|
|
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;
|
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
|
-
}};
|