nappup 1.5.2 → 1.5.4
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/package.json +1 -1
- package/src/helpers/event.js +26 -0
- package/src/index.js +70 -21
- package/src/services/nostr-relays.js +5 -1
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function stringifyEvent (event) {
|
|
2
|
+
event = { ...event }
|
|
3
|
+
|
|
4
|
+
if (typeof event.content === 'string' && event.content.length > 70) {
|
|
5
|
+
event.content = `${event.content.slice(0, 70)}...(${event.content.length})`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof event.sig === 'string' && event.sig.length > 3) {
|
|
9
|
+
event.sig = `${event.sig.slice(0, 3)}...(${event.sig.length})`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (Array.isArray(event.tags)) {
|
|
13
|
+
const totalTagsCount = event.tags.length
|
|
14
|
+
event.tags = event.tags.slice(0, 5).map(tag =>
|
|
15
|
+
Array.isArray(tag)
|
|
16
|
+
? tag.map(val => typeof val === 'string' && val.length > 64 ? `${val.slice(0, 64)}...(${val.length})` : val)
|
|
17
|
+
: tag
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if (totalTagsCount > 5) {
|
|
21
|
+
event.tags.push(`... and ${totalTagsCount - 5} more tags`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return JSON.stringify(event, null, 2)
|
|
26
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import NostrSigner from '#services/nostr-signer.js'
|
|
|
6
6
|
import { streamToChunks, streamToText } from '#helpers/stream.js'
|
|
7
7
|
import { isNostrAppDTagSafe, deriveNostrAppDTag } from '#helpers/app.js'
|
|
8
8
|
import { extractHtmlMetadata, findFavicon, findIndexFile } from '#helpers/app-metadata.js'
|
|
9
|
+
import { stringifyEvent } from '#helpers/event.js'
|
|
9
10
|
import { NAPP_CATEGORIES } from '#config/napp-categories.js'
|
|
10
11
|
|
|
11
12
|
export default async function (...args) {
|
|
@@ -165,13 +166,19 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
165
166
|
|
|
166
167
|
async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLength, log, pause = 0, mimeType, shouldReupload = false }) {
|
|
167
168
|
const writeRelays = (await signer.getRelays()).write
|
|
169
|
+
const relays = [...new Set([...writeRelays, ...nappRelays])]
|
|
168
170
|
let chunkIndex = 0
|
|
169
171
|
for await (const chunk of nmmr.getChunks()) {
|
|
170
172
|
const dTag = chunk.x
|
|
171
173
|
const currentCtag = `${chunk.rootX}:${chunk.index}`
|
|
172
|
-
const { otherCtags, hasCurrentCtag } = await getPreviousCtags(dTag, currentCtag,
|
|
174
|
+
const { otherCtags, hasCurrentCtag, foundEvent, missingRelays } = await getPreviousCtags(dTag, currentCtag, relays, signer)
|
|
173
175
|
if (!shouldReupload && hasCurrentCtag) {
|
|
174
|
-
|
|
176
|
+
if (missingRelays.length === 0) {
|
|
177
|
+
log(`${filename}: Skipping chunk ${++chunkIndex} of ${chunkLength} (already uploaded)`)
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
log(`${filename}: Re-uploading chunk ${++chunkIndex} of ${chunkLength} to ${missingRelays.length} missing relays (out of ${relays.length})`)
|
|
181
|
+
;({ pause } = (await throttledSendEvent(foundEvent, missingRelays, { pause, log, trailingPause: true, minSuccessfulRelays: 0 })))
|
|
175
182
|
continue
|
|
176
183
|
}
|
|
177
184
|
const binaryDataChunk = {
|
|
@@ -188,7 +195,6 @@ async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLength, lo
|
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
const event = await signer.signEvent(binaryDataChunk)
|
|
191
|
-
const relays = [...new Set([...writeRelays, ...nappRelays])]
|
|
192
198
|
const fallbackRelayCount = relays.length - writeRelays.length
|
|
193
199
|
log(`${filename}: Uploading file part ${++chunkIndex} of ${chunkLength} to ${writeRelays.length} relays${fallbackRelayCount > 0 ? ` (+${fallbackRelayCount} fallback)` : ''}`)
|
|
194
200
|
;({ pause } = (await throttledSendEvent(event, relays, { pause, log, trailingPause: true })))
|
|
@@ -217,7 +223,10 @@ async function throttledSendEvent (event, relays, {
|
|
|
217
223
|
else r[1].push(v)
|
|
218
224
|
return r
|
|
219
225
|
}, [[], []])
|
|
220
|
-
|
|
226
|
+
if (unretryableErrors.length > 0) {
|
|
227
|
+
log(`${unretryableErrors.length} unretryable errors:\n${unretryableErrors.map(v => `${v.relay}: ${v.reason.message}`).join('; ')}`)
|
|
228
|
+
console.log('Erroed event:', stringifyEvent(event))
|
|
229
|
+
}
|
|
221
230
|
const unretryableErrorsLength = errors.length - rateLimitErrors.length
|
|
222
231
|
const maybeSuccessfulRelays = relays.length - unretryableErrorsLength
|
|
223
232
|
const hasReachedMaxRetries = retries > maxRetries
|
|
@@ -241,20 +250,24 @@ async function throttledSendEvent (event, relays, {
|
|
|
241
250
|
})
|
|
242
251
|
}
|
|
243
252
|
|
|
244
|
-
async function getPreviousCtags (dTagValue, currentCtagValue,
|
|
253
|
+
async function getPreviousCtags (dTagValue, currentCtagValue, relays, signer) {
|
|
254
|
+
const targetRelays = [...new Set([...relays, ...nappRelays])]
|
|
245
255
|
const storedEvents = (await nostrRelays.getEvents({
|
|
246
256
|
kinds: [34600],
|
|
247
257
|
authors: [await signer.getPublicKey()],
|
|
248
258
|
'#d': [dTagValue],
|
|
249
259
|
limit: 1
|
|
250
|
-
},
|
|
260
|
+
}, targetRelays)).result
|
|
251
261
|
|
|
252
262
|
let hasCurrentCtag = false
|
|
253
263
|
const hasEvent = storedEvents.length > 0
|
|
254
264
|
if (!hasEvent) return { otherCtags: [], hasEvent, hasCurrentCtag }
|
|
255
265
|
|
|
256
266
|
const cTagValues = { [currentCtagValue]: true }
|
|
257
|
-
|
|
267
|
+
storedEvents.sort((a, b) => b.created_at - a.created_at)
|
|
268
|
+
const bestEvent = storedEvents[0]
|
|
269
|
+
const prevTags = bestEvent.tags
|
|
270
|
+
|
|
258
271
|
if (!Array.isArray(prevTags)) return { otherCtags: [], hasEvent, hasCurrentCtag }
|
|
259
272
|
|
|
260
273
|
hasCurrentCtag = prevTags.some(tag =>
|
|
@@ -277,7 +290,11 @@ async function getPreviousCtags (dTagValue, currentCtagValue, writeRelays, signe
|
|
|
277
290
|
return isCTag && isntDuplicate
|
|
278
291
|
})
|
|
279
292
|
|
|
280
|
-
|
|
293
|
+
const matchingEvents = storedEvents.filter(e => e.id === bestEvent.id)
|
|
294
|
+
const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
|
|
295
|
+
const missingRelays = targetRelays.filter(r => !coveredRelays.has(r))
|
|
296
|
+
|
|
297
|
+
return { otherCtags, hasEvent, hasCurrentCtag, foundEvent: bestEvent, missingRelays }
|
|
281
298
|
}
|
|
282
299
|
|
|
283
300
|
async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, shouldReupload = false, log = () => {} }) {
|
|
@@ -299,7 +316,8 @@ async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, s
|
|
|
299
316
|
const events = (await nostrRelays.getEvents({
|
|
300
317
|
kinds: [kind],
|
|
301
318
|
authors: [await signer.getPublicKey()],
|
|
302
|
-
'#d': [dTag]
|
|
319
|
+
'#d': [dTag],
|
|
320
|
+
limit: 1
|
|
303
321
|
}, writeRelays)).result
|
|
304
322
|
|
|
305
323
|
if (events.length > 0) {
|
|
@@ -311,21 +329,32 @@ async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, s
|
|
|
311
329
|
})
|
|
312
330
|
|
|
313
331
|
const mostRecentEvent = events[0]
|
|
314
|
-
const recentFileTags = mostRecentEvent.tags
|
|
332
|
+
const recentFileTags = mostRecentEvent.tags
|
|
333
|
+
.filter(t => t[0] === 'file' && t[2] !== '.well-known/napp.json')
|
|
334
|
+
.sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0))
|
|
315
335
|
|
|
316
|
-
const
|
|
336
|
+
const currentFileTags = fileTags
|
|
337
|
+
.filter(t => t[2] !== '.well-known/napp.json')
|
|
338
|
+
.sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0))
|
|
339
|
+
|
|
340
|
+
const isSame = currentFileTags.length === recentFileTags.length && currentFileTags.every((t, i) => {
|
|
317
341
|
const rt = recentFileTags[i]
|
|
318
342
|
return rt.length >= 4 && rt[1] === t[1] && rt[2] === t[2] && rt[3] === t[3]
|
|
319
343
|
})
|
|
320
344
|
|
|
321
345
|
if (isSame) {
|
|
322
|
-
log(`Bundle based on ${fileTags.length} files is up
|
|
323
|
-
|
|
346
|
+
log(`Bundle based on ${fileTags.length} files is up-to-date (id: ${mostRecentEvent.id} - created_at: ${new Date(mostRecentEvent.created_at * 1000).toISOString()})`)
|
|
347
|
+
|
|
348
|
+
const matchingEvents = events.filter(e => e.id === mostRecentEvent.id)
|
|
349
|
+
const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
|
|
350
|
+
const missingRelays = writeRelays.filter(r => !coveredRelays.has(r))
|
|
351
|
+
|
|
352
|
+
if (missingRelays.length === 0) return mostRecentEvent
|
|
324
353
|
|
|
325
354
|
// nostrRelays.getEvents currently doesn't tell us which event came from which relay,
|
|
326
355
|
// so we re-upload to all relays to ensure consistency
|
|
327
|
-
log(`Re-uploading existing bundle event to
|
|
328
|
-
await throttledSendEvent(mostRecentEvent,
|
|
356
|
+
log(`Re-uploading existing bundle event to ${missingRelays.length} missing relays (out of ${writeRelays.length})`)
|
|
357
|
+
await throttledSendEvent(mostRecentEvent, missingRelays, { pause, trailingPause: true, log, minSuccessfulRelays: 0 })
|
|
329
358
|
return mostRecentEvent
|
|
330
359
|
}
|
|
331
360
|
}
|
|
@@ -369,12 +398,14 @@ async function maybeUploadStall ({
|
|
|
369
398
|
const hasMetadata = Boolean(trimmedName) || Boolean(trimmedSummary) || Boolean(iconRootHash) ||
|
|
370
399
|
Boolean(self) || (countries && countries.length > 0) || (categories && categories.length > 0) || (hashtags && hashtags.length > 0)
|
|
371
400
|
|
|
372
|
-
const
|
|
401
|
+
const relays = [...new Set([...writeRelays, ...nappRelays])]
|
|
402
|
+
|
|
403
|
+
const previousResult = await getPreviousStall(dTag, relays, signer, channel)
|
|
404
|
+
const previous = previousResult?.previous
|
|
373
405
|
if (!previous && !hasMetadata) return { pause }
|
|
374
406
|
|
|
375
407
|
const publishStall = async (event) => {
|
|
376
408
|
const signedEvent = await signer.signEvent(event)
|
|
377
|
-
const relays = [...new Set([...writeRelays, ...nappRelays])]
|
|
378
409
|
return await throttledSendEvent(signedEvent, relays, { pause, log, trailingPause: true })
|
|
379
410
|
}
|
|
380
411
|
|
|
@@ -600,7 +631,18 @@ async function maybeUploadStall ({
|
|
|
600
631
|
}
|
|
601
632
|
}
|
|
602
633
|
|
|
603
|
-
if (!changed)
|
|
634
|
+
if (!changed) {
|
|
635
|
+
const { storedEvents } = previousResult
|
|
636
|
+
|
|
637
|
+
const matchingEvents = storedEvents.filter(e => e.id === previous.id)
|
|
638
|
+
const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
|
|
639
|
+
const missingRelays = relays.filter(r => !coveredRelays.has(r))
|
|
640
|
+
|
|
641
|
+
if (missingRelays.length === 0) return { pause }
|
|
642
|
+
|
|
643
|
+
log(`Re-uploading existing stall event to ${missingRelays.length} missing relays (out of ${relays.length})`)
|
|
644
|
+
return await throttledSendEvent(previous, missingRelays, { pause, log, trailingPause: true, minSuccessfulRelays: 0 })
|
|
645
|
+
}
|
|
604
646
|
|
|
605
647
|
return await publishStall({
|
|
606
648
|
kind,
|
|
@@ -610,7 +652,7 @@ async function maybeUploadStall ({
|
|
|
610
652
|
})
|
|
611
653
|
}
|
|
612
654
|
|
|
613
|
-
async function getPreviousStall (dTagValue,
|
|
655
|
+
async function getPreviousStall (dTagValue, relays, signer, channel) {
|
|
614
656
|
const kind = {
|
|
615
657
|
main: 37348,
|
|
616
658
|
next: 37349,
|
|
@@ -622,8 +664,15 @@ async function getPreviousStall (dTagValue, writeRelays, signer, channel) {
|
|
|
622
664
|
authors: [await signer.getPublicKey()],
|
|
623
665
|
'#d': [dTagValue],
|
|
624
666
|
limit: 1
|
|
625
|
-
},
|
|
667
|
+
}, relays)).result
|
|
626
668
|
|
|
627
669
|
if (storedEvents.length === 0) return null
|
|
628
|
-
|
|
670
|
+
|
|
671
|
+
storedEvents.sort((a, b) => b.created_at - a.created_at)
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
previous: storedEvents[0],
|
|
675
|
+
storedEvents,
|
|
676
|
+
targetRelayCount: relays.length
|
|
677
|
+
}
|
|
629
678
|
}
|
|
@@ -80,6 +80,7 @@ export class NostrRelays {
|
|
|
80
80
|
const relay = await this.#getRelay(url)
|
|
81
81
|
sub = relay.subscribe([filter], {
|
|
82
82
|
onevent: (event) => {
|
|
83
|
+
event.meta = { relay: url }
|
|
83
84
|
events.push(event)
|
|
84
85
|
},
|
|
85
86
|
onclose: err => {
|
|
@@ -115,6 +116,9 @@ export class NostrRelays {
|
|
|
115
116
|
|
|
116
117
|
// Send an event to a list of relays
|
|
117
118
|
async sendEvent (event, relays, timeout = 3000) {
|
|
119
|
+
const eventToSend = event.meta ? { ...event } : event
|
|
120
|
+
if (eventToSend.meta) delete eventToSend.meta
|
|
121
|
+
|
|
118
122
|
const promises = relays.map(async (url) => {
|
|
119
123
|
let timer
|
|
120
124
|
const p = Promise.withResolvers()
|
|
@@ -124,7 +128,7 @@ export class NostrRelays {
|
|
|
124
128
|
}, timeout))
|
|
125
129
|
|
|
126
130
|
const relay = await this.#getRelay(url)
|
|
127
|
-
await relay.publish(
|
|
131
|
+
await relay.publish(eventToSend)
|
|
128
132
|
p.resolve()
|
|
129
133
|
} catch (err) {
|
|
130
134
|
if (err.message?.startsWith('duplicate:')) return p.resolve()
|