nappup 1.2.1 → 1.3.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/package.json +1 -1
- package/src/config/napp-categories.js +23 -0
- package/src/index.js +226 -36
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const NAPP_CATEGORIES = {
|
|
2
|
+
other: [
|
|
3
|
+
'other'
|
|
4
|
+
],
|
|
5
|
+
games: [
|
|
6
|
+
'other', 'action', 'rpg', 'strategy', 'shooter', 'fighting', 'simulation', 'puzzle', 'board', 'gambling', 'racing', 'sports', 'ar', 'vr'
|
|
7
|
+
],
|
|
8
|
+
money: [
|
|
9
|
+
'other', 'crypto', 'loans', 'investments', 'wallet', 'gambling', 'raffle', 'crowdfunding', 'donation', 'jobs'
|
|
10
|
+
],
|
|
11
|
+
shopping: [
|
|
12
|
+
'other', 'marketplace', 'store', 'auction'
|
|
13
|
+
],
|
|
14
|
+
social: [
|
|
15
|
+
'other', 'network', 'messenger', 'blog', 'dating'
|
|
16
|
+
],
|
|
17
|
+
audiovisual: [
|
|
18
|
+
'other', 'podcast', 'music', 'video', 'news'
|
|
19
|
+
],
|
|
20
|
+
utilities: [
|
|
21
|
+
'other', 'weather', 'office', 'finances', 'learning', 'text editor', 'image editor', 'audio editor', 'video editor', 'ar', 'vr', 'ai'
|
|
22
|
+
]
|
|
23
|
+
}
|
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 { NAPP_CATEGORIES } from '#config/napp-categories.js'
|
|
9
10
|
|
|
10
11
|
export default async function (...args) {
|
|
11
12
|
try {
|
|
@@ -34,14 +35,29 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, chan
|
|
|
34
35
|
let nmmr
|
|
35
36
|
const fileMetadata = []
|
|
36
37
|
|
|
38
|
+
// Check for .well-known/napp.json
|
|
39
|
+
const nappJsonFile = fileList.find(f => f.webkitRelativePath.split('/').slice(1).join('/') === '.well-known/napp.json')
|
|
40
|
+
let nappJson = {}
|
|
41
|
+
if (nappJsonFile) {
|
|
42
|
+
try {
|
|
43
|
+
const text = await streamToText(nappJsonFile.stream())
|
|
44
|
+
nappJson = JSON.parse(text)
|
|
45
|
+
fileList = fileList.filter(f => f !== nappJsonFile)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
log('Failed to parse .well-known/napp.json', e)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
const indexFile = findIndexFile(fileList)
|
|
38
|
-
let stallName
|
|
39
|
-
|
|
52
|
+
let stallName = nappJson.stallName?.[0]?.[0]
|
|
53
|
+
let stallSummary = nappJson.stallSummary?.[0]?.[0]
|
|
54
|
+
|
|
55
|
+
if (indexFile && (!stallName || !stallSummary)) {
|
|
40
56
|
try {
|
|
41
57
|
const htmlContent = await streamToText(indexFile.stream())
|
|
42
58
|
const { name, description } = extractHtmlMetadata(htmlContent)
|
|
43
|
-
stallName = name
|
|
44
|
-
stallSummary = description
|
|
59
|
+
if (!stallName) stallName = name
|
|
60
|
+
if (!stallSummary) stallSummary = description
|
|
45
61
|
} catch (err) {
|
|
46
62
|
log('Error extracting HTML metadata:', err)
|
|
47
63
|
}
|
|
@@ -49,8 +65,41 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, chan
|
|
|
49
65
|
const faviconFile = findFavicon(fileList)
|
|
50
66
|
let iconMetadata
|
|
51
67
|
|
|
52
|
-
log(`Processing ${fileList.length} files`)
|
|
53
68
|
let pause = 1000
|
|
69
|
+
|
|
70
|
+
// Upload icon from napp.json if present
|
|
71
|
+
if (nappJson.stallIcon?.[0]?.[0]) {
|
|
72
|
+
try {
|
|
73
|
+
const dataUrl = nappJson.stallIcon[0][0]
|
|
74
|
+
const res = await fetch(dataUrl)
|
|
75
|
+
const blob = await res.blob()
|
|
76
|
+
const mimeType = blob.type
|
|
77
|
+
const extension = mimeType.split('/')[1] || 'bin'
|
|
78
|
+
const filename = `icon.${extension}`
|
|
79
|
+
|
|
80
|
+
log('Uploading icon from napp.json')
|
|
81
|
+
|
|
82
|
+
nmmr = new NMMR()
|
|
83
|
+
const stream = blob.stream()
|
|
84
|
+
let chunkLength = 0
|
|
85
|
+
for await (const chunk of streamToChunks(stream, 51000)) {
|
|
86
|
+
chunkLength++
|
|
87
|
+
await nmmr.append(chunk)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (chunkLength) {
|
|
91
|
+
;({ pause } = (await uploadBinaryDataChunks({ nmmr, signer: nostrSigner, filename, chunkLength, log, pause, mimeType, shouldReupload })))
|
|
92
|
+
iconMetadata = {
|
|
93
|
+
rootHash: nmmr.getRoot(),
|
|
94
|
+
mimeType
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
log('Failed to upload icon from napp.json', e)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
log(`Processing ${fileList.length} files`)
|
|
54
103
|
for (const file of fileList) {
|
|
55
104
|
nmmr = new NMMR()
|
|
56
105
|
const stream = file.stream()
|
|
@@ -85,12 +134,21 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, chan
|
|
|
85
134
|
dTag,
|
|
86
135
|
channel,
|
|
87
136
|
name: stallName,
|
|
137
|
+
nameLang: nappJson.stallName?.[0]?.[1],
|
|
138
|
+
isNameAuto: !nappJson.stallName?.[0]?.[0],
|
|
88
139
|
summary: stallSummary,
|
|
140
|
+
summaryLang: nappJson.stallSummary?.[0]?.[1],
|
|
141
|
+
isSummaryAuto: !nappJson.stallSummary?.[0]?.[0],
|
|
89
142
|
icon: iconMetadata,
|
|
143
|
+
isIconAuto: !nappJson.stallIcon?.[0]?.[0],
|
|
90
144
|
signer: nostrSigner,
|
|
91
145
|
writeRelays,
|
|
92
146
|
log,
|
|
93
|
-
pause
|
|
147
|
+
pause,
|
|
148
|
+
self: nappJson.self?.[0]?.[0],
|
|
149
|
+
countries: nappJson.country,
|
|
150
|
+
categories: nappJson.category,
|
|
151
|
+
hashtags: nappJson.hashtag
|
|
94
152
|
})))
|
|
95
153
|
|
|
96
154
|
log(`Uploading bundle #${dTag}`)
|
|
@@ -244,18 +302,28 @@ async function maybeUploadStall ({
|
|
|
244
302
|
dTag,
|
|
245
303
|
channel,
|
|
246
304
|
name,
|
|
305
|
+
nameLang,
|
|
306
|
+
isNameAuto,
|
|
247
307
|
summary,
|
|
308
|
+
summaryLang,
|
|
309
|
+
isSummaryAuto,
|
|
248
310
|
icon,
|
|
311
|
+
isIconAuto,
|
|
249
312
|
signer,
|
|
250
313
|
writeRelays,
|
|
251
314
|
log,
|
|
252
|
-
pause
|
|
315
|
+
pause,
|
|
316
|
+
self,
|
|
317
|
+
countries,
|
|
318
|
+
categories,
|
|
319
|
+
hashtags
|
|
253
320
|
}) {
|
|
254
321
|
const trimmedName = typeof name === 'string' ? name.trim() : ''
|
|
255
322
|
const trimmedSummary = typeof summary === 'string' ? summary.trim() : ''
|
|
256
323
|
const iconRootHash = icon?.rootHash
|
|
257
324
|
const iconMimeType = icon?.mimeType
|
|
258
|
-
const hasMetadata = Boolean(trimmedName) || Boolean(trimmedSummary) || Boolean(iconRootHash)
|
|
325
|
+
const hasMetadata = Boolean(trimmedName) || Boolean(trimmedSummary) || Boolean(iconRootHash) ||
|
|
326
|
+
Boolean(self) || (countries && countries.length > 0) || (categories && categories.length > 0) || (hashtags && hashtags.length > 0)
|
|
259
327
|
|
|
260
328
|
const previous = await getPreviousStall(dTag, writeRelays, signer, channel)
|
|
261
329
|
if (!previous && !hasMetadata) return { pause }
|
|
@@ -276,27 +344,63 @@ async function maybeUploadStall ({
|
|
|
276
344
|
|
|
277
345
|
if (!previous) {
|
|
278
346
|
const tags = [
|
|
279
|
-
['d', dTag]
|
|
280
|
-
['c', '*']
|
|
347
|
+
['d', dTag]
|
|
281
348
|
]
|
|
282
349
|
|
|
350
|
+
if (countries && countries.length > 0) {
|
|
351
|
+
countries.forEach(c => tags.push(['c', c]))
|
|
352
|
+
} else {
|
|
353
|
+
tags.push(['c', '*'])
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (self) tags.push(['self', self])
|
|
357
|
+
|
|
358
|
+
if (categories) {
|
|
359
|
+
let count = 0
|
|
360
|
+
for (const [cat, subcats] of categories) {
|
|
361
|
+
if (count >= 3) break
|
|
362
|
+
if (Array.isArray(subcats)) {
|
|
363
|
+
for (const sub of subcats) {
|
|
364
|
+
if (count >= 3) break
|
|
365
|
+
if (NAPP_CATEGORIES[cat] && NAPP_CATEGORIES[cat].includes(sub)) {
|
|
366
|
+
tags.push(['l', `napp.${cat}:${sub}`])
|
|
367
|
+
count++
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (hashtags) {
|
|
375
|
+
hashtags.slice(0, 3).forEach(([tag, label]) => {
|
|
376
|
+
const t = tag.replace(/\s/g, '').toLowerCase()
|
|
377
|
+
const row = ['t', t]
|
|
378
|
+
if (label) row.push(label)
|
|
379
|
+
tags.push(row)
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
283
383
|
let hasIcon = false
|
|
284
384
|
let hasName = false
|
|
285
385
|
if (iconRootHash && iconMimeType) {
|
|
286
386
|
hasIcon = true
|
|
287
387
|
tags.push(['icon', iconRootHash, iconMimeType])
|
|
288
|
-
tags.push(['auto', 'icon'])
|
|
388
|
+
if (isIconAuto) tags.push(['auto', 'icon'])
|
|
289
389
|
}
|
|
290
390
|
|
|
291
391
|
if (trimmedName) {
|
|
292
392
|
hasName = true
|
|
293
|
-
|
|
294
|
-
|
|
393
|
+
const row = ['name', trimmedName]
|
|
394
|
+
if (nameLang) row.push(nameLang)
|
|
395
|
+
tags.push(row)
|
|
396
|
+
if (isNameAuto) tags.push(['auto', 'name'])
|
|
295
397
|
}
|
|
296
398
|
|
|
297
399
|
if (trimmedSummary) {
|
|
298
|
-
|
|
299
|
-
|
|
400
|
+
const row = ['summary', trimmedSummary]
|
|
401
|
+
if (summaryLang) row.push(summaryLang)
|
|
402
|
+
tags.push(row)
|
|
403
|
+
if (isSummaryAuto) tags.push(['auto', 'summary'])
|
|
300
404
|
}
|
|
301
405
|
|
|
302
406
|
if (!hasIcon || !hasName) return { pause }
|
|
@@ -314,6 +418,73 @@ async function maybeUploadStall ({
|
|
|
314
418
|
: []
|
|
315
419
|
let changed = false
|
|
316
420
|
|
|
421
|
+
// Helper to remove tags by key
|
|
422
|
+
const removeTags = (key) => {
|
|
423
|
+
let idx
|
|
424
|
+
while ((idx = tags.findIndex(t => Array.isArray(t) && t[0] === key)) !== -1) {
|
|
425
|
+
tags.splice(idx, 1)
|
|
426
|
+
changed = true
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Helper to remove 'l' tags with specific prefix
|
|
431
|
+
const removeLTags = (prefix) => {
|
|
432
|
+
let idx
|
|
433
|
+
while ((idx = tags.findIndex(t => Array.isArray(t) && t[0] === 'l' && t[1].startsWith(prefix))) !== -1) {
|
|
434
|
+
tags.splice(idx, 1)
|
|
435
|
+
changed = true
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Update self
|
|
440
|
+
if (self) {
|
|
441
|
+
removeTags('self')
|
|
442
|
+
tags.push(['self', self])
|
|
443
|
+
changed = true
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Update countries
|
|
447
|
+
if (countries) {
|
|
448
|
+
removeTags('c')
|
|
449
|
+
if (countries.length === 0) {
|
|
450
|
+
tags.push(['c', '*'])
|
|
451
|
+
} else {
|
|
452
|
+
countries.forEach(c => tags.push(['c', c]))
|
|
453
|
+
}
|
|
454
|
+
changed = true
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Update categories
|
|
458
|
+
if (categories) {
|
|
459
|
+
removeLTags('napp.')
|
|
460
|
+
let count = 0
|
|
461
|
+
for (const [cat, subcats] of categories) {
|
|
462
|
+
if (count >= 3) break
|
|
463
|
+
if (Array.isArray(subcats)) {
|
|
464
|
+
for (const sub of subcats) {
|
|
465
|
+
if (count >= 3) break
|
|
466
|
+
if (NAPP_CATEGORIES[cat] && NAPP_CATEGORIES[cat].includes(sub)) {
|
|
467
|
+
tags.push(['l', `napp.${cat}:${sub}`])
|
|
468
|
+
count++
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
changed = true
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Update hashtags
|
|
477
|
+
if (hashtags) {
|
|
478
|
+
removeTags('t')
|
|
479
|
+
hashtags.slice(0, 3).forEach(([tag, label]) => {
|
|
480
|
+
const t = tag.replace(/\s/g, '').toLowerCase()
|
|
481
|
+
const row = ['t', t]
|
|
482
|
+
if (label) row.push(label)
|
|
483
|
+
tags.push(row)
|
|
484
|
+
})
|
|
485
|
+
changed = true
|
|
486
|
+
}
|
|
487
|
+
|
|
317
488
|
const ensureTagValue = (key, updater) => {
|
|
318
489
|
const index = tags.findIndex(tag => Array.isArray(tag) && tag[0] === key)
|
|
319
490
|
if (index === -1) {
|
|
@@ -337,34 +508,53 @@ async function maybeUploadStall ({
|
|
|
337
508
|
return ['d', dTag]
|
|
338
509
|
})
|
|
339
510
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
511
|
+
if (!countries) {
|
|
512
|
+
ensureTagValue('c', (existing) => {
|
|
513
|
+
if (!existing) return ['c', '*']
|
|
514
|
+
const currentValue = typeof existing[1] === 'string' ? existing[1].trim() : ''
|
|
515
|
+
if (currentValue === '') return ['c', '*']
|
|
516
|
+
return existing
|
|
517
|
+
})
|
|
518
|
+
}
|
|
346
519
|
|
|
347
520
|
const hasAuto = (field) => tags.some(tag => Array.isArray(tag) && tag[0] === 'auto' && tag[1] === field)
|
|
521
|
+
const removeAuto = (field) => {
|
|
522
|
+
const idx = tags.findIndex(tag => Array.isArray(tag) && tag[0] === 'auto' && tag[1] === field)
|
|
523
|
+
if (idx !== -1) {
|
|
524
|
+
tags.splice(idx, 1)
|
|
525
|
+
changed = true
|
|
526
|
+
}
|
|
527
|
+
}
|
|
348
528
|
|
|
349
|
-
if (trimmedName
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
529
|
+
if (trimmedName) {
|
|
530
|
+
if (!isNameAuto || hasAuto('name')) {
|
|
531
|
+
ensureTagValue('name', (_) => {
|
|
532
|
+
const row = ['name', trimmedName]
|
|
533
|
+
if (nameLang) row.push(nameLang)
|
|
534
|
+
return row
|
|
535
|
+
})
|
|
536
|
+
if (!isNameAuto) removeAuto('name')
|
|
537
|
+
}
|
|
354
538
|
}
|
|
355
539
|
|
|
356
|
-
if (trimmedSummary
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
540
|
+
if (trimmedSummary) {
|
|
541
|
+
if (!isSummaryAuto || hasAuto('summary')) {
|
|
542
|
+
ensureTagValue('summary', (_) => {
|
|
543
|
+
const row = ['summary', trimmedSummary]
|
|
544
|
+
if (summaryLang) row.push(summaryLang)
|
|
545
|
+
return row
|
|
546
|
+
})
|
|
547
|
+
if (!isSummaryAuto) removeAuto('summary')
|
|
548
|
+
}
|
|
361
549
|
}
|
|
362
550
|
|
|
363
|
-
if (iconRootHash && iconMimeType
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
551
|
+
if (iconRootHash && iconMimeType) {
|
|
552
|
+
if (!isIconAuto || hasAuto('icon')) {
|
|
553
|
+
ensureTagValue('icon', (_) => {
|
|
554
|
+
return ['icon', iconRootHash, iconMimeType]
|
|
555
|
+
})
|
|
556
|
+
if (!isIconAuto) removeAuto('icon')
|
|
557
|
+
}
|
|
368
558
|
}
|
|
369
559
|
|
|
370
560
|
if (!changed) return { pause }
|