tag.ts raw
1 import { Pubkey } from '@/domain'
2 import { TEmoji, TImetaInfo } from '@/types'
3 import { base64 } from '@scure/base'
4 import { isBlurhashValid } from 'blurhash'
5 import { nip19 } from 'nostr-tools'
6 import { normalizeHttpUrl } from './url'
7
8 export function isSameTag(tag1: string[], tag2: string[]) {
9 if (tag1.length !== tag2.length) return false
10 for (let i = 0; i < tag1.length; i++) {
11 if (tag1[i] !== tag2[i]) return false
12 }
13 return true
14 }
15
16 export function tagNameEquals(tagName: string) {
17 return (tag: string[]) => tag[0] === tagName
18 }
19
20 export function generateBech32IdFromETag(tag: string[]) {
21 try {
22 const [, id, relay, markerOrPubkey, pubkey] = tag
23 let author: string | undefined
24 if (markerOrPubkey && Pubkey.isValidHex(markerOrPubkey)) {
25 author = markerOrPubkey
26 } else if (pubkey && Pubkey.isValidHex(pubkey)) {
27 author = pubkey
28 }
29 return nip19.neventEncode({ id, relays: relay ? [relay] : undefined, author })
30 } catch {
31 return undefined
32 }
33 }
34
35 export function generateBech32IdFromATag(tag: string[]) {
36 try {
37 const [, coordinate, relay] = tag
38 const [kind, pubkey, ...items] = coordinate.split(':')
39 const identifier = items.join(':')
40 return nip19.naddrEncode({
41 kind: Number(kind),
42 pubkey,
43 identifier,
44 relays: relay ? [relay] : undefined
45 })
46 } catch {
47 return undefined
48 }
49 }
50
51 export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImetaInfo | null {
52 if (tag[0] !== 'imeta') return null
53 const imeta: Partial<TImetaInfo> = { pubkey }
54
55 for (let i = 1; i < tag.length; i++) {
56 const part = tag[i]
57 const spaceIndex = part.indexOf(' ')
58 if (spaceIndex < 0) continue
59 const k = part.substring(0, spaceIndex)
60 const v = part.substring(spaceIndex + 1)
61
62 switch (k) {
63 case 'url':
64 imeta.url = v
65 break
66 case 'x':
67 imeta.sha256 = v
68 break
69 case 'variant':
70 imeta.variant = v
71 break
72 case 'thumbhash':
73 try {
74 imeta.thumbHash = base64.decode(v)
75 } catch {
76 /***/
77 }
78 break
79 case 'blurhash': {
80 const validRes = isBlurhashValid(v)
81 if (validRes.result) {
82 imeta.blurHash = v
83 }
84 break
85 }
86 case 'dim': {
87 const [width, height] = v.split('x').map(Number)
88 if (width && height) {
89 imeta.dim = { width, height }
90 }
91 break
92 }
93 }
94 }
95
96 if (!imeta.url) return null
97 return imeta as TImetaInfo
98 }
99
100 export function getPubkeysFromPTags(tags: string[][]) {
101 return Array.from(
102 new Set(
103 tags
104 .filter(tagNameEquals('p'))
105 .map(([, pubkey]) => pubkey)
106 .filter((pubkey) => !!pubkey && Pubkey.isValidHex(pubkey))
107 .reverse()
108 )
109 )
110 }
111
112 export function getEmojiInfosFromEmojiTags(tags: string[][] = []) {
113 return tags
114 .map((tag) => {
115 if (tag.length < 3 || tag[0] !== 'emoji') return null
116 return { shortcode: tag[1], url: tag[2] }
117 })
118 .filter(Boolean) as TEmoji[]
119 }
120
121 export function getServersFromServerTags(tags: string[][] = []) {
122 return tags
123 .filter(tagNameEquals('server'))
124 .map(([, url]) => (url ? normalizeHttpUrl(url) : ''))
125 .filter(Boolean)
126 }
127