NIP-32: 標籤系統
Labeling - 為內容添加結構化分類標籤
概述
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 內容添加結構化的分類資訊。 透過命名空間隔離,不同的使用場景可以共存而不會互相干擾。