nappup 1.2.1 → 1.3.0
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 +211 -28
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 }
|
|
@@ -280,23 +348,58 @@ async function maybeUploadStall ({
|
|
|
280
348
|
['c', '*']
|
|
281
349
|
]
|
|
282
350
|
|
|
351
|
+
if (self) tags.push(['self', self])
|
|
352
|
+
|
|
353
|
+
if (countries) {
|
|
354
|
+
countries.forEach(c => tags.push(['country', c]))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (categories) {
|
|
358
|
+
let count = 0
|
|
359
|
+
for (const [cat, subcats] of categories) {
|
|
360
|
+
if (count >= 3) break
|
|
361
|
+
if (Array.isArray(subcats)) {
|
|
362
|
+
for (const sub of subcats) {
|
|
363
|
+
if (count >= 3) break
|
|
364
|
+
if (NAPP_CATEGORIES[cat] && NAPP_CATEGORIES[cat].includes(sub)) {
|
|
365
|
+
tags.push(['l', `napp.${cat}:${sub}`])
|
|
366
|
+
count++
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (hashtags) {
|
|
374
|
+
hashtags.slice(0, 3).forEach(([tag, label]) => {
|
|
375
|
+
const t = tag.replace(/\s/g, '').toLowerCase()
|
|
376
|
+
const row = ['t', t]
|
|
377
|
+
if (label) row.push(label)
|
|
378
|
+
tags.push(row)
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
283
382
|
let hasIcon = false
|
|
284
383
|
let hasName = false
|
|
285
384
|
if (iconRootHash && iconMimeType) {
|
|
286
385
|
hasIcon = true
|
|
287
386
|
tags.push(['icon', iconRootHash, iconMimeType])
|
|
288
|
-
tags.push(['auto', 'icon'])
|
|
387
|
+
if (isIconAuto) tags.push(['auto', 'icon'])
|
|
289
388
|
}
|
|
290
389
|
|
|
291
390
|
if (trimmedName) {
|
|
292
391
|
hasName = true
|
|
293
|
-
|
|
294
|
-
|
|
392
|
+
const row = ['name', trimmedName]
|
|
393
|
+
if (nameLang) row.push(nameLang)
|
|
394
|
+
tags.push(row)
|
|
395
|
+
if (isNameAuto) tags.push(['auto', 'name'])
|
|
295
396
|
}
|
|
296
397
|
|
|
297
398
|
if (trimmedSummary) {
|
|
298
|
-
|
|
299
|
-
|
|
399
|
+
const row = ['summary', trimmedSummary]
|
|
400
|
+
if (summaryLang) row.push(summaryLang)
|
|
401
|
+
tags.push(row)
|
|
402
|
+
if (isSummaryAuto) tags.push(['auto', 'summary'])
|
|
300
403
|
}
|
|
301
404
|
|
|
302
405
|
if (!hasIcon || !hasName) return { pause }
|
|
@@ -314,6 +417,69 @@ async function maybeUploadStall ({
|
|
|
314
417
|
: []
|
|
315
418
|
let changed = false
|
|
316
419
|
|
|
420
|
+
// Helper to remove tags by key
|
|
421
|
+
const removeTags = (key) => {
|
|
422
|
+
let idx
|
|
423
|
+
while ((idx = tags.findIndex(t => Array.isArray(t) && t[0] === key)) !== -1) {
|
|
424
|
+
tags.splice(idx, 1)
|
|
425
|
+
changed = true
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Helper to remove 'l' tags with specific prefix
|
|
430
|
+
const removeLTags = (prefix) => {
|
|
431
|
+
let idx
|
|
432
|
+
while ((idx = tags.findIndex(t => Array.isArray(t) && t[0] === 'l' && t[1].startsWith(prefix))) !== -1) {
|
|
433
|
+
tags.splice(idx, 1)
|
|
434
|
+
changed = true
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Update self
|
|
439
|
+
if (self) {
|
|
440
|
+
removeTags('self')
|
|
441
|
+
tags.push(['self', self])
|
|
442
|
+
changed = true
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Update countries
|
|
446
|
+
if (countries) {
|
|
447
|
+
removeTags('country')
|
|
448
|
+
countries.forEach(c => tags.push(['country', c]))
|
|
449
|
+
changed = true
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Update categories
|
|
453
|
+
if (categories) {
|
|
454
|
+
removeLTags('napp.')
|
|
455
|
+
let count = 0
|
|
456
|
+
for (const [cat, subcats] of categories) {
|
|
457
|
+
if (count >= 3) break
|
|
458
|
+
if (Array.isArray(subcats)) {
|
|
459
|
+
for (const sub of subcats) {
|
|
460
|
+
if (count >= 3) break
|
|
461
|
+
if (NAPP_CATEGORIES[cat] && NAPP_CATEGORIES[cat].includes(sub)) {
|
|
462
|
+
tags.push(['l', `napp.${cat}:${sub}`])
|
|
463
|
+
count++
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
changed = true
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Update hashtags
|
|
472
|
+
if (hashtags) {
|
|
473
|
+
removeTags('t')
|
|
474
|
+
hashtags.slice(0, 3).forEach(([tag, label]) => {
|
|
475
|
+
const t = tag.replace(/\s/g, '').toLowerCase()
|
|
476
|
+
const row = ['t', t]
|
|
477
|
+
if (label) row.push(label)
|
|
478
|
+
tags.push(row)
|
|
479
|
+
})
|
|
480
|
+
changed = true
|
|
481
|
+
}
|
|
482
|
+
|
|
317
483
|
const ensureTagValue = (key, updater) => {
|
|
318
484
|
const index = tags.findIndex(tag => Array.isArray(tag) && tag[0] === key)
|
|
319
485
|
if (index === -1) {
|
|
@@ -345,26 +511,43 @@ async function maybeUploadStall ({
|
|
|
345
511
|
})
|
|
346
512
|
|
|
347
513
|
const hasAuto = (field) => tags.some(tag => Array.isArray(tag) && tag[0] === 'auto' && tag[1] === field)
|
|
514
|
+
const removeAuto = (field) => {
|
|
515
|
+
const idx = tags.findIndex(tag => Array.isArray(tag) && tag[0] === 'auto' && tag[1] === field)
|
|
516
|
+
if (idx !== -1) {
|
|
517
|
+
tags.splice(idx, 1)
|
|
518
|
+
changed = true
|
|
519
|
+
}
|
|
520
|
+
}
|
|
348
521
|
|
|
349
|
-
if (trimmedName
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
522
|
+
if (trimmedName) {
|
|
523
|
+
if (!isNameAuto || hasAuto('name')) {
|
|
524
|
+
ensureTagValue('name', (_) => {
|
|
525
|
+
const row = ['name', trimmedName]
|
|
526
|
+
if (nameLang) row.push(nameLang)
|
|
527
|
+
return row
|
|
528
|
+
})
|
|
529
|
+
if (!isNameAuto) removeAuto('name')
|
|
530
|
+
}
|
|
354
531
|
}
|
|
355
532
|
|
|
356
|
-
if (trimmedSummary
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
533
|
+
if (trimmedSummary) {
|
|
534
|
+
if (!isSummaryAuto || hasAuto('summary')) {
|
|
535
|
+
ensureTagValue('summary', (_) => {
|
|
536
|
+
const row = ['summary', trimmedSummary]
|
|
537
|
+
if (summaryLang) row.push(summaryLang)
|
|
538
|
+
return row
|
|
539
|
+
})
|
|
540
|
+
if (!isSummaryAuto) removeAuto('summary')
|
|
541
|
+
}
|
|
361
542
|
}
|
|
362
543
|
|
|
363
|
-
if (iconRootHash && iconMimeType
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
544
|
+
if (iconRootHash && iconMimeType) {
|
|
545
|
+
if (!isIconAuto || hasAuto('icon')) {
|
|
546
|
+
ensureTagValue('icon', (_) => {
|
|
547
|
+
return ['icon', iconRootHash, iconMimeType]
|
|
548
|
+
})
|
|
549
|
+
if (!isIconAuto) removeAuto('icon')
|
|
550
|
+
}
|
|
368
551
|
}
|
|
369
552
|
|
|
370
553
|
if (!changed) return { pause }
|