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