跳至主要內容
進階

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 - 長文內容(可書籤的事件類型)

參考資源

已複製連結

討論

已複製到剪貼簿