NIP-45: Counting Results
使用 COUNT 請求高效獲取事件數量統計
概述
NIP-45 定義了 COUNT 請求類型,允許客戶端向中繼器查詢符合條件的事件數量, 而無需下載所有事件。這對於顯示追蹤者數量、按讚數、回覆數等統計資訊非常有用。
COUNT 請求
COUNT 請求的格式與 REQ 類似,但使用 COUNT 動詞:
["COUNT", "<subscription_id>", <filter>, ...] 過濾器
過濾器格式與 NIP-01 的 REQ 請求相同:
{
"kinds": [1],
"authors": ["pubkey1", "pubkey2"],
"#e": ["event_id"],
"#p": ["pubkey"],
"since": 1700000000,
"until": 1700100000
} COUNT 回應
中繼器回應包含計數結果:
["COUNT", "<subscription_id>", {"count": 238}] 可選的近似值標記
當計數可能不精確時,中繼器可以標記為近似值:
["COUNT", "<subscription_id>", {"count": 238, "approximate": true}] 使用範例
統計追蹤者數量
// 請求
["COUNT", "followers", {"kinds": [3], "#p": ["目標用戶公鑰"]}]
// 回應
["COUNT", "followers", {"count": 1523}] 統計貼文按讚數
// 請求
["COUNT", "likes", {"kinds": [7], "#e": ["貼文事件ID"]}]
// 回應
["COUNT", "likes", {"count": 42}] 統計回覆數
// 請求
["COUNT", "replies", {"kinds": [1], "#e": ["原始貼文ID"]}]
// 回應
["COUNT", "replies", {"count": 15}] 統計用戶貼文數
// 請求
["COUNT", "posts", {"kinds": [1], "authors": ["用戶公鑰"]}]
// 回應
["COUNT", "posts", {"count": 892}] TypeScript 實作
基礎計數函數
interface CountResult {
count: number;
approximate?: boolean;
}
async function countEvents(
relay: WebSocket,
filter: object
): Promise<CountResult> {
return new Promise((resolve, reject) => {
const subId = Math.random().toString(36).substring(7);
const timeout = setTimeout(() => {
reject(new Error('Count request timeout'));
}, 10000);
const handler = (event: MessageEvent) => {
const data = JSON.parse(event.data);
if (data[0] === 'COUNT' && data[1] === subId) {
clearTimeout(timeout);
relay.removeEventListener('message', handler);
resolve(data[2]);
}
};
relay.addEventListener('message', handler);
relay.send(JSON.stringify(['COUNT', subId, filter]));
});
}
// 使用範例
const result = await countEvents(relay, {
kinds: [7],
'#e': ['event_id'],
});
console.log(`按讚數: ${result.count}`); 多中繼器計數
interface MultiRelayCountResult {
total: number;
byRelay: Map<string, number>;
hasApproximate: boolean;
}
async function countFromMultipleRelays(
relayUrls: string[],
filter: object
): Promise<MultiRelayCountResult> {
const results = await Promise.allSettled(
relayUrls.map(async (url) => {
const ws = new WebSocket(url);
await new Promise((resolve) => (ws.onopen = resolve));
try {
const result = await countEvents(ws, filter);
return { url, ...result };
} finally {
ws.close();
}
})
);
const byRelay = new Map<string, number>();
let hasApproximate = false;
let maxCount = 0;
for (const result of results) {
if (result.status === 'fulfilled') {
byRelay.set(result.value.url, result.value.count);
maxCount = Math.max(maxCount, result.value.count);
if (result.value.approximate) {
hasApproximate = true;
}
}
}
return {
total: maxCount, // 取最大值作為總數
byRelay,
hasApproximate,
};
} 社交統計類別
class NostrStats {
private relays: string[];
constructor(relays: string[]) {
this.relays = relays;
}
// 獲取追蹤者數量
async getFollowerCount(pubkey: string): Promise<number> {
const result = await countFromMultipleRelays(this.relays, {
kinds: [3],
'#p': [pubkey],
});
return result.total;
}
// 獲取關注數量
async getFollowingCount(pubkey: string): Promise<number> {
// 需要獲取 kind 3 事件並計算 p 標籤數量
// COUNT 無法直接計算這個
return 0;
}
// 獲取貼文按讚數
async getReactionCount(eventId: string): Promise<number> {
const result = await countFromMultipleRelays(this.relays, {
kinds: [7],
'#e': [eventId],
});
return result.total;
}
// 獲取貼文回覆數
async getReplyCount(eventId: string): Promise<number> {
const result = await countFromMultipleRelays(this.relays, {
kinds: [1],
'#e': [eventId],
});
return result.total;
}
// 獲取貼文轉發數
async getRepostCount(eventId: string): Promise<number> {
const result = await countFromMultipleRelays(this.relays, {
kinds: [6],
'#e': [eventId],
});
return result.total;
}
// 獲取貼文 Zap 數量
async getZapCount(eventId: string): Promise<number> {
const result = await countFromMultipleRelays(this.relays, {
kinds: [9735],
'#e': [eventId],
});
return result.total;
}
// 獲取用戶貼文數量
async getPostCount(pubkey: string): Promise<number> {
const result = await countFromMultipleRelays(this.relays, {
kinds: [1],
authors: [pubkey],
});
return result.total;
}
}
// 使用範例
const stats = new NostrStats([
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol',
]);
const pubkey = '用戶公鑰';
const followers = await stats.getFollowerCount(pubkey);
const posts = await stats.getPostCount(pubkey);
console.log(`追蹤者: ${followers}, 貼文: ${posts}`); React Hook
import { useState, useEffect } from 'react';
interface EventStats {
reactions: number;
replies: number;
reposts: number;
zaps: number;
loading: boolean;
}
function useEventStats(eventId: string, relays: string[]): EventStats {
const [stats, setStats] = useState<EventStats>({
reactions: 0,
replies: 0,
reposts: 0,
zaps: 0,
loading: true,
});
useEffect(() => {
const nostrStats = new NostrStats(relays);
Promise.all([
nostrStats.getReactionCount(eventId),
nostrStats.getReplyCount(eventId),
nostrStats.getRepostCount(eventId),
nostrStats.getZapCount(eventId),
]).then(([reactions, replies, reposts, zaps]) => {
setStats({
reactions,
replies,
reposts,
zaps,
loading: false,
});
});
}, [eventId, relays]);
return stats;
}
// 使用範例
function PostStats({ eventId }: { eventId: string }) {
const stats = useEventStats(eventId, RELAYS);
if (stats.loading) {
return <div>載入中...</div>;
}
return (
<div className="stats">
<span>❤️ {stats.reactions}</span>
<span>💬 {stats.replies}</span>
<span>🔁 {stats.reposts}</span>
<span>⚡ {stats.zaps}</span>
</div>
);
} 中繼器支援檢測
async function supportsCount(relayUrl: string): Promise<boolean> {
try {
const response = await fetch(
relayUrl.replace('wss://', 'https://').replace('ws://', 'http://'),
{ headers: { Accept: 'application/nostr+json' } }
);
const info = await response.json();
return info.supported_nips?.includes(45) ?? false;
} catch {
return false;
}
}
// 過濾支援 COUNT 的中繼器
async function getCountSupportedRelays(relays: string[]): Promise<string[]> {
const results = await Promise.all(
relays.map(async (relay) => ({
relay,
supported: await supportsCount(relay),
}))
);
return results.filter((r) => r.supported).map((r) => r.relay);
} 限制與注意事項
- 中繼器支援:並非所有中繼器都支援 COUNT
- 準確性:不同中繼器可能返回不同數值
- 近似值:大量資料時可能返回近似值
- 效能限制:某些中繼器可能限制 COUNT 請求頻率
- 無法聚合:無法直接進行 SUM、AVG 等聚合運算
最佳實踐
- 快取結果:統計數據變化不頻繁,可以快取
- 降級處理:不支援時改用 REQ 計數
- 多中繼器查詢:取最大值獲得更準確的統計
- 顯示近似:當 approximate 為 true 時顯示 ≈ 符號
相關 NIPs
參考資源
已複製連結