跳至主要內容
進階 Nostr NIP-32 標籤 labeling taxonomy

NIP-32: 標籤系統

Labeling - 為內容添加結構化分類標籤

10 分鐘

概述

NIP-32 定義了一套標籤系統,允許用戶為事件、人物、中繼器、地址和主題添加結構化的分類標籤。 這個系統支援多種命名空間,讓不同的應用場景(如內容審核、分類、評級等)可以使用統一的標籤機制。

核心概念: 標籤系統使用 kind 1985 事件類型,透過 L(命名空間) 和 l(標籤值)標籤對內容進行分類。

標籤事件結構

標籤事件使用 kind 1985,包含命名空間和標籤值:

標籤 格式 說明
L ["L", "<namespace>"] 標籤命名空間(Label Namespace)
l ["l", "<label>", "<namespace>"] 標籤值,第三個元素對應 L 標籤
e ["e", "<event-id>", "<relay-hint>"] 標籤目標事件
p ["p", "<pubkey>", "<relay-hint>"] 標籤目標用戶
r ["r", "<relay-url>"] 標籤目標中繼器
a ["a", "<naddr>"] 標籤可替換事件地址
t ["t", "<topic>"] 標籤主題標籤

命名空間

命名空間用於區分不同來源和用途的標籤。建議使用明確的標識符:

ugc(用戶生成內容)

保留命名空間,用於用戶自定義標籤。當 l 標籤沒有指定命名空間時,默認使用 ugc。

ISO 標準

可使用 ISO 標準作為命名空間,如 ISO-639-1 表示語言代碼。

反向域名標記

推薦使用反向域名標記,如 com.example.ontology

# 前綴

# 開頭表示標準 Nostr 標籤關聯,如 #t 表示主題標籤。

使用範例

標記事件語言

{
  "kind": 1985,
  "tags": [
    ["L", "ISO-639-1"],
    ["l", "zh", "ISO-639-1"],
    ["e", "<event-id>", "wss://relay.example.com"]
  ],
  "content": ""
}

內容分類標籤

{
  "kind": 1985,
  "tags": [
    ["L", "com.example.content-type"],
    ["l", "news", "com.example.content-type"],
    ["l", "technology", "com.example.content-type"],
    ["e", "<event-id>", "wss://relay.example.com"]
  ],
  "content": "這是一篇科技新聞相關的貼文"
}

用戶生成標籤(UGC)

{
  "kind": 1985,
  "tags": [
    ["L", "ugc"],
    ["l", "有趣", "ugc"],
    ["l", "推薦", "ugc"],
    ["e", "<event-id>", "wss://relay.example.com"]
  ],
  "content": ""
}

標記用戶(身份標籤)

{
  "kind": 1985,
  "tags": [
    ["L", "com.example.trust"],
    ["l", "verified", "com.example.trust"],
    ["p", "<pubkey>", "wss://relay.example.com"]
  ],
  "content": "此用戶已通過身份驗證"
}

標記中繼器

{
  "kind": 1985,
  "tags": [
    ["L", "com.example.relay-type"],
    ["l", "paid", "com.example.relay-type"],
    ["l", "fast", "com.example.relay-type"],
    ["r", "wss://premium.relay.example.com"]
  ],
  "content": ""
}

自我標記

除了使用 kind 1985 標籤事件外,也可以在其他事件中直接添加 L 和 l 標籤進行自我標記:

// 自我標記的貼文(kind 1)
{
  "kind": 1,
  "tags": [
    ["L", "ISO-639-1"],
    ["l", "en", "ISO-639-1"],
    ["L", "#t"],
    ["l", "bitcoin", "#t"],
    ["t", "bitcoin"]
  ],
  "content": "Bitcoin is the future of money!"
}

注意: 自我標記讓事件發布者可以預先分類自己的內容,方便客戶端過濾和搜尋。 #t 命名空間表示這個標籤與 t(主題)標籤關聯。

TypeScript 實作

import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools'

// 標籤命名空間常量
const NAMESPACES = {
  UGC: 'ugc',
  LANGUAGE: 'ISO-639-1',
  CONTENT_TYPE: 'com.example.content-type',
  TRUST: 'com.example.trust',
} as const

