nappup 1.4.2 → 1.5.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/package.json +1 -1
- package/src/index.js +106 -24
- package/src/services/nostr-relays.js +8 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import NMMR from 'nmmr'
|
|
2
2
|
import { appEncode } from '#helpers/nip19.js'
|
|
3
3
|
import Base93Encoder from '#services/base93-encoder.js'
|
|
4
|
-
import nostrRelays from '#services/nostr-relays.js'
|
|
4
|
+
import nostrRelays, { nappRelays } from '#services/nostr-relays.js'
|
|
5
5
|
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'
|
|
@@ -151,8 +151,8 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
151
151
|
hashtags: nappJson.hashtag
|
|
152
152
|
})))
|
|
153
153
|
|
|
154
|
-
log(`Uploading bundle
|
|
155
|
-
const bundle = await uploadBundle({ dTag, channel, fileMetadata, signer: nostrSigner, pause })
|
|
154
|
+
log(`Uploading bundle ${dTag}`)
|
|
155
|
+
const bundle = await uploadBundle({ dTag, channel, fileMetadata, signer: nostrSigner, pause, shouldReupload, log })
|
|
156
156
|
|
|
157
157
|
const appEntity = appEncode({
|
|
158
158
|
dTag: bundle.tags.find(v => v[0] === 'd')[1],
|
|
@@ -165,13 +165,19 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
165
165
|
|
|
166
166
|
async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLength, log, pause = 0, mimeType, shouldReupload = false }) {
|
|
167
167
|
const writeRelays = (await signer.getRelays()).write
|
|
168
|
+
const relays = [...new Set([...writeRelays, ...nappRelays])]
|
|
168
169
|
let chunkIndex = 0
|
|
169
170
|
for await (const chunk of nmmr.getChunks()) {
|
|
170
171
|
const dTag = chunk.x
|
|
171
172
|
const currentCtag = `${chunk.rootX}:${chunk.index}`
|
|
172
|
-
const { otherCtags, hasCurrentCtag } = await getPreviousCtags(dTag, currentCtag,
|
|
173
|
+
const { otherCtags, hasCurrentCtag, foundEvent, missingRelays } = await getPreviousCtags(dTag, currentCtag, relays, signer)
|
|
173
174
|
if (!shouldReupload && hasCurrentCtag) {
|
|
174
|
-
|
|
175
|
+
if (missingRelays.length === 0) {
|
|
176
|
+
log(`${filename}: Skipping chunk ${++chunkIndex} of ${chunkLength} (already uploaded)`)
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
log(`${filename}: Re-uploading chunk ${++chunkIndex} of ${chunkLength} to ${missingRelays.length} missing relays (out of ${relays.length})`)
|
|
180
|
+
;({ pause } = (await throttledSendEvent(foundEvent, missingRelays, { pause, log, trailingPause: true })))
|
|
175
181
|
continue
|
|
176
182
|
}
|
|
177
183
|
const binaryDataChunk = {
|
|
@@ -188,8 +194,9 @@ async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLength, lo
|
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
const event = await signer.signEvent(binaryDataChunk)
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
const fallbackRelayCount = relays.length - writeRelays.length
|
|
198
|
+
log(`${filename}: Uploading file part ${++chunkIndex} of ${chunkLength} to ${writeRelays.length} relays${fallbackRelayCount > 0 ? ` (+${fallbackRelayCount} fallback)` : ''}`)
|
|
199
|
+
;({ pause } = (await throttledSendEvent(event, relays, { pause, log, trailingPause: true })))
|
|
193
200
|
}
|
|
194
201
|
return { pause }
|
|
195
202
|
}
|
|
@@ -239,20 +246,24 @@ async function throttledSendEvent (event, relays, {
|
|
|
239
246
|
})
|
|
240
247
|
}
|
|
241
248
|
|
|
242
|
-
async function getPreviousCtags (dTagValue, currentCtagValue,
|
|
249
|
+
async function getPreviousCtags (dTagValue, currentCtagValue, relays, signer) {
|
|
250
|
+
const targetRelays = [...new Set([...relays, ...nappRelays])]
|
|
243
251
|
const storedEvents = (await nostrRelays.getEvents({
|
|
244
252
|
kinds: [34600],
|
|
245
253
|
authors: [await signer.getPublicKey()],
|
|
246
254
|
'#d': [dTagValue],
|
|
247
255
|
limit: 1
|
|
248
|
-
},
|
|
256
|
+
}, targetRelays)).result
|
|
249
257
|
|
|
250
258
|
let hasCurrentCtag = false
|
|
251
259
|
const hasEvent = storedEvents.length > 0
|
|
252
260
|
if (!hasEvent) return { otherCtags: [], hasEvent, hasCurrentCtag }
|
|
253
261
|
|
|
254
262
|
const cTagValues = { [currentCtagValue]: true }
|
|
255
|
-
|
|
263
|
+
storedEvents.sort((a, b) => b.created_at - a.created_at)
|
|
264
|
+
const bestEvent = storedEvents[0]
|
|
265
|
+
const prevTags = bestEvent.tags
|
|
266
|
+
|
|
256
267
|
if (!Array.isArray(prevTags)) return { otherCtags: [], hasEvent, hasCurrentCtag }
|
|
257
268
|
|
|
258
269
|
hasCurrentCtag = prevTags.some(tag =>
|
|
@@ -275,26 +286,78 @@ async function getPreviousCtags (dTagValue, currentCtagValue, writeRelays, signe
|
|
|
275
286
|
return isCTag && isntDuplicate
|
|
276
287
|
})
|
|
277
288
|
|
|
278
|
-
|
|
289
|
+
const matchingEvents = storedEvents.filter(e => e.id === bestEvent.id)
|
|
290
|
+
const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
|
|
291
|
+
const missingRelays = targetRelays.filter(r => !coveredRelays.has(r))
|
|
292
|
+
|
|
293
|
+
return { otherCtags, hasEvent, hasCurrentCtag, foundEvent: bestEvent, missingRelays }
|
|
279
294
|
}
|
|
280
295
|
|
|
281
|
-
async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0 }) {
|
|
296
|
+
async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, shouldReupload = false, log = () => {} }) {
|
|
282
297
|
const kind = {
|
|
283
298
|
main: 37448, // stable
|
|
284
299
|
next: 37449, // insider
|
|
285
300
|
draft: 37450 // vibe coded preview
|
|
286
301
|
}[channel] ?? 37448
|
|
302
|
+
|
|
303
|
+
const fileTags = fileMetadata.map(v => ['file', v.rootHash, v.filename, v.mimeType])
|
|
304
|
+
const tags = [
|
|
305
|
+
['d', dTag],
|
|
306
|
+
...fileTags
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
const writeRelays = [...new Set([...(await signer.getRelays()).write, ...nappRelays])]
|
|
310
|
+
|
|
311
|
+
if (!shouldReupload) {
|
|
312
|
+
const events = (await nostrRelays.getEvents({
|
|
313
|
+
kinds: [kind],
|
|
314
|
+
authors: [await signer.getPublicKey()],
|
|
315
|
+
'#d': [dTag],
|
|
316
|
+
limit: 1
|
|
317
|
+
}, writeRelays)).result
|
|
318
|
+
|
|
319
|
+
if (events.length > 0) {
|
|
320
|
+
events.sort((a, b) => {
|
|
321
|
+
if (b.created_at !== a.created_at) return b.created_at - a.created_at
|
|
322
|
+
if (a.id < b.id) return -1
|
|
323
|
+
if (a.id > b.id) return 1
|
|
324
|
+
return 0
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const mostRecentEvent = events[0]
|
|
328
|
+
const recentFileTags = mostRecentEvent.tags.filter(t => t[0] === 'file')
|
|
329
|
+
|
|
330
|
+
const isSame = fileTags.length === recentFileTags.length && fileTags.every((t, i) => {
|
|
331
|
+
const rt = recentFileTags[i]
|
|
332
|
+
return rt.length >= 4 && rt[1] === t[1] && rt[2] === t[2] && rt[3] === t[3]
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
if (isSame) {
|
|
336
|
+
log(`Bundle based on ${fileTags.length} files is up-to-date (id: ${mostRecentEvent.id} - created_at: ${new Date(mostRecentEvent.created_at * 1000).toISOString()})`)
|
|
337
|
+
|
|
338
|
+
const matchingEvents = events.filter(e => e.id === mostRecentEvent.id)
|
|
339
|
+
const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
|
|
340
|
+
const missingRelays = writeRelays.filter(r => !coveredRelays.has(r))
|
|
341
|
+
|
|
342
|
+
if (missingRelays.length === 0) return mostRecentEvent
|
|
343
|
+
|
|
344
|
+
// nostrRelays.getEvents currently doesn't tell us which event came from which relay,
|
|
345
|
+
// so we re-upload to all relays to ensure consistency
|
|
346
|
+
log(`Re-uploading existing bundle event to ${missingRelays.length} missing relays (out of ${writeRelays.length})`)
|
|
347
|
+
await throttledSendEvent(mostRecentEvent, missingRelays, { pause, trailingPause: true, log })
|
|
348
|
+
return mostRecentEvent
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
287
353
|
const appBundle = {
|
|
288
354
|
kind,
|
|
289
|
-
tags
|
|
290
|
-
['d', dTag],
|
|
291
|
-
...fileMetadata.map(v => ['file', v.rootHash, v.filename, v.mimeType])
|
|
292
|
-
],
|
|
355
|
+
tags,
|
|
293
356
|
content: '',
|
|
294
357
|
created_at: Math.floor(Date.now() / 1000)
|
|
295
358
|
}
|
|
296
359
|
const event = await signer.signEvent(appBundle)
|
|
297
|
-
await throttledSendEvent(event,
|
|
360
|
+
await throttledSendEvent(event, writeRelays, { pause, trailingPause: true, log })
|
|
298
361
|
return event
|
|
299
362
|
}
|
|
300
363
|
|
|
@@ -325,13 +388,14 @@ async function maybeUploadStall ({
|
|
|
325
388
|
const hasMetadata = Boolean(trimmedName) || Boolean(trimmedSummary) || Boolean(iconRootHash) ||
|
|
326
389
|
Boolean(self) || (countries && countries.length > 0) || (categories && categories.length > 0) || (hashtags && hashtags.length > 0)
|
|
327
390
|
|
|
328
|
-
const
|
|
391
|
+
const relays = [...new Set([...writeRelays, ...nappRelays])]
|
|
392
|
+
|
|
393
|
+
const previousResult = await getPreviousStall(dTag, relays, signer, channel)
|
|
394
|
+
const previous = previousResult?.previous
|
|
329
395
|
if (!previous && !hasMetadata) return { pause }
|
|
330
396
|
|
|
331
397
|
const publishStall = async (event) => {
|
|
332
398
|
const signedEvent = await signer.signEvent(event)
|
|
333
|
-
// App stores are fetching stall events just from 44b relay for now
|
|
334
|
-
const relays = [...new Set([...writeRelays, 'wss://relay.44billion.net'])]
|
|
335
399
|
return await throttledSendEvent(signedEvent, relays, { pause, log, trailingPause: true })
|
|
336
400
|
}
|
|
337
401
|
|
|
@@ -557,7 +621,18 @@ async function maybeUploadStall ({
|
|
|
557
621
|
}
|
|
558
622
|
}
|
|
559
623
|
|
|
560
|
-
if (!changed)
|
|
624
|
+
if (!changed) {
|
|
625
|
+
const { storedEvents } = previousResult
|
|
626
|
+
|
|
627
|
+
const matchingEvents = storedEvents.filter(e => e.id === previous.id)
|
|
628
|
+
const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
|
|
629
|
+
const missingRelays = relays.filter(r => !coveredRelays.has(r))
|
|
630
|
+
|
|
631
|
+
if (missingRelays.length === 0) return { pause }
|
|
632
|
+
|
|
633
|
+
log(`Re-uploading existing stall event to ${missingRelays.length} missing relays (out of ${relays.length})`)
|
|
634
|
+
return await throttledSendEvent(previous, missingRelays, { pause, log, trailingPause: true })
|
|
635
|
+
}
|
|
561
636
|
|
|
562
637
|
return await publishStall({
|
|
563
638
|
kind,
|
|
@@ -567,7 +642,7 @@ async function maybeUploadStall ({
|
|
|
567
642
|
})
|
|
568
643
|
}
|
|
569
644
|
|
|
570
|
-
async function getPreviousStall (dTagValue,
|
|
645
|
+
async function getPreviousStall (dTagValue, relays, signer, channel) {
|
|
571
646
|
const kind = {
|
|
572
647
|
main: 37348,
|
|
573
648
|
next: 37349,
|
|
@@ -579,8 +654,15 @@ async function getPreviousStall (dTagValue, writeRelays, signer, channel) {
|
|
|
579
654
|
authors: [await signer.getPublicKey()],
|
|
580
655
|
'#d': [dTagValue],
|
|
581
656
|
limit: 1
|
|
582
|
-
},
|
|
657
|
+
}, relays)).result
|
|
583
658
|
|
|
584
659
|
if (storedEvents.length === 0) return null
|
|
585
|
-
|
|
660
|
+
|
|
661
|
+
storedEvents.sort((a, b) => b.created_at - a.created_at)
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
previous: storedEvents[0],
|
|
665
|
+
storedEvents,
|
|
666
|
+
targetRelayCount: relays.length
|
|
667
|
+
}
|
|
586
668
|
}
|
|
@@ -15,6 +15,9 @@ export const freeRelays = [
|
|
|
15
15
|
'wss://relay.damus.io',
|
|
16
16
|
'wss://relay.nostr.band'
|
|
17
17
|
]
|
|
18
|
+
export const nappRelays = [
|
|
19
|
+
'wss://relay.44billion.net'
|
|
20
|
+
]
|
|
18
21
|
|
|
19
22
|
// Interacts with Nostr relays
|
|
20
23
|
export class NostrRelays {
|
|
@@ -77,6 +80,7 @@ export class NostrRelays {
|
|
|
77
80
|
const relay = await this.#getRelay(url)
|
|
78
81
|
sub = relay.subscribe([filter], {
|
|
79
82
|
onevent: (event) => {
|
|
83
|
+
event.meta = { relay: url }
|
|
80
84
|
events.push(event)
|
|
81
85
|
},
|
|
82
86
|
onclose: err => {
|
|
@@ -112,6 +116,9 @@ export class NostrRelays {
|
|
|
112
116
|
|
|
113
117
|
// Send an event to a list of relays
|
|
114
118
|
async sendEvent (event, relays, timeout = 3000) {
|
|
119
|
+
const eventToSend = event.meta ? { ...event } : event
|
|
120
|
+
if (eventToSend.meta) delete eventToSend.meta
|
|
121
|
+
|
|
115
122
|
const promises = relays.map(async (url) => {
|
|
116
123
|
let timer
|
|
117
124
|
const p = Promise.withResolvers()
|
|
@@ -121,7 +128,7 @@ export class NostrRelays {
|
|
|
121
128
|
}, timeout))
|
|
122
129
|
|
|
123
130
|
const relay = await this.#getRelay(url)
|
|
124
|
-
await relay.publish(
|
|
131
|
+
await relay.publish(eventToSend)
|
|
125
132
|
p.resolve()
|
|
126
133
|
} catch (err) {
|
|
127
134
|
if (err.message?.startsWith('duplicate:')) return p.resolve()
|