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 CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/44billion/nappup.git"
7
7
  },
8
8
  "license": "MIT",
9
- "version": "1.5.2",
9
+ "version": "1.5.4",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
@@ -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, writeRelays, signer)
174
+ const { otherCtags, hasCurrentCtag, foundEvent, missingRelays } = await getPreviousCtags(dTag, currentCtag, relays, signer)
173
175
  if (!shouldReupload && hasCurrentCtag) {
174
- log(`${filename}: Skipping chunk ${++chunkIndex} of ${chunkLength} (already uploaded)`)
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
- log(`${unretryableErrors.length} Unretryable errors\n: ${unretryableErrors.map(v => `${v.relay}: ${v.reason.message}`).join('; ')}`)
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, writeRelays, signer) {
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
- }, [...new Set([...writeRelays, ...nappRelays])])).result
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
- const prevTags = storedEvents.sort((a, b) => b.created_at - a.created_at)[0].tags
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
- return { otherCtags, hasEvent, hasCurrentCtag }
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.filter(t => t[0] === 'file')
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 isSame = fileTags.length === recentFileTags.length && fileTags.every((t, i) => {
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 to date (id: ${mostRecentEvent.id} - created_at: ${new Date(mostRecentEvent.created_at * 1000).toISOString()})`)
323
- if (events.length === writeRelays.length && events.every(e => e.id === mostRecentEvent.id)) return mostRecentEvent
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 all ${writeRelays.length} relays`)
328
- await throttledSendEvent(mostRecentEvent, writeRelays, { pause, trailingPause: true, log })
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 previous = await getPreviousStall(dTag, writeRelays, signer, channel)
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) return { pause }
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, writeRelays, signer, channel) {
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
- }, [...new Set([...writeRelays, ...nappRelays])])).result
667
+ }, relays)).result
626
668
 
627
669
  if (storedEvents.length === 0) return null
628
- return storedEvents.sort((a, b) => b.created_at - a.created_at)[0]
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(event)
131
+ await relay.publish(eventToSend)
128
132
  p.resolve()
129
133
  } catch (err) {
130
134
  if (err.message?.startsWith('duplicate:')) return p.resolve()