跳至主要內容
入門

NIP-18 轉發

Nostr 轉發機制,用於分享和放大其他用戶的內容。

6 分鐘

概述

NIP-18 定義了 Nostr 的轉發(Repost)機制,類似於 Twitter 的轉推功能。 轉發讓用戶可以分享其他人的內容給自己的追蹤者,幫助優質內容獲得更廣泛的傳播。

事件類型

NIP-18 定義了兩種轉發事件:

Kind 名稱 用途
6 Repost 轉發 kind 1 短文(筆記)
16 Generic Repost 轉發任意類型的事件

轉發結構 (Kind 6)

Kind 6 用於轉發 kind 1 的短文筆記:

{`{
  "kind": 6,
  "tags": [
    ["e", "<原始事件 ID>", "<中繼器 URL>"],
    ["p", "<原始作者公鑰>"]
  ],
  "content": "",
  "pubkey": "<轉發者公鑰>",
  ...
}`}

包含原始事件

為了讓客戶端可以立即顯示內容而不需額外查詢,可以在 content 中包含原始事件的 JSON 字串:

{`{
  "kind": 6,
  "tags": [
    ["e", "abc123...", "wss://relay.damus.io"],
    ["p", "def456..."]
  ],
  "content": "{\"id\":\"abc123...\",\"pubkey\":\"def456...\",\"kind\":1,\"content\":\"原始內容\",\"tags\":[],\"created_at\":1234567890,\"sig\":\"...\"}",
  ...
}`}

通用轉發 (Kind 16)

Kind 16 可以轉發任意類型的事件,需要額外的 k 標籤指定原始事件類型:

{`{
  "kind": 16,
  "tags": [
    ["e", "<原始事件 ID>", "<中繼器 URL>"],
    ["p", "<原始作者公鑰>"],
    ["k", "<原始事件 kind>"]
  ],
  "content": "",
  ...
}`}

轉發長文範例

{`{
  "kind": 16,
  "tags": [
    ["e", "article123...", "wss://relay.example.com"],
    ["p", "author456..."],
    ["k", "30023"]
  ],
  "content": "",
  ...
}`}

實作

建立轉發

{`import { finalizeEvent, getPublicKey } from 'nostr-tools';

interface Event {
  id: string;
  pubkey: string;
  kind: number;
  content: string;
  tags: string[][];
  created_at: number;
  sig: string;
}

// 轉發 kind 1 筆記
function createRepost(
  privateKey: Uint8Array,
  originalEvent: Event,
  relay: string,
  includeContent: boolean = true
) {
  const tags: string[][] = [
    ['e', originalEvent.id, relay],
    ['p', originalEvent.pubkey],
  ];

  const event = {
    kind: 6,
    created_at: Math.floor(Date.now() / 1000),
    tags,
    content: includeContent ? JSON.stringify(originalEvent) : '',
  };

  return finalizeEvent(event, privateKey);
}

// 通用轉發(任意 kind)
function createGenericRepost(
  privateKey: Uint8Array,
  originalEvent: Event,
  relay: string,
  includeContent: boolean = true
) {
  const tags: string[][] = [
    ['e', originalEvent.id, relay],
    ['p', originalEvent.pubkey],
    ['k', originalEvent.kind.toString()],
  ];

  const event = {
    kind: 16,
    created_at: Math.floor(Date.now() / 1000),
    tags,
    content: includeContent ? JSON.stringify(originalEvent) : '',
  };

  return finalizeEvent(event, privateKey);
}

// 使用範例
const repost = createRepost(privateKey, originalNote, 'wss://relay.damus.io');
const articleRepost = createGenericRepost(privateKey, articleEvent, 'wss://nos.lol');`}

解析轉發

{`// 解析轉發事件
function parseRepost(repostEvent: Event): {
  originalEventId: string;
  originalAuthor: string;
  originalKind: number;
  embeddedEvent: Event | null;
  relay: string | null;
} {
  const eTag = repostEvent.tags.find(t => t[0] === 'e');
  const pTag = repostEvent.tags.find(t => t[0] === 'p');
  const kTag = repostEvent.tags.find(t => t[0] === 'k');

  // 嘗試解析嵌入的原始事件
  let embeddedEvent: Event | null = null;
  if (repostEvent.content) {
    try {
      embeddedEvent = JSON.parse(repostEvent.content);
    } catch {
      // content 不是有效的 JSON
    }
  }

  // 判斷原始事件類型
  let originalKind = 1; // 預設 kind 6 轉發的是 kind 1
  if (repostEvent.kind === 16 && kTag) {
    originalKind = parseInt(kTag[1], 10);
  } else if (embeddedEvent) {
    originalKind = embeddedEvent.kind;
  }

  return {
    originalEventId: eTag?.[1] || '',
    originalAuthor: pTag?.[1] || '',
    originalKind,
    embeddedEvent,
    relay: eTag?.[2] || null,
  };
}`}

