NIP-48: 代理標籤
標記從其他協議橋接到 Nostr 的內容來源
概述
NIP-48 定義了 proxy 標籤,用於標記從其他協議(如 ActivityPub、AT Protocol、RSS) 橋接到
Nostr 的內容。這讓客戶端可以識別重複內容、顯示原始來源連結, 或在去重時優先處理原生 Nostr 內容。
標籤格式
["proxy", "<id>", "<protocol>"] | 欄位 | 說明 |
|---|---|
id | 原始內容的唯一識別符,格式因協議而異 |
protocol | 來源協議名稱 |
支援的協議
| 協議 | ID 格式 | 範例 |
|---|---|---|
activitypub | ActivityPub 物件 URL | https://mastodon.social/users/alice/statuses/123 |
atproto | AT URI | at://did:plc:xxx/app.bsky.feed.post/abc |
rss | RSS Feed URL + guid | https://blog.example.com/feed.xml#post-123 |
web | 網頁 URL | https://twitter.com/jack/status/20 |
範例
從 ActivityPub 橋接
{
"kind": 1,
"content": "這是從 Mastodon 橋接過來的貼文!",
"tags": [
["proxy", "https://mastodon.social/users/alice/statuses/123456789", "activitypub"]
]
} 從 Bluesky (AT Protocol) 橋接
{
"kind": 1,
"content": "這是從 Bluesky 橋接過來的貼文!",
"tags": [
["proxy", "at://did:plc:abc123/app.bsky.feed.post/xyz789", "atproto"]
]
} 從 RSS 橋接
{
"kind": 1,
"content": "這是部落格的新文章摘要...",
"tags": [
["proxy", "https://blog.example.com/feed.xml#article-2024-01-15", "rss"]
]
} 從 Twitter/X 橋接
{
"kind": 1,
"content": "這是從 Twitter 橋接過來的推文!",
"tags": [
["proxy", "https://twitter.com/jack/status/20", "web"]
]
} TypeScript 實作
建立代理事件
import { finalizeEvent } from 'nostr-tools';
type ProxyProtocol = 'activitypub' | 'atproto' | 'rss' | 'web';
interface ProxySource {
id: string;
protocol: ProxyProtocol;
}
function createProxiedEvent(
content: string,
source: ProxySource,
secretKey: Uint8Array,
additionalTags: string[][] = []
) {
const tags: string[][] = [
['proxy', source.id, source.protocol],
...additionalTags,
];
const event = {
kind: 1,
content,
tags,
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, secretKey);
}
// 使用範例:橋接 Mastodon 貼文
const mastodonPost = createProxiedEvent(
'這是一則從 Mastodon 橋接的貼文!',
{
id: 'https://mastodon.social/users/alice/statuses/123456789',
protocol: 'activitypub',
},
secretKey
); 檢測代理事件
interface ProxyInfo {
id: string;
protocol: string;
isProxied: boolean;
}
function getProxyInfo(event: any): ProxyInfo | null {
const proxyTag = event.tags.find((t: string[]) => t[0] === 'proxy');
if (!proxyTag || proxyTag.length < 3) {
return null;
}
return {
id: proxyTag[1],
protocol: proxyTag[2],
isProxied: true,
};
}
function isProxiedEvent(event: any): boolean {
return event.tags.some((t: string[]) => t[0] === 'proxy');
}
// 使用範例
const proxyInfo = getProxyInfo(event);
if (proxyInfo) {
console.log(`來源協議: ${proxyInfo.protocol}`);
console.log(`原始 ID: ${proxyInfo.id}`);
} 去重處理
interface DeduplicatedEvent {
event: any;
isNative: boolean;
proxySource?: ProxyInfo;
}
function deduplicateEvents(events: any[]): DeduplicatedEvent[] {
const proxyMap = new Map<string, any>();
const nativeEvents: any[] = [];
// 分類事件
events.forEach((event) => {
const proxyInfo = getProxyInfo(event);
if (proxyInfo) {
// 記錄代理事件,以原始 ID 為鍵
const existing = proxyMap.get(proxyInfo.id);
if (!existing || event.created_at > existing.created_at) {
proxyMap.set(proxyInfo.id, event);
}
} else {
nativeEvents.push(event);
}
});
// 合併結果,優先顯示原生事件
const result: DeduplicatedEvent[] = [];
nativeEvents.forEach((event) => {
result.push({ event, isNative: true });
});
proxyMap.forEach((event) => {
const proxyInfo = getProxyInfo(event)!;
result.push({
event,
isNative: false,
proxySource: proxyInfo,
});
});
return result.sort((a, b) => b.event.created_at - a.event.created_at);
} 顯示來源連結
function getSourceUrl(proxyInfo: ProxyInfo): string | null {
switch (proxyInfo.protocol) {
case 'activitypub':
case 'web':
// 直接使用 URL
return proxyInfo.id;
case 'atproto':
// 將 AT URI 轉換為 Bluesky 網頁連結
const match = proxyInfo.id.match(
/at:\/\/(did:[^\/]+)\/app\.bsky\.feed\.post\/(.+)/
);
if (match) {
return `https://bsky.app/profile/${match[1]}/post/${match[2]}`;
}
return null;
case 'rss':
// 移除 fragment,返回 feed URL
return proxyInfo.id.split('#')[0];
default:
return null;
}
}
// React 元件範例
function ProxyBadge({ event }: { event: any }) {
const proxyInfo = getProxyInfo(event);
if (!proxyInfo) {
return null;
}
const sourceUrl = getSourceUrl(proxyInfo);
const protocolLabels: Record<string, string> = {
activitypub: 'Fediverse',
atproto: 'Bluesky',
rss: 'RSS',
web: 'Web',
};
return (
<div className="proxy-badge">
<span>來自 {protocolLabels[proxyInfo.protocol] || proxyInfo.protocol}</span>
{sourceUrl && (
<a href={sourceUrl} target="_blank" rel="noopener noreferrer">
查看原文
</a>
)}
</div>
);
} 使用場景
跨平台橋接
- 將 Mastodon/Fediverse 內容同步到 Nostr
- 將 Bluesky 貼文橋接到 Nostr
- 將部落格 RSS 更新發布到 Nostr
- 歸檔 Twitter 內容到 Nostr
內容去重
- 識別從多個來源橋接的相同內容
- 優先顯示原生 Nostr 內容
- 合併來自不同橋接服務的重複貼文
來源追蹤
- 顯示內容的原始來源平台
- 提供返回原始內容的連結
- 讓用戶了解內容的真實來源
最佳實踐
- 使用唯一識別符:確保 ID 在該協議中是全局唯一的
- 保持連結有效:原始來源 URL 應該是可訪問的
- 標記所有橋接內容:讓用戶和客戶端能識別非原生內容
- 處理來源失效:原始內容可能被刪除,優雅處理
相關 NIPs
參考資源
已複製連結