跳至主要內容
進階

NIP-65 中繼器列表

深入了解 Nostr 的中繼器列表元數據,用於發現用戶使用的中繼器。

10 分鐘

什麼是 NIP-65?

NIP-65 定義了用戶的中繼器列表元數據(Relay List Metadata),讓其他用戶和客戶端 知道應該從哪些中繼器讀取該用戶的事件,以及向哪些中繼器發送給該用戶的事件。

核心概念: Kind 10002 事件包含用戶的中繼器偏好,區分「讀取」和「寫入」中繼器, 讓 Nostr 網路更有效率地路由訊息。

事件結構

{
  "kind": 10002,
  "pubkey": "<user-pubkey>",
  "content": "",
  "tags": [
    ["r", "wss://relay1.com"],              // 讀+寫
    ["r", "wss://relay2.com", "read"],      // 只讀
    ["r", "wss://relay3.com", "write"]      // 只寫
  ],
  "created_at": 1234567890,
  "id": "...",
  "sig": "..."
}

中繼器類型

標記 讀取 寫入 用途
(無標記) 主要中繼器,用於讀寫
"read" 從此中繼器讀取他人事件
"write" 發布自己的事件到此中繼器

使用場景

📖

找到用戶的貼文

查看用戶的 write 中繼器,從那裡獲取他們的事件

✉️

發送私訊

發送到用戶的 read 中繼器,確保他們能收到

💬

回覆貼文

發送到原作者的 read 中繼器

📢

廣播通知

使用 write 中繼器發布,讓關注者能找到

程式碼範例

發布中繼器列表

async function publishRelayList(relays) {
  const event = {
    kind: 10002,
    created_at: Math.floor(Date.now() / 1000),
    content: '',
    tags: relays.map(r => {
      if (r.type === 'read') return ['r', r.url, 'read']
      if (r.type === 'write') return ['r', r.url, 'write']
      return ['r', r.url]  // 讀+寫
    })
  }

  const signed = await window.nostr.signEvent(event)
  // 發送到多個中繼器
  for (const relay of connectedRelays) {
    relay.send(JSON.stringify(['EVENT', signed]))
  }

  return signed
}

// 使用
await publishRelayList([
  { url: 'wss://relay.damus.io' },           // 讀+寫
  { url: 'wss://nos.lol' },                  // 讀+寫
  { url: 'wss://relay.snort.social', type: 'read' },
  { url: 'wss://nostr.wine', type: 'write' }
])

查詢用戶的中繼器列表

async function getUserRelays(pubkey) {
  return new Promise((resolve) => {
    const sub = relay.subscribe([{
      kinds: [10002],
      authors: [pubkey],
      limit: 1
    }])

    let relayList = null

    sub.on('event', (event) => {
      relayList = parseRelayList(event)
    })

    sub.on('eose', () => {
      sub.close()
      resolve(relayList)
    })
  })
}

function parseRelayList(event) {
  const relays = { read: [], write: [] }

  for (const tag of event.tags) {
    if (tag[0] !== 'r') continue

    const url = tag[1]
    const type = tag[2]

    if (!type || type === 'write') {
      relays.write.push(url)
    }
    if (!type || type === 'read') {
      relays.read.push(url)
    }
  }

  return relays
}

// 使用
const relays = await getUserRelays('npub1...')
console.log('讀取中繼器:', relays.read)
console.log('寫入中繼器:', relays.write)

發送事件到用戶的中繼器

async function sendToUser(event, targetPubkey) {
  // 1. 獲取目標用戶的中繼器列表
  const userRelays = await getUserRelays(targetPubkey)

  if (!userRelays || userRelays.read.length === 0) {
    console.log('找不到用戶的中繼器,使用預設')
    userRelays = { read: DEFAULT_RELAYS }
  }

  // 2. 連接到用戶的 read 中繼器
  const connections = await Promise.all(
    userRelays.read.map(url => connectToRelay(url))
  )

  // 3. 發送事件
  for (const relay of connections) {
    relay.send(JSON.stringify(['EVENT', event]))
  }
}

// 發送私訊到用戶的中繼器
await sendToUser(encryptedDM, recipientPubkey)

Outbox 模型

NIP-65 實現了「Outbox 模型」,讓訊息路由更有效率:

# Outbox 模型原理

Alice 的設定:
  write: [relay1, relay2]  // Alice 發布到這裡
  read:  [relay3, relay4]  // Alice 從這裡讀取

Bob 想看 Alice 的貼文:
  → 查詢 Alice 的 kind 10002
  → 連接到 Alice 的 write 中繼器 (relay1, relay2)
  → 從那裡獲取 Alice 的事件

Bob 想發私訊給 Alice:
  → 查詢 Alice 的 kind 10002
  → 連接到 Alice 的 read 中繼器 (relay3, relay4)
  → 發送到那裡,Alice 才能收到

與 kind 3 的區別

特性 Kind 3(關注列表) Kind 10002(中繼器列表)
主要用途 儲存關注的用戶 宣告中繼器偏好
中繼器資訊 附帶在 content(已過時) 主要功能,使用標籤
讀/寫區分 不支援 支援
推薦使用 只用於關注列表 用於中繼器設定

最佳實踐

推薦做法

  • • 保持 3-5 個中繼器
  • • 至少有一個讀+寫中繼器
  • • 使用穩定可靠的中繼器
  • • 定期更新列表

注意事項

  • • 不要列出太多中繼器
  • • 確保中繼器可訪問
  • • 考慮地理位置分散
  • • 測試 read/write 設定

查詢示例

// 查詢單個用戶的中繼器列表
["REQ", "relay-list", {
  "kinds": [10002],
  "authors": ["<pubkey>"],
  "limit": 1
}]

// 批量查詢多個用戶的中繼器列表
["REQ", "relay-lists", {
  "kinds": [10002],
  "authors": ["<pubkey1>", "<pubkey2>", "<pubkey3>"]
}]

// 注意:kind 10002 是可替換事件
// 只需要最新的一個版本

客戶端建議: 實作 NIP-65 可以大幅提升訊息送達率。當用戶關注某人時, 查詢他們的中繼器列表,並優先連接到這些中繼器。

已複製連結
已複製到剪貼簿