interface LabelTarget {
  type: 'event' | 'pubkey' | 'relay' | 'address' | 'topic'
  value: string
  relayHint?: string
}

interface LabelData {
  namespace: string
  labels: string[]
  target: LabelTarget
  content?: string
}

// 創建標籤事件
function createLabelEvent(
  data: LabelData,
  secretKey: Uint8Array
): object {
  const tags: string[][] = []

  // 添加命名空間
  tags.push(['L', data.namespace])

  // 添加標籤值
  for (const label of data.labels) {
    tags.push(['l', label, data.namespace])
  }

  // 添加目標
  switch (data.target.type) {
    case 'event':
      tags.push(['e', data.target.value, data.target.relayHint || ''])
      break
    case 'pubkey':
      tags.push(['p', data.target.value, data.target.relayHint || ''])
      break
    case 'relay':
      tags.push(['r', data.target.value])
      break
    case 'address':
      tags.push(['a', data.target.value])
      break
    case 'topic':
      tags.push(['t', data.target.value])
      break
  }

  const event = {
    kind: 1985,
    created_at: Math.floor(Date.now() / 1000),
    tags,
    content: data.content || '',
  }

  return finalizeEvent(event, secretKey)
}

// 標記事件語言
function labelEventLanguage(
  eventId: string,
  languageCode: string,
  relayHint: string,
  secretKey: Uint8Array
): object {
  return createLabelEvent({
    namespace: NAMESPACES.LANGUAGE,
    labels: [languageCode],
    target: {
      type: 'event',
      value: eventId,
      relayHint,
    },
  }, secretKey)
}

// 標記用戶信任等級
function labelUserTrust(
  pubkey: string,
  trustLevel: string,
  relayHint: string,
  secretKey: Uint8Array
): object {
  return createLabelEvent({
    namespace: NAMESPACES.TRUST,
    labels: [trustLevel],
    target: {
      type: 'pubkey',
      value: pubkey,
      relayHint,
    },
  }, secretKey)
}

// 用戶生成標籤
function createUGCLabel(
  eventId: string,
  labels: string[],
  relayHint: string,
  secretKey: Uint8Array
): object {
  return createLabelEvent({
    namespace: NAMESPACES.UGC,
    labels,
    target: {
      type: 'event',
      value: eventId,
      relayHint,
    },
  }, secretKey)
}

// 為自己的貼文添加自我標記
function createSelfLabeledNote(
  content: string,
  language: string,
  topics: string[],
  secretKey: Uint8Array
): object {
  const tags: string[][] = []

  // 語言標籤
  tags.push(['L', NAMESPACES.LANGUAGE])
  tags.push(['l', language, NAMESPACES.LANGUAGE])

  // 主題標籤
  if (topics.length > 0) {
    tags.push(['L', '#t'])
    for (const topic of topics) {
      tags.push(['l', topic, '#t'])
      tags.push(['t', topic])
    }
  }

  const event = {
    kind: 1,
    created_at: Math.floor(Date.now() / 1000),
    tags,
    content,
  }

  return finalizeEvent(event, secretKey)
}

// 使用範例
const sk = generateSecretKey()

// 標記事件語言為中文
const languageLabel = labelEventLanguage(
  'abc123...',
  'zh',
  'wss://relay.example.com',
  sk
)

// 標記用戶為已驗證
const trustLabel = labelUserTrust(
  'pubkey123...',
  'verified',
  'wss://relay.example.com',
  sk
)

// 用戶生成標籤
const ugcLabel = createUGCLabel(
  'event456...',
  ['有趣', '推薦', '必讀'],
  'wss://relay.example.com',
  sk
)

// 創建帶自我標記的貼文
const selfLabeledNote = createSelfLabeledNote(
  'Bitcoin 是未來的貨幣!',
  'zh',
  ['bitcoin', 'cryptocurrency'],
  sk
)

查詢標籤

import { SimplePool } from 'nostr-tools'

const pool = new SimplePool()
const relays = ['wss://relay.example.com']

