進階
NIP-51 列表
Nostr 列表機制,用於管理靜音、置頂、書籤等多種列表類型。
10 分鐘
概述
NIP-51 定義了一套標準化的列表機制,讓使用者可以管理各種類型的列表:靜音名單、置頂筆記、書籤收藏等。 這些列表使用特定的 kind 編號,支援公開和私密兩種模式。
列表類型
NIP-51 定義了多種列表類型,每種使用不同的 kind 編號:
| Kind | 名稱 | 用途 | 可參數化 |
|---|---|---|---|
| 10000 | Mute list | 靜音用戶、關鍵字 | 否 |
| 10001 | Pin list | 置頂筆記 | 否 |
| 10003 | Bookmark list | 書籤收藏 | 否 |
| 10004 | Communities list | 加入的社群 | 否 |
| 10005 | Public chats list | 公開聊天室 | 否 |
| 30000 | People sets | 分類用戶列表 | 是 |
| 30001 | Bookmark sets | 分類書籤 | 是 |
| 30003 | Emoji sets | 自訂表情包 | 是 |
靜音列表 (Kind 10000)
靜音列表用於隱藏特定用戶、關鍵字或筆記。客戶端應遵守此列表,過濾相關內容。
事件結構
{`{
"kind": 10000,
"tags": [
["p", "<被靜音用戶的公鑰>"],
["p", "<另一個被靜音用戶>"],
["t", "spam"],
["word", "討厭的關鍵字"],
["e", "<被靜音的筆記 ID>"]
],
"content": "<加密的私密靜音列表>",
...
}`} 支援的標籤
p- 靜音用戶公鑰e- 靜音特定筆記t- 靜音 hashtagword- 靜音關鍵字
TypeScript 範例
{`import { getPublicKey, finalizeEvent } from 'nostr-tools';
import { encrypt } from 'nostr-tools/nip04';
// 建立靜音列表
async function createMuteList(
privateKey: Uint8Array,
publicMutes: string[],
privateMutes: string[]
) {
const pubkey = getPublicKey(privateKey);
// 公開的靜音標籤
const tags = publicMutes.map(pk => ['p', pk]);
// 私密的靜音列表(加密儲存)
const privateContent = JSON.stringify(
privateMutes.map(pk => ['p', pk])
);
const encryptedContent = await encrypt(
privateKey,
pubkey,
privateContent
);
const event = {
kind: 10000,
created_at: Math.floor(Date.now() / 1000),
tags,
content: encryptedContent,
};
return finalizeEvent(event, privateKey);
}
// 使用範例
const muteEvent = await createMuteList(
privateKey,
['<公開靜音的公鑰>'],
['<私密靜音的公鑰>']
);`} 置頂列表 (Kind 10001)
置頂列表讓用戶可以標記重要的筆記,客戶端可以將這些筆記顯示在個人檔案頂部。
{`{
"kind": 10001,
"tags": [
["e", "<置頂筆記的事件 ID>", "<中繼器 URL>"],
["e", "<另一個置頂筆記>", "<中繼器 URL>"]
],
"content": "",
...
}`} TypeScript 範例
{`// 建立置頂列表
function createPinList(privateKey: Uint8Array, pinnedNotes: {id: string, relay: string}[]) {
const tags = pinnedNotes.map(note => ['e', note.id, note.relay]);
const event = {
kind: 10001,
created_at: Math.floor(Date.now() / 1000),
tags,
content: '',
};
return finalizeEvent(event, privateKey);
}
// 使用範例
const pinEvent = createPinList(privateKey, [
{ id: 'abc123...', relay: 'wss://relay.damus.io' },
{ id: 'def456...', relay: 'wss://nos.lol' },
]);`} 書籤列表 (Kind 10003)
書籤列表用於收藏感興趣的筆記,類似於社交媒體的「儲存」功能。
{`{
"kind": 10003,
"tags": [
["e", "<書籤筆記 ID>", "<中繼器 URL>"],
["a", "30023:<作者公鑰>:<文章 d-tag>", "<中繼器 URL>"],
["t", "bitcoin"],
["r", "https://example.com/article"]
],
"content": "<加密的私密書籤>",
...
}`} 支援的標籤
e- 書籤筆記 IDa- 書籤可替換事件(如長文)t- 書籤 hashtagr- 書籤外部 URL
可參數化列表
Kind 30000-30003 是可參數化(parameterized)列表,使用 d 標籤區分不同的列表實例。 這讓用戶可以建立多個同類型的列表。
人物集合 (Kind 30000)
{`{
"kind": 30000,
"tags": [
["d", "bitcoiners"],
["p", "<用戶公鑰1>"],
["p", "<用戶公鑰2>"],
["p", "<用戶公鑰3>"]
],
"content": "比特幣開發者們",
...
}`} 書籤集合 (Kind 30001)
{`{
"kind": 30001,
"tags": [
["d", "programming-articles"],
["e", "<筆記 ID>", "<中繼器>"],
["a", "30023::", "<中繼器>"]
],
"content": "程式設計文章收藏",
...
}`} 表情集合 (Kind 30003)
{`{
"kind": 30003,
"tags": [
["d", "my-emojis"],
["emoji", "sats", "https://example.com/sats.png"],
["emoji", "bitcoin", "https://example.com/bitcoin.gif"],
["emoji", "ln", "https://example.com/lightning.png"]
],
"content": "我的自訂表情包",
...
}`} 私密條目
列表可以同時包含公開和私密的條目。私密條目使用 NIP-04 加密後儲存在 content 欄位中。
{`import { encrypt, decrypt } from 'nostr-tools/nip04';
// 解密私密列表內容
async function decryptPrivateEntries(
event: Event,
privateKey: Uint8Array
) {
if (!event.content) return [];
try {
const decrypted = await decrypt(
privateKey,
event.pubkey,
event.content
);
return JSON.parse(decrypted);
} catch {
return [];
}
}
// 合併公開與私密標籤
async function getAllTags(event: Event, privateKey: Uint8Array) {
const publicTags = event.tags;
const privateTags = await decryptPrivateEntries(event, privateKey);
return [...publicTags, ...privateTags];
}`} 查詢列表
{`import { SimplePool } from 'nostr-tools';
const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
// 查詢用戶的靜音列表
async function getMuteList(pubkey: string) {
const events = await pool.querySync(relays, {
kinds: [10000],
authors: [pubkey],
limit: 1,
});
return events[0] || null;
}
// 查詢用戶的所有書籤集合
async function getBookmarkSets(pubkey: string) {
return await pool.querySync(relays, {
kinds: [30001],
authors: [pubkey],
});
}
// 查詢特定的人物集合
async function getPeopleSet(pubkey: string, setName: string) {
const events = await pool.querySync(relays, {
kinds: [30000],
authors: [pubkey],
'#d': [setName],
limit: 1,
});
return events[0] || null;
}`} 更新列表
列表使用可替換事件機制。同一 kind 和 d 標籤的新事件會替換舊事件。 更新時需要發布包含完整列表的新事件。
{`// 新增項目到書籤列表
async function addBookmark(
privateKey: Uint8Array,
currentEvent: Event | null,
newNoteId: string,
relay: string
) {
// 取得現有的 e 標籤
const existingTags = currentEvent?.tags.filter(t => t[0] === 'e') || [];
// 新增新書籤
const tags = [
...existingTags,
['e', newNoteId, relay],
];
const event = {
kind: 10003,
created_at: Math.floor(Date.now() / 1000),
tags,
content: currentEvent?.content || '',
};
return finalizeEvent(event, privateKey);
}
// 從列表移除項目
function removeFromList(
currentEvent: Event,
tagType: string,
valueToRemove: string
) {
const filteredTags = currentEvent.tags.filter(
t => !(t[0] === tagType && t[1] === valueToRemove)
);
return {
...currentEvent,
tags: filteredTags,
created_at: Math.floor(Date.now() / 1000),
};
}`} 客戶端實作建議
- 靜音列表:啟動時載入,用於過濾時間軸、通知和搜尋結果
- 置頂列表:在個人檔案頁面頂部顯示置頂筆記
- 書籤列表:提供書籤管理介面,支援分類整理
- 私密條目:解密後在本地快取,避免頻繁解密
- 列表同步:監聽列表更新事件,保持即時同步
相關 NIP
參考資源
已複製連結