NIP-27: 文字引用
在 Nostr 文字內容中引用其他事件和用戶的標準方法
概述
NIP-27 標準化了客戶端如何在可讀文字內容(如 kind 1 短文和 kind 30023 長文)中
處理對用戶和事件的內嵌引用。透過使用 nostr: URI 前綴, 客戶端可以將引用轉換為可點擊的連結、預覽卡片或其他豐富的呈現方式。
提及格式
提及使用 NIP-21 的 nostr: URI 格式:
用戶提及
nostr:npub1xxx...
nostr:nprofile1qqsw3dy8cpu... 事件引用
nostr:note1xxx...
nostr:nevent1qqsxxx...
nostr:naddr1qqsxxx... 範例
提及用戶
{
"kind": 1,
"content": "剛看到 nostr:npub1qqqqqq... 發的精彩文章!",
"tags": [
["p", "<mentioned-pubkey>"]
]
} 引用事件
{
"kind": 1,
"content": "這個討論很有意思:nostr:nevent1qqsxxx...",
"tags": [
["q", "<event-id>", "wss://relay.example.com", "<author-pubkey>"]
]
} 引用長文
{
"kind": 1,
"content": "推薦這篇教學:nostr:naddr1qqxnzd3exsmxxxx...",
"tags": [
["q", "30023:<author-pubkey>:article-slug", "wss://relay.example.com"]
]
} 標籤要求
包含標籤是可選的,但在以下情況建議添加:
- 希望通知被提及的用戶
- 希望被引用的事件能識別此提及為回覆
用戶提及標籤
["p", "<pubkey>", "<relay-url>"] 事件引用標籤 (q 標籤)
// 引用一般事件
["q", "<event-id>", "<relay-url>", "<author-pubkey>"]
// 引用可定址事件
["q", "<kind>:<pubkey>:<d-tag>", "<relay-url>"] 客戶端行為
建立事件時
- 將提及直接插入
.content中,使用 NIP-21 格式 - 可以提供自動完成功能幫助用戶插入提及
- 根據需要決定是否添加通知標籤
讀取事件時
- 解析內容中的
nostr:URI - 將用戶提及轉換為可點擊的個人資料連結
- 將事件引用轉換為預覽卡片或嵌入內容
- 連結到內部頁面或外部 Web 客戶端
TypeScript 實作
建立帶引用的事件
import { finalizeEvent, generateSecretKey, nip19 } from 'nostr-tools';
interface MentionedUser {
pubkey: string;
relay?: string;
}
interface QuotedEvent {
eventId?: string;
kind?: number;
pubkey?: string;
identifier?: string;
relay?: string;
}
function createNoteWithReferences(
content: string,
mentions: MentionedUser[] = [],
quotes: QuotedEvent[] = [],
secretKey: Uint8Array
) {
const tags: string[][] = [];
// 添加用戶提及標籤
mentions.forEach((mention) => {
const pTag = ['p', mention.pubkey];
if (mention.relay) pTag.push(mention.relay);
tags.push(pTag);
});
// 添加事件引用標籤
quotes.forEach((quote) => {
if (quote.eventId) {
// 一般事件
const qTag = ['q', quote.eventId];
if (quote.relay) qTag.push(quote.relay);
if (quote.pubkey) qTag.push(quote.pubkey);
tags.push(qTag);
} else if (quote.kind && quote.pubkey && quote.identifier) {
// 可定址事件
const address = `${quote.kind}:${quote.pubkey}:${quote.identifier}`;
const qTag = ['q', address];
if (quote.relay) qTag.push(quote.relay);
tags.push(qTag);
}
});
const event = {
kind: 1,
content,
tags,
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, secretKey);
}
// 使用範例
const sk = generateSecretKey();
// 建立 nostr: URI
const userNpub = nip19.npubEncode('pubkey-hex...');
const eventNevent = nip19.neventEncode({
id: 'event-id-hex...',
relays: ['wss://relay.example.com'],
author: 'author-pubkey...',
});
const content = `
剛看到 nostr:${userNpub} 的精彩分享!
這篇文章值得一讀:nostr:${eventNevent}
`;
const event = createNoteWithReferences(
content,
[{ pubkey: 'pubkey-hex...', relay: 'wss://relay.example.com' }],
[{ eventId: 'event-id-hex...', relay: 'wss://relay.example.com', pubkey: 'author-pubkey...' }],
sk
); 解析內容中的引用
import { nip19 } from 'nostr-tools';
interface ParsedReference {
type: 'npub' | 'nprofile' | 'note' | 'nevent' | 'naddr';
data: any;
raw: string;
start: number;
end: number;
}
function parseReferences(content: string): ParsedReference[] {
const regex = /nostr:(n(?:pub|profile|ote|event|addr)1[a-z0-9]+)/gi;
const references: ParsedReference[] = [];
let match;
while ((match = regex.exec(content)) !== null) {
const raw = match[1];
try {
const decoded = nip19.decode(raw);
references.push({
type: decoded.type as ParsedReference['type'],
data: decoded.data,
raw: match[0],
start: match.index,
end: match.index + match[0].length,
});
} catch {
// 無效的 bech32,忽略
}
}
return references;
}
// 使用範例
const refs = parseReferences(content);
refs.forEach((ref) => {
console.log(`類型: ${ref.type}`);
console.log(`資料:`, ref.data);
}); 渲染豐富內容
interface RenderOptions {
onProfileClick?: (pubkey: string) => void;
onEventClick?: (eventId: string) => void;
}
function renderRichContent(
content: string,
options: RenderOptions = {}
): string {
const references = parseReferences(content);
// 從後往前替換,避免位置偏移
let result = content;
for (let i = references.length - 1; i >= 0; i--) {
const ref = references[i];
let replacement = '';
switch (ref.type) {
case 'npub':
replacement = `@${ref.data.slice(0, 8)}...`;
break;
case 'nprofile':
replacement = `@${ref.data.pubkey.slice(0, 8)}...`;
break;
case 'note':
replacement = `載入中...`;
break;
case 'nevent':
replacement = `載入中...`;
break;
case 'naddr':
replacement = `載入中...`;
break;
}
result = result.slice(0, ref.start) + replacement + result.slice(ref.end);
}
return result;
}
// React 元件範例
function RichTextContent({ content }: { content: string }) {
const references = parseReferences(content);
const parts: JSX.Element[] = [];
let lastIndex = 0;
references.forEach((ref, i) => {
// 添加引用前的普通文字
if (ref.start > lastIndex) {
parts.push(
{content.slice(lastIndex, ref.start)}
);
}
// 添加引用元件
switch (ref.type) {
case 'npub':
case 'nprofile':
const pubkey = ref.type === 'npub' ? ref.data : ref.data.pubkey;
parts.push(
);
break;
case 'note':
case 'nevent':
const eventId = ref.type === 'note' ? ref.data : ref.data.id;
parts.push(
);
break;
case 'naddr':
parts.push(
);
break;
}
lastIndex = ref.end;
});
// 添加剩餘文字
if (lastIndex < content.length) {
parts.push({content.slice(lastIndex)});
}
return <>{parts}>;
} 最佳實踐
- 使用 nprofile/nevent:包含中繼器提示,提高可發現性
- 添加通知標籤:讓被提及的用戶收到通知
- 提供自動完成:幫助用戶輕鬆插入提及
- 優雅降級:無法解析時顯示原始 URI
- 快取引用內容:避免重複抓取同一事件
相關 NIPs
- NIP-21:nostr: URI - URI 格式定義
- NIP-19:bech32 編碼 - npub、nevent 等格式
- NIP-10:回覆與標記 - 回覆機制
- NIP-18:轉發 - 引用轉發
參考資源
已複製連結