// 查詢特定事件的所有標籤
async function getLabelsForEvent(eventId: string) {
  const labels = await pool.querySync(relays, {
    kinds: [1985],
    '#e': [eventId],
  })
  return labels
}

// 查詢特定命名空間的標籤
async function getLabelsByNamespace(namespace: string) {
  const labels = await pool.querySync(relays, {
    kinds: [1985],
    '#L': [namespace],
  })
  return labels
}

// 查詢特定標籤值
async function getEventsByLabel(label: string, namespace?: string) {
  const filter: any = {
    kinds: [1985],
    '#l': [label],
  }

  if (namespace) {
    filter['#L'] = [namespace]
  }

  const labels = await pool.querySync(relays, filter)
  return labels
}

// 查詢某用戶的所有標籤
async function getLabelsForUser(pubkey: string) {
  const labels = await pool.querySync(relays, {
    kinds: [1985],
    '#p': [pubkey],
  })
  return labels
}

// 查詢中文內容
async function getChineseContent() {
  const labels = await pool.querySync(relays, {
    kinds: [1985],
    '#L': ['ISO-639-1'],
    '#l': ['zh'],
  })

  // 提取被標記的事件 ID
  const eventIds = labels
    .flatMap(l => l.tags.filter(t => t[0] === 'e').map(t => t[1]))

  // 獲取實際事件
  if (eventIds.length > 0) {
    return await pool.querySync(relays, {
      ids: eventIds,
    })
  }

  return []
}

// 解析標籤事件
function parseLabels(event: any): {
  namespace: string
  labels: string[]
  targets: { type: string; value: string }[]
} {
  const namespaceTags = event.tags.filter((t: string[]) => t[0] === 'L')
  const labelTags = event.tags.filter((t: string[]) => t[0] === 'l')

  const namespace = namespaceTags[0]?.[1] || 'ugc'
  const labels = labelTags.map((t: string[]) => t[1])

  const targets: { type: string; value: string }[] = []
  for (const tag of event.tags) {
    if (tag[0] === 'e') targets.push({ type: 'event', value: tag[1] })
    if (tag[0] === 'p') targets.push({ type: 'pubkey', value: tag[1] })
    if (tag[0] === 'r') targets.push({ type: 'relay', value: tag[1] })
    if (tag[0] === 'a') targets.push({ type: 'address', value: tag[1] })
    if (tag[0] === 't') targets.push({ type: 'topic', value: tag[1] })
  }

  return { namespace, labels, targets }
}

應用場景

🌐 語言標記

使用 ISO-639-1 標記內容語言,讓客戶端可以過濾顯示特定語言的內容。

🏷️ 內容分類

為貼文添加分類標籤如「新聞」、「教程」、「討論」等,方便內容發現。

✅ 信任標記

標記用戶的信任等級或驗證狀態,建立去中心化的信任網絡。

🛡️ 內容審核

結合 NIP-56 舉報系統,審核員可以添加審核結果標籤。

⭐ 評級系統

為內容添加品質評級,如「高品質」、「精選」、「推薦」等。

📊 中繼器分類

標記中繼器的特性,如「付費」、「免費」、「快速」、「專業」等。

最佳實踐

✓ 使用明確的命名空間

使用 ISO 標準或反向域名標記,避免命名空間衝突。保持命名空間公開且非專有。

✓ 標籤值保持簡短

標籤應該是簡短、有意義的字串。較長的解釋應放在 content 欄位中。

✓ 添加中繼器提示

使用 e 和 p 標籤時,應包含中繼器提示以幫助客戶端找到目標。

⚠ 單一命名空間原則

每個標籤事件應限制在單一命名空間內。如需多個命名空間,創建多個標籤事件。

⚠ l 標籤必須包含命名空間標記

當使用 L 標籤時,l 標籤必須包含第三個元素對應 L 標籤的值。

相關 NIP

總結

NIP-32 標籤系統提供了一個靈活的機制,讓用戶和應用可以為 Nostr 內容添加結構化的分類資訊。 透過命名空間隔離,不同的使用場景可以共存而不會互相干擾。

Kind 1985
標籤事件類型
L + l
命名空間 + 標籤值
5 種
標籤目標類型
已複製連結
已複製到剪貼簿