跳至主要內容
進階

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 - 靜音 hashtag
  • word - 靜音關鍵字

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 - 書籤筆記 ID
  • a - 書籤可替換事件(如長文)
  • t - 書籤 hashtag
  • r - 書籤外部 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-01 - 基本協議與事件結構
  • NIP-02 - 聯絡人列表(關注列表)
  • NIP-04 - 加密直接訊息(用於私密條目)
  • NIP-23 - 長文內容(可書籤的事件類型)

參考資源

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