進階
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 可以大幅提升訊息送達率。當用戶關注某人時, 查詢他們的中繼器列表,並優先連接到這些中繼器。
已複製連結