查詢轉發

{`import { SimplePool } from 'nostr-tools';

const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];

// 查詢用戶的所有轉發
async function getUserReposts(pubkey: string) {
  return await pool.querySync(relays, {
    kinds: [6, 16],
    authors: [pubkey],
    limit: 50,
  });
}

// 查詢特定事件的轉發數量
async function getRepostCount(eventId: string): Promise {
  const reposts = await pool.querySync(relays, {
    kinds: [6, 16],
    '#e': [eventId],
  });

  return reposts.length;
}

// 查詢特定事件的所有轉發者
async function getReposters(eventId: string): Promise {
  const reposts = await pool.querySync(relays, {
    kinds: [6, 16],
    '#e': [eventId],
  });

  return [...new Set(reposts.map(r => r.pubkey))];
}

// 檢查用戶是否已轉發
async function hasUserReposted(
  userPubkey: string,
  eventId: string
): Promise {
  const reposts = await pool.querySync(relays, {
    kinds: [6, 16],
    authors: [userPubkey],
    '#e': [eventId],
    limit: 1,
  });

  return reposts.length > 0;
}`}

取得原始事件

{`// 取得轉發的原始事件
async function getOriginalEvent(repostEvent: Event): Promise {
  const parsed = parseRepost(repostEvent);

  // 優先使用嵌入的事件
  if (parsed.embeddedEvent) {
    // 驗證嵌入事件的 ID 是否正確
    if (parsed.embeddedEvent.id === parsed.originalEventId) {
      return parsed.embeddedEvent;
    }
  }

  // 從中繼器查詢
  const searchRelays = parsed.relay
    ? [parsed.relay, ...relays]
    : relays;

  const events = await pool.querySync(searchRelays, {
    ids: [parsed.originalEventId],
    limit: 1,
  });

  return events[0] || null;
}`}

顯示建議

客戶端顯示轉發時的建議做法:

  • 顯示「轉發」標記和轉發者資訊
  • 顯示原始內容和原作者
  • 點擊可跳轉到原始事件
  • 轉發時間和原始發布時間都應顯示
{`// React 組件範例
function RepostNote({ repostEvent }: { repostEvent: Event }) {
  const [originalEvent, setOriginalEvent] = useState(null);
  const [reposter, setReposter] = useState(null);

  useEffect(() => {
    // 載入轉發者資訊
    loadProfile(repostEvent.pubkey).then(setReposter);

    // 載入原始事件
    getOriginalEvent(repostEvent).then(setOriginalEvent);
  }, [repostEvent]);

  if (!originalEvent) {
    return 
載入中...
; } return (
🔁 {reposter?.name || shortenPubkey(repostEvent.pubkey)} 轉發了 {formatTime(repostEvent.created_at)}
); }`}

引用轉發

NIP-18 專注於純轉發。如果要添加評論(引用轉發),應使用 kind 1 筆記 並引用原始事件:

{`// 引用轉發(帶評論)
function createQuoteRepost(
  privateKey: Uint8Array,
  originalEvent: Event,
  comment: string,
  relay: string
) {
  // 使用 NIP-19 的 nevent 格式引用
  const nevent = nip19.neventEncode({
    id: originalEvent.id,
    relays: [relay],
    author: originalEvent.pubkey,
  });

  const event = {
    kind: 1,
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ['e', originalEvent.id, relay, 'mention'],
      ['p', originalEvent.pubkey],
      ['q', originalEvent.id],  // 引用標籤
    ],
    content: \`\${comment}\\n\\nnostr:\${nevent}\`,
  };

  return finalizeEvent(event, privateKey);
}`}

注意事項

  • 驗證嵌入內容:如果使用嵌入的事件,應驗證其簽名
  • 中繼器提示e 標籤的第三個值是中繼器提示,幫助查詢原始事件
  • 取消轉發:使用 NIP-09 刪除轉發事件
  • 重複轉發:客戶端應防止用戶重複轉發同一事件
  • NIP-01 - 基本協議與事件結構
  • NIP-09 - 事件刪除(取消轉發)
  • NIP-10 - 回覆與標記
  • NIP-25 - 反應(另一種互動方式)

參考資源

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