入門
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
參考資源
已複製連結